feat(codebase): react native support (#153)

re #148, re #137, re #106, re #89, re #16, re #6

* build(rollup): native bundle

* feat(native): stylized components

* build(typescript): react native check

* feat(typing): improves

* build(tsconfig): by environment

* test(web native): setup

* test(native): support

* docs(readme): native documentation
This commit is contained in:
Danilo Woznica
2019-09-25 13:29:46 +01:00
parent 578ee06ede
commit a04f788057
54 changed files with 26503 additions and 220 deletions

6
.eslintrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": "react-app",
"rules": {
"@typescript-eslint/no-angle-bracket-type-assertion": 0 // I don't know wtf it is
}
}

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ yarn-error.log
node_modules
dist
coverage
/native
.docz/
.rpt2_cache
settings.json

View File

@@ -1,16 +1,14 @@
language: node_js
sudo: false
node_js:
- 9
- 10
cache:
directories:
- node_modules
install:
- npm install
- npm install -g codecov
script:
- npm run test
- npm run build
after_success:
- npm run coverage
- npm run release

206
README.md
View File

@@ -5,26 +5,28 @@
<img width="400" alt="Example's react-content-loader" src="https://user-images.githubusercontent.com/4838076/34308760-ec55df82-e735-11e7-843b-2e311fa7b7d0.gif" />
</p>
SVG-Powered component to easily create placeholder loadings (like Facebook's cards loading).
SVG-Powered component to easily create placeholder loadings (like Facebook's cards loading).
## Features
- :gear: **Customizable:** Feel free to change the colors, speed, sizes and even **RTL**;
- :ok_hand: **Plug and play:** with many presets to use, see the [examples](#examples);
- :pencil2: **DIY:** use the [create-content-loader](https://danilowoz.github.io/create-content-loader/) to create your own custom loaders easily;
- ⚛️ **Lightweight:** only **1.4kB** gzipped and **0 dependencies**;
- :gear: **Customizable:** Feel free to change the colors, speed, sizes and even **RTL**;
- :ok_hand: **Plug and play:** with many presets to use, see the [examples](#examples);
- :pencil2: **DIY:** use the [create-content-loader](https://danilowoz.github.io/create-content-loader/) to create your own custom loaders easily;
- 📱 **React Native support**: same API, as same powerful features;
- ⚛️ **Really lightweight:** less than **2kB** and **0 dependencies** for web version;
## Index
- [Getting Started](#getting-started)
- [Getting Started](#getting-started)
- [Usage](#usage)
- [Native](#native)
- [Options](#options)
- [Examples](#examples)
- [Similar packages](#similar-packages)
- [Similar packages](#similar-packages)
- [Development](#development)
- [Known Issues](#known-issues)
- [Known Issues](#known-issues)
## Getting Started
## Getting Started
```sh
npm i react-content-loader --save
@@ -34,13 +36,13 @@ npm i react-content-loader --save
yarn add react-content-loader
```
CDN from [JSDELIVR](https://www.jsdelivr.com/package/npm/react-content-loader)
CDN from [JSDELIVR](https://www.jsdelivr.com/package/npm/react-content-loader)
## Usage
There are two ways to use it:
There are two ways to use it:
**1. Presets, see the [examples](#examples):**
**1. Presets, see the [examples](#examples):**
```jsx
import ContentLoader, { Facebook } from 'react-content-loader'
@@ -49,12 +51,12 @@ const MyLoader = () => <ContentLoader />
const MyFacebookLoader = () => <Facebook />
```
**2. Custom mode, see the [online tool](https://danilowoz.github.io/create-react-content-loader/)**
**2. Custom mode, see the [online tool](https://danilowoz.github.io/create-react-content-loader/)**
```jsx
const MyLoader = () => (
<ContentLoader>
{/* Only SVG shapes */}
{/* Only SVG shapes */}    
<rect x="0" y="0" rx="5" ry="5" width="70" height="70" />
<rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
<rect x="80" y="40" rx="3" ry="3" width="250" height="10" />
@@ -65,79 +67,110 @@ const MyLoader = () => (
**Still not clear?** Take a look at this working example at [codesandbox.io](https://codesandbox.io/s/moojk887z9)
Or try the components editable demo hands-on and install it from [bit.dev](https://bit.dev/danilowoz/react-content-loader)
## Native
`react-content-loader` can be used with React Native in the same way as web version with the same import:
**1. Presets, see the [examples](#examples):**
```jsx
import ContentLoader, { Facebook } from 'react-content-loader/native'
const MyLoader = () => <ContentLoader />
const MyFacebookLoader = () => <Facebook />
```
**2. Custom mode**
**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:
```jsx
import ContentLoader, { Rect, Circle } from 'react-content-loader/native'
const MyLoader = () => (
<ContentLoader>
<Circle cx="30" cy="30" r="30" />
<Rect x="0" y="0" rx="5" ry="5" width="70" height="70" />
<Rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
<Rect x="80" y="40" rx="3" ry="3" width="250" height="10" />
</ContentLoader>
)
```
## Options
#### **`animate?: boolean`**
Defaults to `true`. Opt-out of animations with `false`
Defaults to `true`. Opt-out of animations with `false`
#### **`ariaLabel? string | boolean`**
#### **`ariaLabel? string | boolean`** - _Web only_
Defaults to `Loading interface...`. It's used to describe what element it is. Use `false` to remove.
Defaults to `Loading interface...`. It's used to describe what element it is. Use `false` to remove.
#### **`baseUrl? string`**
#### **`baseUrl? string`** - _Web only_
Required if you're using `<base url="/" />` in your `<head/>`. Defaults to an empty string. This prop is common used as: `<ContentLoader baseUrl={window.location.pathname} />` which will fill the SVG attribute with the relative path. Related [#93](https://github.com/danilowoz/react-content-loader/issues/93).
Required if you're using `<base url="/" />` document `<head/>`. 
Defaults to an empty string. This prop is common used as: `<ContentLoader baseUrl={window.location.pathname} />` which will fill the SVG attribute with the relative path. Related [#93](https://github.com/danilowoz/react-content-loader/issues/93).
#### **`speed?: number`**
Defaults to `2`. Animation speed in seconds.
Defaults to `2`. Animation speed in seconds.
#### **`interval?: number`**
#### **`interval?: number`** - _Web only_
Defaults to `0.25`. Interval of time between runs of the animation, as a fraction of the animation speed.
Defaults to `0.25`. Interval of time between runs of the animation, as a fraction of the animation speed.
#### **`className? string`**
Defaults to an empty string. The classname will be set in the `<svg />` element.
Defaults to an empty string. The classname will be set in the `<svg />` element.
#### **`width? number`**
Defaults to `400`. It will be set in the viewbox attr in the `<svg />`.
Defaults to `400`. It will be set in the viewbox attr in the `<svg />`.
#### **`height? number`**
Defaults to `130`. It will be set in the viewbox attr in the `<svg />`.
Defaults to `130`. It will be set in the viewbox attr in the `<svg />`.
#### **`gradientRatio? number`**
#### **`gradientRatio? number`** - _Web only_
Defaults to `2`. Width of the animated gradient as a fraction of the viewbox width.
Defaults to `2`. Width of the animated gradient as a fraction of the viewbox width.
#### **`rtl? boolean`**
Defaults to `false`. Content right-to-left.
Defaults to `false`. Content right-to-left.
#### **`preserveAspectRatio?: string`**
Defaults to `xMidYMid meet`. Aspect ratio option of `<svg/>`. See the available options [here](https://github.com/danilowoz/react-content-loader/blob/master/src/interface.ts#L7).
Defaults to `xMidYMid meet`. Aspect ratio option of `<svg/>`. See the available options [here](https://github.com/danilowoz/react-content-loader/blob/master/src/interface.ts#L7).
#### **`primaryColor?: string`**
Defaults to `#f3f3f3` which is used as background of animation.
Defaults to `#f3f3f3` which is used as background of animation.
#### **`secondaryColor?: string`**
Defaults to `#ecebeb` which is used as the placeholder/layer of animation.
Defaults to `#ecebeb` which is used as the placeholder/layer of animation.
#### **`primaryOpacity?: string`**
#### **`primaryOpacity?: string`** - _Web only_
Defaults to `1`. Background opacity (0 = transparent, 1 = opaque) used to solve a issue in [Safari](#safari--ios)
Defaults to `1`. Background opacity (0 = transparent, 1 = opaque) used to solve a issue in [Safari](#safari--ios)
#### **`secondaryOpacity?: string`**
#### **`secondaryOpacity?: string`** - _Web only_
Defaults to `1`. Animation opacity (0 = transparent, 1 = opaque) used to solve a issue in [Safari](#safari--ios)
Defaults to `1`. Animation opacity (0 = transparent, 1 = opaque) used to solve a issue in [Safari](#safari--ios)
#### **`style?: React.CSSProperties`**
Defaults to an empty object.
Defaults to an empty object.
#### **`uniquekey?: string`**
#### **`uniquekey?: string`** - _Web only_
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).
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).
## Examples
##### Facebook Style
##### Facebook Style
```jsx
import { Facebook } from 'react-content-loader'
@@ -145,9 +178,9 @@ import { Facebook } from 'react-content-loader'
const MyFacebookLoader = () => <Facebook />
```
![Facebook Style](https://user-images.githubusercontent.com/4838076/34308760-ec55df82-e735-11e7-843b-2e311fa7b7d0.gif)
![Facebook Style](https://user-images.githubusercontent.com/4838076/34308760-ec55df82-e735-11e7-843b-2e311fa7b7d0.gif)
##### Instagram Style
##### Instagram Style
```jsx
import { Instagram } from 'react-content-loader'
@@ -155,9 +188,9 @@ import { Instagram } from 'react-content-loader'
const MyInstagramLoader = () => <Instagram />
```
![Instagram Style](https://cloud.githubusercontent.com/assets/4838076/22555637/749f9e26-e94b-11e6-84ff-83cd415c1eb9.gif)
![Instagram Style](https://cloud.githubusercontent.com/assets/4838076/22555637/749f9e26-e94b-11e6-84ff-83cd415c1eb9.gif)
##### Code Style
##### Code Style
```jsx
import { Code } from 'react-content-loader'
@@ -165,9 +198,9 @@ import { Code } from 'react-content-loader'
const MyCodeLoader = () => <Code />
```
![Code Style](https://cloud.githubusercontent.com/assets/4838076/22555473/effa54c2-e94a-11e6-9128-9b608bcc69d9.gif)
![Code Style](https://cloud.githubusercontent.com/assets/4838076/22555473/effa54c2-e94a-11e6-9128-9b608bcc69d9.gif)
##### List Style
##### List Style
```jsx
import { List } from 'react-content-loader'
@@ -175,9 +208,9 @@ import { List } from 'react-content-loader'
const MyListLoader = () => <List />
```
![List Style](https://user-images.githubusercontent.com/4838076/36352948-b8931430-149e-11e8-9f4b-3f00bc444a6d.gif)
![List Style](https://user-images.githubusercontent.com/4838076/36352948-b8931430-149e-11e8-9f4b-3f00bc444a6d.gif)
##### Bullet list Style
##### Bullet list Style
```jsx
import { BulletList } from 'react-content-loader'
@@ -185,12 +218,12 @@ import { BulletList } from 'react-content-loader'
const MyBulletListLoader = () => <BulletList />
```
![Bullet list Style](https://user-images.githubusercontent.com/4838076/31998372-59817bac-b96e-11e7-8ef8-07f61670ee18.gif)
![Bullet list Style](https://user-images.githubusercontent.com/4838076/31998372-59817bac-b96e-11e7-8ef8-07f61670ee18.gif)
### Custom Style
### Custom Style
For the custom mode, use the
[online tool](https://danilowoz.github.io/create-react-content-loader/).
For the custom mode, use the
[online tool](https://danilowoz.github.io/create-react-content-loader/).
```jsx
const MyLoader = () => (
@@ -200,10 +233,14 @@ const MyLoader = () => (
primaryColor={'#333'}
secondaryColor={'#999'}
>
{/* Only SVG shapes */}
    {/* Only SVG shapes */}
    
<rect x="0" y="0" rx="5" ry="5" width="70" height="70" />
    
<rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
    
<rect x="80" y="40" rx="3" ry="3" width="250" height="10" />
  
</ContentLoader>
)
```
@@ -212,69 +249,70 @@ const MyLoader = () => (
![Custom](https://user-images.githubusercontent.com/4838076/36352947-b87019a8-149e-11e8-99ba-c71c2bcf8733.gif)
## Similar packages
## Similar packages
- 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);
- 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);
- [Preact](https://github.com/bonitasoft/preact-content-loader);
- Vue.js: [vue-content-loading](https://github.com/LucasLeandro1204/vue-content-loading), [vue-content-loader](https://github.com/egoist/vue-content-loader);
- Angular: [ngx-content-loading](https://github.com/Gbuomprisco/ngx-content-loading), [ngx-content-loader](https://github.com/NetanelBasal/ngx-content-loader).
- Vue.js: [vue-content-loading](https://github.com/LucasLeandro1204/vue-content-loading), [vue-content-loader](https://github.com/egoist/vue-content-loader);
- Angular: [ngx-content-loading](https://github.com/Gbuomprisco/ngx-content-loading), [ngx-content-loader](https://github.com/NetanelBasal/ngx-content-loader).
## Development
Fork the repo then clone it
Fork the repo then clone it
`$ git clone git@github.com:YourUsername/react-content-loader.git && cd react-content-loader`
```
$ git clone git@github.com:YourUsername/react-content-loader.git && cd react-content-loader
```
`$ yarn`: Install the dependencies;
`$ npm i`: Install the dependencies;
`$ yarn build`: Build to production;
`$ npm run build`: Build to production;
`$ yarn dev`: Run the docz to see your changes;
`$ npm run dev`: Run the docz to see your changes;
`$ yarn test`: Run all tests: type checking and unit tests;
`$ npm run test`: Run all tests: type checking, unit tests on web and native;
`$ yarn test:watch`: Watch unit tests;
`$ yarn test:watch`: Watch unit tests;
`$ yarn tsc`: Typescript checking;
`$ yarn tsc`: Typescript checking;
`$ yarn tsc:watch`: Typescript checking with watching;
`$ yarn tsc:watch`: Typescript checking with watching;
### Commit messages
### Commit messages
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.
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.
## License
[MIT](https://github.com/danilowoz/react-content-loader/blob/master/LICENSE)
## Known Issues
## Known Issues
##### **Alpha is not working: Safari / iOS**
##### **Alpha is not working: Safari / iOS**
When using `rgba` as a `primaryColor` or `secondaryColor` 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 an `rgba` value for `primaryColor`/`secondaryColor`, use the `rgb` equivalent and move the alpha channel value to the `primaryOpacity`/`secondaryOpacity` props.
When using `rgba` as a `primaryColor` or `secondaryColor` 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 an `rgba` value for `primaryColor`/`secondaryColor`, use the `rgb` equivalent and move the alpha channel value to the `primaryOpacity`/`secondaryOpacity` props.
```jsx
{/* Opaque color in Safari and iOS */}
{/* Opaque color in Safari and iOS */}
<ContentLoader
primaryColor="rgba(0,0,0,0.06)"
secondaryColor="rgba(0,0,0,0.12)">
  primaryColor="rgba(0,0,0,0.06)"
  secondaryColor="rgba(0,0,0,0.12)">
```
```
{/_ Semi-transparent color in Safari and iOS _/}
{/_ Semi-transparent color in Safari and iOS _/}
<ContentLoader
primaryColor="rgb(0,0,0)"
secondaryColor="rgb(0,0,0)"
primaryOpacity={0.06}
secondaryOpacity={0.12}>
    primaryColor="rgb(0,0,0)"
    secondaryColor="rgb(0,0,0)"
    primaryOpacity={0.06}
    secondaryOpacity={0.12}>
```
##### **Black box in Safari / iOS (again)**
##### **Black box in Safari / iOS (again)**
Using 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 `<head />` and issue solved.
Using 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 `<head />` and issue solved.
<img width="200" alt="Black box" src="https://user-images.githubusercontent.com/11562881/39406054-2f308de6-4bce-11e8-91fb-bbb35e29fc10.png" />
<img width="200" alt="Black box" src="https://user-images.githubusercontent.com/11562881/39406054-2f308de6-4bce-11e8-91fb-bbb35e29fc10.png" />
See: [#93](https://github.com/danilowoz/react-content-loader/issues/93) / [109](https://github.com/danilowoz/react-content-loader/issues/109)
See: [#93](https://github.com/danilowoz/react-content-loader/issues/93) / [109](

View File

@@ -0,0 +1,18 @@
jest.mock('Animated', () => {
return {
Value: () => {
return {
addListener: callback => callback({ value: 0 }),
setValue: () => {},
}
},
timing: (value, config) => {
return {
start: callback => {
value.setValue(config.toValue)
callback && callback()
},
}
},
}
})

64
__mocks__/react-native-svg.js vendored Normal file
View File

@@ -0,0 +1,64 @@
import React from 'react';
const createComponent = function(name) {
return class extends React.Component {
// overwrite the displayName, since this is a class created dynamically
static displayName = name;
render() {
return React.createElement(name, this.props, this.props.children);
}
};
};
// Mock all react-native-svg exports
// from https://github.com/magicismight/react-native-svg/blob/master/index.js
const Svg = createComponent('Svg');
const Circle = createComponent('Circle');
const Ellipse = createComponent('Ellipse');
const G = createComponent('G');
const Text = createComponent('Text');
const TextPath = createComponent('TextPath');
const TSpan = createComponent('TSpan');
const Path = createComponent('Path');
const Polygon = createComponent('Polygon');
const Polyline = createComponent('Polyline');
const Line = createComponent('Line');
const Rect = createComponent('Rect');
const Use = createComponent('Use');
const Image = createComponent('Image');
const Symbol = createComponent('Symbol');
const Defs = createComponent('Defs');
const LinearGradient = createComponent('LinearGradient');
const RadialGradient = createComponent('RadialGradient');
const Stop = createComponent('Stop');
const ClipPath = createComponent('ClipPath');
const Pattern = createComponent('Pattern');
const Mask = createComponent('Mask');
export {
Svg,
Circle,
Ellipse,
G,
Text,
TextPath,
TSpan,
Path,
Polygon,
Polyline,
Line,
Rect,
Use,
Image,
Symbol,
Defs,
LinearGradient,
RadialGradient,
Stop,
ClipPath,
Pattern,
Mask,
};
export default Svg;

View File

@@ -15,7 +15,7 @@ import ContentLoader, {
## Usage
## Different Type of Loaders
## Presets
### Facebook Style Loader

23
jest.native.config.js Normal file
View File

@@ -0,0 +1,23 @@
const { defaults: tsjPreset } = require('ts-jest/presets')
module.exports = {
...tsjPreset,
preset: "react-native",
collectCoverage: true,
coverageDirectory: './coverage/native',
"transformIgnorePatterns": [
"/node_modules/(?!react-native|react-clone-referenced-element|react-navigation)"
],
transform: {
...tsjPreset.transform,
'^.+\\.js$': require.resolve('react-native/jest/preprocessor.js'),
},
testRegex: '/src/native/__tests__/.*(\\.|/)(test|spec)\\.[jt]sx?$',
setupFiles: ['./__mocks__/jestSetupFile.js'],
globals: {
'ts-jest': {
babelConfig: false,
tsConfig: 'tsconfig.test.json',
},
},
};

17
jest.web.config.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
verbose: true,
collectCoverage: true,
coverageDirectory: './coverage/',
transform: {
'^.+\\.(t|j)sx?$': 'ts-jest',
},
testRegex: '/src/__tests__/.*(\\.|/)(test|spec)\\.[jt]sx?$',
roots: ['<rootDir>/src'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
globals: {
'ts-jest': {
babelConfig: false,
tsConfig: 'tsconfig.test.json',
},
},
}

24941
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "react-content-loader",
"version": "3.4.2",
"version": "4.4.2",
"description": "SVG-Powered component to easily create placeholder loadings (like Facebook cards loading)",
"repository": {
"type": "git",
@@ -11,79 +11,101 @@
"main": "dist/react-content-loader.cjs.js",
"module": "dist/react-content-loader.es.js",
"jsnext:main": "dist/react-content-loader.es.js",
"types": "dist/src/index.d.ts",
"types": "dist/index.d.ts",
"bugs": {
"url": "https://github.com/danilowoz/react-content-loader/issues"
},
"homepage": "https://github.com/danilowoz/react-content-loader",
"keywords": [
"react",
"facebook-style",
"react-native",
"skeleton",
"placeholder",
"loader",
"loading",
"content",
"svg"
],
"files": [
"dist"
"dist",
"native"
],
"sideEffects": false,
"scripts": {
"dev": "docz dev",
"build": "rm -fr ./dist && rollup -c",
"test": "npm run tsc && jest",
"test:watch": "jest tests --watch",
"tsc": "node_modules/.bin/tsc",
"tsc:watch": "npm run tsc -- --watch",
"build": "rm -fr ./dist ./native && rollup -c && npm run test:size",
"test": "npm run test:tsc && npm run test:unit && npm run build",
"test:unit": "npm run test:unit:web && npm run test:unit:native",
"test:unit:web": "jest -c jest.web.config.js",
"test:unit:native": "jest -c jest.native.config.js",
"test:watch": "npm run test:unit -- --watch",
"test:size": "bundlesize",
"test:tsc": "node_modules/.bin/tsc",
"test:tsc:watch": "npm run tsc -- --watch",
"commit": "git-cz",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"coverage": "codecov",
"lint": "eslint --ext .jsx --ext .ts,.tsx src/",
"release": "semantic-release"
},
"dependencies": {
"react-native-svg": "9.6.4"
},
"devDependencies": {
"@babel/core": "^7.4.4",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.4.4",
"@babel/runtime": "^7.4.4",
"@commitlint/cli": "^7.6.1",
"@commitlint/config-conventional": "^7.6.0",
"@types/jest": "^24.0.13",
"@types/react": "^16.8.17",
"@types/react-dom": "^16.8.4",
"@types/react-test-renderer": "^16.8.1",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^24.8.0",
"commitizen": "^3.1.1",
"cz-conventional-changelog": "^2.1.0",
"docz": "^1.2.0",
"docz-theme-default": "^1.2.0",
"husky": "^2.3.0",
"jest": "^24.8.0",
"prettier": "^1.17.1",
"react": "^16.7.0",
"react-dom": "^16.8.6",
"react-test-renderer": "^16.8.6",
"remark-emoji": "^2.0.2",
"rollup": "1.12.3",
"rollup-plugin-analyzer": "^3.0.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-typescript2": "^0.21.1",
"rollup-plugin-uglify": "^6.0.2",
"semantic-release": "^15.13.12",
"ts-jest": "^24.0.2",
"tslint": "^5.16.0",
"tslint-config-prettier": "^1.18.0",
"typescript": "^3.4.5"
"@babel/core": "7.5.5",
"@babel/polyfill": "7.4.4",
"@babel/preset-env": "7.5.5",
"@babel/preset-react": "7.0.0",
"@babel/register": "7.5.5",
"@babel/runtime": "7.5.5",
"@commitlint/cli": "8.1.0",
"@commitlint/config-conventional": "8.1.0",
"@types/jest": "24.0.18",
"@types/react": "16.9.2",
"@types/react-dom": "16.9.0",
"@types/react-native": "0.60.11",
"@types/react-test-renderer": "16.9.0",
"@typescript-eslint/eslint-plugin": "2.1.0",
"@typescript-eslint/parser": "2.1.0",
"babel-core": "6.26.3",
"babel-eslint": "10.0.3",
"babel-jest": "24.8.0",
"bundlesize": "0.18.0",
"commitizen": "4.0.3",
"cz-conventional-changelog": "3.0.2",
"docz": "1.2.0",
"docz-theme-default": "1.2.0",
"eslint": "6.3.0",
"eslint-config-react-app": "5.0.1",
"eslint-plugin-flowtype": "4.3.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.14.3",
"eslint-plugin-react-hooks": "2.0.1",
"husky": "3.0.4",
"jest": "24.9.0",
"metro-react-native-babel-preset": "0.56.0",
"prettier": "1.18.2",
"react": "16.9.0",
"react-dom": "16.9.0",
"react-native": "0.60.5",
"react-test-renderer": "16.9.0",
"remark-emoji": "2.0.2",
"rollup": "1.20.2",
"rollup-plugin-copy": "3.1.0",
"rollup-plugin-replace": "2.2.0",
"rollup-plugin-typescript2": "0.23.0",
"rollup-plugin-uglify": "6.0.2",
"semantic-release": "15.13.24",
"ts-jest": "24.0.2",
"typescript": "3.5.3"
},
"peerDependencies": {
"react": "^16.8.6"
"react": "^16.0.0",
"react-native": "^0.60.5"
},
"dependencies": {},
"husky": {
"hooks": {
"pre-commit": "npm run format && npm run tsc",
"pre-commit": "npm run lint && npm run format",
"pre-push": "npm run test",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
@@ -98,24 +120,14 @@
"path": "./node_modules/cz-conventional-changelog"
}
},
"jest": {
"verbose": true,
"collectCoverage": true,
"coverageDirectory": "./coverage/",
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
"bundlesize": [
{
"path": "./dist/react-content-loader.min.js",
"maxSize": "2 kB"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
"roots": [
"<rootDir>/src"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
{
"path": "./native/react-content-loader.native.es.js",
"maxSize": "3 kB"
}
]
}

View File

@@ -1,11 +1,25 @@
import replace from 'rollup-plugin-replace'
import { uglify } from 'rollup-plugin-uglify'
import typescript from 'rollup-plugin-typescript2'
import analyze from 'rollup-plugin-analyzer'
import copy from 'rollup-plugin-copy'
import pkg from './package.json'
const mergeAll = objs => Object.assign({}, ...objs)
const cjs = {
exports: 'named',
format: 'cjs',
sourcemap: true,
};
const esm = {
format: 'es',
sourcemap: true,
};
const globals = { react: 'React', 'react-dom': 'ReactDOM' };
const commonPlugins = [
typescript({
typescript: require('typescript'),
@@ -13,7 +27,6 @@ const commonPlugins = [
]
const configBase = {
input: 'src/index.ts',
output: {
exports: 'named',
},
@@ -27,16 +40,14 @@ const configBase = {
const umdConfig = mergeAll([
configBase,
{
input: 'src/index.ts',
output: mergeAll([
configBase.output,
{
file: `dist/${pkg.name}.js`,
format: 'umd',
name: 'ContentLoader',
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
globals
},
]),
external: Object.keys(pkg.peerDependencies || {}),
@@ -46,6 +57,7 @@ const umdConfig = mergeAll([
const devUmdConfig = mergeAll([
umdConfig,
{
input: 'src/index.ts',
plugins: umdConfig.plugins.concat(
replace({
'process.env.NODE_ENV': JSON.stringify('development'),
@@ -57,6 +69,7 @@ const devUmdConfig = mergeAll([
const prodUmdConfig = mergeAll([
umdConfig,
{
input: 'src/index.ts',
output: mergeAll([
umdConfig.output,
{ file: umdConfig.output.file.replace(/\.js$/, '.min.js') },
@@ -81,12 +94,29 @@ const prodUmdConfig = mergeAll([
const webConfig = mergeAll([
configBase,
{
input: 'src/index.ts',
output: [
mergeAll([configBase.output, { file: pkg.module, format: 'es' }]),
mergeAll([configBase.output, { file: pkg.main, format: 'cjs' }]),
mergeAll([configBase.output, { ...esm, file: pkg.module }]),
mergeAll([configBase.output, { ...cjs, file: pkg.main, }]),
],
plugins: configBase.plugins.concat(analyze()),
plugins: configBase.plugins.concat(),
},
])
export default [devUmdConfig, prodUmdConfig, webConfig]
const nativeConfig = mergeAll([
configBase,
{
input: './src/native/index.ts',
output: [
mergeAll([configBase.output, { ...esm, file: `native/${pkg.name}.native.es.js` }]),
mergeAll([configBase.output, { ...cjs, file: `native/${pkg.name}.native.cjs.js`, }]),
],
plugins: configBase.plugins.concat(copy({
targets: [
{ src: 'src/native/package.json', dest: 'native' },
]
}))
},
])
export default [devUmdConfig, prodUmdConfig, webConfig, nativeConfig]

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import Svg from './Svg'
import { IContentLoaderProps } from './interface'
import Svg from './Svg'
import { IContentLoaderProps } from './'
export const defaultProps: IContentLoaderProps = {
animate: true,
@@ -29,13 +29,12 @@ const InitialComponent: React.FunctionComponent<
const ContentLoader = (props: IContentLoaderProps) => {
const mergedProps = { ...defaultProps, ...props }
const children = props.children ? (
props.children
) : (
<InitialComponent {...mergedProps} />
)
return <Svg {...mergedProps}>{children}</Svg>
return (
<Svg {...mergedProps}>
{props.children || <InitialComponent {...mergedProps} />}
</Svg>
)
}
export default ContentLoader

View File

@@ -1,6 +1,6 @@
import * as React from 'react'
import { IContentLoaderProps } from './interface'
import { IContentLoaderProps } from './'
import uid from './uid'
export default ({

View File

@@ -94,7 +94,7 @@ describe('Svg', () => {
expect(linearGradient.props.id).toBe(`${uniquekey}-idGradient`)
})
it('render two components with diferents ids', () => {
it('render two components with different ids', () => {
// Wrapper
const { clipPath, linearGradient } = partsOfComponent

View File

@@ -0,0 +1,8 @@
import pkg from '../../package.json'
describe('package.json', () => {
it(`has only one dependency which is for support react-native`, () => {
expect('react-native-svg' in pkg.dependencies).toBe(true)
expect(Object.keys(pkg.dependencies).length).toBe(1)
})
})

View File

@@ -1,7 +0,0 @@
import pkg from '../../package.json'
describe('package.json', () => {
it(`has no dependencies`, () => {
expect(Object.keys(pkg.dependencies).length).toBe(0)
})
})

View File

@@ -1,5 +1,44 @@
import ContentLoader from './Holder'
export { IContentLoaderProps } from './interface'
export interface IContentLoaderProps {
animate?: boolean
ariaLabel?: string
children?: React.ReactNode
baseUrl?: string
className?: string
height?: number
preserveAspectRatio?:
| 'none'
| 'xMinYMin meet'
| 'xMidYMin meet'
| 'xMaxYMin meet'
| 'xMinYMid meet'
| 'xMidYMid meet'
| 'xMaxYMid meet'
| 'xMinYMax meet'
| 'xMidYMax meet'
| 'xMaxYMax meet'
| 'xMinYMin slice'
| 'xMidYMin slice'
| 'xMaxYMin slice'
| 'xMinYMid slice'
| 'xMidYMid slice'
| 'xMaxYMid slice'
| 'xMinYMax slice'
| 'xMidYMax slice'
| 'xMaxYMax slice'
primaryColor?: string
primaryOpacity?: number
rtl?: boolean
secondaryColor?: string
secondaryOpacity?: number
speed?: number
interval?: number
style?: React.CSSProperties
uniquekey?: string
width?: number
gradientRatio?: number
}
export { default as Facebook } from './stylized/FacebookStyle'
export { default as Instagram } from './stylized/InstagramStyle'

50
src/native/Holder.tsx Normal file
View File

@@ -0,0 +1,50 @@
import * as React from 'react'
import { Circle, Rect } from 'react-native-svg'
import Svg from './Svg'
import { IContentLoaderProps } from './'
type DefaultProps = Pick<
Required<IContentLoaderProps>,
| 'animate'
| 'height'
| 'preserveAspectRatio'
| 'primaryColor'
| 'rtl'
| 'secondaryColor'
| 'speed'
| 'style'
| 'width'
>
export const defaultProps: DefaultProps = {
animate: true,
height: 130,
preserveAspectRatio: 'none',
primaryColor: '#f0f0f0',
rtl: false,
secondaryColor: '#e0e0e0',
speed: 1,
style: {},
width: 400,
}
const InitialComponent: React.FunctionComponent<
IContentLoaderProps
> = props => (
<Rect x="0" y="0" rx="5" ry="5" width={props.width} height={props.height} />
)
const ContentLoader = (props: IContentLoaderProps) => {
const mergedProps = { ...defaultProps, ...props }
return (
<Svg {...mergedProps}>
{props.children || <InitialComponent {...mergedProps} />}
</Svg>
)
}
export { Circle, Rect }
export default ContentLoader

132
src/native/Svg.tsx Normal file
View File

@@ -0,0 +1,132 @@
import React, { Component } from 'react'
import { Animated } from 'react-native'
import Svg, {
ClipPath,
Defs,
LinearGradient,
Rect,
Stop,
} from 'react-native-svg'
import { IContentLoaderProps } from './'
import offsetValueBound from './offsetValueBound'
import uid from '../uid'
type RequiredIContentLoaderProps = IContentLoaderProps &
Pick<
Required<IContentLoaderProps>,
| 'animate'
| 'height'
| 'preserveAspectRatio'
| 'primaryColor'
| 'rtl'
| 'secondaryColor'
| 'speed'
| 'style'
| 'width'
>
interface State {
offset: number
}
class NativeSvg extends Component<RequiredIContentLoaderProps, State> {
state = { offset: -1 }
animatedValue = new Animated.Value(0)
idClip = this.props.id ? `${this.props.id}-idClip` : uid()
idGradient = this.props.id ? `${this.props.id}-idGradient` : uid()
setAnimation = () => {
// Turn in seconds to keep compatible with web one
const durInSeconds = this.props.speed * 1000
Animated.timing(this.animatedValue, {
toValue: 2,
delay: durInSeconds,
duration: durInSeconds,
useNativeDriver: true,
}).start(() => {
this.animatedValue.setValue(-1)
this.setAnimation()
})
}
componentDidMount = () => {
if (this.props.animate) {
this.setAnimation()
this.animatedValue.addListener(({ value }) => {
this.setState({
offset: value,
})
})
}
}
render() {
const {
children,
height,
primaryColor,
secondaryColor,
width,
rtl,
style,
...props
} = this.props
const offset1 = offsetValueBound(this.state.offset - 1)
const offset2 = offsetValueBound(this.state.offset)
const offset3 = offsetValueBound(this.state.offset + 1)
const rtlStyle = rtl ? { transform: [{ rotateY: '180deg' }] } : {}
const composedStyle = { ...style, ...rtlStyle }
// Remove unnecessary keys
delete props.id
delete props.animate
delete props.speed
return (
<Svg
viewBox={`0 0 ${width} ${height}`}
width={width}
height={height}
preserveAspectRatio="none"
style={composedStyle}
{...props}
>
<Rect
x="0"
y="0"
width={width}
height={height}
fill={`url(#${this.idClip})`}
clipPath={`url(#${this.idGradient})`}
/>
<Defs>
<ClipPath id={this.idGradient}>{children}</ClipPath>
<LinearGradient
id={this.idClip}
x1={'-100%'}
y1={0}
x2={'100%'}
y2={0}
>
<Stop offset={offset1} stopColor={primaryColor} />
<Stop offset={offset2} stopColor={secondaryColor} />
<Stop offset={offset3} stopColor={primaryColor} />
</LinearGradient>
</Defs>
</Svg>
)
}
}
export default NativeSvg

View File

@@ -0,0 +1,127 @@
import * as React from 'react'
import * as renderer from 'react-test-renderer'
import * as ShallowRenderer from 'react-test-renderer/shallow'
import ContentLoader, { Rect, Circle } from '../Holder'
describe('Holder', () => {
describe('when type is custom', () => {
const customWrapper = renderer.create(
<ContentLoader animate={false}>
<Rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
<Rect x="82" y="44" rx="3" ry="3" width="250" height="10" />
<Circle cx="35" cy="35" r="35" />
</ContentLoader>
).root
it('should render custom element', () => {
const rect = customWrapper.findAllByType(Rect)
const circle = customWrapper.findAllByType(Circle)
expect(rect.length).toBe(3)
expect(circle.length).toBe(1)
})
})
describe('Props are propagated', () => {
const noPropsComponent = ShallowRenderer.createRenderer()
noPropsComponent.render(<ContentLoader />)
const withPropsComponent = ShallowRenderer.createRenderer()
withPropsComponent.render(
<ContentLoader
animate={false}
height={200}
preserveAspectRatio="xMaxYMax meet"
primaryColor="#000"
rtl
secondaryColor="#fff"
speed={10}
style={{ marginBottom: '10px' }}
width={200}
/>
)
const { props: propsFromEmpty } = noPropsComponent.getRenderOutput()
const { props: propsFromFullField } = withPropsComponent.getRenderOutput()
it("`speed` is a number and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.speed).toBe('number')
expect(propsFromEmpty.speed).toBe(1)
// custom props
expect(typeof propsFromFullField.speed).toBe('number')
expect(propsFromFullField.speed).toBe(10)
})
it("`height` is a number and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.height).toBe('number')
expect(propsFromEmpty.height).toBe(130)
// custom props
expect(typeof propsFromFullField.height).toBe('number')
expect(propsFromFullField.height).toBe(200)
})
it("`width` is a number and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.width).toBe('number')
expect(propsFromEmpty.width).toBe(400)
// custom props
expect(typeof propsFromFullField.width).toBe('number')
expect(propsFromFullField.width).toBe(200)
})
it("`animate` is a boolean and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.animate).toBe('boolean')
expect(propsFromEmpty.animate).toBe(true)
// custom props
expect(typeof propsFromFullField.animate).toBe('boolean')
expect(propsFromFullField.animate).toBe(false)
})
it("`primaryColor` is a string and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.primaryColor).toBe('string')
expect(propsFromEmpty.primaryColor).toBe('#f0f0f0')
// custom props
expect(typeof propsFromFullField.primaryColor).toBe('string')
expect(propsFromFullField.primaryColor).toBe('#000')
})
it("`secondaryColor` is a string and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.secondaryColor).toBe('string')
expect(propsFromEmpty.secondaryColor).toBe('#e0e0e0')
// custom props
expect(typeof propsFromFullField.secondaryColor).toBe('string')
expect(propsFromFullField.secondaryColor).toBe('#fff')
})
it("`preserveAspectRatio` is a string and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.preserveAspectRatio).toBe('string')
expect(propsFromEmpty.preserveAspectRatio).toBe('none')
// custom props
expect(typeof propsFromFullField.preserveAspectRatio).toBe('string')
expect(propsFromFullField.preserveAspectRatio).toBe('xMaxYMax meet')
})
it("`style` is an object and it's used", () => {
// defaultProps
expect(propsFromEmpty.style).toMatchObject({})
// custom props
expect(propsFromFullField.style).toMatchObject({ marginBottom: '10px' })
})
it("`rtl` is a boolean and it's used", () => {
// defaultProps
expect(typeof propsFromEmpty.rtl).toBe('boolean')
expect(propsFromEmpty.rtl).toBe(false)
// custom props
expect(typeof propsFromFullField.rtl).toBe('boolean')
expect(propsFromFullField.rtl).toBe(true)
})
})
})

View File

@@ -0,0 +1,80 @@
import * as React from 'react'
import * as renderer from 'react-test-renderer'
import ContentLoader from '..'
import Svg, { ClipPath, LinearGradient, Stop } from 'react-native-svg'
interface IPredicateArgs {
type: any
props: any
}
describe('Svg', () => {
const wrapper = renderer.create(<ContentLoader animate={false} />).root
const predicateRectClipPath = ({ type, props }: IPredicateArgs) =>
type === 'Rect' && props.clipPath
const partsOfComponent = {
allLinearGradient: wrapper.findAllByType(LinearGradient),
allRectClipPath: wrapper.findAll(predicateRectClipPath),
allStops: wrapper.findAllByType(Stop),
clipPath: wrapper.findByType(ClipPath),
linearGradient: wrapper.findByType(LinearGradient),
rectClipPath: wrapper.find(predicateRectClipPath),
svg: wrapper.findByType(Svg),
}
describe('it has basic elements necessary to work ', () => {
it('has a `rect` with `clipPath`', () => {
const { allRectClipPath } = partsOfComponent
expect(allRectClipPath.length).toBe(1)
})
it('has a `linearGradient`', () => {
const { allLinearGradient } = partsOfComponent
expect(allLinearGradient.length).toBe(1)
})
it('has three `stop`', () => {
const { allStops } = partsOfComponent
expect(allStops.length).toBe(3)
})
it('has `stop` inside the `linearGradient`', () => {
const { linearGradient } = partsOfComponent
const stopsIntoLinearGradient = linearGradient.findAllByType(Stop)
expect(stopsIntoLinearGradient.length).toBe(3)
})
})
describe('unique key', () => {
it('render two components with different ids', () => {
// Wrapper
const { clipPath, linearGradient } = partsOfComponent
// Another component
const anotherComp = renderer.create(<ContentLoader animate={false} />)
.root
const anotherClipPath = anotherComp.findByType(ClipPath)
const anotherLinearGradient = anotherComp.findByType(LinearGradient)
expect(clipPath.props.id).not.toBe(anotherClipPath.props.id)
expect(linearGradient.props.id).not.toBe(anotherLinearGradient.props.id)
})
it('clipPath id and rect clipPath url are the same', () => {
const { clipPath, rectClipPath } = partsOfComponent
expect(rectClipPath.props.clipPath).toBe(`url(#${clipPath.props.id})`)
})
it('linearGradient id and rect clipPath fill are the same', () => {
const { linearGradient, rectClipPath } = partsOfComponent
expect(rectClipPath.props.fill).toBe(`url(#${linearGradient.props.id})`)
})
})
})

View File

@@ -0,0 +1,14 @@
import offsetValueBound from '../offsetValueBound'
describe('offset value bound ', () => {
it('should return an integer', () => {
expect(offsetValueBound(-1)).toBe(0)
expect(offsetValueBound(0)).toBe(0)
})
it('should return a max value 1', () => {
expect(offsetValueBound(1)).toBe(1)
expect(offsetValueBound(1.1)).toBe(1)
expect(offsetValueBound(2)).toBe(1)
})
})

View File

@@ -0,0 +1,21 @@
import 'react-native'
import * as React from 'react'
import * as renderer from 'react-test-renderer'
import BulletListStyle from '../../stylized/BulletListStyle'
describe('BulletListStyle', () => {
const wrapper = renderer.create(
<BulletListStyle id="BulletListStyle" animate={false} speed={20} />
)
test('renders correctly', () => {
const tree = wrapper.toJSON()
expect(tree).toMatchSnapshot()
})
test('props are propagated ', () => {
expect(wrapper.root.props.speed).toEqual(20)
})
})

View File

@@ -0,0 +1,21 @@
import 'react-native'
import * as React from 'react'
import * as renderer from 'react-test-renderer'
import CodeStyle from '../../stylized/CodeStyle'
describe('CodeStyle', () => {
const wrapper = renderer.create(
<CodeStyle id="CodeStyle" animate={false} speed={20} />
)
test('renders correctly', () => {
const tree = wrapper.toJSON()
expect(tree).toMatchSnapshot()
})
test('props are propagated ', () => {
expect(wrapper.root.props.speed).toEqual(20)
})
})

View File

@@ -0,0 +1,21 @@
import 'react-native'
import * as React from 'react'
import * as renderer from 'react-test-renderer'
import FacebookStyle from '../../stylized/FacebookStyle'
describe('FacebookStyle', () => {
const wrapper = renderer.create(
<FacebookStyle id="FacebookStyle" animate={false} speed={20} />
)
test('renders correctly', () => {
const tree = wrapper.toJSON()
expect(tree).toMatchSnapshot()
})
test('props are propagated ', () => {
expect(wrapper.root.props.speed).toEqual(20)
})
})

View File

@@ -0,0 +1,21 @@
import 'react-native'
import * as React from 'react'
import * as renderer from 'react-test-renderer'
import InstagramStyle from '../../stylized/InstagramStyle'
describe('InstagramStyle', () => {
const wrapper = renderer.create(
<InstagramStyle id="InstagramStyle" animate={false} speed={20} />
)
test('renders correctly', () => {
const tree = wrapper.toJSON()
expect(tree).toMatchSnapshot()
})
test('props are propagated ', () => {
expect(wrapper.root.props.speed).toEqual(20)
})
})

View File

@@ -0,0 +1,21 @@
import 'react-native'
import * as React from 'react'
import * as renderer from 'react-test-renderer'
import ListStyle from '../../stylized/ListStyle'
describe('ListStyle', () => {
const wrapper = renderer.create(
<ListStyle id="ListStyle" animate={false} speed={20} />
)
test('renders correctly', () => {
const tree = wrapper.toJSON()
expect(tree).toMatchSnapshot()
})
test('props are propagated ', () => {
expect(wrapper.root.props.speed).toEqual(20)
})
})

View File

@@ -0,0 +1,98 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulletListStyle renders correctly 1`] = `
<Svg
height={130}
preserveAspectRatio="none"
style={Object {}}
viewBox="0 0 400 130"
width={400}
>
<Rect
clipPath="url(#BulletListStyle-idGradient)"
fill="url(#BulletListStyle-idClip)"
height={130}
width={400}
x="0"
y="0"
/>
<Defs>
<ClipPath
id="BulletListStyle-idGradient"
>
<Circle
cx="10"
cy="20"
r="8"
/>
<Rect
height="10"
rx="5"
ry="5"
width="220"
x="25"
y="15"
/>
<Circle
cx="10"
cy="50"
r="8"
/>
<Rect
height="10"
rx="5"
ry="5"
width="220"
x="25"
y="45"
/>
<Circle
cx="10"
cy="80"
r="8"
/>
<Rect
height="10"
rx="5"
ry="5"
width="220"
x="25"
y="75"
/>
<Circle
cx="10"
cy="110"
r="8"
/>
<Rect
height="10"
rx="5"
ry="5"
width="220"
x="25"
y="105"
/>
</ClipPath>
<LinearGradient
id="BulletListStyle-idClip"
x1="-100%"
x2="100%"
y1={0}
y2={0}
>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
<Stop
offset={0}
stopColor="#e0e0e0"
/>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
</LinearGradient>
</Defs>
</Svg>
`;

View File

@@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeStyle renders correctly 1`] = `
<Svg
height={130}
preserveAspectRatio="none"
style={Object {}}
viewBox="0 0 400 130"
width={400}
>
<Rect
clipPath="url(#CodeStyle-idGradient)"
fill="url(#CodeStyle-idClip)"
height={130}
width={400}
x="0"
y="0"
/>
<Defs>
<ClipPath
id="CodeStyle-idGradient"
>
<Rect
height="10"
rx="3"
ry="3"
width="70"
x="0"
y="0"
/>
<Rect
height="10"
rx="3"
ry="3"
width="100"
x="80"
y="0"
/>
<Rect
height="10"
rx="3"
ry="3"
width="10"
x="190"
y="0"
/>
<Rect
height="10"
rx="3"
ry="3"
width="130"
x="15"
y="20"
/>
<Rect
height="10"
rx="3"
ry="3"
width="130"
x="155"
y="20"
/>
<Rect
height="10"
rx="3"
ry="3"
width="90"
x="15"
y="40"
/>
<Rect
height="10"
rx="3"
ry="3"
width="60"
x="115"
y="40"
/>
<Rect
height="10"
rx="3"
ry="3"
width="60"
x="185"
y="40"
/>
<Rect
height="10"
rx="3"
ry="3"
width="30"
x="0"
y="60"
/>
</ClipPath>
<LinearGradient
id="CodeStyle-idClip"
x1="-100%"
x2="100%"
y1={0}
y2={0}
>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
<Stop
offset={0}
stopColor="#e0e0e0"
/>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
</LinearGradient>
</Defs>
</Svg>
`;

View File

@@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FacebookStyle renders correctly 1`] = `
<Svg
height={130}
preserveAspectRatio="none"
style={Object {}}
viewBox="0 0 400 130"
width={400}
>
<Rect
clipPath="url(#FacebookStyle-idGradient)"
fill="url(#FacebookStyle-idClip)"
height={130}
width={400}
x="0"
y="0"
/>
<Defs>
<ClipPath
id="FacebookStyle-idGradient"
>
<Rect
height="6"
rx="4"
ry="4"
width="117"
x="70"
y="15"
/>
<Rect
height="6"
rx="3"
ry="3"
width="85"
x="70"
y="35"
/>
<Rect
height="6"
rx="3"
ry="3"
width="350"
x="0"
y="80"
/>
<Rect
height="6"
rx="3"
ry="3"
width="380"
x="0"
y="100"
/>
<Rect
height="6"
rx="3"
ry="3"
width="201"
x="0"
y="120"
/>
<Circle
cx="30"
cy="30"
r="30"
/>
</ClipPath>
<LinearGradient
id="FacebookStyle-idClip"
x1="-100%"
x2="100%"
y1={0}
y2={0}
>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
<Stop
offset={0}
stopColor="#e0e0e0"
/>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
</LinearGradient>
</Defs>
</Svg>
`;

View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InstagramStyle renders correctly 1`] = `
<Svg
height={480}
preserveAspectRatio="none"
style={Object {}}
viewBox="0 0 400 480"
width={400}
>
<Rect
clipPath="url(#InstagramStyle-idGradient)"
fill="url(#InstagramStyle-idClip)"
height={480}
width={400}
x="0"
y="0"
/>
<Defs>
<ClipPath
id="InstagramStyle-idGradient"
>
<Circle
cx="30"
cy="30"
r="30"
/>
<Rect
height="13"
rx="4"
ry="4"
width="100"
x="75"
y="13"
/>
<Rect
height="8"
rx="4"
ry="4"
width="50"
x="75"
y="37"
/>
<Rect
height="400"
rx="5"
ry="5"
width="400"
x="0"
y="70"
/>
</ClipPath>
<LinearGradient
id="InstagramStyle-idClip"
x1="-100%"
x2="100%"
y1={0}
y2={0}
>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
<Stop
offset={0}
stopColor="#e0e0e0"
/>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
</LinearGradient>
</Defs>
</Svg>
`;

View File

@@ -0,0 +1,94 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ListStyle renders correctly 1`] = `
<Svg
height={130}
preserveAspectRatio="none"
style={Object {}}
viewBox="0 0 400 130"
width={400}
>
<Rect
clipPath="url(#ListStyle-idGradient)"
fill="url(#ListStyle-idClip)"
height={130}
width={400}
x="0"
y="0"
/>
<Defs>
<ClipPath
id="ListStyle-idGradient"
>
<Rect
height="10"
rx="3"
ry="3"
width="250"
x="0"
y="0"
/>
<Rect
height="10"
rx="3"
ry="3"
width="220"
x="20"
y="20"
/>
<Rect
height="10"
rx="3"
ry="3"
width="170"
x="20"
y="40"
/>
<Rect
height="10"
rx="3"
ry="3"
width="250"
x="0"
y="60"
/>
<Rect
height="10"
rx="3"
ry="3"
width="200"
x="20"
y="80"
/>
<Rect
height="10"
rx="3"
ry="3"
width="80"
x="20"
y="100"
/>
</ClipPath>
<LinearGradient
id="ListStyle-idClip"
x1="-100%"
x2="100%"
y1={0}
y2={0}
>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
<Stop
offset={0}
stopColor="#e0e0e0"
/>
<Stop
offset={0}
stopColor="#f0f0f0"
/>
</LinearGradient>
</Defs>
</Svg>
`;

11
src/native/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import ContentLoader, { Rect, Circle } from './Holder'
export { IContentLoaderProps } from './interface'
export { default as Facebook } from './stylized/FacebookStyle'
export { default as Instagram } from './stylized/InstagramStyle'
export { default as Code } from './stylized/CodeStyle'
export { default as List } from './stylized/ListStyle'
export { default as BulletList } from './stylized/BulletListStyle'
export { Rect, Circle }
export default ContentLoader

View File

@@ -1,8 +1,8 @@
import * as ReactNative from 'react-native'
export interface IContentLoaderProps {
animate?: boolean
ariaLabel?: string
children?: React.ReactNode
baseUrl?: string
className?: string
height?: number
preserveAspectRatio?:
@@ -25,15 +25,11 @@ export interface IContentLoaderProps {
| 'xMinYMax slice'
| 'xMidYMax slice'
| 'xMaxYMax slice'
id?: string
primaryColor?: string
primaryOpacity?: number
rtl?: boolean
secondaryColor?: string
secondaryOpacity?: number
speed?: number
interval?: number
style?: React.CSSProperties
uniquekey?: string
style?: ReactNative.ViewStyle
width?: number
gradientRatio?: number
}

View File

@@ -0,0 +1,13 @@
const offsetValueBound = (value: number): number => {
if (value > 1) {
return 1
}
if (value < 0) {
return 0
}
return value
}
export default offsetValueBound

8
src/native/package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "react-content-loader/native",
"private": true,
"main": "./react-content-loader.native.cjs.js",
"module": "./react-content-loader.native.es.js",
"jsnext:main": "./react-content-loader.native.es.js",
"types": "./native/index.d.ts"
}

View File

@@ -0,0 +1,17 @@
import * as React from 'react'
import Holder, { Rect, Circle } from '../Holder'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>
<Circle cx="10" cy="20" r="8" />
<Rect x="25" y="15" rx="5" ry="5" width="220" height="10" />
<Circle cx="10" cy="50" r="8" />
<Rect x="25" y="45" rx="5" ry="5" width="220" height="10" />
<Circle cx="10" cy="80" r="8" />
<Rect x="25" y="75" rx="5" ry="5" width="220" height="10" />
<Circle cx="10" cy="110" r="8" />
<Rect x="25" y="105" rx="5" ry="5" width="220" height="10" />
</Holder>
)

View File

@@ -0,0 +1,21 @@
import * as React from 'react'
import Holder, { Rect } from '../Holder'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>
<Rect x="0" y="0" rx="3" ry="3" width="70" height="10" />
<Rect x="80" y="0" rx="3" ry="3" width="100" height="10" />
<Rect x="190" y="0" rx="3" ry="3" width="10" height="10" />
<Rect x="15" y="20" rx="3" ry="3" width="130" height="10" />
<Rect x="155" y="20" rx="3" ry="3" width="130" height="10" />
<Rect x="15" y="40" rx="3" ry="3" width="90" height="10" />
<Rect x="115" y="40" rx="3" ry="3" width="60" height="10" />
<Rect x="185" y="40" rx="3" ry="3" width="60" height="10" />
<Rect x="0" y="60" rx="3" ry="3" width="30" height="10" />
</Holder>
)

View File

@@ -0,0 +1,15 @@
import * as React from 'react'
import Holder, { Rect, Circle } from '../Holder'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>
<Rect x="70" y="15" rx="4" ry="4" width="117" height="6" />
<Rect x="70" y="35" rx="3" ry="3" width="85" height="6" />
<Rect x="0" y="80" rx="3" ry="3" width="350" height="6" />
<Rect x="0" y="100" rx="3" ry="3" width="380" height="6" />
<Rect x="0" y="120" rx="3" ry="3" width="201" height="6" />
<Circle cx="30" cy="30" r="30" />
</Holder>
)

View File

@@ -0,0 +1,14 @@
import * as React from 'react'
import Holder, { Rect, Circle } from '../Holder'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props} height={480}>
<Circle cx="30" cy="30" r="30" />
<Rect x="75" y="13" rx="4" ry="4" width="100" height="13" />
<Rect x="75" y="37" rx="4" ry="4" width="50" height="8" />
<Rect x="0" y="70" rx="5" ry="5" width="400" height="400" />
</Holder>
)

View File

@@ -0,0 +1,15 @@
import * as React from 'react'
import Holder, { Rect } from '../Holder'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>
<Rect x="0" y="0" rx="3" ry="3" width="250" height="10" />
<Rect x="20" y="20" rx="3" ry="3" width="220" height="10" />
<Rect x="20" y="40" rx="3" ry="3" width="170" height="10" />
<Rect x="0" y="60" rx="3" ry="3" width="250" height="10" />
<Rect x="20" y="80" rx="3" ry="3" width="200" height="10" />
<Rect x="20" y="100" rx="3" ry="3" width="80" height="10" />
</Holder>
)

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import Holder from '../Holder'
import { IContentLoaderProps } from '../interface'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import Holder from '../Holder'
import { IContentLoaderProps } from '../interface'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import Holder from '../Holder'
import { IContentLoaderProps } from '../interface'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import Holder from '../Holder'
import { IContentLoaderProps } from '../interface'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props} height={480}>

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import Holder from '../Holder'
import { IContentLoaderProps } from '../interface'
import { IContentLoaderProps } from '../'
export default (props: IContentLoaderProps) => (
<Holder {...props}>

17
tsconfig.base.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es5",
"jsx": "react",
"lib": ["es6"],
"declaration": true,
"declarationDir": "./dist/types",
"declarationMap": true,
"noEmit": true,
"sourceMap": true,
"esModuleInterop": true,
"noImplicitAny": false,
"resolveJsonModule": true
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "src/__tests__"]
}

View File

@@ -1,18 +1,3 @@
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./dist/types",
"outDir": "./dist",
"target": "es5",
"jsx": "react",
"sourceMap": true,
"noImplicitAny": true,
"resolveJsonModule": true, // To load package.json
"esModuleInterop": true,
"moduleResolution": "node"
},
"include": [
"./src/**/*"
],
"exclude": ["node_modules"]
}
"extends": "./tsconfig.base.json"
}

7
tsconfig.test.json Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["dom"],
"noEmit": true
}
}

View File

@@ -1,7 +0,0 @@
{
"defaultSeverity": "error",
"extends": ["tslint:recommended", "tslint-config-prettier"],
"jsRules": {},
"rules": {},
"rulesDirectory": []
}