├── .babelrc
├── .gitignore
├── .travis.yml
├── README.md
├── demo.gif
├── example
├── bundle.js
├── index.d.ts
├── index.html
├── index.tsx
├── style.css
└── webpack.config.js
├── index.d.ts
├── index.js
├── index.tsx
├── package-lock.json
├── package.json
├── test
├── index.html
├── server.js
└── test.js
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | { "presets": ["react", "es2015"] }
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache:
3 | directories:
4 | - node_modules
5 | - test/node_modules
6 | node_js:
7 | - '10'
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-animate-on-change
2 |
3 | [](https://travis-ci.org/arve0/react-animate-on-change)
4 |
5 | Animate your react components on props or state changes, in contrast to [entries added/removed from arrays](https://facebook.github.io/react/docs/animation.html).
6 |
7 |
8 |
9 | ## Install
10 | ```sh
11 | npm install react-animate-on-change react
12 | ```
13 |
14 | ## Usage
15 | ```js
16 | import AnimateOnChange from 'react-animate-on-change'
17 | // CommonJS:
18 | // const AnimateOnChange = require('react-animate-on-change').default
19 |
20 | // functional component
21 | const Score = (props) =>
22 |
26 | Score: {props.score}
27 |
28 | ```
29 |
30 | The example above will (roughly) render to:
31 |
32 | **On enter or changes in `props.diff` or `props.score`:**
33 | ```html
34 |
35 | Score: 100
36 |
37 | ```
38 |
39 | **On animation end:**
40 | ```html
41 |
42 | Score: 100
43 |
44 | ```
45 |
46 | Also, [see the example folder](example).
47 |
48 | ## Props
49 | `baseClassName {string}` : Base class name that be added to the component.
50 |
51 | `animationClassName {string}` : Animation class name. Added when `animate == true`. Removed when the event [`animationend`](http://www.w3.org/TR/css3-animations/#animationend) is triggered.
52 |
53 | `animate {bool}` : Whether component should animate.
54 |
55 | `customTag {string}` : HTML tag of the component.
56 |
57 | `onAnimationEnd {() => void)}` : Callback which is called when animation ends.
58 |
59 | ## Develop
60 | Setup:
61 | ```sh
62 | npm install
63 | cd test
64 | npm install
65 | cd ..
66 | ```
67 |
68 | Add tests in [test/client-tests.js](client-tests.js), start tests with:
69 | ```
70 | npm test
71 | ```
72 |
73 | Build and view example:
74 | ```
75 | npm run build-example
76 | open example/index.html
77 | ```
78 |
79 | ## Known issues
80 | - The browser must support CSS3 animations.
81 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arve0/react-animate-on-change/5e1cd6cc8d3fe1c9cb45608a643710de86ede7f2/demo.gif
--------------------------------------------------------------------------------
/example/index.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-animate-on-change exmaple
8 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { createStore, Store } from 'redux'
4 | import { connect, Provider } from 'react-redux'
5 | import AnimateOnChange from '../index.js'
6 |
7 | const initialState = {
8 | diff: 0,
9 | score: 0
10 | }
11 |
12 | interface Action {
13 | type: string,
14 | diff: number
15 | }
16 |
17 | const reducer = (state = initialState, action: Action) => {
18 | switch (action.type) {
19 | case 'INCREMENT_SCORE':
20 | return { ...state,
21 | diff: action.diff,
22 | score: state.score + action.diff }
23 | default:
24 | return state
25 | }
26 | }
27 |
28 | const store: Store = createStore(reducer)
29 | setInterval(() => {
30 | store.dispatch({
31 | type: 'INCREMENT_SCORE',
32 | diff: 10
33 | })
34 | }, 2000)
35 |
36 | interface Props {
37 | diff: number,
38 | score: number,
39 | }
40 |
41 | function handleClick(): void {
42 | console.log('click!');
43 | }
44 |
45 | const AppComponent = ({ diff, score }: Props) =>
46 |
47 |
54 | Score: {score}
55 |
56 |
57 |
58 | const App = connect(s => s)(AppComponent)
59 |
60 | // @ts-ignore
61 | ReactDOM.render(,
62 | document.getElementById('root'))
63 |
--------------------------------------------------------------------------------
/example/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arve0/react-animate-on-change/5e1cd6cc8d3fe1c9cb45608a643710de86ede7f2/example/style.css
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 |
3 | module.exports = {
4 | entry: path.resolve(__dirname, 'index.tsx'),
5 | output: {
6 | path: path.resolve(__dirname),
7 | filename: 'bundle.js'
8 | },
9 | module: {
10 | rules: [
11 | {
12 | test: /\.tsx?$/,
13 | use: 'ts-loader',
14 | exclude: /node_modules/
15 | },
16 | ]
17 | },
18 | resolve: {
19 | extensions: [ '.tsx', '.ts', '.js' ]
20 | },
21 | mode: 'development'
22 | }
23 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | export interface Props {
3 | children: any;
4 | animate: boolean;
5 | baseClassName: string;
6 | animationClassName: string;
7 | customTag?: string;
8 | onAnimationEnd?: () => void;
9 | }
10 | export interface State {
11 | animating: boolean;
12 | clearAnimationClass: boolean;
13 | }
14 | interface AnimateOnChange {
15 | elm: HTMLElement;
16 | setElementRef: (ref: HTMLElement) => void;
17 | }
18 | /**
19 | * # AnimateOnChange component.
20 | * Adds `animationClassName` when `animate` is true, then removes
21 | * `animationClassName` when animation is done (event `animationend` is
22 | * triggered).
23 | *
24 | * @prop {string} baseClassName - Base class name.
25 | * @prop {string} animationClassName - Class added when `animate == true`.
26 | * @prop {bool} animate - Wheter to animate component.
27 | */
28 | declare class AnimateOnChange extends Component implements AnimateOnChange {
29 | constructor(props: Props);
30 | componentDidMount(): void;
31 | componentWillUnmount(): void;
32 | addEventListener(type: string, elm: HTMLElement, eventHandler: (e: Event) => void): void;
33 | removeEventListeners(type: string, elm: HTMLElement, eventHandler: (e: Event) => void): void;
34 | updateEvents(type: string, newEvent: string): void;
35 | animationStart(e: Event): void;
36 | animationEnd(e: Event): void;
37 | shouldComponentUpdate(nextProps: Props, nextState: State): boolean;
38 | render(): JSX.Element;
39 | }
40 | export default AnimateOnChange;
41 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = Object.setPrototypeOf ||
4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
6 | return function (d, b) {
7 | extendStatics(d, b);
8 | function __() { this.constructor = d; }
9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
10 | };
11 | })();
12 | var __assign = (this && this.__assign) || Object.assign || function(t) {
13 | for (var s, i = 1, n = arguments.length; i < n; i++) {
14 | s = arguments[i];
15 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
16 | t[p] = s[p];
17 | }
18 | return t;
19 | };
20 | var __rest = (this && this.__rest) || function (s, e) {
21 | var t = {};
22 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
23 | t[p] = s[p];
24 | if (s != null && typeof Object.getOwnPropertySymbols === "function")
25 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
26 | t[p[i]] = s[p[i]];
27 | return t;
28 | };
29 | var __importStar = (this && this.__importStar) || function (mod) {
30 | if (mod && mod.__esModule) return mod;
31 | var result = {};
32 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
33 | result["default"] = mod;
34 | return result;
35 | };
36 | Object.defineProperty(exports, "__esModule", { value: true });
37 | var react_1 = __importStar(require("react"));
38 | var events = {
39 | start: ['animationstart', 'webkitAnimationStart', 'mozAnimationStart', 'oanimationstart', 'MSAnimationStart'],
40 | end: ['animationend', 'webkitAnimationEnd', 'mozAnimationEnd', 'oanimationend', 'MSAnimationEnd'],
41 | startRemoved: [],
42 | endRemoved: []
43 | };
44 | /**
45 | * # AnimateOnChange component.
46 | * Adds `animationClassName` when `animate` is true, then removes
47 | * `animationClassName` when animation is done (event `animationend` is
48 | * triggered).
49 | *
50 | * @prop {string} baseClassName - Base class name.
51 | * @prop {string} animationClassName - Class added when `animate == true`.
52 | * @prop {bool} animate - Wheter to animate component.
53 | */
54 | var AnimateOnChange = /** @class */ (function (_super) {
55 | __extends(AnimateOnChange, _super);
56 | function AnimateOnChange(props) {
57 | var _this = _super.call(this, props) || this;
58 | _this.state = { animating: false, clearAnimationClass: false };
59 | _this.animationStart = _this.animationStart.bind(_this);
60 | _this.animationEnd = _this.animationEnd.bind(_this);
61 | _this.setElementRef = function (ref) {
62 | _this.elm = ref;
63 | };
64 | return _this;
65 | }
66 | AnimateOnChange.prototype.componentDidMount = function () {
67 | this.addEventListener('start', this.elm, this.animationStart);
68 | this.addEventListener('end', this.elm, this.animationEnd);
69 | };
70 | AnimateOnChange.prototype.componentWillUnmount = function () {
71 | this.removeEventListeners('start', this.elm, this.animationStart);
72 | this.removeEventListeners('end', this.elm, this.animationEnd);
73 | };
74 | AnimateOnChange.prototype.addEventListener = function (type, elm, eventHandler) {
75 | // until an event has been triggered bind them all
76 | events[type].map(function (event) {
77 | // console.log(`adding ${event}`)
78 | // @ts-ignore
79 | elm.addEventListener(event, eventHandler);
80 | });
81 | };
82 | AnimateOnChange.prototype.removeEventListeners = function (type, elm, eventHandler) {
83 | events[type].map(function (event) {
84 | // console.log(`removing ${event}`)
85 | // @ts-ignore
86 | elm.removeEventListener(event, eventHandler);
87 | });
88 | };
89 | AnimateOnChange.prototype.updateEvents = function (type, newEvent) {
90 | // console.log(`updating ${type} event to ${newEvent}`)
91 | events[type + 'Removed'] = events[type].filter(function (e) { return e !== newEvent; });
92 | events[type] = [newEvent];
93 | };
94 | AnimateOnChange.prototype.animationStart = function (e) {
95 | if (events['start'].length > 1) {
96 | this.updateEvents('start', e.type);
97 | this.removeEventListeners('startRemoved', this.elm, this.animationStart);
98 | }
99 | this.setState({ animating: true, clearAnimationClass: false });
100 | };
101 | AnimateOnChange.prototype.animationEnd = function (e) {
102 | if (events['end'].length > 1) {
103 | this.updateEvents('end', e.type);
104 | this.removeEventListeners('endRemoved', this.elm, this.animationStart);
105 | }
106 | // send separate, animation state change will not render
107 | this.setState({ clearAnimationClass: true }); // renders
108 | this.setState({ animating: false, clearAnimationClass: false });
109 | if (typeof this.props.onAnimationEnd === 'function') {
110 | this.props.onAnimationEnd();
111 | }
112 | };
113 | AnimateOnChange.prototype.shouldComponentUpdate = function (nextProps, nextState) {
114 | if (this.state.animating !== nextState.animating) {
115 | // do not render on animation change
116 | return false;
117 | }
118 | return true;
119 | };
120 | AnimateOnChange.prototype.render = function () {
121 | var clearAnimationClass = this.state.clearAnimationClass;
122 | var _a = this.props, baseClassName = _a.baseClassName, animate = _a.animate, animationClassName = _a.animationClassName, customTag = _a.customTag, children = _a.children, onAnimationEnd = _a.onAnimationEnd, // unpack, such that otherProps does not contain it
123 | otherProps = __rest(_a, ["baseClassName", "animate", "animationClassName", "customTag", "children", "onAnimationEnd"]);
124 | var className = baseClassName;
125 | if (animate && !clearAnimationClass) {
126 | className += " " + animationClassName;
127 | }
128 | var Tag = customTag || 'span';
129 | return react_1.default.createElement(Tag, __assign({ ref: this.setElementRef, className: className }, otherProps), children);
130 | };
131 | return AnimateOnChange;
132 | }(react_1.Component));
133 | exports.default = AnimateOnChange;
134 |
--------------------------------------------------------------------------------
/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ReactInstance } from 'react'
2 |
3 | interface Events {
4 | [index:string]: string[]
5 | }
6 |
7 | const events: Events = {
8 | start: ['animationstart', 'webkitAnimationStart', 'mozAnimationStart', 'oanimationstart', 'MSAnimationStart'],
9 | end: ['animationend', 'webkitAnimationEnd', 'mozAnimationEnd', 'oanimationend', 'MSAnimationEnd'],
10 | startRemoved: [],
11 | endRemoved: []
12 | }
13 |
14 | export interface Props {
15 | children: any,
16 | animate: boolean,
17 | baseClassName: string,
18 | animationClassName: string,
19 | customTag?: string,
20 | onAnimationEnd?: () => void,
21 | }
22 |
23 | export interface State {
24 | animating: boolean,
25 | clearAnimationClass: boolean
26 | }
27 |
28 | interface AnimateOnChange {
29 | elm: HTMLElement,
30 | setElementRef: (ref: HTMLElement) => void
31 | }
32 |
33 | /**
34 | * # AnimateOnChange component.
35 | * Adds `animationClassName` when `animate` is true, then removes
36 | * `animationClassName` when animation is done (event `animationend` is
37 | * triggered).
38 | *
39 | * @prop {string} baseClassName - Base class name.
40 | * @prop {string} animationClassName - Class added when `animate == true`.
41 | * @prop {bool} animate - Wheter to animate component.
42 | */
43 | class AnimateOnChange extends Component implements AnimateOnChange {
44 | constructor (props: Props) {
45 | super(props)
46 | this.state = { animating: false, clearAnimationClass: false }
47 | this.animationStart = this.animationStart.bind(this)
48 | this.animationEnd = this.animationEnd.bind(this)
49 |
50 | this.setElementRef = (ref) => {
51 | this.elm = ref
52 | }
53 | }
54 |
55 | componentDidMount () {
56 | this.addEventListener('start', this.elm, this.animationStart)
57 | this.addEventListener('end', this.elm, this.animationEnd)
58 | }
59 |
60 | componentWillUnmount () {
61 | this.removeEventListeners('start', this.elm, this.animationStart)
62 | this.removeEventListeners('end', this.elm, this.animationEnd)
63 | }
64 |
65 | addEventListener (type: string, elm: HTMLElement, eventHandler: (e: Event) => void) {
66 | // until an event has been triggered bind them all
67 | events[type].map(event => {
68 | // console.log(`adding ${event}`)
69 | // @ts-ignore
70 | elm.addEventListener(event, eventHandler)
71 | })
72 | }
73 |
74 | removeEventListeners (type: string, elm: HTMLElement, eventHandler: (e: Event) => void) {
75 | events[type].map(event => {
76 | // console.log(`removing ${event}`)
77 | // @ts-ignore
78 | elm.removeEventListener(event, eventHandler)
79 | })
80 | }
81 |
82 | updateEvents (type: string, newEvent: string) {
83 | // console.log(`updating ${type} event to ${newEvent}`)
84 | events[type + 'Removed'] = events[type].filter(e => e !== newEvent)
85 | events[type] = [newEvent]
86 | }
87 |
88 | animationStart (e: Event) {
89 | if (events['start'].length > 1) {
90 | this.updateEvents('start', e.type)
91 | this.removeEventListeners('startRemoved', this.elm, this.animationStart)
92 | }
93 | this.setState({ animating: true, clearAnimationClass: false })
94 | }
95 |
96 | animationEnd (e: Event) {
97 | if (events['end'].length > 1) {
98 | this.updateEvents('end', e.type)
99 | this.removeEventListeners('endRemoved', this.elm, this.animationStart)
100 | }
101 | // send separate, animation state change will not render
102 | this.setState({ clearAnimationClass: true }) // renders
103 | this.setState({ animating: false, clearAnimationClass: false })
104 |
105 | if (typeof this.props.onAnimationEnd === 'function') {
106 | this.props.onAnimationEnd()
107 | }
108 | }
109 |
110 | shouldComponentUpdate (nextProps: Props, nextState: State) {
111 | if (this.state.animating !== nextState.animating) {
112 | // do not render on animation change
113 | return false
114 | }
115 | return true
116 | }
117 |
118 | render () {
119 | const { clearAnimationClass } = this.state;
120 | const {
121 | baseClassName,
122 | animate,
123 | animationClassName,
124 | customTag,
125 | children,
126 | onAnimationEnd, // unpack, such that otherProps does not contain it
127 | ...otherProps
128 | } = this.props;
129 |
130 | let className = baseClassName
131 |
132 | if (animate && !clearAnimationClass) {
133 | className += ` ${animationClassName}`
134 | }
135 |
136 | let Tag = customTag || 'span';
137 |
138 | return
139 | {children}
140 |
141 | }
142 | }
143 |
144 | export default AnimateOnChange
145 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-animate-on-change",
3 | "version": "2.2.0",
4 | "description": "Animate your components on state change",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "tsc",
8 | "build-example": "cd example && webpack",
9 | "postversion": "npm run build && npm run build-example && git add . && git commit --amend --no-edit",
10 | "postpublish": "git push && git push --tags",
11 | "test": "npm run build && mocha"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/arve0/react-animate-on-change.git"
16 | },
17 | "keywords": [
18 | "react-component",
19 | "react",
20 | "animate",
21 | "animation",
22 | "css"
23 | ],
24 | "author": "Arve Seljebu",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/arve0/react-animate-on-change/issues"
28 | },
29 | "homepage": "https://github.com/arve0/react-animate-on-change#readme",
30 | "devDependencies": {
31 | "@types/node": "^10.3.1",
32 | "@types/react": "^16.3.16",
33 | "@types/react-dom": "^16.0.6",
34 | "@types/react-redux": "^6.0.2",
35 | "debug": "^3.1.0",
36 | "mocha": "^6.2.3",
37 | "puppeteer-firefox": "^0.5.0",
38 | "react": ">15.0.0",
39 | "react-dom": ">15.0.0",
40 | "react-redux": "^5.0.7",
41 | "redux": "^4.0.0",
42 | "ts-loader": "^4.4.1",
43 | "typescript": "^2.9.1",
44 | "webpack": "^4.42.1",
45 | "webpack-cli": "^3.0.3"
46 | },
47 | "peerDependencies": {
48 | "react": ">15.0.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Browser testing with puppeteer
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/server.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const fs = require("fs");
3 | const { normalize, join } = require("path");
4 |
5 | let rootPath;
6 | const mimeTypes = {
7 | 'html': 'text/html',
8 | 'jpeg': 'image/jpeg',
9 | 'jpg': 'image/jpeg',
10 | 'png': 'image/png',
11 | 'js': 'text/javascript',
12 | 'css': 'text/css',
13 | 'default': 'text/plain',
14 | };
15 |
16 | const webserver = http.createServer(sendFile);
17 |
18 | function sendFile(request, response) {
19 | let path = request.url || '';
20 | let safePath = normalize(path).replace('^(\.\.[\/\\])+', '');
21 | if (safePath === '' || safePath === '/' || safePath === '\\') {
22 | safePath = 'index.html';
23 | }
24 | let filename = join(rootPath, safePath);
25 | if (!fs.existsSync(filename)) {
26 | console.log('Server: 404 - ' + request.method + ': ' + request.url);
27 | response.statusCode = 404;
28 | response.end('File not found.');
29 | }
30 | else {
31 | // console.log('Server: 200 - ' + request.method + ': ' + request.url);
32 | let headers = {
33 | // @ts-ignore
34 | 'Content-Type': mimeTypes[filename.split('.').pop()] || mimeTypes['default']
35 | };
36 | response.writeHead(200, headers);
37 | response.end(fs.readFileSync(filename));
38 | }
39 | }
40 |
41 | function start(path = __dirname, port = 8888) {
42 | rootPath = path
43 | return new Promise((resolve, reject) => {
44 | webserver.listen(port, (err) => {
45 | if (err) {
46 | return reject(err);
47 | }
48 | console.log(`Server: Listening on port ${port}`);
49 | resolve();
50 | });
51 | });
52 | }
53 | exports.start = start;
54 | function shutdown() {
55 | webserver.close();
56 | }
57 | exports.shutdown = shutdown;
58 | //# sourceMappingURL=server.js.map
59 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const server = require('./server')
2 | const configuration = require('../package.json')
3 | const puppeteer = configuration.devDependencies.puppeteer
4 | ? require('puppeteer')
5 | : require('puppeteer-firefox');
6 | const path = require('path')
7 | const assert = require('assert')
8 |
9 | // configuration
10 | const rootPath = path.join(__dirname, '..') // which folder to serve over http
11 | const port = 8888 // which port to use for http server
12 | const mainPage = `http://localhost:${port}/test/index.html`
13 | const headless = true // false: show browser, true: hide browser
14 | const slowMo = false // true: each browser action will take 100 milliseconds
15 | ? 100
16 | : 0
17 |
18 | const ANIMATION_TIME = slowMo ? 1000 : 100
19 | const ANIMATION_SETTLE = slowMo ? 500 : 50
20 | const STYLE = `
21 | #root {
22 | margin-top: 50vh;
23 | margin-left: 50vw;
24 | }
25 | .base {
26 | background-color: black;
27 | color: white;
28 | border-radius: 3px;
29 | padding: 5px;
30 | width: 100px;
31 | }
32 | .fade {
33 | animation-name: fade-in;
34 | animation-duration: ${ANIMATION_TIME}ms;
35 | }
36 | @keyframes fade-in {
37 | 0% { opacity: 0; }
38 | 100% { opacity: 1; }
39 | }`
40 |
41 | // globals
42 | let browser = null
43 | let page = null
44 |
45 | before(async function () {
46 | this.timeout(30 * 1000) // starting browser may take more than 2 seconds
47 |
48 | await server.start(rootPath, port)
49 | browser = await puppeteer.launch({ headless, slowMo })
50 | page = (await browser.pages())[0]
51 |
52 | page.on('console', async function (msg) {
53 | if (msg.type() === 'error' && msg.args().length) {
54 | let args = await Promise.all(msg.args().map(arg => arg.jsonValue()))
55 | console.error("Browser console.error:", ...args)
56 | } else {
57 | console.log(msg.text())
58 | }
59 | })
60 | })
61 |
62 | beforeEach(async function () {
63 | await page.goto(mainPage)
64 | await page.addStyleTag({ content: STYLE })
65 | await page.evaluate(setup)
66 | })
67 |
68 | after(function () {
69 | browser.close()
70 | server.shutdown()
71 | })
72 |
73 | describe('tests', function () {
74 | this.timeout(slowMo === 0 ? 2000 : 0)
75 |
76 | it('should render to dom', async () => {
77 | await page.evaluate(renderAnimated)
78 | await page.waitFor(10)
79 | const children = await page.evaluate(() => root.children.length)
80 | assert.equal(children, 1, 'no children in root')
81 | const tag = await page.evaluate(() => root.children[0].tagName)
82 | assert.equal(tag, 'SPAN', `custom tag 'span' not rendered, got ${tag}`)
83 | })
84 |
85 | it('animation class name is added on enter', async () => {
86 | await page.evaluate(renderAnimated)
87 | await page.waitFor(10)
88 | const fadeElement = await page.$('.fade')
89 | assert.notDeepStrictEqual(fadeElement, null, 'animation class not added')
90 | })
91 |
92 | it('removes animation class', async () => {
93 | await page.evaluate(renderAnimated)
94 | await page.waitFor(ANIMATION_TIME + ANIMATION_SETTLE)
95 | const fadeElement = await page.$('.fade')
96 | assert.deepStrictEqual(fadeElement, null, 'animation class not removed')
97 | })
98 |
99 | it('adds animation class on props change', async () => {
100 | await page.evaluate(renderUpdateProps, ANIMATION_TIME, ANIMATION_SETTLE)
101 |
102 | await page.waitFor(ANIMATION_TIME + ANIMATION_SETTLE)
103 | let fadeElement = await page.$('.fade')
104 | assert.deepStrictEqual(fadeElement, null, 'animation class not removed')
105 |
106 | await page.waitFor(ANIMATION_SETTLE + 0.5 * ANIMATION_TIME)
107 | const textContent = await page.$eval('.fade', element => element.textContent)
108 | assert.equal(textContent, 'updated text')
109 | })
110 |
111 | it('define custom tag', async () => {
112 | await page.evaluate(renderAnimated, { tag: 'div' })
113 | const tag = await page.$eval('#root', root => root.children[0].tagName)
114 | assert.equal(tag, 'DIV', `custom tag 'div' not rendered, got ${tag}`)
115 | })
116 |
117 | it('calls back when animation complete', async () => {
118 | await page.evaluate(renderAnimated, { cb: true })
119 | await page.waitFor(ANIMATION_TIME + ANIMATION_SETTLE)
120 | const callCount = await page.$eval('#root', root => root.callCount)
121 | assert.equal(callCount, 1)
122 | })
123 |
124 | it('should support other props', async () => {
125 | await page.evaluate(renderAnimated, { someProp: 'asdf' })
126 | const someProp = await page.$eval('.base', base => base.getAttribute('someProp'))
127 | assert.equal(someProp, 'asdf', 'attribute someProp not rendered to dom')
128 | })
129 | })
130 |
131 | function setup() {
132 | window.root = document.getElementById('root')
133 |
134 | window.Animated = ({ children = 'text', tag = 'span', cb, ...rest }) =>
135 | React.createElement(AnimateOnChange, Object.assign({
136 | baseClassName: "base",
137 | animationClassName: "fade",
138 | animate: true,
139 | customTag: tag,
140 | onAnimationEnd: cb
141 | }, rest), children)
142 |
143 | class UpdatePropsAfterAnimationTimePlus2xAnimationSettle extends React.Component {
144 | constructor(props) {
145 | super(props)
146 | this.state = {
147 | text: 'initial text'
148 | }
149 | setTimeout(() => {
150 | this.setState({ text: 'updated text' })
151 | }, props.ANIMATION_TIME + 2 * props.ANIMATION_SETTLE)
152 | }
153 | render() {
154 | return React.createElement(AnimateOnChange, {
155 | baseClassName: "base",
156 | animationClassName: "fade",
157 | animate: true,
158 | }, this.state.text)
159 | }
160 | }
161 | window.UpdatePropsAfterAnimationTimePlus2xAnimationSettle = UpdatePropsAfterAnimationTimePlus2xAnimationSettle
162 | }
163 |
164 | function renderAnimated(props) {
165 | if (props && props.cb) {
166 | props.cb = () => {
167 | if (!root.callCount) { root.callCount = 0 }
168 | root.callCount += 1
169 | }
170 | }
171 | ReactDOM.render(React.createElement(Animated, props), root)
172 | }
173 |
174 | // AnimateOnChange with prop change after ANIMATION_TIME + 2 * ANIMATION_SETTLE
175 | function renderUpdateProps(ANIMATION_TIME, ANIMATION_SETTLE) {
176 | ReactDOM.render(React.createElement(UpdatePropsAfterAnimationTimePlus2xAnimationSettle, {
177 | ANIMATION_TIME, ANIMATION_SETTLE
178 | }), root)
179 | }
180 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "*.tsx"
4 | ],
5 | "compilerOptions": {
6 | "target": "es5",
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "jsx": "react",
10 | "declaration": true,
11 | "strict": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------