├── .codesandbox └── tasks.json ├── .devcontainer └── devcontainer.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierrc.js ├── .storybook ├── main.ts ├── preview-head.html └── preview.tsx ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __mocks__ └── react-native-svg.js ├── babel.config.js ├── docs └── index.stories.tsx ├── jest.native.config.js ├── jest.web.config.js ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── native │ ├── ContentLoader.tsx │ ├── Svg.tsx │ ├── __tests__ │ │ ├── ContentLoader.test.tsx │ │ ├── Svg.test.tsx │ │ ├── __snapshots__ │ │ │ └── snapshots.test.tsx.snap │ │ ├── presets │ │ │ ├── BulletListStyle.test.tsx │ │ │ ├── CodeStyle.test.tsx │ │ │ ├── FacebookStyle.test.tsx │ │ │ ├── InstagramStyle.test.tsx │ │ │ ├── ListStyle.test.tsx │ │ │ └── __snapshots__ │ │ │ │ ├── BulletListStyle.test.tsx.snap │ │ │ │ ├── CodeStyle.test.tsx.snap │ │ │ │ ├── FacebookStyle.test.tsx.snap │ │ │ │ ├── InstagramStyle.test.tsx.snap │ │ │ │ └── ListStyle.test.tsx.snap │ │ └── snapshots.test.tsx │ ├── index.ts │ ├── package.json │ └── presets │ │ ├── BulletListStyle.tsx │ │ ├── CodeStyle.tsx │ │ ├── FacebookStyle.tsx │ │ ├── InstagramStyle.tsx │ │ └── ListStyle.tsx ├── shared │ └── uid.ts └── web │ ├── ContentLoader.tsx │ ├── Svg.tsx │ ├── __tests__ │ ├── ContentLoader.test.tsx │ ├── Svg.test.tsx │ ├── __snapshots__ │ │ └── snapshots.test.tsx.snap │ ├── index.test.tsx │ ├── presets │ │ ├── BulletListStyle.test.tsx │ │ ├── CodeStyle.test.tsx │ │ ├── FacebookStyle.test.tsx │ │ ├── InstagramStyle.test.tsx │ │ ├── ListStyle.test.tsx │ │ └── __snapshots__ │ │ │ ├── BulletListStyle.test.tsx.snap │ │ │ ├── CodeStyle.test.tsx.snap │ │ │ ├── FacebookStyle.test.tsx.snap │ │ │ ├── InstagramStyle.test.tsx.snap │ │ │ └── ListStyle.test.tsx.snap │ ├── snapshots.test.tsx │ └── uid.test.tsx │ ├── index.ts │ └── presets │ ├── BulletListStyle.tsx │ ├── CodeStyle.tsx │ ├── FacebookStyle.tsx │ ├── InstagramStyle.tsx │ └── ListStyle.tsx ├── tsconfig.base.json ├── tsconfig.json └── tsconfig.test.json /.codesandbox/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // These tasks will run in order when initializing your CodeSandbox project. 3 | "setupTasks": [ 4 | { 5 | "name": "Install Dependencies", 6 | "command": "pnpm install" 7 | } 8 | ], 9 | 10 | // These tasks can be run from CodeSandbox. Running one will open a log in the app. 11 | "tasks": { 12 | "dev": { 13 | "name": "dev", 14 | "command": "pnpm run dev", 15 | "runAtStart": true 16 | }, 17 | "build": { 18 | "name": "build", 19 | "command": "pnpm run build", 20 | "runAtStart": false 21 | }, 22 | "build:docs": { 23 | "name": "build:docs", 24 | "command": "pnpm run build:docs", 25 | "runAtStart": false 26 | }, 27 | "test": { 28 | "name": "test", 29 | "command": "pnpm run test", 30 | "runAtStart": false 31 | }, 32 | "test:unit": { 33 | "name": "test:unit", 34 | "command": "pnpm run test:unit", 35 | "runAtStart": false 36 | }, 37 | "test:unit:web": { 38 | "name": "test:unit:web", 39 | "command": "pnpm run test:unit:web", 40 | "runAtStart": false 41 | }, 42 | "test:unit:native": { 43 | "name": "test:unit:native", 44 | "command": "pnpm run test:unit:native", 45 | "runAtStart": false 46 | }, 47 | "test:watch": { 48 | "name": "test:watch", 49 | "command": "pnpm run test:watch", 50 | "runAtStart": false 51 | }, 52 | "test:tsc": { 53 | "name": "test:tsc", 54 | "command": "pnpm run test:tsc", 55 | "runAtStart": false 56 | }, 57 | "test:tsc:watch": { 58 | "name": "test:tsc:watch", 59 | "command": "pnpm run test:tsc:watch", 60 | "runAtStart": false 61 | }, 62 | "commit": { 63 | "name": "commit", 64 | "command": "pnpm run commit", 65 | "runAtStart": false 66 | }, 67 | "format": { 68 | "name": "format", 69 | "command": "pnpm run format", 70 | "runAtStart": false 71 | }, 72 | "release": { 73 | "name": "release", 74 | "command": "pnpm run release", 75 | "runAtStart": false 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye" 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "yarn install", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [danilowoz] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What did you do? 2 | Please include the actual source code causing the issue. 3 | 4 | ## What did you expect to happen? 5 | Please mention the expected behaviour. 6 | 7 | ## What happened actually? 8 | 9 | ### Which versions of react-content-loader, and which browser are affected by this issue? 10 | Please also mention the version of react. 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | In this section, you should give the overview of the problem and the proposed changes. 3 | 4 | ## Related Issue #[issue number] 5 | If this PR is fixing any issue, then please include - Related Issue #[issue number] 6 | 7 | ## Any Breaking Changes 8 | If this PR is introducing any breaking changes then mention them in this section. 9 | 10 | ## Checklist 11 | - [] Are all the test cases passing? 12 | - [] If any new feature has been added, then are the test cases updated/added? 13 | - [] Has the documentation been updated for the proposed change, if required? -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | name: Lint, typecheck and test 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - uses: pnpm/action-setup@v3 19 | with: 20 | version: 8 21 | 22 | - name: Use Node.js 20 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 20 26 | cache: 'pnpm' 27 | 28 | - name: Install dependencies 29 | run: pnpm i 30 | 31 | - name: Run tests 32 | run: pnpm run test 33 | 34 | - name: Build 35 | run: pnpm run build 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | prepare: 10 | runs-on: ubuntu-latest 11 | name: Checks 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | 17 | - uses: pnpm/action-setup@v3 18 | with: 19 | version: 8 20 | 21 | - name: Use Node.js 20 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 20 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm i 29 | 30 | - name: Run tests 31 | run: pnpm run test 32 | 33 | build-and-release: 34 | runs-on: ubuntu-latest 35 | needs: prepare 36 | name: Release 37 | 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v3 41 | 42 | - uses: pnpm/action-setup@v3 43 | with: 44 | version: 8 45 | 46 | - name: Use Node.js 20 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: 20 50 | cache: 'pnpm' 51 | 52 | - name: Install dependencies 53 | run: pnpm i 54 | 55 | - name: Build 56 | run: pnpm run build 57 | 58 | - name: Release 59 | env: 60 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | run: pnpm run release 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | yarn-error.log 9 | node_modules 10 | dist 11 | coverage 12 | /native 13 | .docz/ 14 | .rpt2_cache 15 | settings.json 16 | *.code-workspace 17 | yarn.lock 18 | yarn-error.log 19 | docs-build -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@significa/prettier-config"); 2 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | const config: StorybookConfig = { 3 | stories: ["../docs/**/*.mdx", "../docs/**/*.stories.@(js|jsx|ts|tsx)"], 4 | addons: [ 5 | 6 | ], 7 | framework: { 8 | name: "@storybook/react-vite", 9 | options: {}, 10 | }, 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Preview } from "@storybook/react"; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | 8 | }, 9 | decorators: [ 10 | (Story) => ( 11 |
12 | 13 |
14 | ), 15 | ], 16 | }; 17 | 18 | export default preview; 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at danilowoz@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Julian Ćwirko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | react-content-loader 3 |

4 |

5 | Example's react-content-loader 6 |

7 | 8 | SVG-Powered component to easily create placeholder loadings (like Facebook's cards loading). 9 | 10 | ## Features 11 | 12 | - :gear: **Customizable:** Feel free to change the colors, speed, sizes, and even **RTL**; 13 | - :ok_hand: **Plug and play:** with many presets to use, see the [examples](http://danilowoz.com/create-content-loader/#gallery); 14 | - :pencil2: **DIY:** use the [create-content-loader](https://danilowoz.com/create-content-loader) to create your own custom loaders easily; 15 | - 📱 **React Native support**: same API, as same powerful features; 16 | - ⚛️ **Really lightweight:** less than **2kB** and **0 dependencies** for web version; 17 | 18 | ## Index 19 | 20 | - [Getting Started](#gettingstarted) 21 | - [Usage](#usage) 22 | - [Native](#native) 23 | - [Options](#options) 24 | - [Examples](#examples) 25 | - [Troubleshooting](#troubleshooting) 26 | - [Similar packages](#similarpackages) 27 | - [Development](#development) 28 | 29 | ## Getting Started 30 | 31 | ```sh 32 | npm i react-content-loader --save 33 | ``` 34 | 35 | ```sh 36 | yarn add react-content-loader 37 | ``` 38 | 39 | ### For React Native 40 | 41 | ```sh 42 | npm i react-content-loader react-native-svg --save 43 | ``` 44 | 45 | ```sh 46 | yarn add react-content-loader react-native-svg 47 | ``` 48 | 49 | CDN from [JSDELIVR](https://www.jsdelivr.com/package/npm/react-content-loader) 50 | 51 | ## Usage 52 | 53 | There are two ways to use it: 54 | 55 | **1. Presets, see the [examples](https://danilowoz.com/react-content-loader/):** 56 | 57 | ```jsx 58 | import ContentLoader, { Facebook } from 'react-content-loader' 59 | 60 | const MyLoader = () => 61 | const MyFacebookLoader = () => 62 | ``` 63 | 64 | **2. Custom mode, see the [online tool](https://danilowoz.com/create-content-loader)** 65 | 66 | ```jsx 67 | const MyLoader = () => ( 68 | 69 | {/* Only SVG shapes */}     70 | 71 | 72 | 73 | 74 | ) 75 | ``` 76 | 77 | **Still not clear?** Take a look at this working example at [codesandbox.io](https://codesandbox.io/s/moojk887z9) 78 | Or try the components editable demo hands-on and install it from [bit.dev](https://bit.dev/danilowoz/react-content-loader) 79 | 80 | ## Native 81 | 82 | `react-content-loader` can be used with React Native in the same way as web version with the same import: 83 | 84 | **1. Presets, see the [examples](#examples):** 85 | 86 | ```jsx 87 | import ContentLoader, { Facebook } from 'react-content-loader/native' 88 | 89 | const MyLoader = () => 90 | const MyFacebookLoader = () => 91 | ``` 92 | 93 | **2. Custom mode** 94 | 95 | **To create custom loaders there is an important difference:** as React Native doesn't have any native module for SVG components, it's necessary to import the shapes from [react-native-svg](https://github.com/react-native-community/react-native-svg) or use the named export Rect and Circle from `react-content-loader` import: 96 | 97 | ```jsx 98 | import ContentLoader, { Rect, Circle } from 'react-content-loader/native' 99 | 100 | const MyLoader = () => ( 101 | 102 | 103 | 104 | 105 | 106 | ) 107 | ``` 108 | 109 | ## Options 110 | 111 | |
Prop name and type
| Environment | Description | 112 | | ---------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 113 | | **`animate?: boolean`**
Defaults to `true` | React DOM
React Native | Opt-out of animations with `false` | 114 | | **`title?: string`**
Defaults to `Loading...` | React DOM only | It's used to describe what element it is. 
Use `''` (empty string) to remove. | 115 | | **`baseUrl?: string`**
Defaults to an empty string | React DOM only | Required if you're using `` document ``. 
This prop is common used as: 
`` which will fill the SVG attribute with the relative path. Related [#93](https://github.com/danilowoz/react-content-loader/issues/93). | 116 | | **`speed?: number`**
Defaults to `1.2` | React DOM
React Native | Animation speed in seconds. | 117 | | **`viewBox?: string`**
Defaults to `undefined` | React DOM
React Native | Use viewBox props to set a custom viewBox value,
for more information about how to use it,
read the article [How to Scale SVG](https://css-tricks.com/scale-svg/). | 118 | | **`gradientRatio?: number`**
Defaults to `1.2` | React DOM only | Width of the animated gradient as a fraction of the view box width. | 119 | | **`rtl?: boolean`**
Defaults to `false` | React DOM
React Native | Content right-to-left. | 120 | | **`backgroundColor?: string`**
Defaults to `#f5f6f7` | React DOM
React Native | Used as background of animation. | 121 | | **`foregroundColor?: string`**
Defaults to `#eee` | React DOM
React Native | Used as the foreground of animation. | 122 | | **`backgroundOpacity?: number`**
Defaults to `1` | React DOM
React Native | Background opacity (0 = transparent, 1 = opaque)
used to solve an issue in [Safari](#safari--ios) | 123 | | **`foregroundOpacity?: number`**
Defaults to `1` | React DOM
React Native | Animation opacity (0 = transparent, 1 = opaque)
used to solve an issue in [Safari](#safari--ios) | 124 | | **`style?: React.CSSProperties`**
Defaults to `{}` | React DOM only | | 125 | | **`uniqueKey?: string`**
Defaults to random unique id | React DOM only | Use the same value of prop key, 
that will solve inconsistency on the SSR, see more [here](https://github.com/danilowoz/react-content-loader/issues/78). | 126 | | **`beforeMask?: JSX.Element`**
Defaults to null | React DOM
React Native | Define custom shapes before content, 
see more [here](https://github.com/danilowoz/react-content-loader/issues/266). | 127 | 128 | See all options [live](https://danilowoz.com/react-content-loader/) 129 | 130 | ## Examples 131 | 132 | ##### Facebook Style 133 | 134 | ```jsx 135 | import { Facebook } from 'react-content-loader' 136 | 137 | const MyFacebookLoader = () => 138 | ``` 139 | 140 | Facebook Style 141 | 142 | ##### Instagram Style 143 | 144 | ```jsx 145 | import { Instagram } from 'react-content-loader' 146 | 147 | const MyInstagramLoader = () => 148 | ``` 149 | 150 | Instagram Style 151 | 152 | ##### Code Style 153 | 154 | ```jsx 155 | import { Code } from 'react-content-loader' 156 | 157 | const MyCodeLoader = () => 158 | ``` 159 | 160 | Code Style 161 | 162 | ##### List Style 163 | 164 | ```jsx 165 | import { List } from 'react-content-loader' 166 | 167 | const MyListLoader = () => 168 | ``` 169 | 170 | List Style 171 | 172 | ##### Bullet list Style 173 | 174 | ```jsx 175 | import { BulletList } from 'react-content-loader' 176 | 177 | const MyBulletListLoader = () => 178 | ``` 179 | 180 | Bullet list Style 181 | 182 | ### Custom Style 183 | 184 | For the custom mode, use the 185 | [online tool](https://danilowoz.com/create-content-loader). 186 | 187 | ```jsx 188 | const MyLoader = () => ( 189 | 196 | {/* Only SVG shapes */} 197 | 198 | 199 | 200 | 201 | ) 202 | ``` 203 | 204 | ![Custom](https://user-images.githubusercontent.com/4838076/36352947-b87019a8-149e-11e8-99ba-c71c2bcf8733.gif) 205 | 206 | ## Troubleshooting 207 | 208 | #### Responsive - Mobile version 209 | 210 | In order to avoid unexpected behavior, the package doesn't have opinioned settings. So if it needs to be responsive, have in mind that the output of the package is a regular SVG, so it just needs the same attributes to become a regular SVG responsive, which means: 211 | 212 | ```jsx 213 | import { Code } from 'react-content-loader' 214 | 215 | const MyCodeLoader = () => ( 216 | 222 | ) 223 | ``` 224 | 225 | #### Server-side rendering (SSR) - Match snapshot 226 | 227 | As the main component generates random values to match the id of the SVG element with background style, it can encounter unexpected errors and unmatching warning on render, once the random value of id will be generated twice, in case of SSR: server and client; or in case of snapshot test: on the first match and re-running the test. 228 | 229 | To fix it, set the prop [`uniqueKey`](https://github.com/danilowoz/react-content-loader#uniquekey-string---web-only), then the id will not be random anymore: 230 | 231 | ```jsx 232 | import { Facebook } from 'react-content-loader' 233 | 234 | const MyFacebookLoader = () => 235 | ``` 236 | 237 | #### **Alpha is not working: Safari / iOS** 238 | 239 | When using `rgba` as a `backgroundColor` or `foregroundColor` value, [Safari does not respect the alpha channel](https://github.com/w3c/svgwg/issues/180), meaning that the color will be opaque. To prevent this, instead of using a `rgba` value for `backgroundColor`/`foregroundColor`, use the `rgb` equivalent and move the alpha channel value to the `backgroundOpacity`/`foregroundOpacity` props. 240 | 241 | ```jsx 242 | {/* Opaque color in Safari and iOS */} 243 | 246 | 247 | 248 | {/_ Semi-transparent color in Safari and iOS _/} 249 | 254 | 255 | 256 | ``` 257 | 258 | #### **Black box in Safari / iOS (again)** 259 | 260 | Using the base tag on a page that contains SVG elements fails to render and it looks like a black box. Just remove the **base-href** tag from the `` and the issue has been solved. 261 | 262 | black box 263 | 264 | See: [#93](https://github.com/danilowoz/react-content-loader/issues/93) / [109](https://github.com/danilowoz/react-content-loader/issues/109) 265 | 266 | #### Browser supports SVG-Animate 267 | 268 | Old browsers don't support animation in SVG ([compatibility list](https://caniuse.com/#search=SVGAnimateElement)), and if your project must support IE, for examples, here's a couple of ways to make sure that browser supports SVG Animate: 269 | 270 | - `window.SVGAnimateElement` 271 | - `document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#SVG-Animation", "1.1")` 272 | - Or even use https://modernizr.com/ 273 | 274 | ## Similar packages 275 | 276 | - React Native: [rn-placeholder](https://github.com/mfrachet/rn-placeholder), [react-native-svg-animated-linear-gradient](https://github.com/virusvn/react-native-svg-animated-linear-gradient); 277 | - [Preact](https://github.com/bonitasoft/preact-content-loader); 278 | - Vue.js: [vue-content-loading](https://github.com/LucasLeandro1204/vue-content-loading), [vue-content-loader](https://github.com/egoist/vue-content-loader); 279 | - Angular: [ngx-content-loading](https://github.com/Gbuomprisco/ngx-content-loading), [ngx-content-loader](https://github.com/NetanelBasal/ngx-content-loader). 280 | 281 | --- 282 | 283 | ## Development 284 | 285 | Fork the repo and then clone it 286 | 287 | ``` 288 | $ git clone git@github.com:YourUsername/react-content-loader.git && cd react-content-loader 289 | ``` 290 | 291 | `$ npm i`: Install the dependencies; 292 | 293 | `$ npm run build`: Build to production; 294 | 295 | `$ npm run dev`: Run the Storybook to see your changes; 296 | 297 | `$ npm run test`: Run all tests: type checking, unit tests on web and native; 298 | 299 | `$ npm run test:watch`: Watch unit tests; 300 | 301 | ### React Native 302 | 303 | As React Native doesn't support symbolic links (to link the dependency to another folder) and as there is no playground to check your contributions (like storybook), this is recommended strategy to run the project locally: 304 | 305 | 1. Create a new React Native from scratch, either Metro or create-react-native-app; 306 | 2. Install the dependency to your root project: 307 | `yarn add react-content-loader react-native-svg` 308 | 3. Open the project just created and clone this repository there; 309 | 4. Create your loading component and point the `react-content-loader` to the project just cloned, like: 310 | `import ContentLoader, { Rect, Circle } from './react-content-loader/native'` 311 | 312 | ### Commit messages 313 | 314 | Commit messages should follow the [commit message convention](https://conventionalcommits.org/) so, changelogs could be generated automatically by that. Commit messages are validated automatically upon commit. If you aren't familiar with the commit message convention, you can use yarn commit (or `npm run commit`) instead of git commit, which provides an interactive CLI for generating proper commit messages. 315 | 316 | ## License 317 | 318 | [MIT](https://github.com/danilowoz/react-content-loader/blob/master/LICENSE) 319 | -------------------------------------------------------------------------------- /__mocks__/react-native-svg.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const createComponent = function(name) { 4 | return class extends React.Component { 5 | // overwrite the displayName, since this is a class created dynamically 6 | static displayName = name; 7 | 8 | render() { 9 | return React.createElement(name, this.props, this.props.children); 10 | } 11 | }; 12 | }; 13 | 14 | // Mock all react-native-svg exports 15 | // from https://github.com/magicismight/react-native-svg/blob/master/index.js 16 | const Svg = createComponent('Svg'); 17 | const Circle = createComponent('Circle'); 18 | const Ellipse = createComponent('Ellipse'); 19 | const G = createComponent('G'); 20 | const Text = createComponent('Text'); 21 | const TextPath = createComponent('TextPath'); 22 | const TSpan = createComponent('TSpan'); 23 | const Path = createComponent('Path'); 24 | const Polygon = createComponent('Polygon'); 25 | const Polyline = createComponent('Polyline'); 26 | const Line = createComponent('Line'); 27 | const Rect = createComponent('Rect'); 28 | const Use = createComponent('Use'); 29 | const Image = createComponent('Image'); 30 | const Symbol = createComponent('Symbol'); 31 | const Defs = createComponent('Defs'); 32 | const LinearGradient = createComponent('LinearGradient'); 33 | const RadialGradient = createComponent('RadialGradient'); 34 | const Stop = createComponent('Stop'); 35 | const ClipPath = createComponent('ClipPath'); 36 | const Pattern = createComponent('Pattern'); 37 | const Mask = createComponent('Mask'); 38 | 39 | export { 40 | Svg, 41 | Circle, 42 | Ellipse, 43 | G, 44 | Text, 45 | TextPath, 46 | TSpan, 47 | Path, 48 | Polygon, 49 | Polyline, 50 | Line, 51 | Rect, 52 | Use, 53 | Image, 54 | Symbol, 55 | Defs, 56 | LinearGradient, 57 | RadialGradient, 58 | Stop, 59 | ClipPath, 60 | Pattern, 61 | Mask, 62 | }; 63 | 64 | export default Svg; -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | 'module:metro-react-native-babel-preset', 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [['@babel/plugin-transform-private-methods', { loose: true }]], 7 | } 8 | -------------------------------------------------------------------------------- /docs/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SyntaxHighlighter from 'react-syntax-highlighter' 3 | import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs' 4 | 5 | import ContentLoader, { 6 | BulletList, 7 | Code, 8 | Facebook, 9 | Instagram, 10 | List, 11 | } from '../src/web' 12 | 13 | export default { 14 | title: 'React Content Loader', 15 | } 16 | 17 | const SyntaxCode = ({ children }) => { 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | 25 | /** 26 | * Animated 27 | */ 28 | export const animate = () => { 29 | return ( 30 | <> 31 | {''} 32 | 33 | 34 | {''} 35 | 36 | 37 | ) 38 | } 39 | 40 | animate.story = { 41 | parameters: { 42 | notes: `##\`animate?: boolean\` 43 | 44 | Defaults to \`true\`. Opt-out of animations with \`false\``, 45 | }, 46 | } 47 | 48 | /** 49 | * Background color 50 | */ 51 | export const backgroundColor = () => { 52 | return ( 53 | <> 54 | {''} 55 | 56 | 57 | ) 58 | } 59 | 60 | backgroundColor.story = { 61 | parameters: { 62 | notes: `## \`backgroundColor?: string\` 63 | 64 | Defaults to \`#f5f6f7\` which is used as background of animation.`, 65 | }, 66 | } 67 | 68 | /** 69 | * Foreground color 70 | */ 71 | export const foregroundColor = () => { 72 | return ( 73 | <> 74 | {''} 75 | 76 | 77 | ) 78 | } 79 | 80 | foregroundColor.story = { 81 | parameters: { 82 | notes: `## \`foregroundColor?: string\` 83 | 84 | Defaults to \`#eee\` which is used as foreground of animation.`, 85 | }, 86 | } 87 | 88 | /** 89 | * Background opacity 90 | */ 91 | export const backgroundOpacity = () => { 92 | return ( 93 | <> 94 | {''} 95 | 96 | 97 | ) 98 | } 99 | 100 | backgroundOpacity.story = { 101 | parameters: { 102 | notes: `## \`backgroundOpacity?: number\` 103 | 104 | Defaults to \`1\`. Background opacity (0 = transparent, 1 = opaque) used to solve a issue in [Safari](#bugfix-in-safari)`, 105 | }, 106 | } 107 | 108 | /** 109 | * Foreground opacity 110 | */ 111 | export const foregroundOpacity = () => { 112 | return ( 113 | <> 114 | {''} 115 | 116 | 117 | ) 118 | } 119 | 120 | foregroundOpacity.story = { 121 | parameters: { 122 | notes: `## \`foregroundOpacity?: number\` 123 | 124 | Defaults to \`1\`. Animation opacity (0 = transparent, 1 = opaque) used to solve a issue in [Safari](#bugfix-in-safari)`, 125 | }, 126 | } 127 | 128 | /** 129 | * Base URL 130 | */ 131 | export const baseURL = () => { 132 | return ( 133 | <> 134 | {''} 135 | 136 | 137 | ) 138 | } 139 | 140 | baseURL.story = { 141 | parameters: { 142 | notes: `## \`baseUrl?: string\` 143 | 144 | Required if you're using \`\` in the \`\`. Defaults to an empty string. This prop is common used as: \`\` which will fill the SVG attribute with the relative path. Related [#93](https://github.com/danilowoz/react-content-loader/issues/93). 145 | `, 146 | }, 147 | } 148 | 149 | /** 150 | * Children 151 | */ 152 | export const children = () => { 153 | return ( 154 | <> 155 |

Custom

156 | {` 157 | 158 | 159 | 160 | `} 161 | 162 | 163 | 164 | 165 | 166 | 167 |

Default

168 | {``} 169 | 170 | 171 | ) 172 | } 173 | 174 | children.story = { parameters: { notes: `## \`children?: ReactNode\`` } } 175 | 176 | /** 177 | * Gradient Ratio 178 | */ 179 | export const gradientRatio = () => { 180 | return ( 181 | <> 182 | {``} 187 | 192 | 193 | {``} 198 | 203 | 204 | ) 205 | } 206 | 207 | gradientRatio.story = { 208 | parameters: { 209 | notes: `## \`gradientRatio?: number\` 210 | 211 | Defaults to \`2\`. Width of the animated gradient as a fraction of the viewbox width.`, 212 | }, 213 | } 214 | 215 | /** 216 | * Gradient Direction 217 | */ 218 | export const gradientDirection = () => { 219 | return ( 220 | <> 221 | {``} 222 | 223 | 224 | {``} 225 | 226 | 227 | ) 228 | } 229 | 230 | gradientDirection.story = { 231 | parameters: { 232 | notes: `## \`gradientDirection?: 'left-right' | 'top-bottom' \` 233 | 234 | Defaults to \`left-right\`. Direction in which the gradient is animated. Useful to implement top-down animations`, 235 | }, 236 | } 237 | 238 | /** 239 | * Speed 240 | */ 241 | export const speed = () => { 242 | return ( 243 | <> 244 | {``} 245 | 246 | {``} 247 | 248 | 249 | ) 250 | } 251 | 252 | speed.story = { 253 | parameters: { 254 | notes: `## \`speed?: number\` 255 | 256 | Defaults to \`1.2\`. Animation speed in seconds.`, 257 | }, 258 | } 259 | 260 | /** 261 | * RTL 262 | */ 263 | export const RTL = () => { 264 | return ( 265 | <> 266 | {``} 267 | 268 | 269 | ) 270 | } 271 | 272 | RTL.story = { 273 | parameters: { 274 | notes: `## \`rtl?: boolean\` 275 | 276 | Defaults to \`false\`. Content right-to-left.`, 277 | }, 278 | } 279 | 280 | /** 281 | * Unique key 282 | */ 283 | export const uniqueKey = () => { 284 | return ( 285 | <> 286 | {``} 287 | 288 | 289 | ) 290 | } 291 | 292 | uniqueKey.story = { 293 | parameters: { 294 | notes: `## \`uniqueKey?: string\` 295 | 296 | Defaults to random unique id. Use the same value of prop key, that will solve inconsistency on the SSR, see more [here](https://github.com/danilowoz/react-content-loader/issues/78).`, 297 | }, 298 | } 299 | 300 | /** 301 | * Responsive 302 | */ 303 | export const responsive = () => { 304 | return ( 305 |
306 | {"'"} 307 | 308 |
309 | ) 310 | } 311 | 312 | /** 313 | * Title 314 | */ 315 | export const title = () => { 316 | return ( 317 | <> 318 | {``} 319 | 320 | 321 | ) 322 | } 323 | 324 | title.story = { 325 | parameters: { 326 | notes: `## \`title?: string | boolean\` 327 | 328 | Defaults to \`Loading interface...\`. It's used to describe what element it is. Use \`false\` to remove. 329 | `, 330 | }, 331 | } 332 | 333 | /** 334 | * View box 335 | */ 336 | export const viewBox = () => { 337 | return ( 338 | <> 339 | {''} 340 | 341 | 342 | {''} 343 | 344 | 345 | ) 346 | } 347 | 348 | viewBox.story = { 349 | parameters: { 350 | notes: `## \`viewBox?: string\` 351 | 352 | Use viewbox props to set viewbox value. 353 | Additionally, pass viewBox props as empty string to remove viewBox.`, 354 | }, 355 | } 356 | 357 | /** 358 | * Presets 359 | */ 360 | export const presets = () => { 361 | return ( 362 | <> 363 | {''} 364 | 365 | 366 | {''} 367 | 368 | 369 | {''} 370 | 371 | 372 | {''} 373 | 374 | 375 | {''} 376 | 377 | 378 | ) 379 | } 380 | 381 | /** 382 | * Content loader vs SVG 383 | */ 384 | export const contentLoaderVsSVG = () => { 385 | return ( 386 | <> 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | ) 408 | } 409 | 410 | /** 411 | * beforeMask 412 | */ 413 | export const BeforeMask = () => { 414 | return ( 415 | <> 416 | 417 | {` 421 | } 422 | > 423 | 424 | 425 | 426 | 427 | `} 428 | 429 | 441 | } 442 | > 443 | 444 | 445 | 446 | 447 | 448 | 449 | ) 450 | } 451 | -------------------------------------------------------------------------------- /jest.native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | transformIgnorePatterns: [ 4 | 'node_modules/.pnpm/(?!react-native-payfort-sdk|react-native)/', 5 | ], 6 | testRegex: '/src/native/__tests__/.*(\\.|/)(test|spec)\\.[jt]sx?$', 7 | } 8 | -------------------------------------------------------------------------------- /jest.web.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | transform: { 4 | '^.+\\.(t|j)sx?$': 'ts-jest', 5 | }, 6 | testRegex: '/src/web/__tests__/.*(\\.|/)(test|spec)\\.[jt]sx?$', 7 | roots: ['/src'], 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 9 | preset: 'ts-jest', 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-content-loader", 3 | "version": "6.2.1", 4 | "description": "SVG-Powered component to easily create placeholder loadings (like Facebook cards loading)", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/danilowoz/react-content-loader" 8 | }, 9 | "author": "Danilo Woznica ", 10 | "license": "MIT", 11 | "main": "dist/react-content-loader.cjs.js", 12 | "module": "dist/react-content-loader.es.js", 13 | "jsnext:main": "dist/react-content-loader.es.js", 14 | "types": "dist/web/index.d.ts", 15 | "exports": { 16 | ".": { 17 | "types": "./dist/web/index.d.ts", 18 | "require": "./dist/react-content-loader.cjs.js", 19 | "import": "./dist/react-content-loader.es.js", 20 | "default": "./dist/react-content-loader.cjs.js" 21 | }, 22 | "./native": { 23 | "types": "./native/native/index.d.ts", 24 | "require": "./native/react-content-loader.native.cjs.js", 25 | "import": "./native/react-content-loader.native.es.js", 26 | "default": "./native/react-content-loader.native.cjs.js" 27 | } 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/danilowoz/react-content-loader/issues" 31 | }, 32 | "homepage": "https://github.com/danilowoz/react-content-loader", 33 | "keywords": [ 34 | "react", 35 | "react-native", 36 | "skeleton", 37 | "placeholder", 38 | "loader", 39 | "loading", 40 | "content", 41 | "svg" 42 | ], 43 | "files": [ 44 | "dist", 45 | "native" 46 | ], 47 | "sideEffects": false, 48 | "scripts": { 49 | "dev": "storybook dev -p 6006", 50 | "build": "rm -fr ./dist ./native && rollup -c", 51 | "build:docs": "build-storybook -o docs-build", 52 | "test": "npm run test:tsc && npm run test:unit", 53 | "test:unit": "npm run test:unit:web && npm run test:unit:native", 54 | "test:unit:web": "jest -c jest.web.config.js", 55 | "test:unit:native": "jest -c jest.native.config.js", 56 | "test:watch": "npm run test:unit -- --watch", 57 | "test:tsc": "tsc", 58 | "test:tsc:watch": "npm run tsc -- --watch", 59 | "commit": "git-cz", 60 | "format": "prettier --write \"src/**/*.{ts,tsx}\"", 61 | "release": "semantic-release" 62 | }, 63 | "devDependencies": { 64 | "@babel/plugin-transform-private-methods": "^7.23.3", 65 | "@babel/preset-react": "^7.23.3", 66 | "@babel/preset-typescript": "^7.23.3", 67 | "@babel/runtime": "^7.24.0", 68 | "@commitlint/cli": "^11.0.0", 69 | "@commitlint/config-conventional": "8.2.0", 70 | "@significa/prettier-config": "0.0.9", 71 | "@storybook/react": "^7.6.17", 72 | "@storybook/react-vite": "^7.6.17", 73 | "@storybook/storybook-deployer": "^2.8.6", 74 | "@types/jest": "24.0.24", 75 | "@types/react": "18.2.64", 76 | "@types/react-dom": "18.2.21", 77 | "@types/react-test-renderer": "18.0.7", 78 | "@typescript-eslint/eslint-plugin": "2.13.0", 79 | "awesome-typescript-loader": "5.2.1", 80 | "commitizen": "^4.2.1", 81 | "cz-conventional-changelog": "3.0.2", 82 | "husky": "3.1.0", 83 | "jest": "29.7.0", 84 | "jest-environment-jsdom": "^29.7.0", 85 | "metro-react-native-babel-preset": "^0.77.0", 86 | "prettier": "1.19.1", 87 | "react": "^18.2.0", 88 | "react-dom": "^18.2.0", 89 | "react-native": "^0.73.3", 90 | "react-native-svg": "15.1.0", 91 | "react-syntax-highlighter": "^12.2.1", 92 | "react-test-renderer": "18.2.0", 93 | "rollup": "1.27.14", 94 | "rollup-plugin-copy": "3.1.0", 95 | "rollup-plugin-replace": "2.2.0", 96 | "rollup-plugin-typescript2": "0.25.3", 97 | "rollup-plugin-uglify": "6.0.4", 98 | "semantic-release": "^17.4.2", 99 | "storybook": "^7.6.17", 100 | "ts-jest": "^29.1.2", 101 | "tslib": "^2.6.2", 102 | "typescript": "5.4.2" 103 | }, 104 | "peerDependencies": { 105 | "react": ">=16.0.0" 106 | }, 107 | "husky": { 108 | "hooks": { 109 | "pre-commit": "npm run format", 110 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 111 | } 112 | }, 113 | "commitlint": { 114 | "extends": [ 115 | "@commitlint/config-conventional" 116 | ] 117 | }, 118 | "config": { 119 | "commitizen": { 120 | "path": "./node_modules/cz-conventional-changelog" 121 | } 122 | }, 123 | "engines": { 124 | "node": ">=10" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | import replace from 'rollup-plugin-replace' 3 | import { uglify } from 'rollup-plugin-uglify' 4 | import typescript from 'rollup-plugin-typescript2' 5 | import copy from 'rollup-plugin-copy' 6 | 7 | import pkg from './package.json' 8 | 9 | const mergeAll = objs => Object.assign({}, ...objs) 10 | 11 | const cjs = { 12 | exports: 'named', 13 | format: 'cjs', 14 | sourcemap: true, 15 | } 16 | 17 | const esm = { 18 | format: 'es', 19 | sourcemap: true, 20 | } 21 | 22 | const globals = { react: 'React', 'react-dom': 'ReactDOM' } 23 | 24 | const commonPlugins = [ 25 | typescript({ 26 | typescript: require('typescript'), 27 | }), 28 | ] 29 | 30 | const configBase = { 31 | output: { 32 | exports: 'named', 33 | }, 34 | external: [ 35 | ...Object.keys(pkg.dependencies || {}), 36 | ...Object.keys(pkg.peerDependencies || {}), 37 | ], 38 | plugins: commonPlugins, 39 | } 40 | 41 | const umdConfig = mergeAll([ 42 | configBase, 43 | { 44 | input: 'src/web/index.ts', 45 | output: mergeAll([ 46 | configBase.output, 47 | { 48 | file: `dist/${pkg.name}.js`, 49 | format: 'umd', 50 | name: 'ContentLoader', 51 | globals, 52 | }, 53 | ]), 54 | external: Object.keys(pkg.peerDependencies || {}), 55 | }, 56 | ]) 57 | 58 | const devUmdConfig = mergeAll([ 59 | umdConfig, 60 | { 61 | input: 'src/web/index.ts', 62 | plugins: umdConfig.plugins.concat( 63 | replace({ 64 | 'process.env.NODE_ENV': JSON.stringify('development'), 65 | }) 66 | ), 67 | }, 68 | ]) 69 | 70 | const prodUmdConfig = mergeAll([ 71 | umdConfig, 72 | { 73 | input: 'src/web/index.ts', 74 | output: mergeAll([ 75 | umdConfig.output, 76 | { file: umdConfig.output.file.replace(/\.js$/, '.min.js') }, 77 | ]), 78 | }, 79 | { 80 | plugins: umdConfig.plugins.concat( 81 | replace({ 82 | 'process.env.NODE_ENV': JSON.stringify('production'), 83 | }), 84 | uglify({ 85 | compress: { 86 | pure_getters: true, 87 | unsafe: true, 88 | unsafe_comps: true, 89 | }, 90 | }) 91 | ), 92 | }, 93 | ]) 94 | 95 | const webConfig = mergeAll([ 96 | configBase, 97 | { 98 | input: 'src/web/index.ts', 99 | output: [ 100 | mergeAll([configBase.output, { ...esm, file: pkg.module }]), 101 | mergeAll([configBase.output, { ...cjs, file: pkg.main }]), 102 | ], 103 | plugins: configBase.plugins.concat(), 104 | }, 105 | ]) 106 | 107 | const nativeConfig = mergeAll([ 108 | configBase, 109 | { 110 | input: './src/native/index.ts', 111 | output: [ 112 | mergeAll([ 113 | configBase.output, 114 | { ...esm, file: `native/${pkg.name}.native.es.js` }, 115 | ]), 116 | mergeAll([ 117 | configBase.output, 118 | { ...cjs, file: `native/${pkg.name}.native.cjs.js` }, 119 | ]), 120 | ], 121 | plugins: configBase.plugins.concat( 122 | copy({ 123 | targets: [{ src: 'src/native/package.json', dest: 'native' }], 124 | }) 125 | ), 126 | }, 127 | ]) 128 | 129 | export default [devUmdConfig, prodUmdConfig, webConfig, nativeConfig] 130 | -------------------------------------------------------------------------------- /src/native/ContentLoader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Circle, Path, Rect } from 'react-native-svg' 3 | 4 | import { Facebook, IContentLoaderProps } from '.' 5 | import Svg from './Svg' 6 | 7 | const ContentLoader: React.FC = props => 8 | props.children ? : 9 | 10 | export { Circle, Rect, Path } 11 | 12 | export default ContentLoader 13 | -------------------------------------------------------------------------------- /src/native/Svg.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, isValidElement } from 'react' 2 | import { Animated } from 'react-native' 3 | import Svg, { 4 | ClipPath, 5 | Defs, 6 | LinearGradient, 7 | Rect, 8 | Stop, 9 | } from 'react-native-svg' 10 | 11 | import uid from '../shared/uid' 12 | import { IContentLoaderProps } from './' 13 | 14 | const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient) 15 | 16 | class NativeSvg extends Component { 17 | static defaultProps = { 18 | animate: true, 19 | backgroundColor: '#f5f6f7', 20 | backgroundOpacity: 1, 21 | foregroundColor: '#eee', 22 | foregroundOpacity: 1, 23 | rtl: false, 24 | speed: 1.2, 25 | interval: 0.25, 26 | style: {}, 27 | beforeMask: null, 28 | } 29 | 30 | animatedValue = new Animated.Value(-1) 31 | 32 | fixedId = this.props.uniqueKey || uid() 33 | 34 | idClip = `${this.fixedId}-diff` 35 | 36 | idGradient = `${this.fixedId}-animated-diff` 37 | 38 | unmounted = false 39 | 40 | setAnimation = () => { 41 | // props.speed is in seconds as it is compatible with web 42 | // convert to milliseconds 43 | const durMs = this.props.speed * 1000 44 | const delay = durMs * this.props.interval 45 | 46 | Animated.timing(this.animatedValue, { 47 | toValue: 2, 48 | delay: delay, 49 | duration: durMs, 50 | useNativeDriver: true, 51 | }).start(() => { 52 | if (!this.unmounted && this.props.animate) { 53 | this.animatedValue.setValue(-1) 54 | this.setAnimation() 55 | } 56 | }) 57 | } 58 | 59 | componentDidMount = () => { 60 | if (this.props.animate) { 61 | this.setAnimation() 62 | } 63 | } 64 | 65 | componentDidUpdate(prevProps: IContentLoaderProps) { 66 | if (!prevProps.animate && this.props.animate) { 67 | this.setAnimation() 68 | } 69 | } 70 | 71 | componentWillUnmount() { 72 | this.unmounted = true 73 | } 74 | 75 | render() { 76 | const { 77 | children, 78 | backgroundColor, 79 | backgroundOpacity, 80 | foregroundColor, 81 | foregroundOpacity, 82 | rtl, 83 | style, 84 | beforeMask, 85 | ...props 86 | } = this.props 87 | 88 | const x1Animation = this.animatedValue.interpolate({ 89 | extrapolate: 'clamp', 90 | inputRange: [-1, 2], 91 | outputRange: ['-100%', '100%'], 92 | }) 93 | 94 | const x2Animation = this.animatedValue.interpolate({ 95 | extrapolate: 'clamp', 96 | inputRange: [-1, 2], 97 | outputRange: ['0%', '200%'], 98 | }) 99 | 100 | const rtlStyle: object = rtl ? { transform: [{ rotateY: '180deg' }] } : {} 101 | const svgStyle = Object.assign(Object.assign({}, style), rtlStyle) 102 | 103 | // Remove unnecessary keys 104 | delete props.uniqueKey 105 | delete props.animate 106 | delete props.speed 107 | 108 | return ( 109 | 110 | {beforeMask && isValidElement(beforeMask) ? beforeMask : null} 111 | 112 | 120 | 121 | 122 | {children} 123 | 124 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | ) 138 | } 139 | } 140 | 141 | export default NativeSvg 142 | -------------------------------------------------------------------------------- /src/native/__tests__/ContentLoader.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | import * as ShallowRenderer from 'react-test-renderer/shallow' 4 | 5 | import ContentLoader, { Circle, Rect } from '../ContentLoader' 6 | 7 | jest.useFakeTimers() 8 | 9 | describe('ContentLoader', () => { 10 | describe('when type is custom', () => { 11 | const customWrapper = renderer.create( 12 | 13 | 14 | 15 | 16 | 17 | ).root 18 | 19 | it('should render custom element', () => { 20 | const rect = customWrapper.findAllByType(Rect) 21 | const circle = customWrapper.findAllByType(Circle) 22 | 23 | expect(rect.length).toBe(3) 24 | expect(circle.length).toBe(1) 25 | }) 26 | }) 27 | 28 | describe('Props are propagated', () => { 29 | const noPropsComponent = ShallowRenderer.createRenderer() 30 | noPropsComponent.render( 31 | 32 | 33 | 34 | ) 35 | 36 | const withPropsComponent = ShallowRenderer.createRenderer() 37 | withPropsComponent.render( 38 | } 49 | > 50 | 51 | 52 | ) 53 | 54 | const { props: propsFromEmpty } = noPropsComponent.getRenderOutput() 55 | const { props: propsFromFullField } = withPropsComponent.getRenderOutput() 56 | 57 | it("`speed` is a number and it's used", () => { 58 | // defaultProps 59 | expect(typeof propsFromEmpty.speed).toBe('number') 60 | expect(propsFromEmpty.speed).toBe(1.2) 61 | // custom props 62 | expect(typeof propsFromFullField.speed).toBe('number') 63 | expect(propsFromFullField.speed).toBe(10) 64 | }) 65 | 66 | it("`height` is a number and it's used", () => { 67 | // custom props 68 | expect(typeof propsFromFullField.height).toBe('number') 69 | expect(propsFromFullField.height).toBe(200) 70 | }) 71 | 72 | it("`width` is a number and it's used", () => { 73 | // custom props 74 | expect(typeof propsFromFullField.width).toBe('number') 75 | expect(propsFromFullField.width).toBe(200) 76 | }) 77 | 78 | it("`animate` is a boolean and it's used", () => { 79 | // defaultProps 80 | expect(typeof propsFromEmpty.animate).toBe('boolean') 81 | expect(propsFromEmpty.animate).toBe(true) 82 | // custom props 83 | expect(typeof propsFromFullField.animate).toBe('boolean') 84 | expect(propsFromFullField.animate).toBe(false) 85 | }) 86 | 87 | it("`backgroundColor` is a string and it's used", () => { 88 | // defaultProps 89 | expect(typeof propsFromEmpty.backgroundColor).toBe('string') 90 | expect(propsFromEmpty.backgroundColor).toBe('#f5f6f7') 91 | // custom props 92 | expect(typeof propsFromFullField.backgroundColor).toBe('string') 93 | expect(propsFromFullField.backgroundColor).toBe('#000') 94 | }) 95 | 96 | it("`foregroundColor` is a string and it's used", () => { 97 | // defaultProps 98 | expect(typeof propsFromEmpty.foregroundColor).toBe('string') 99 | expect(propsFromEmpty.foregroundColor).toBe('#eee') 100 | // custom props 101 | expect(typeof propsFromFullField.foregroundColor).toBe('string') 102 | expect(propsFromFullField.foregroundColor).toBe('#fff') 103 | }) 104 | 105 | it("`preserveAspectRatio` is a string and it's used", () => { 106 | // custom props 107 | expect(typeof propsFromFullField.preserveAspectRatio).toBe('string') 108 | expect(propsFromFullField.preserveAspectRatio).toBe('xMaxYMax meet') 109 | }) 110 | 111 | it("`style` is an object and it's used", () => { 112 | // defaultProps 113 | expect(propsFromEmpty.style).toMatchObject({}) 114 | // custom props 115 | expect(propsFromFullField.style).toMatchObject({ marginBottom: 10 }) 116 | }) 117 | 118 | it("`rtl` is a boolean and it's used", () => { 119 | // defaultProps 120 | expect(typeof propsFromEmpty.rtl).toBe('boolean') 121 | expect(propsFromEmpty.rtl).toBe(false) 122 | // custom props 123 | expect(typeof propsFromFullField.rtl).toBe('boolean') 124 | expect(propsFromFullField.rtl).toBe(true) 125 | }) 126 | 127 | it("`beforeMask` is a JSX Element and it's used", () => { 128 | // defaultProps 129 | expect(typeof propsFromEmpty.beforeMask).toBe('object') 130 | expect(propsFromEmpty.beforeMask).toBe(null) 131 | // custom props 132 | expect(typeof propsFromFullField.beforeMask).toBe('object') 133 | expect(propsFromFullField.beforeMask).toEqual() 134 | }) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /src/native/__tests__/Svg.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Svg, { ClipPath, LinearGradient, Stop } from 'react-native-svg' 3 | import * as renderer from 'react-test-renderer' 4 | 5 | import ContentLoader, { Rect } from '..' 6 | 7 | interface IPredicateArgs { 8 | type: any 9 | props: any 10 | } 11 | 12 | jest.useFakeTimers() 13 | 14 | describe('Svg', () => { 15 | const wrapper = renderer.create().root 16 | const predicateRectClipPath = ({ type, props }: IPredicateArgs) => 17 | type === 'Rect' && props.clipPath 18 | const partsOfComponent = { 19 | allLinearGradient: wrapper.findAllByType(LinearGradient), 20 | allRectClipPath: wrapper.findAll(predicateRectClipPath), 21 | allStops: wrapper.findAllByType(Stop), 22 | clipPath: wrapper.findByType(ClipPath), 23 | linearGradient: wrapper.findByType(LinearGradient), 24 | rectClipPath: wrapper.find(predicateRectClipPath), 25 | svg: wrapper.findByType(Svg), 26 | } 27 | 28 | describe('it has basic elements necessary to work ', () => { 29 | it('has a `rect` with `clipPath`', () => { 30 | const { allRectClipPath } = partsOfComponent 31 | 32 | expect(allRectClipPath.length).toBe(1) 33 | }) 34 | 35 | it('has a `linearGradient`', () => { 36 | const { allLinearGradient } = partsOfComponent 37 | 38 | expect(allLinearGradient.length).toBe(1) 39 | }) 40 | 41 | it('has three `stop`', () => { 42 | const { allStops } = partsOfComponent 43 | 44 | expect(allStops.length).toBe(3) 45 | }) 46 | 47 | it('has `stop` inside the `linearGradient`', () => { 48 | const { linearGradient } = partsOfComponent 49 | const stopsIntoLinearGradient = linearGradient.findAllByType(Stop) 50 | 51 | expect(stopsIntoLinearGradient.length).toBe(3) 52 | }) 53 | }) 54 | 55 | describe('unique key', () => { 56 | it('render two components with different ids', () => { 57 | // Wrapper 58 | const { clipPath, linearGradient } = partsOfComponent 59 | 60 | // Another component 61 | const anotherComp = renderer.create() 62 | .root 63 | const anotherClipPath = anotherComp.findByType(ClipPath) 64 | const anotherLinearGradient = anotherComp.findByType(LinearGradient) 65 | 66 | expect(clipPath.props.id).not.toBe(anotherClipPath.props.id) 67 | expect(linearGradient.props.id).not.toBe(anotherLinearGradient.props.id) 68 | }) 69 | 70 | it('clipPath id and rect clipPath url are the same', () => { 71 | const { clipPath, rectClipPath } = partsOfComponent 72 | 73 | expect(rectClipPath.props.clipPath).toBe(`url(#${clipPath.props.id})`) 74 | }) 75 | 76 | it('linearGradient id and rect clipPath fill are the same', () => { 77 | const { linearGradient, rectClipPath } = partsOfComponent 78 | 79 | expect(rectClipPath.props.fill).toBe(`url(#${linearGradient.props.id})`) 80 | }) 81 | }) 82 | 83 | describe('beforeMask', () => { 84 | it('beforeMask is used', () => { 85 | const wrapperWithBeforeMask = renderer.create( 86 | } /> 87 | ).root 88 | 89 | const beforeMask = wrapperWithBeforeMask.findByProps({ 90 | x: '123', 91 | }) 92 | 93 | expect(beforeMask.props.x).toBe('123') 94 | }) 95 | 96 | it('beforeMask should be a JSX Element', () => { 97 | const wrapperWithBeforeMask = renderer.create( 98 | // @ts-ignore 99 | } /> 100 | ).root 101 | 102 | expect(() => { 103 | wrapperWithBeforeMask.findByProps({ 104 | x: '123', 105 | }) 106 | }).toThrow('No instances found with props: {"x":"123"}') 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /src/native/__tests__/__snapshots__/snapshots.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ContentLoader snapshots renders correctly the basic version 1`] = ` 4 | 11 | 19 | 20 | 23 | 30 | 37 | 44 | 51 | 58 | 63 | 64 | 73 | 77 | 81 | 85 | 86 | 87 | 88 | `; 89 | 90 | exports[`ContentLoader snapshots renders correctly with beforeMask 1`] = ` 91 | 95 | 98 | 101 | 109 | 110 | 113 | 114 | 115 | 124 | 128 | 132 | 136 | 137 | 138 | 139 | `; 140 | 141 | exports[`ContentLoader snapshots renders correctly with beforeMask 2`] = ` 142 | 146 | 154 | 155 | 158 | 159 | 160 | 169 | 173 | 177 | 181 | 182 | 183 | 184 | `; 185 | 186 | exports[`ContentLoader snapshots renders correctly with viewBox defined 1`] = ` 187 | 194 | 202 | 203 | 206 | 213 | 220 | 227 | 234 | 241 | 246 | 247 | 256 | 260 | 264 | 268 | 269 | 270 | 271 | `; 272 | 273 | exports[`ContentLoader snapshots renders correctly with viewBox defined and sizes defined too 1`] = ` 274 | 281 | 289 | 290 | 293 | 300 | 307 | 314 | 321 | 328 | 333 | 334 | 343 | 347 | 351 | 355 | 356 | 357 | 358 | `; 359 | 360 | exports[`ContentLoader snapshots renders correctly with viewBox empty 1`] = ` 361 | 368 | 376 | 377 | 380 | 387 | 394 | 401 | 408 | 415 | 420 | 421 | 430 | 434 | 438 | 442 | 443 | 444 | 445 | `; 446 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/BulletListStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native' 2 | 3 | import * as React from 'react' 4 | import * as renderer from 'react-test-renderer' 5 | 6 | import BulletListStyle from '../../presets/BulletListStyle' 7 | 8 | jest.useFakeTimers() 9 | 10 | describe('BulletListStyle', () => { 11 | const wrapper = renderer.create( 12 | 13 | ) 14 | 15 | test('renders correctly', () => { 16 | const tree = wrapper.toJSON() 17 | 18 | expect(tree).toMatchSnapshot() 19 | }) 20 | 21 | test('props are propagated ', () => { 22 | expect(wrapper.root.props.speed).toEqual(20) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/CodeStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native' 2 | 3 | import * as React from 'react' 4 | import * as renderer from 'react-test-renderer' 5 | 6 | import CodeStyle from '../../presets/CodeStyle' 7 | 8 | jest.useFakeTimers() 9 | 10 | describe('CodeStyle', () => { 11 | const wrapper = renderer.create( 12 | 13 | ) 14 | 15 | test('renders correctly', () => { 16 | const tree = wrapper.toJSON() 17 | 18 | expect(tree).toMatchSnapshot() 19 | }) 20 | 21 | test('props are propagated ', () => { 22 | expect(wrapper.root.props.speed).toEqual(20) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/FacebookStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native' 2 | 3 | import * as React from 'react' 4 | import * as renderer from 'react-test-renderer' 5 | 6 | import FacebookStyle from '../../presets/FacebookStyle' 7 | 8 | jest.useFakeTimers() 9 | 10 | describe('FacebookStyle', () => { 11 | const wrapper = renderer.create( 12 | 13 | ) 14 | 15 | test('renders correctly', () => { 16 | const tree = wrapper.toJSON() 17 | 18 | expect(tree).toMatchSnapshot() 19 | }) 20 | 21 | test('props are propagated ', () => { 22 | expect(wrapper.root.props.speed).toEqual(20) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/InstagramStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native' 2 | 3 | import * as React from 'react' 4 | import * as renderer from 'react-test-renderer' 5 | 6 | import InstagramStyle from '../../presets/InstagramStyle' 7 | 8 | jest.useFakeTimers() 9 | 10 | describe('InstagramStyle', () => { 11 | const wrapper = renderer.create( 12 | 13 | ) 14 | 15 | test('renders correctly', () => { 16 | const tree = wrapper.toJSON() 17 | 18 | expect(tree).toMatchSnapshot() 19 | }) 20 | 21 | test('props are propagated ', () => { 22 | expect(wrapper.root.props.speed).toEqual(20) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/ListStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native' 2 | 3 | import * as React from 'react' 4 | import * as renderer from 'react-test-renderer' 5 | 6 | import ListStyle from '../../presets/ListStyle' 7 | 8 | jest.useFakeTimers() 9 | 10 | describe('ListStyle', () => { 11 | const wrapper = renderer.create( 12 | 13 | ) 14 | 15 | test('renders correctly', () => { 16 | const tree = wrapper.toJSON() 17 | 18 | expect(tree).toMatchSnapshot() 19 | }) 20 | 21 | test('props are propagated ', () => { 22 | expect(wrapper.root.props.speed).toEqual(20) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/__snapshots__/BulletListStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BulletListStyle renders correctly 1`] = ` 4 | 11 | 19 | 20 | 23 | 28 | 36 | 41 | 49 | 54 | 62 | 67 | 75 | 76 | 85 | 89 | 93 | 97 | 98 | 99 | 100 | `; 101 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/__snapshots__/CodeStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CodeStyle renders correctly 1`] = ` 4 | 11 | 19 | 20 | 23 | 30 | 37 | 44 | 51 | 58 | 65 | 72 | 79 | 80 | 89 | 93 | 97 | 101 | 102 | 103 | 104 | `; 105 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/__snapshots__/FacebookStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FacebookStyle renders correctly 1`] = ` 4 | 11 | 19 | 20 | 23 | 30 | 37 | 44 | 51 | 58 | 63 | 64 | 73 | 77 | 81 | 85 | 86 | 87 | 88 | `; 89 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/__snapshots__/InstagramStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`InstagramStyle renders correctly 1`] = ` 4 | 11 | 19 | 20 | 23 | 28 | 36 | 44 | 52 | 53 | 62 | 66 | 70 | 74 | 75 | 76 | 77 | `; 78 | -------------------------------------------------------------------------------- /src/native/__tests__/presets/__snapshots__/ListStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ListStyle renders correctly 1`] = ` 4 | 11 | 19 | 20 | 23 | 31 | 39 | 47 | 55 | 63 | 71 | 72 | 81 | 85 | 89 | 93 | 94 | 95 | 96 | `; 97 | -------------------------------------------------------------------------------- /src/native/__tests__/snapshots.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import ContentLoader, { Rect } from '../ContentLoader' 5 | 6 | jest.useFakeTimers() 7 | 8 | describe('ContentLoader snapshots', () => { 9 | test('renders correctly the basic version', () => { 10 | const wrapper = renderer.create( 11 | 12 | ) 13 | const tree = wrapper.toJSON() 14 | 15 | expect(tree).toMatchSnapshot() 16 | }) 17 | 18 | test('renders correctly with viewBox empty', () => { 19 | const wrapper = renderer.create( 20 | 21 | ) 22 | const tree = wrapper.toJSON() 23 | 24 | expect(tree).toMatchSnapshot() 25 | }) 26 | 27 | test('renders correctly with viewBox defined', () => { 28 | const wrapper = renderer.create( 29 | 34 | ) 35 | const tree = wrapper.toJSON() 36 | 37 | expect(tree).toMatchSnapshot() 38 | }) 39 | 40 | test('renders correctly with viewBox defined and sizes defined too', () => { 41 | const wrapper = renderer.create( 42 | 49 | ) 50 | const tree = wrapper.toJSON() 51 | 52 | expect(tree).toMatchSnapshot() 53 | }) 54 | 55 | test('renders correctly with beforeMask', () => { 56 | let wrapper = renderer.create( 57 | 61 | 62 | 63 | 64 | } 65 | > 66 | 67 | 68 | ) 69 | let tree = wrapper.toJSON() 70 | 71 | expect(tree).toMatchSnapshot() 72 | 73 | // with wrong type 74 | wrapper = renderer.create( 75 | // @ts-ignore 76 | }> 77 | 78 | 79 | ) 80 | tree = wrapper.toJSON() 81 | 82 | expect(tree).toMatchSnapshot() 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/native/index.ts: -------------------------------------------------------------------------------- 1 | import { SvgProps } from 'react-native-svg' 2 | 3 | import ContentLoader from './ContentLoader' 4 | 5 | export interface IContentLoaderProps extends SvgProps { 6 | animate?: boolean 7 | backgroundColor?: string 8 | backgroundOpacity?: number 9 | foregroundColor?: string 10 | foregroundOpacity?: number 11 | rtl?: boolean 12 | speed?: number 13 | interval?: number 14 | uniqueKey?: string 15 | beforeMask?: JSX.Element 16 | } 17 | 18 | export { default as Facebook } from './presets/FacebookStyle' 19 | export { default as Instagram } from './presets/InstagramStyle' 20 | export { default as Code } from './presets/CodeStyle' 21 | export { default as List } from './presets/ListStyle' 22 | export { default as BulletList } from './presets/BulletListStyle' 23 | 24 | export { Circle, Rect, Path } from './ContentLoader' 25 | export default ContentLoader 26 | -------------------------------------------------------------------------------- /src/native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-content-loader/native", 3 | "private": true, 4 | "main": "./react-content-loader.native.cjs.js", 5 | "module": "./react-content-loader.native.es.js", 6 | "jsnext:main": "./react-content-loader.native.es.js", 7 | "types": "./native/index.d.ts", 8 | "dependencies": { 9 | "react-native-svg": "9.6.4" 10 | }, 11 | "peerDependencies": { 12 | "react": "^16.0.0", 13 | "react-native": "^0.60.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/native/presets/BulletListStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader, { Circle, Rect } from '../ContentLoader' 5 | 6 | const ReactContentLoaderBulletList: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default ReactContentLoaderBulletList 20 | -------------------------------------------------------------------------------- /src/native/presets/CodeStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader, { Rect } from '../ContentLoader' 5 | 6 | const ReactContentLoaderCode: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default ReactContentLoaderCode 20 | -------------------------------------------------------------------------------- /src/native/presets/FacebookStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader, { Circle, Rect } from '../ContentLoader' 5 | 6 | const ReactContentLoaderFacebook: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | 17 | export default ReactContentLoaderFacebook 18 | -------------------------------------------------------------------------------- /src/native/presets/InstagramStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader, { Circle, Rect } from '../ContentLoader' 5 | 6 | const ReactContentLoaderInstagram: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | 15 | export default ReactContentLoaderInstagram 16 | -------------------------------------------------------------------------------- /src/native/presets/ListStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader, { Rect } from '../ContentLoader' 5 | 6 | const ReactContentLoaderListStyle: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | 17 | export default ReactContentLoaderListStyle 18 | -------------------------------------------------------------------------------- /src/shared/uid.ts: -------------------------------------------------------------------------------- 1 | export default (): string => 2 | Math.random() 3 | .toString(36) 4 | .substring(6) 5 | -------------------------------------------------------------------------------- /src/web/ContentLoader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Facebook, IContentLoaderProps } from '.' 4 | import Svg from './Svg' 5 | 6 | const ContentLoader: React.FC = props => 7 | props.children ? : 8 | 9 | export default ContentLoader 10 | -------------------------------------------------------------------------------- /src/web/Svg.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import uid from '../shared/uid' 4 | import { IContentLoaderProps } from './' 5 | 6 | const SVG: React.FC = ({ 7 | animate = true, 8 | backgroundColor = '#f5f6f7', 9 | backgroundOpacity = 1, 10 | baseUrl = '', 11 | children, 12 | foregroundColor = '#eee', 13 | foregroundOpacity = 1, 14 | gradientRatio = 2, 15 | uniqueKey, 16 | rtl = false, 17 | speed = 1.2, 18 | style = {}, 19 | title = 'Loading...', 20 | beforeMask = null, 21 | ...props 22 | }) => { 23 | const fixedId = uniqueKey || uid() 24 | const idClip = `${fixedId}-diff` 25 | const idGradient = `${fixedId}-animated-diff` 26 | const idAria = `${fixedId}-aria` 27 | 28 | const rtlStyle = rtl ? { transform: 'scaleX(-1)' } : null 29 | const dur = `${speed}s` 30 | 31 | const from = `${gradientRatio * -1} 0` 32 | const to = `${gradientRatio} 0` 33 | 34 | return ( 35 | 41 | {title ? {title} : null} 42 | {beforeMask && React.isValidElement(beforeMask) ? beforeMask : null} 43 | 52 | 53 | 54 | {children} 55 | 56 | 60 | 65 | 66 | 71 | 72 | 77 | 78 | {animate && ( 79 | 86 | )} 87 | 88 | 89 | 90 | ) 91 | } 92 | 93 | export default SVG 94 | -------------------------------------------------------------------------------- /src/web/__tests__/ContentLoader.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | import * as ShallowRenderer from 'react-test-renderer/shallow' 4 | 5 | import ContentLoader from '../ContentLoader' 6 | 7 | describe('ContentLoader', () => { 8 | describe('when type is custom', () => { 9 | const customWrapper = renderer.create( 10 | 11 | 12 | 13 | 14 | 15 | ).root 16 | 17 | it('should render custom element', () => { 18 | const rect = customWrapper.findAllByType('rect') 19 | const circle = customWrapper.findAllByType('circle') 20 | 21 | expect(rect.length).toBe(3) 22 | expect(circle.length).toBe(1) 23 | }) 24 | }) 25 | 26 | describe('Props are propagated', () => { 27 | const noPropsComponent = ShallowRenderer.createRenderer() 28 | noPropsComponent.render( 29 | 30 | 31 | 32 | ) 33 | 34 | const withPropsComponent = ShallowRenderer.createRenderer() 35 | withPropsComponent.render( 36 | } 53 | > 54 | 55 | 56 | ) 57 | 58 | const { props: propsFromFullfield } = withPropsComponent.getRenderOutput() 59 | 60 | it("`speed` is a number and it's used", () => { 61 | // custom props 62 | expect(typeof propsFromFullfield.speed).toBe('number') 63 | expect(propsFromFullfield.speed).toBe(10) 64 | }) 65 | 66 | it("`height` is a number and it's used", () => { 67 | // custom props 68 | expect(typeof propsFromFullfield.height).toBe('number') 69 | expect(propsFromFullfield.height).toBe(200) 70 | }) 71 | 72 | it("`width` is a number and it's used", () => { 73 | // custom props 74 | expect(typeof propsFromFullfield.width).toBe('number') 75 | expect(propsFromFullfield.width).toBe(200) 76 | }) 77 | 78 | it("`gradientRatio` is a number and it's used", () => { 79 | // custom props 80 | expect(typeof propsFromFullfield.gradientRatio).toBe('number') 81 | expect(propsFromFullfield.gradientRatio).toBe(0.5) 82 | }) 83 | 84 | it("`animate` is a boolean and it's used", () => { 85 | // custom props 86 | expect(typeof propsFromFullfield.animate).toBe('boolean') 87 | expect(propsFromFullfield.animate).toBe(false) 88 | }) 89 | 90 | it("`backgroundColor` is a string and it's used", () => { 91 | // custom props 92 | expect(typeof propsFromFullfield.backgroundColor).toBe('string') 93 | expect(propsFromFullfield.backgroundColor).toBe('#000') 94 | }) 95 | 96 | it("`foregroundColor` is a string and it's used", () => { 97 | // custom props 98 | expect(typeof propsFromFullfield.foregroundColor).toBe('string') 99 | expect(propsFromFullfield.foregroundColor).toBe('#fff') 100 | }) 101 | 102 | it("`backgroundOpacity` is a number and it's used", () => { 103 | // custom props 104 | expect(typeof propsFromFullfield.backgroundOpacity).toBe('number') 105 | expect(propsFromFullfield.backgroundOpacity).toBe(0.06) 106 | }) 107 | 108 | it("`foregroundOpacity` is a number and it's used", () => { 109 | // custom props 110 | expect(typeof propsFromFullfield.foregroundOpacity).toBe('number') 111 | expect(propsFromFullfield.foregroundOpacity).toBe(0.12) 112 | }) 113 | 114 | it("`preserveAspectRatio` is a string and it's used", () => { 115 | // custom props 116 | expect(typeof propsFromFullfield.preserveAspectRatio).toBe('string') 117 | expect(propsFromFullfield.preserveAspectRatio).toBe('xMaxYMax meet') 118 | }) 119 | 120 | it("`style` is an object and it's used", () => { 121 | // custom props 122 | expect(propsFromFullfield.style).toMatchObject({ marginBottom: '10px' }) 123 | }) 124 | 125 | it("`rtl` is a boolean and it's used", () => { 126 | // custom props 127 | expect(typeof propsFromFullfield.rtl).toBe('boolean') 128 | expect(propsFromFullfield.rtl).toBe(true) 129 | }) 130 | 131 | it("`title` is a string and it's used", () => { 132 | // custom props 133 | expect(typeof propsFromFullfield.title).toBe('string') 134 | expect(propsFromFullfield.title).toBe('My custom loading title') 135 | }) 136 | 137 | it("`baseUrl` is a string and it's used", () => { 138 | // custom props 139 | expect(typeof propsFromFullfield.baseUrl).toBe('string') 140 | expect(propsFromFullfield.baseUrl).toBe('/mypage') 141 | }) 142 | 143 | it("`uniqueKey` is a string and it's used", () => { 144 | // custom props 145 | expect(typeof propsFromFullfield.uniqueKey).toBe('string') 146 | expect(propsFromFullfield.uniqueKey).toBe('my-id') 147 | }) 148 | 149 | it("`beforeMask` is a JSX Element and it's used", () => { 150 | // custom props 151 | expect(typeof propsFromFullfield.beforeMask).toBe('object') 152 | expect(propsFromFullfield.beforeMask).toEqual() 153 | }) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /src/web/__tests__/Svg.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import Svg from '..' 5 | 6 | interface PredicateArgs { 7 | type: any 8 | props: any 9 | } 10 | 11 | describe('Svg', () => { 12 | const wrapper = renderer.create().root 13 | const predicateRectClipPath = ({ type, props }: PredicateArgs) => 14 | type === 'rect' && props.clipPath 15 | const partsOfComponent = { 16 | allLinearGradient: wrapper.findAllByType('linearGradient'), 17 | allRectClipPath: wrapper.findAll(predicateRectClipPath), 18 | allStops: wrapper.findAllByType('stop'), 19 | clipPath: wrapper.findByType('clipPath'), 20 | linearGradient: wrapper.findByType('linearGradient'), 21 | rectClipPath: wrapper.find(predicateRectClipPath), 22 | svg: wrapper.findByType('svg'), 23 | title: wrapper.findByType('title'), 24 | } 25 | 26 | it('`baseUrl` is used correctly', () => { 27 | const baseUrl = '/page-path' 28 | const wrapperWithBaseUrl = renderer.create().root 29 | 30 | const clipPath = wrapperWithBaseUrl.findByType('clipPath') 31 | const linearGradient = wrapperWithBaseUrl.findByType('linearGradient') 32 | const rectClipPath = wrapperWithBaseUrl.find(predicateRectClipPath) 33 | 34 | expect(rectClipPath.props.clipPath).toBe( 35 | `url(${baseUrl}#${clipPath.props.id})` 36 | ) 37 | expect(rectClipPath.props.style.fill).toBe( 38 | `url(${baseUrl}#${linearGradient.props.id})` 39 | ) 40 | }) 41 | 42 | describe('it has basic elements necessary to work ', () => { 43 | it('has a `rect` with `clipPath`', () => { 44 | const { allRectClipPath } = partsOfComponent 45 | 46 | expect(allRectClipPath.length).toBe(1) 47 | }) 48 | 49 | it('has a `linearGradient`', () => { 50 | const { allLinearGradient } = partsOfComponent 51 | 52 | expect(allLinearGradient.length).toBe(1) 53 | }) 54 | 55 | it('has three `stop`', () => { 56 | const { allStops } = partsOfComponent 57 | 58 | expect(allStops.length).toBe(3) 59 | }) 60 | 61 | it('has `stop` inside the `linearGradient`', () => { 62 | const { linearGradient } = partsOfComponent 63 | const stopsIntoLinearGradient = linearGradient.findAllByType('stop') 64 | 65 | expect(stopsIntoLinearGradient.length).toBe(3) 66 | }) 67 | }) 68 | 69 | describe('unique key', () => { 70 | it('`id` does not generate undefined `id` values for SVG', () => { 71 | const { clipPath, linearGradient } = partsOfComponent 72 | 73 | expect(clipPath.props.id).not.toBe(undefined) 74 | expect(linearGradient.props.id).not.toBe(undefined) 75 | }) 76 | 77 | it('custom `id` is used', () => { 78 | const id = 'my-unique-key' 79 | const wrapperid = renderer.create() 80 | 81 | const clipPath = wrapperid.root.findByType('clipPath') 82 | const linearGradient = wrapperid.root.findByType('linearGradient') 83 | 84 | expect(clipPath.props.id).toBe(`${id}-diff`) 85 | expect(linearGradient.props.id).toBe(`${id}-animated-diff`) 86 | }) 87 | 88 | it('render two components with different ids', () => { 89 | // Wrapper 90 | const { clipPath, linearGradient } = partsOfComponent 91 | 92 | // Another component 93 | const anotherComp = renderer.create().root 94 | const anotherClipPath = anotherComp.findByType('clipPath') 95 | const anotherLinearGradient = anotherComp.findByType('linearGradient') 96 | 97 | expect(clipPath.props.id).not.toBe(anotherClipPath.props.id) 98 | expect(linearGradient.props.id).not.toBe(anotherLinearGradient.props.id) 99 | }) 100 | 101 | it('clipPath id and rect clipPath url are the same', () => { 102 | const { clipPath, rectClipPath } = partsOfComponent 103 | 104 | expect(rectClipPath.props.clipPath).toBe(`url(#${clipPath.props.id})`) 105 | }) 106 | 107 | it('linearGradient id and rect clipPath fill are the same', () => { 108 | const { linearGradient, rectClipPath } = partsOfComponent 109 | 110 | expect(rectClipPath.props.style.fill).toBe( 111 | `url(#${linearGradient.props.id})` 112 | ) 113 | }) 114 | }) 115 | 116 | describe('a11y', () => { 117 | it('svg has aria-labelledby', () => { 118 | const { svg } = partsOfComponent 119 | 120 | expect(typeof svg.props['aria-labelledby']).toBe('string') 121 | expect(svg.props['aria-labelledby'].length).not.toBe(0) 122 | }) 123 | 124 | it('aria-labelledby point to title', () => { 125 | const { svg } = partsOfComponent 126 | const ariaId = svg.props['aria-labelledby'] 127 | 128 | const title = wrapper.findByType('title') 129 | expect(title.props.id).toBe(ariaId) 130 | }) 131 | 132 | it('svg has role', () => { 133 | const { svg } = partsOfComponent 134 | 135 | expect(typeof svg.props['role']).toBe('string') 136 | expect(svg.props['role']).toBe('img') 137 | }) 138 | 139 | it('svg has a title', () => { 140 | const { title } = partsOfComponent 141 | 142 | expect(typeof title.props.children).toBe('string') 143 | expect(title.props.children.length).not.toBe(0) 144 | }) 145 | }) 146 | 147 | describe('beforeMask', () => { 148 | it('beforeMask is used', () => { 149 | const wrapperWithBeforeMask = renderer.create( 150 | } /> 151 | ).root 152 | 153 | const beforeMask = wrapperWithBeforeMask.findByProps({ 154 | role: 'beforeMask', 155 | }) 156 | 157 | expect(beforeMask.props.role).toBe('beforeMask') 158 | }) 159 | 160 | it('beforeMask should be a JSX Element', () => { 161 | const wrapperWithBeforeMask = renderer.create( 162 | // @ts-ignore 163 | } /> 164 | ).root 165 | 166 | expect(() => { 167 | wrapperWithBeforeMask.findByProps({ 168 | role: 'beforeMask', 169 | }) 170 | }).toThrow('No instances found with props: {"role":"beforeMask"}') 171 | }) 172 | }) 173 | }) 174 | -------------------------------------------------------------------------------- /src/web/__tests__/__snapshots__/snapshots.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ContentLoader snapshots renders correctly the basic version 1`] = ` 4 | 10 | 13 | Loading... 14 | 15 | 28 | 29 | 32 | 39 | 46 | 53 | 60 | 67 | 72 | 73 | 77 | 82 | 87 | 92 | 99 | 100 | 101 | 102 | `; 103 | 104 | exports[`ContentLoader snapshots renders correctly with beforeMask 1`] = ` 105 | 110 | 113 | Loading... 114 | 115 | 118 | 121 | 134 | 135 | 138 | 139 | 140 | 144 | 149 | 154 | 159 | 166 | 167 | 168 | 169 | `; 170 | 171 | exports[`ContentLoader snapshots renders correctly with beforeMask 2`] = ` 172 | 177 | 180 | Loading... 181 | 182 | 195 | 196 | 199 | 200 | 201 | 205 | 210 | 215 | 220 | 227 | 228 | 229 | 230 | `; 231 | 232 | exports[`ContentLoader snapshots renders correctly with viewBox defined 1`] = ` 233 | 239 | 242 | Loading... 243 | 244 | 257 | 258 | 261 | 268 | 275 | 282 | 289 | 296 | 301 | 302 | 306 | 311 | 316 | 321 | 328 | 329 | 330 | 331 | `; 332 | 333 | exports[`ContentLoader snapshots renders correctly with viewBox defined and sizes defined too 1`] = ` 334 | 342 | 345 | Loading... 346 | 347 | 360 | 361 | 364 | 371 | 378 | 385 | 392 | 399 | 404 | 405 | 409 | 414 | 419 | 424 | 431 | 432 | 433 | 434 | `; 435 | 436 | exports[`ContentLoader snapshots renders correctly with viewBox empty 1`] = ` 437 | 443 | 446 | Loading... 447 | 448 | 461 | 462 | 465 | 472 | 479 | 486 | 493 | 500 | 505 | 506 | 510 | 515 | 520 | 525 | 532 | 533 | 534 | 535 | `; 536 | -------------------------------------------------------------------------------- /src/web/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import * as React from 'react' 6 | import * as ReactDOM from 'react-dom' 7 | 8 | import ContentLoader from '..' 9 | 10 | describe('index', () => { 11 | const div = document.createElement('div') 12 | div.id = 'root' 13 | document.body.appendChild(div) 14 | 15 | it('renders', () => { 16 | ReactDOM.render(, document.getElementById('root')) 17 | }) 18 | 19 | it('renders a SVG as root element ', () => { 20 | ReactDOM.render(, document.getElementById('root')) 21 | const svgElement = document 22 | .getElementById('root') 23 | .getElementsByTagName('svg') 24 | 25 | expect(svgElement.length).toBe(1) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/BulletListStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import BulletListStyle from '../../presets/BulletListStyle' 5 | 6 | describe('BulletListStyle', () => { 7 | const wrapper = renderer.create( 8 | 9 | ) 10 | 11 | test('renders correctly', () => { 12 | const tree = wrapper.toJSON() 13 | 14 | expect(tree).toMatchSnapshot() 15 | }) 16 | 17 | test('props are propagated ', () => { 18 | expect(wrapper.root.props.speed).toEqual(20) 19 | expect(wrapper.root.props.uniqueKey).toEqual('BulletListStyle') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/CodeStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import CodeStyle from '../../presets/CodeStyle' 5 | 6 | describe('CodeStyle', () => { 7 | const wrapper = renderer.create( 8 | 9 | ) 10 | 11 | test('renders correctly', () => { 12 | const tree = wrapper.toJSON() 13 | 14 | expect(tree).toMatchSnapshot() 15 | }) 16 | 17 | test('props are propagated ', () => { 18 | expect(wrapper.root.props.speed).toEqual(20) 19 | expect(wrapper.root.props.uniqueKey).toEqual('CodeStyle') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/FacebookStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import FacebookStyle from '../../presets/FacebookStyle' 5 | 6 | describe('FacebookStyle', () => { 7 | const wrapper = renderer.create( 8 | 9 | ) 10 | 11 | test('renders correctly', () => { 12 | const tree = wrapper.toJSON() 13 | 14 | expect(tree).toMatchSnapshot() 15 | }) 16 | 17 | test('props are propagated ', () => { 18 | expect(wrapper.root.props.speed).toEqual(20) 19 | expect(wrapper.root.props.uniqueKey).toEqual('FacebookStyle') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/InstagramStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import InstagramStyle from '../../presets/InstagramStyle' 5 | 6 | describe('InstagramStyle', () => { 7 | const wrapper = renderer.create( 8 | 9 | ) 10 | 11 | test('renders correctly', () => { 12 | const tree = wrapper.toJSON() 13 | 14 | expect(tree).toMatchSnapshot() 15 | }) 16 | 17 | test('props are propagated ', () => { 18 | expect(wrapper.root.props.speed).toEqual(20) 19 | expect(wrapper.root.props.uniqueKey).toEqual('InstagramStyle') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/ListStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import ListStyle from '../../presets/ListStyle' 5 | 6 | describe('ListStyle', () => { 7 | const wrapper = renderer.create( 8 | 9 | ) 10 | 11 | test('renders correctly', () => { 12 | const tree = wrapper.toJSON() 13 | 14 | expect(tree).toMatchSnapshot() 15 | }) 16 | 17 | test('props are propagated ', () => { 18 | expect(wrapper.root.props.speed).toEqual(20) 19 | expect(wrapper.root.props.uniqueKey).toEqual('ListStyle') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/__snapshots__/BulletListStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BulletListStyle renders correctly 1`] = ` 4 | 10 | 13 | Loading... 14 | 15 | 28 | 29 | 32 | 37 | 45 | 50 | 58 | 63 | 71 | 76 | 84 | 85 | 89 | 94 | 99 | 104 | 111 | 112 | 113 | 114 | `; 115 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/__snapshots__/CodeStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CodeStyle renders correctly 1`] = ` 4 | 10 | 13 | Loading... 14 | 15 | 28 | 29 | 32 | 39 | 46 | 53 | 60 | 67 | 74 | 81 | 88 | 89 | 93 | 98 | 103 | 108 | 115 | 116 | 117 | 118 | `; 119 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/__snapshots__/FacebookStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FacebookStyle renders correctly 1`] = ` 4 | 10 | 13 | Loading... 14 | 15 | 28 | 29 | 32 | 39 | 46 | 53 | 60 | 67 | 72 | 73 | 77 | 82 | 87 | 92 | 99 | 100 | 101 | 102 | `; 103 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/__snapshots__/InstagramStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`InstagramStyle renders correctly 1`] = ` 4 | 10 | 13 | Loading... 14 | 15 | 28 | 29 | 32 | 37 | 45 | 53 | 61 | 62 | 66 | 71 | 76 | 81 | 88 | 89 | 90 | 91 | `; 92 | -------------------------------------------------------------------------------- /src/web/__tests__/presets/__snapshots__/ListStyle.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ListStyle renders correctly 1`] = ` 4 | 10 | 13 | Loading... 14 | 15 | 28 | 29 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 81 | 85 | 90 | 95 | 100 | 107 | 108 | 109 | 110 | `; 111 | -------------------------------------------------------------------------------- /src/web/__tests__/snapshots.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | 4 | import ContentLoader from '../ContentLoader' 5 | 6 | describe('ContentLoader snapshots', () => { 7 | test('renders correctly the basic version', () => { 8 | const wrapper = renderer.create() 9 | const tree = wrapper.toJSON() 10 | 11 | expect(tree).toMatchSnapshot() 12 | }) 13 | 14 | test('renders correctly with viewBox empty', () => { 15 | const wrapper = renderer.create( 16 | 17 | ) 18 | const tree = wrapper.toJSON() 19 | 20 | expect(tree).toMatchSnapshot() 21 | }) 22 | 23 | test('renders correctly with viewBox defined', () => { 24 | const wrapper = renderer.create( 25 | 26 | ) 27 | const tree = wrapper.toJSON() 28 | 29 | expect(tree).toMatchSnapshot() 30 | }) 31 | 32 | test('renders correctly with viewBox defined and sizes defined too', () => { 33 | const wrapper = renderer.create( 34 | 40 | ) 41 | const tree = wrapper.toJSON() 42 | 43 | expect(tree).toMatchSnapshot() 44 | }) 45 | 46 | test('renders correctly with beforeMask', () => { 47 | let wrapper = renderer.create( 48 | 52 | 53 | 54 | 55 | } 56 | > 57 | 58 | 59 | ) 60 | let tree = wrapper.toJSON() 61 | 62 | expect(tree).toMatchSnapshot() 63 | 64 | // with wrong type 65 | wrapper = renderer.create( 66 | // @ts-ignore 67 | }> 68 | 69 | 70 | ) 71 | tree = wrapper.toJSON() 72 | 73 | expect(tree).toMatchSnapshot() 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/web/__tests__/uid.test.tsx: -------------------------------------------------------------------------------- 1 | import uid from '../../shared/uid' 2 | 3 | describe('unique id', () => { 4 | const options = 100 5 | // @ts-ignore To avoid adding polyfill for `fill` becoz fill is ES6 feature and our target is ES5 6 | const ids = new Array(options).fill(' ').map(() => uid()) 7 | // @ts-ignore To avoid adding polyfill for `from` 8 | const unique = Array.from(new Set(ids)) 9 | 10 | it(`should have ${options} diferents ids`, () => { 11 | expect(unique.length).toBe(options) 12 | }) 13 | 14 | it(`return a string`, () => { 15 | expect(typeof uid()).toBe('string') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/web/index.ts: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from 'react' 2 | 3 | import ContentLoader from './ContentLoader' 4 | 5 | export interface IContentLoaderProps extends SVGAttributes { 6 | animate?: boolean 7 | backgroundColor?: string 8 | backgroundOpacity?: number 9 | baseUrl?: string 10 | foregroundColor?: string 11 | foregroundOpacity?: number 12 | gradientRatio?: number 13 | rtl?: boolean 14 | speed?: number 15 | title?: string 16 | uniqueKey?: string 17 | beforeMask?: JSX.Element 18 | } 19 | 20 | export { default as Facebook } from './presets/FacebookStyle' 21 | export { default as Instagram } from './presets/InstagramStyle' 22 | export { default as Code } from './presets/CodeStyle' 23 | export { default as List } from './presets/ListStyle' 24 | export { default as BulletList } from './presets/BulletListStyle' 25 | 26 | export default ContentLoader 27 | -------------------------------------------------------------------------------- /src/web/presets/BulletListStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader from '../ContentLoader' 5 | 6 | const ReactContentLoaderBulletList: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default ReactContentLoaderBulletList 20 | -------------------------------------------------------------------------------- /src/web/presets/CodeStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader from '../ContentLoader' 5 | 6 | const ReactContentLoaderCode: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default ReactContentLoaderCode 20 | -------------------------------------------------------------------------------- /src/web/presets/FacebookStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader from '../ContentLoader' 5 | 6 | const ReactContentLoaderFacebook: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | 17 | export default ReactContentLoaderFacebook 18 | -------------------------------------------------------------------------------- /src/web/presets/InstagramStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader from '../ContentLoader' 5 | 6 | const ReactContentLoaderInstagram: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | 15 | export default ReactContentLoaderInstagram 16 | -------------------------------------------------------------------------------- /src/web/presets/ListStyle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { IContentLoaderProps } from '..' 4 | import ContentLoader from '../ContentLoader' 5 | 6 | const ReactContentLoaderListStyle: React.FC = props => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | 17 | export default ReactContentLoaderListStyle 18 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "./dist/types", 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "jsx": "react", 8 | "lib": ["es6", "dom"], 9 | "noEmit": true, 10 | "noImplicitAny": false, 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "sourceMap": true, 14 | "target": "es5" 15 | }, 16 | "include": ["./src/**/*"], 17 | "exclude": ["node_modules", "src/web/__tests__"] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["dom"] 5 | } 6 | } 7 | --------------------------------------------------------------------------------