├── .eslintrc
├── .github
└── ISSUE_TEMPLATE
│ ├── bug-report-.md
│ └── feature-request-.md
├── .gitignore
├── .npmignore
├── .prettierrc
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── babel.config.js
├── demos
├── color_change.gif
├── direction_change.gif
├── layout_change.gif
└── main.gif
├── package-lock.json
├── package.json
├── setup-test.ts
├── src
├── Constants.ts
├── SkeletonContent.tsx
├── __tests__
│ ├── SkeletonContent.test.tsx
│ └── __snapshots__
│ │ └── SkeletonContent.test.tsx.snap
└── index.ts
└── tsconfig.json
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb-typescript-prettier"],
3 | "parserOptions": {
4 | "project": "./tsconfig.json"
5 | },
6 | "env": {
7 | "jest": true
8 | },
9 | "rules": {
10 | "import/no-extraneous-dependencies": [
11 | "error",
12 | {
13 | "devDependencies": true
14 | }
15 | ],
16 | "react/jsx-props-no-spreading": [
17 | "error",
18 | {
19 | "custom": "ignore"
20 | }
21 | ],
22 | "@typescript-eslint/no-non-null-assertion": "off",
23 | "@typescript-eslint/no-explicit-any": "off",
24 | "@typescript-eslint/naming-convention": [
25 | "error",
26 | {
27 | "selector": "interface",
28 | "format": ["PascalCase"],
29 | "custom": {
30 | "regex": "^I[A-Z]",
31 | "match": true
32 | }
33 | }
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Bug report \U0001F41B"
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: alexZajac
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Smartphone (please complete the following information):**
27 | - Device: [e.g. iPhone6]
28 | - OS: [e.g. iOS8.1]
29 | - PAckage version [e.g. 22]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request ✨
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: alexZajac
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | *.log
3 | npm-debug.log
4 | # Runtime data
5 | tmp
6 | build
7 | dist
8 | # Dependency directory
9 | node_modules
10 | /lib
11 | .jest
12 | .vscode
13 | coverage
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | *.log
3 | npm-debug.log
4 | # Dependency directory
5 | node_modules
6 | # Runtime data
7 | tmp
8 | tsconfig.json
9 | src
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - lts/*
4 | branches:
5 | only:
6 | - master
7 | cache: npm
8 | before_install:
9 | - npm update
10 | install:
11 | - npm install
12 | script:
13 | - npm test -- -u
14 | - npm run coveralls -- -u
15 |
--------------------------------------------------------------------------------
/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
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at dev@alexandrezajac.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to react-native-skeleton-content
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
6 |
7 | #### Table Of Contents
8 |
9 | [Code of Conduct](#code-of-conduct)
10 |
11 | [How Can I Contribute?](#how-can-i-contribute)
12 | * [Reporting Bugs](#reporting-bugs)
13 | * [Suggesting Enhancements](#suggesting-enhancements)
14 | * [Your First Code Contribution](#your-first-code-contribution)
15 | * [Pull Requests](#pull-requests)
16 |
17 | [Styleguides](#styleguides)
18 | * [Git Commit Messages](#git-commit-messages)
19 |
20 | [Additional Notes](#additional-notes)
21 | * [Issue and Pull Request Labels](#issue-and-pull-request-labels)
22 |
23 | ## Code of Conduct
24 |
25 | This project and everyone participating in it is governed by the [react-native-skeleton-content Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [dev@alexandrezajac.com](mailto:dev@alexandrezajac.com).
26 |
27 | ## How Can I Contribute?
28 |
29 | ### Reporting Bugs
30 |
31 | This section guides you through submitting a bug report for react-native-skeleton-content. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.
32 |
33 | When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/alexZajac/react-native-skeleton-content/blob/master/.github/ISSUE_TEMPLATE/bug_report.md), the information it asks for helps us resolve issues faster.
34 |
35 | > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
36 |
37 |
38 | #### How Do I Submit A (Good) Bug Report?
39 |
40 | Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on that repository and provide the following information by filling in [the template](https://github.com/alexZajac/react-native-skeleton-content/blob/master/.github/ISSUE_TEMPLATE/bug_report.md).
41 |
42 | Explain the problem and include additional details to help maintainers reproduce the problem:
43 |
44 | * **Use a clear and descriptive title** for the issue to identify the problem.
45 | * **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**.
46 | * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
47 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
48 | * **Explain which behavior you expected to see instead and why.**
49 | * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
50 | * **If the problem is related to performance or memory**, include a CPU profile capture.
51 | * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
52 |
53 | Provide more context by answering these questions:
54 |
55 | * **Did the problem start happening recently** or was this always a problem?
56 | * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
57 |
58 | Include details about your configuration and environment:
59 |
60 | * **What's the name and version of the OS you're using**?
61 |
62 | ### Suggesting Enhancements
63 |
64 | This section guides you through submitting an enhancement suggestion for react-native-skeleton-content, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:.
65 |
66 | When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://github.com/alexZajac/react-native-skeleton-content/blob/master/.github/ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
67 |
68 | #### How Do I Submit A (Good) Enhancement Suggestion?
69 |
70 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on that repository and provide the following information:
71 |
72 | * **Use a clear and descriptive title** for the issue to identify the suggestion.
73 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
74 | * **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
75 | * **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
76 | * **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of react-native-skeleton-content which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
77 | * **Explain why this enhancement would be useful** to most react-native-skeleton-content users.
78 | * **Specify the name and version of the OS you're using.**
79 |
80 | ### Your First Code Contribution
81 |
82 | Unsure where to begin contributing to react-native-skeleton-content? You can start by looking through these `beginner` and `help-wanted` issues:
83 |
84 | * **Beginner issues** - issues which should only require a few lines of code, and a test or two.
85 | * **Help wanted issues** - issues which should be a bit more involved than `beginner` issues.
86 |
87 | Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
88 |
89 | ### Pull Requests
90 |
91 | The process described here has several goals:
92 |
93 | - Maintain react-native-skeleton-content's quality
94 | - Fix problems that are important to users
95 | - Engage the community in working toward the best possible react-native-skeleton-content
96 | - Enable a sustainable system for react-native-skeleton-content's maintainers to review contributions
97 |
98 | Please follow these steps to have your contribution considered by the maintainers:
99 |
100 | 1. Select the proper [template](https://github.com/alexZajac/react-native-skeleton-content/blob/master/PULL_REQUEST_TEMPLATE.md)
101 | 2. Follow the [styleguides](#styleguides)
102 | 3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing What if the status checks are failing?
If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
103 |
104 | While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
105 |
106 | ## Styleguides
107 |
108 | ### Git Commit Messages
109 |
110 | * Use the present tense ("Add feature" not "Added feature")
111 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
112 | * Limit the first line to 72 characters or less
113 | * Reference issues and pull requests liberally after the first line
114 | * Consider starting the commit message with an applicable emoji:
115 | * :art: `:art:` when improving the format/structure of the code
116 | * :sparkles: `:sparkles:` when implemeting a new feature
117 | * :racehorse: `:racehorse:` when improving performance
118 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks
119 | * :memo: `:memo:` when writing docs
120 | * :bug: `:bug:` when fixing a bug
121 | * :fire: `:fire:` when removing code or files
122 | * :green_heart: `:green_heart:` when fixing the CI build
123 | * :white_check_mark: `:white_check_mark:` when adding tests
124 | * :lock: `:lock:` when dealing with security
125 | * :arrow_up: `:arrow_up:` when upgrading dependencies
126 | * :arrow_down: `:arrow_down:` when downgrading dependencies
127 | * :shirt: `:shirt:` when removing linter warnings
128 |
129 | ## Additional Notes
130 |
131 | ### Issue and Pull Request Labels
132 |
133 | This section lists the labels we use to help us track and manage issues and pull requests.
134 |
135 | [GitHub search](https://help.github.com/articles/searching-issues/) makes it easy to use labels for finding groups of issues or pull requests you're interested in.
136 |
137 | The labels are loosely grouped by their purpose, but it's not required that every issue have a label from every group or that an issue can't have more than one label from the same group.
138 |
139 | #### Type of Issue and Issue State
140 |
141 | | Label name | Description |
142 | | --- | --- |
143 | | `enhancement` | Feature requests. |
144 | | `bug` | Confirmed bugs or reports that are very likely to be bugs. |
145 | | `question` | Questions more than bug reports or feature requests (e.g. how do I do X). |
146 | | `feedback` | General feedback more than bug reports or feature requests. |
147 | | `help-wanted` | The Freact-native-skeleton-content core team would appreciate help from the community in resolving these issues. |
148 | | `beginner` | Less complex issues which would be good first issues to work on for users who want to contribute to react-native-skeleton-content. |
149 | | `more-information-needed` | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). |
150 | | `needs-reproduction` | Likely bugs, but haven't been reliably reproduced. |
151 | | `blocked` | Issues blocked on other issues. |
152 | | `duplicate` | Issues which are duplicates of other issues, i.e. they have been reported before. |
153 | | `wontfix` | The react-native-skeleton-content core team has decided not to fix these issues for now, either because they're working as intended or for some other reason. |
154 | | `invalid` | Issues which aren't valid (e.g. user errors). |
155 |
156 | #### Topic Categories
157 |
158 | | Label name | Description |
159 | | --- | --- |
160 | | `documentation` | Related to any type of documentation. |
161 | | `performance` | Related to performance. |
162 | | `security` | Related to security. |
163 | | `ui` | Related to visual design. |
164 | | `api` | Related to react-native-skeleton-content's API. |
165 | | `uncaught-exception` | Issues about uncaught exceptions. |
166 | | `crash` | Reports of react-native-skeleton-content completely crashing. |
167 | | `encoding` | Related to character encoding. |
168 | | `git` | Related to Git functionality (e.g. problems with gitignore files or with showing the correct file status). |
169 |
170 | #### Pull Request Labels
171 |
172 | | Label name | Description
173 | | --- | --- |
174 | | `work-in-progress` | Pull requests which are still being worked on, more changes will follow. |
175 | | `needs-review` | Pull requests which need code review, and approval from maintainers or Freact-native-skeleton-content core team. |
176 | | `under-review` | Pull requests being reviewed by maintainers or react-native-skeleton-content core team. |
177 | | `requires-changes` | Pull requests which need to be updated based on review comments and then reviewed again. |
178 | | `needs-testing` | Pull requests which need manual testing.|
179 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Alexandre Zajac
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Hello there! 👋
2 | Please follow the steps below to tell us about your contribution:
3 |
4 | 1. Copy the correct template for your contribution
5 | - 🐛 Are you fixing a bug? Copy the template from [there](https://gist.github.com/alexZajac/d82e0ed1d75cc6b06bc747d1b00aafca)
6 | - 📈 Are you improving performance? Copy the template from [there](https://gist.github.com/alexZajac/01b83283abb8993550f4ab8c7f721e5c)
7 | - 💻 Are you changing functionality? Copy the template from [there](https://gist.github.com/alexZajac/b02b7428066eca7b2ea81b608a1a0412)
8 | 2. Replace this text with the contents of the template
9 | 3. Fill in all sections of the template
10 | 4. Click "Create pull request"
11 | 5. Feel free to ping me at [dev@alexandrezajac.com](mailto:dev@alexandrezajac.com)
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## React Native Skeleton Content
2 |
3 |
4 |
5 | > If you are not using expo, please head up to [this page](https://github.com/alexZajac/react-native-skeleton-content-nonexpo) instead.
6 |
7 |
8 |
9 | React native Skeleton Content, a simple yet fully customizable component made to achieve loading animation in a Skeleton-style. Works in both iOS and Android.
10 |
11 | ### New Features
12 |
13 | - The package has been rewritten to Hooks and is using the declarative [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) package for animations
14 | - It now supports nested layouts for children bones, see an example on [this snack](https://snack.expo.io/@alexandrezajac/skeleton-content-demo)
15 | - It finally supports percentages dimensions for bones, for any type of animation!
16 |
17 | [](https://travis-ci.org/alexZajac/react-native-skeleton-content)
18 | [](https://coveralls.io/github/alexZajac/react-native-skeleton-content?branch=master)
19 | [](https://www.npmjs.com/package/react-native-skeleton-content)
20 |
21 | - [React Native Skeleton Content](#react-native-skeleton-content)
22 | - [Installation](#installation)
23 | - [Usage](#usage)
24 | - [Props](#props)
25 | - [Examples](#examples)
26 | - [Playground](#playground)
27 |
28 | ### Installation
29 |
30 | `npm install react-native-skeleton-content`
31 |
32 | ### Usage
33 |
34 | 1. Import react-native-skeleton-content:
35 |
36 | ```javascript
37 | import SkeletonContent from 'react-native-skeleton-content';
38 | ```
39 |
40 | 2. Once you create the SkeletonContent, you have two options:
41 |
42 | - **Child Layout** : The component will figure out the layout of its bones with the dimensions of its direct children.
43 | - **Custom Layout** : You provide a prop `layout` to the component specifying the size of the bones (see the [Examples](#examples) section below). Below is an example with a custom layout. A key prop for each child is optional but highly recommended.
44 |
45 | ```jsx
46 | export default function Placeholder() {
47 | return (
48 |
56 | Your content
57 | Other content
58 |
59 | );
60 | }
61 | ```
62 |
63 | 3. Then simply sync the prop `isLoading` to your state to show/hide the SkeletonContent when the assets/data are available to the user.
64 |
65 | ```jsx
66 | export default function Placeholder () {
67 | const [loading, setLoading] = useState(true);
68 | return (
69 |
72 | {...otherProps}
73 | />
74 | )
75 | }
76 | ```
77 |
78 | ### Props
79 |
80 | | Name | Type | Default | Description |
81 | | ------------------ | ---------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
82 | | isLoading | bool | **required** | Shows the Skeleton bones when true |
83 | | layout | array of objects | [] | A custom layout for the Skeleton bones |
84 | | duration | number | 1200 ms | Duration of one cycle of animation |
85 | | containerStyle | object | flex: 1 | The style applied to the View containing the bones |
86 | | easing | Easing | bezier(0.5, 0, 0.25, 1) | Easing of the bones animation |
87 | | animationType | string | "shiver" | The animation to be used for animating the bones (see demos below) |
88 | | animationDirection | string | "horizontalRight" | Used only for shiver animation, describes the direction and end-point (ex: horizontalRight goes on the x-axis from left to right) |
89 | | boneColor | string | "#E1E9EE" | Color of the bones |
90 | | highlightColor | string | "#F2F8FC" | Color of the highlight of the bones |
91 |
92 | **Note**: The Easing type function is the one provided by [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated), so if you want to change the default you will have to install it as a dependency.
93 |
94 | ### Examples
95 |
96 | See the playground section to experiment :
97 | **1** - Changing the direction of the animation (animationDirection prop) :
98 |
99 |
100 |
101 |
102 |
103 | ```javascript
104 | export default function Placeholder () {
105 | return (
106 |
110 | ...
111 | />
112 | )
113 | }
114 | ```
115 |
116 | **2** - Changing the colors and switching to "pulse" animation (boneColor, highlightColor and animationType prop) :
117 |
118 |
119 |
120 |
121 |
122 | ```jsx
123 | export default function Placeholder () {
124 | return (
125 |
131 | ...
132 | />
133 | )
134 | }
135 | ```
136 |
137 | **3** - Customizing the layout of the bones (layout prop) :
138 |
139 |
140 |
141 |
142 |
143 | ```jsx
144 | export default function Placeholder () {
145 | return (
146 |
157 | ...
158 | />
159 | )
160 | }
161 | ```
162 |
163 | ### Playground
164 |
165 | You can test out the features and different props easily on [**Snack**](https://snack.expo.io/@alexandrezajac/skeleton-content-demo).
166 | Don't hesitate to take contact if anything is unclear !
167 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true)
3 | return {
4 | presets: ['babel-preset-expo', '@babel/preset-typescript'],
5 | }
6 | }
--------------------------------------------------------------------------------
/demos/color_change.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexZajac/react-native-skeleton-content/23fdb0ddcbcadc6af80f6770af62470a4a5054f2/demos/color_change.gif
--------------------------------------------------------------------------------
/demos/direction_change.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexZajac/react-native-skeleton-content/23fdb0ddcbcadc6af80f6770af62470a4a5054f2/demos/direction_change.gif
--------------------------------------------------------------------------------
/demos/layout_change.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexZajac/react-native-skeleton-content/23fdb0ddcbcadc6af80f6770af62470a4a5054f2/demos/layout_change.gif
--------------------------------------------------------------------------------
/demos/main.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexZajac/react-native-skeleton-content/23fdb0ddcbcadc6af80f6770af62470a4a5054f2/demos/main.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-skeleton-content",
3 | "version": "1.0.28",
4 | "description": "A simple and fully customizable React Native component that implements a skeleton-like loader",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "keywords": [
8 | "react native",
9 | "skeleton",
10 | "loader",
11 | "placeholders",
12 | "gradient",
13 | "animation"
14 | ],
15 | "files": [
16 | "lib/**/*"
17 | ],
18 | "scripts": {
19 | "build": "tsc -p tsconfig.json",
20 | "format": "prettier --write src/**/*.{ts,tsx}",
21 | "lint": "eslint --fix src/**/*.{ts,tsx}",
22 | "prepare": "npm run build",
23 | "prepublishOnly": "npm test && npm run lint",
24 | "preversion": "npm run lint",
25 | "version": "npm run format && git add -A src",
26 | "postversion": "git push && git push --tags",
27 | "test": "jest --no-watchman",
28 | "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls"
29 | },
30 | "jest": {
31 | "preset": "jest-expo",
32 | "setupFiles": [
33 | "./setup-test.ts"
34 | ],
35 | "moduleFileExtensions": [
36 | "js",
37 | "jsx",
38 | "json",
39 | "ts",
40 | "tsx"
41 | ],
42 | "transform": {
43 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
44 | },
45 | "testMatch": [
46 | "**/*.test.ts?(x)"
47 | ],
48 | "coveragePathIgnorePatterns": [
49 | "./src/__tests__/"
50 | ]
51 | },
52 | "repository": {
53 | "type": "git",
54 | "url": "git+https://github.com/alexZajac/react-native-skeleton-content.git"
55 | },
56 | "author": "Alexandre Zajac",
57 | "license": "MIT",
58 | "bugs": {
59 | "url": "https://github.com/alexZajac/react-native-skeleton-content/issues"
60 | },
61 | "homepage": "https://github.com/alexZajac/react-native-skeleton-content#readme",
62 | "devDependencies": {
63 | "@babel/preset-typescript": "^7.18.6",
64 | "@types/jest": "^26.0.24",
65 | "@types/react": "18.0.0",
66 | "@types/react-native": "^0.69.5",
67 | "@types/react-test-renderer": "18.0.0",
68 | "babel-preset-expo": "^9.2.0",
69 | "coveralls": "^3.1.1",
70 | "eslint": "8.22.0",
71 | "eslint-config-airbnb-typescript-prettier": "^5.0.0",
72 | "jest": "^26.0.24",
73 | "jest-expo": "^46.0.0",
74 | "prettier": "^2.7.1",
75 | "react": "18.0.0",
76 | "react-native": "^0.69.5",
77 | "react-native-gesture-handler": "^2.5.0",
78 | "react-test-renderer": "18.0.0",
79 | "typescript": "4.4.4"
80 | },
81 | "dependencies": {
82 | "expo-linear-gradient": "^11.4.0",
83 | "react-native-reanimated": "2.10.0",
84 | "react-native-redash": "^16.3.0"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/setup-test.ts:
--------------------------------------------------------------------------------
1 | jest.mock("react-native-reanimated", () =>
2 | jest.requireActual("./node_modules/react-native-reanimated/mock"),
3 | );
4 | jest.mock('react-native-gesture-handler', () => {});
--------------------------------------------------------------------------------
/src/Constants.ts:
--------------------------------------------------------------------------------
1 | import { StyleProp, ViewStyle } from 'react-native';
2 | import Animated, { EasingNode } from 'react-native-reanimated';
3 |
4 | type _animationType = 'none' | 'shiver' | 'pulse' | undefined;
5 | type _animationDirection =
6 | | 'horizontalLeft'
7 | | 'horizontalRight'
8 | | 'verticalTop'
9 | | 'verticalDown'
10 | | 'diagonalDownLeft'
11 | | 'diagonalDownRight'
12 | | 'diagonalTopLeft'
13 | | 'diagonalTopRight'
14 | | undefined;
15 |
16 | export interface ICustomViewStyle extends ViewStyle {
17 | children?: ICustomViewStyle[];
18 | key?: number | string;
19 | }
20 |
21 | export interface ISkeletonContentProps {
22 | isLoading: boolean;
23 | layout?: ICustomViewStyle[];
24 | duration?: number;
25 | containerStyle?: StyleProp;
26 | animationType?: _animationType;
27 | animationDirection?: _animationDirection;
28 | boneColor?: string;
29 | highlightColor?: string;
30 | easing?: Animated.EasingNodeFunction;
31 | children?: any;
32 | }
33 |
34 | export interface IDirection {
35 | x: number;
36 | y: number;
37 | }
38 |
39 | export const DEFAULT_BORDER_RADIUS = 4;
40 | export const DEFAULT_DURATION = 1200;
41 | export const DEFAULT_ANIMATION_TYPE: _animationType = 'shiver';
42 | export const DEFAULT_ANIMATION_DIRECTION: _animationDirection =
43 | 'horizontalRight';
44 | export const DEFAULT_BONE_COLOR = '#E1E9EE';
45 | export const DEFAULT_HIGHLIGHT_COLOR = '#F2F8FC';
46 | export const DEFAULT_EASING: Animated.EasingNodeFunction = EasingNode.bezier(
47 | 0.5,
48 | 0,
49 | 0.25,
50 | 1
51 | );
52 | export const DEFAULT_LOADING = true;
53 |
--------------------------------------------------------------------------------
/src/SkeletonContent.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { LayoutChangeEvent, StyleSheet, View } from 'react-native';
3 | import { LinearGradient } from 'expo-linear-gradient';
4 | import Animated, { interpolateNode } from 'react-native-reanimated';
5 | import {
6 | interpolateColor,
7 | loop,
8 | useValue,
9 | } from 'react-native-redash/lib/module/v1';
10 | import {
11 | ICustomViewStyle,
12 | DEFAULT_ANIMATION_DIRECTION,
13 | DEFAULT_ANIMATION_TYPE,
14 | DEFAULT_BONE_COLOR,
15 | DEFAULT_BORDER_RADIUS,
16 | DEFAULT_EASING,
17 | DEFAULT_DURATION,
18 | DEFAULT_HIGHLIGHT_COLOR,
19 | DEFAULT_LOADING,
20 | ISkeletonContentProps,
21 | IDirection,
22 | } from './Constants';
23 |
24 | const { useCode, set, cond, eq } = Animated;
25 | const { useState, useCallback } = React;
26 |
27 | const styles = StyleSheet.create({
28 | absoluteGradient: {
29 | height: '100%',
30 | position: 'absolute',
31 | width: '100%',
32 | },
33 | container: {
34 | alignItems: 'center',
35 | flex: 1,
36 | justifyContent: 'center',
37 | },
38 | gradientChild: {
39 | flex: 1,
40 | },
41 | });
42 |
43 | const useLayout = () => {
44 | const [size, setSize] = useState({ width: 0, height: 0 });
45 |
46 | const onLayout = useCallback((event: LayoutChangeEvent) => {
47 | const { width, height } = event.nativeEvent.layout;
48 | setSize({ width, height });
49 | }, []);
50 |
51 | return [size, onLayout];
52 | };
53 |
54 | const SkeletonContent: React.FunctionComponent = ({
55 | containerStyle = styles.container,
56 | easing = DEFAULT_EASING,
57 | duration = DEFAULT_DURATION,
58 | layout = [],
59 | animationType = DEFAULT_ANIMATION_TYPE,
60 | animationDirection = DEFAULT_ANIMATION_DIRECTION,
61 | isLoading = DEFAULT_LOADING,
62 | boneColor = DEFAULT_BONE_COLOR,
63 | highlightColor = DEFAULT_HIGHLIGHT_COLOR,
64 | children,
65 | }) => {
66 | const animationValue = useValue(0);
67 | const loadingValue = useValue(isLoading ? 1 : 0);
68 | const shiverValue = useValue(animationType === 'shiver' ? 1 : 0);
69 |
70 | const [componentSize, onLayout] = useLayout();
71 |
72 | useCode(
73 | () =>
74 | cond(eq(loadingValue, 1), [
75 | cond(
76 | eq(shiverValue, 1),
77 | [
78 | set(
79 | animationValue,
80 | loop({
81 | duration,
82 | easing,
83 | })
84 | ),
85 | ],
86 | [
87 | set(
88 | animationValue,
89 | loop({
90 | duration: duration! / 2,
91 | easing,
92 | boomerang: true,
93 | })
94 | ),
95 | ]
96 | ),
97 | ]),
98 | [loadingValue, shiverValue, animationValue]
99 | );
100 |
101 | const getBoneWidth = (boneLayout: ICustomViewStyle): number =>
102 | (typeof boneLayout.width === 'string'
103 | ? componentSize.width
104 | : boneLayout.width) || 0;
105 | const getBoneHeight = (boneLayout: ICustomViewStyle): number =>
106 | (typeof boneLayout.height === 'string'
107 | ? componentSize.height
108 | : boneLayout.height) || 0;
109 |
110 | const getGradientEndDirection = (
111 | boneLayout: ICustomViewStyle
112 | ): IDirection => {
113 | let direction = { x: 0, y: 0 };
114 | if (animationType === 'shiver') {
115 | if (
116 | animationDirection === 'horizontalLeft' ||
117 | animationDirection === 'horizontalRight'
118 | ) {
119 | direction = { x: 1, y: 0 };
120 | } else if (
121 | animationDirection === 'verticalTop' ||
122 | animationDirection === 'verticalDown'
123 | ) {
124 | direction = { x: 0, y: 1 };
125 | } else if (
126 | animationDirection === 'diagonalTopRight' ||
127 | animationDirection === 'diagonalDownRight' ||
128 | animationDirection === 'diagonalDownLeft' ||
129 | animationDirection === 'diagonalTopLeft'
130 | ) {
131 | const boneWidth = getBoneWidth(boneLayout);
132 | const boneHeight = getBoneHeight(boneLayout);
133 | if (boneWidth && boneHeight && boneWidth > boneHeight)
134 | return { x: 0, y: 1 };
135 | return { x: 1, y: 0 };
136 | }
137 | }
138 | return direction;
139 | };
140 |
141 | const getBoneStyles = (boneLayout: ICustomViewStyle): ICustomViewStyle => {
142 | const { backgroundColor, borderRadius } = boneLayout;
143 | const boneWidth = getBoneWidth(boneLayout);
144 | const boneHeight = getBoneHeight(boneLayout);
145 | const boneStyle: ICustomViewStyle = {
146 | width: boneWidth,
147 | height: boneHeight,
148 | borderRadius: borderRadius || DEFAULT_BORDER_RADIUS,
149 | ...boneLayout,
150 | };
151 | if (animationType !== 'pulse') {
152 | boneStyle.overflow = 'hidden';
153 | boneStyle.backgroundColor = backgroundColor || boneColor;
154 | }
155 | if (
156 | animationDirection === 'diagonalDownRight' ||
157 | animationDirection === 'diagonalDownLeft' ||
158 | animationDirection === 'diagonalTopRight' ||
159 | animationDirection === 'diagonalTopLeft'
160 | ) {
161 | boneStyle.justifyContent = 'center';
162 | boneStyle.alignItems = 'center';
163 | }
164 | return boneStyle;
165 | };
166 |
167 | const getGradientSize = (boneLayout: ICustomViewStyle): ICustomViewStyle => {
168 | const boneWidth = getBoneWidth(boneLayout);
169 | const boneHeight = getBoneHeight(boneLayout);
170 | const gradientStyle: ICustomViewStyle = {};
171 | if (
172 | animationDirection === 'diagonalDownRight' ||
173 | animationDirection === 'diagonalDownLeft' ||
174 | animationDirection === 'diagonalTopRight' ||
175 | animationDirection === 'diagonalTopLeft'
176 | ) {
177 | gradientStyle.width = boneWidth;
178 | gradientStyle.height = boneHeight;
179 | if (boneHeight >= boneWidth) gradientStyle.height *= 1.5;
180 | else gradientStyle.width *= 1.5;
181 | }
182 | return gradientStyle;
183 | };
184 |
185 | const getStaticBoneStyles = (
186 | boneLayout: ICustomViewStyle
187 | ): (ICustomViewStyle | { backgroundColor: any })[] => {
188 | const pulseStyles = [
189 | getBoneStyles(boneLayout),
190 | {
191 | backgroundColor: interpolateColor(animationValue, {
192 | inputRange: [0, 1],
193 | outputRange: [boneColor!, highlightColor!],
194 | }),
195 | },
196 | ];
197 | if (animationType === 'none') pulseStyles.pop();
198 | return pulseStyles;
199 | };
200 |
201 | const getPositionRange = (boneLayout: ICustomViewStyle): number[] => {
202 | const outputRange: number[] = [];
203 | // use layout dimensions for percentages (string type)
204 | const boneWidth = getBoneWidth(boneLayout);
205 | const boneHeight = getBoneHeight(boneLayout);
206 |
207 | if (animationDirection === 'horizontalRight') {
208 | outputRange.push(-boneWidth, +boneWidth);
209 | } else if (animationDirection === 'horizontalLeft') {
210 | outputRange.push(+boneWidth, -boneWidth);
211 | } else if (animationDirection === 'verticalDown') {
212 | outputRange.push(-boneHeight, +boneHeight);
213 | } else if (animationDirection === 'verticalTop') {
214 | outputRange.push(+boneHeight, -boneHeight);
215 | }
216 | return outputRange;
217 | };
218 |
219 | const getGradientTransform = (boneLayout: ICustomViewStyle): object => {
220 | let transform = {};
221 | const boneWidth = getBoneWidth(boneLayout);
222 | const boneHeight = getBoneHeight(boneLayout);
223 | if (
224 | animationDirection === 'verticalTop' ||
225 | animationDirection === 'verticalDown' ||
226 | animationDirection === 'horizontalLeft' ||
227 | animationDirection === 'horizontalRight'
228 | ) {
229 | const interpolatedPosition = interpolateNode(animationValue, {
230 | inputRange: [0, 1],
231 | outputRange: getPositionRange(boneLayout),
232 | });
233 | if (
234 | animationDirection === 'verticalTop' ||
235 | animationDirection === 'verticalDown'
236 | ) {
237 | transform = { translateY: interpolatedPosition };
238 | } else {
239 | transform = { translateX: interpolatedPosition };
240 | }
241 | } else if (
242 | animationDirection === 'diagonalDownRight' ||
243 | animationDirection === 'diagonalTopRight' ||
244 | animationDirection === 'diagonalDownLeft' ||
245 | animationDirection === 'diagonalTopLeft'
246 | ) {
247 | const diagonal = Math.sqrt(
248 | boneHeight * boneHeight + boneWidth * boneWidth
249 | );
250 | const mainDimension = Math.max(boneHeight, boneWidth);
251 | const oppositeDimension =
252 | mainDimension === boneWidth ? boneHeight : boneWidth;
253 | const diagonalAngle = Math.acos(mainDimension / diagonal);
254 | let rotateAngle =
255 | animationDirection === 'diagonalDownRight' ||
256 | animationDirection === 'diagonalTopLeft'
257 | ? Math.PI / 2 - diagonalAngle
258 | : Math.PI / 2 + diagonalAngle;
259 | const additionalRotate =
260 | animationDirection === 'diagonalDownRight' ||
261 | animationDirection === 'diagonalTopLeft'
262 | ? 2 * diagonalAngle
263 | : -2 * diagonalAngle;
264 | const distanceFactor = (diagonal + oppositeDimension) / 2;
265 | if (mainDimension === boneWidth && boneWidth !== boneHeight)
266 | rotateAngle += additionalRotate;
267 | const sinComponent = Math.sin(diagonalAngle) * distanceFactor;
268 | const cosComponent = Math.cos(diagonalAngle) * distanceFactor;
269 | let xOutputRange: number[];
270 | let yOutputRange: number[];
271 | if (
272 | animationDirection === 'diagonalDownRight' ||
273 | animationDirection === 'diagonalTopLeft'
274 | ) {
275 | xOutputRange =
276 | animationDirection === 'diagonalDownRight'
277 | ? [-sinComponent, sinComponent]
278 | : [sinComponent, -sinComponent];
279 | yOutputRange =
280 | animationDirection === 'diagonalDownRight'
281 | ? [-cosComponent, cosComponent]
282 | : [cosComponent, -cosComponent];
283 | } else {
284 | xOutputRange =
285 | animationDirection === 'diagonalDownLeft'
286 | ? [-sinComponent, sinComponent]
287 | : [sinComponent, -sinComponent];
288 | yOutputRange =
289 | animationDirection === 'diagonalDownLeft'
290 | ? [cosComponent, -cosComponent]
291 | : [-cosComponent, cosComponent];
292 | if (mainDimension === boneHeight && boneWidth !== boneHeight) {
293 | xOutputRange.reverse();
294 | yOutputRange.reverse();
295 | }
296 | }
297 | let translateX = interpolateNode(animationValue, {
298 | inputRange: [0, 1],
299 | outputRange: xOutputRange,
300 | });
301 | let translateY = interpolateNode(animationValue, {
302 | inputRange: [0, 1],
303 | outputRange: yOutputRange,
304 | });
305 | // swapping the translates if width is the main dim
306 | if (mainDimension === boneWidth)
307 | [translateX, translateY] = [translateY, translateX];
308 | const rotate = `${rotateAngle}rad`;
309 | transform = { translateX, translateY, rotate };
310 | }
311 | return transform;
312 | };
313 |
314 | const getBoneContainer = (
315 | layoutStyle: ICustomViewStyle,
316 | childrenBones: JSX.Element[],
317 | key: number | string
318 | ) => (
319 |
320 | {childrenBones}
321 |
322 | );
323 |
324 | const getStaticBone = (
325 | layoutStyle: ICustomViewStyle,
326 | key: number | string
327 | ): JSX.Element => (
328 |
332 | );
333 |
334 | const getShiverBone = (
335 | layoutStyle: ICustomViewStyle,
336 | key: number | string
337 | ): JSX.Element => {
338 | const animatedStyle: any = {
339 | transform: [getGradientTransform(layoutStyle)],
340 | ...getGradientSize(layoutStyle),
341 | };
342 | return (
343 |
344 |
345 |
351 |
352 |
353 | );
354 | };
355 |
356 | const getBones = (
357 | bonesLayout: ICustomViewStyle[] | undefined,
358 | childrenItems: any,
359 | prefix: string | number = ''
360 | ): JSX.Element[] => {
361 | if (bonesLayout && bonesLayout.length > 0) {
362 | const iterator: number[] = new Array(bonesLayout.length).fill(0);
363 | return iterator.map((_, i) => {
364 | // has a nested layout
365 | if (bonesLayout[i].children && bonesLayout[i].children!.length > 0) {
366 | const containerPrefix = bonesLayout[i].key || `bone_container_${i}`;
367 | const { children: childBones, ...layoutStyle } = bonesLayout[i];
368 | return getBoneContainer(
369 | layoutStyle,
370 | getBones(childBones, [], containerPrefix),
371 | containerPrefix
372 | );
373 | }
374 | if (animationType === 'pulse' || animationType === 'none') {
375 | return getStaticBone(bonesLayout[i], prefix ? `${prefix}_${i}` : i);
376 | }
377 | return getShiverBone(bonesLayout[i], prefix ? `${prefix}_${i}` : i);
378 | });
379 | // no layout, matching children's layout
380 | }
381 | return React.Children.map(childrenItems, (child, i) => {
382 | const styling = child.props.style || {};
383 | if (animationType === 'pulse' || animationType === 'none') {
384 | return getStaticBone(styling, i);
385 | }
386 | return getShiverBone(styling, i);
387 | });
388 | };
389 |
390 | return (
391 |
392 | {isLoading ? getBones(layout!, children) : children}
393 |
394 | );
395 | };
396 |
397 | export default React.memo(SkeletonContent);
398 |
--------------------------------------------------------------------------------
/src/__tests__/SkeletonContent.test.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | import React from 'react';
3 | import Animated from 'react-native-reanimated';
4 |
5 | import { LinearGradient } from 'expo-linear-gradient';
6 | import { create } from 'react-test-renderer';
7 | import SkeletonContent from '../SkeletonContent';
8 | import {
9 | ISkeletonContentProps,
10 | DEFAULT_BONE_COLOR,
11 | DEFAULT_HIGHLIGHT_COLOR,
12 | DEFAULT_BORDER_RADIUS,
13 | } from '../Constants';
14 |
15 | const staticStyles = {
16 | borderRadius: DEFAULT_BORDER_RADIUS,
17 | overflow: 'hidden',
18 | backgroundColor: DEFAULT_BONE_COLOR,
19 | };
20 |
21 | describe('SkeletonComponent test suite', () => {
22 | it('should render empty alone', () => {
23 | const tree = create().toJSON();
24 | expect(tree).toMatchSnapshot();
25 | });
26 |
27 | it('should have the correct layout when loading', () => {
28 | const layout = [
29 | {
30 | width: 240,
31 | height: 100,
32 | marginBottom: 10,
33 | },
34 | {
35 | width: 180,
36 | height: 40,
37 | borderRadius: 20,
38 | backgroundColor: 'grey',
39 | },
40 | ];
41 | const props: ISkeletonContentProps = {
42 | layout,
43 | isLoading: true,
44 | animationType: 'none',
45 | };
46 | const instance = create();
47 | const component = instance.root;
48 | const bones = component.findAllByType(Animated.View);
49 |
50 | // two bones and parent component
51 | expect(bones.length).toEqual(layout.length + 1);
52 | expect(bones[0].props.style).toEqual({
53 | alignItems: 'center',
54 | flex: 1,
55 | justifyContent: 'center',
56 | });
57 | // default props that are not set
58 | expect(bones[1].props.style).toEqual([{ ...layout[0], ...staticStyles }]);
59 | expect(bones[2].props.style).toEqual([
60 | { overflow: 'hidden', ...layout[1] },
61 | ]);
62 | expect(instance.toJSON()).toMatchSnapshot();
63 | });
64 |
65 | it('should render the correct bones for children', () => {
66 | const props: ISkeletonContentProps = {
67 | isLoading: true,
68 | animationType: 'shiver',
69 | };
70 | const w1 = { height: 100, width: 200 };
71 | const w2 = { height: 120, width: 20 };
72 | const w3 = { height: 80, width: 240 };
73 | const children = [w1, w2, w3];
74 | function TestComponent({
75 | isLoading,
76 | animationType,
77 | }: ISkeletonContentProps) {
78 | return (
79 |
80 | {children.map((c) => (
81 |
82 | ))}
83 |
84 | );
85 | }
86 | const instance = create();
87 | let component = instance.root;
88 | // finding children count
89 | let bones = component.findAllByType(LinearGradient);
90 | expect(bones.length).toEqual(children.length);
91 | // finding styles of wrapper views
92 | bones = component.findAllByType(Animated.View);
93 | expect(bones[1].props.style).toEqual({
94 | ...staticStyles,
95 | ...w1,
96 | });
97 | expect(bones[3].props.style).toEqual({
98 | ...staticStyles,
99 | ...w2,
100 | });
101 | expect(bones[5].props.style).toEqual({
102 | ...staticStyles,
103 | ...w3,
104 | });
105 |
106 | // re-update with pulse animation
107 | instance.update();
108 | component = instance.root;
109 | bones = component.findAllByType(Animated.View);
110 | // cannot test interpolated background color
111 | expect(bones[1].props.style).toEqual([
112 | {
113 | ...w1,
114 | borderRadius: DEFAULT_BORDER_RADIUS,
115 | },
116 | { backgroundColor: { ' __value': 4278190080 } },
117 | ]);
118 | expect(bones[2].props.style).toEqual([
119 | {
120 | ...w2,
121 | borderRadius: DEFAULT_BORDER_RADIUS,
122 | },
123 | { backgroundColor: { ' __value': 4278190080 } },
124 | ]);
125 | expect(bones[3].props.style).toEqual([
126 | {
127 | ...w3,
128 | borderRadius: DEFAULT_BORDER_RADIUS,
129 | },
130 | { backgroundColor: { ' __value': 4278190080 } },
131 | ]);
132 | expect(instance.toJSON()).toMatchSnapshot();
133 | });
134 |
135 | it('should have correct props and layout between loading states', () => {
136 | const w1 = { width: 240, height: 100, marginBottom: 10 };
137 | const w2 = { width: 180, height: 40 };
138 | const layout = [w1, w2];
139 | const props: ISkeletonContentProps = {
140 | layout,
141 | isLoading: true,
142 | animationType: 'shiver',
143 | };
144 | const childStyle = { fontSize: 24 };
145 | const instance = create(
146 |
147 |
148 |
149 | );
150 | const component = instance.root;
151 | let bones = component.findAllByType(LinearGradient);
152 | // one animated view child for each bone + parent
153 | expect(bones.length).toEqual(layout.length);
154 | bones = component.findAllByType(Animated.View);
155 | expect(bones[1].props.style).toEqual({
156 | ...staticStyles,
157 | ...w1,
158 | });
159 | expect(bones[3].props.style).toEqual({
160 | ...staticStyles,
161 | ...w2,
162 | });
163 | let children = component.findAllByType(Text);
164 | // no child since it's loading
165 | expect(children.length).toEqual(0);
166 |
167 | // update props
168 | instance.update(
169 |
170 |
171 |
172 | );
173 |
174 | bones = instance.root.findAllByType(LinearGradient);
175 | expect(bones.length).toEqual(0);
176 |
177 | children = instance.root.findAllByType(Text);
178 | expect(children.length).toEqual(1);
179 | expect(children[0].props.style).toEqual(childStyle);
180 |
181 | // re-update to loading state
182 | instance.update(
183 |
184 |
185 |
186 | );
187 |
188 | bones = instance.root.findAllByType(LinearGradient);
189 | expect(bones.length).toEqual(layout.length);
190 | bones = component.findAllByType(Animated.View);
191 | expect(bones[1].props.style).toEqual({
192 | ...staticStyles,
193 | ...w1,
194 | });
195 | expect(bones[3].props.style).toEqual({
196 | ...staticStyles,
197 | ...w2,
198 | });
199 | children = instance.root.findAllByType(Text);
200 | // no child since it's loading
201 | expect(children.length).toEqual(0);
202 |
203 | // snapshot
204 | expect(instance.toJSON()).toMatchSnapshot();
205 | });
206 |
207 | it('should support nested layouts', () => {
208 | const layout: any = [
209 | {
210 | flexDirection: 'row',
211 | width: 320,
212 | height: 300,
213 | children: [
214 | {
215 | width: 200,
216 | height: 120,
217 | },
218 | {
219 | width: 180,
220 | height: 100,
221 | },
222 | ],
223 | },
224 | {
225 | width: 180,
226 | height: 40,
227 | borderRadius: 20,
228 | backgroundColor: 'grey',
229 | },
230 | ];
231 | const props: ISkeletonContentProps = {
232 | layout,
233 | isLoading: true,
234 | animationType: 'shiver',
235 | };
236 | const instance = create();
237 | const component = instance.root;
238 | let bones = component.findAllByType(LinearGradient);
239 | // three overall bones
240 | expect(bones.length).toEqual(3);
241 | bones = component.findAllByType(Animated.View);
242 |
243 | expect(bones[1].props.style).toEqual({
244 | flexDirection: 'row',
245 | width: 320,
246 | height: 300,
247 | });
248 | // testing that styles for nested layout and last child persist
249 | expect(bones[2].props.style).toEqual({
250 | ...staticStyles,
251 | ...layout[0].children[0],
252 | });
253 | expect(bones[4].props.style).toEqual({
254 | ...staticStyles,
255 | ...layout[0].children[1],
256 | });
257 | expect(bones[6].props.style).toEqual({
258 | ...staticStyles,
259 | ...layout[1],
260 | });
261 | expect(instance.toJSON()).toMatchSnapshot();
262 | });
263 |
264 | it('should support percentage for child size', () => {
265 | const parentHeight = 300;
266 | const parentWidth = 320;
267 | const containerStyle = {
268 | width: parentWidth,
269 | height: parentHeight,
270 | };
271 | const layout = [
272 | {
273 | width: '20%',
274 | height: '50%',
275 | borderRadius: 20,
276 | backgroundColor: 'grey',
277 | },
278 | {
279 | width: '50%',
280 | height: '10%',
281 | borderRadius: 10,
282 | },
283 | ];
284 | const props: ISkeletonContentProps = {
285 | layout,
286 | isLoading: true,
287 | animationType: 'shiver',
288 | containerStyle,
289 | };
290 | const instance = create();
291 | const component = instance.root;
292 | let bones = component.findAllByType(LinearGradient);
293 |
294 | expect(bones.length).toEqual(layout.length);
295 | // get parent
296 | bones = component.findAllByType(Animated.View);
297 | // testing that styles of childs corresponds to percentages
298 | expect(bones[1].props.style).toEqual({
299 | ...staticStyles,
300 | ...layout[0],
301 | });
302 | expect(bones[3].props.style).toEqual({
303 | ...staticStyles,
304 | ...layout[1],
305 | });
306 | expect(instance.toJSON()).toMatchSnapshot();
307 | });
308 |
309 | it('should have the correct gradient properties', () => {
310 | let customProps: ISkeletonContentProps = {
311 | layout: [
312 | {
313 | width: 240,
314 | height: 100,
315 | marginBottom: 10,
316 | },
317 | ],
318 | isLoading: true,
319 | animationDirection: 'diagonalDownLeft',
320 | };
321 | function TestComponent(props: ISkeletonContentProps) {
322 | return (
323 |
324 |
325 |
326 | );
327 | }
328 | const component = create();
329 | let gradient = component.root.findByType(LinearGradient);
330 | expect(gradient).toBeDefined();
331 | expect(gradient.props.start).toEqual({ x: 0, y: 0 });
332 | expect(gradient.props.end).toEqual({ x: 0, y: 1 });
333 |
334 | // change layout on diagonal component
335 | customProps = {
336 | ...customProps,
337 | layout: [
338 | {
339 | width: 240,
340 | height: 300,
341 | },
342 | ],
343 | };
344 | component.update(
345 |
346 |
347 |
348 | );
349 |
350 | gradient = component.root.findByType(LinearGradient);
351 | expect(gradient).toBeDefined();
352 | expect(gradient.props.start).toEqual({ x: 0, y: 0 });
353 | expect(gradient.props.end).toEqual({ x: 1, y: 0 });
354 |
355 | component.update(
356 |
357 |
358 |
359 | );
360 |
361 | gradient = component.root.findByType(LinearGradient);
362 | expect(gradient).toBeDefined();
363 | expect(gradient.props.start).toEqual({ x: 0, y: 0 });
364 | expect(gradient.props.end).toEqual({ x: 1, y: 0 });
365 |
366 | component.update(
367 |
368 |
369 |
370 | );
371 |
372 | gradient = component.root.findByType(LinearGradient);
373 | expect(gradient).toBeDefined();
374 | expect(gradient.props.start).toEqual({ x: 0, y: 0 });
375 | expect(gradient.props.end).toEqual({ x: 0, y: 1 });
376 |
377 | component.update(
378 |
379 |
380 |
381 | );
382 |
383 | gradient = component.root.findByType(LinearGradient);
384 | expect(gradient).toBeDefined();
385 | expect(gradient.props.start).toEqual({ x: 0, y: 0 });
386 | expect(gradient.props.end).toEqual({ x: 0, y: 1 });
387 |
388 | component.update(
389 |
390 |
391 |
392 | );
393 |
394 | gradient = component.root.findByType(LinearGradient);
395 | expect(gradient).toBeDefined();
396 | expect(gradient.props.start).toEqual({ x: 0, y: 0 });
397 | expect(gradient.props.end).toEqual({ x: 1, y: 0 });
398 |
399 | component.update(
400 |
401 |
402 |
403 | );
404 |
405 | gradient = component.root.findByType(LinearGradient);
406 | expect(gradient).toBeDefined();
407 | expect(gradient.props.start).toEqual({ x: 0, y: 0 });
408 | expect(gradient.props.end).toEqual({ x: 1, y: 0 });
409 |
410 | expect(gradient.props.colors).toEqual([
411 | DEFAULT_BONE_COLOR,
412 | DEFAULT_HIGHLIGHT_COLOR,
413 | DEFAULT_BONE_COLOR,
414 | ]);
415 | });
416 | });
417 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/SkeletonContent.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SkeletonComponent test suite should have correct props and layout between loading states 1`] = `
4 |
14 |
26 |
44 |
69 |
70 |
71 |
82 |
100 |
125 |
126 |
127 |
128 | `;
129 |
130 | exports[`SkeletonComponent test suite should have the correct layout when loading 1`] = `
131 |
141 |
155 |
168 |
169 | `;
170 |
171 | exports[`SkeletonComponent test suite should render empty alone 1`] = `
172 |
182 | `;
183 |
184 | exports[`SkeletonComponent test suite should render the correct bones for children 1`] = `
185 |
195 |
211 |
227 |
243 |
244 | `;
245 |
246 | exports[`SkeletonComponent test suite should support nested layouts 1`] = `
247 |
257 |
266 |
277 |
295 |
320 |
321 |
322 |
333 |
351 |
376 |
377 |
378 |
379 |
390 |
408 |
433 |
434 |
435 |
436 | `;
437 |
438 | exports[`SkeletonComponent test suite should support percentage for child size 1`] = `
439 |
448 |
459 |
477 |
502 |
503 |
504 |
515 |
533 |
558 |
559 |
560 |
561 | `;
562 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import SkeletonContent from './SkeletonContent';
2 |
3 | export default SkeletonContent;
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "skipLibCheck": true,
5 | "module": "commonjs",
6 | "jsx": "react-native",
7 | "outDir": "./lib",
8 | "strict": true,
9 | "declaration": true,
10 | "moduleResolution": "node",
11 | "baseUrl": "./",
12 | "allowSyntheticDefaultImports": true,
13 | "esModuleInterop": true,
14 | "paths": {
15 | "react-native-redash/lib/module/v1": [
16 | "./node_modules/react-native-redash/lib/typescript/v1/index.d.ts"
17 | ]
18 | }
19 | },
20 | "include": ["./src"],
21 | "exclude": [
22 | "**/node_modules/**",
23 | "**/lib/**",
24 | "**/coverage/**",
25 | "**/demos/**"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------