From 91f63f3e0fb9088030e8a67a26cf6cd34930ece5 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 May 2018 09:53:57 +0800 Subject: [PATCH 01/41] transition check inline --- examples/alert.js | 6 +- examples/hide-todo.js | 5 +- examples/simple-animation.js | 32 ++-- examples/simple-remove.js | 2 +- examples/simple.js | 6 +- examples/todo-animation.js | 8 +- examples/todo.js | 6 +- examples/transitionAppear.js | 32 ++-- examples/transitionLeave.js | 2 +- package.json | 7 +- src/Animate.js | 336 --------------------------------- src/Animate.jsx | 123 ++++++++++++ src/AnimateChild.js | 86 --------- src/AnimateChild.jsx | 173 +++++++++++++++++ src/ChildrenUtils.js | 101 ---------- src/util.js | 52 ++--- tests/multiple.spec.js | 6 +- tests/single-animation.spec.js | 8 +- tests/single-common.spec.js | 28 +-- tests/single.spec.js | 9 +- 20 files changed, 405 insertions(+), 623 deletions(-) delete mode 100644 src/Animate.js create mode 100644 src/Animate.jsx delete mode 100644 src/AnimateChild.js create mode 100644 src/AnimateChild.jsx delete mode 100644 src/ChildrenUtils.js diff --git a/examples/alert.js b/examples/alert.js index 07a876a..359f650 100644 --- a/examples/alert.js +++ b/examples/alert.js @@ -1,10 +1,11 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import './assets/index.less'; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import Animate from 'rc-animate'; +import './assets/index.less'; + let seed = 0; class Alert extends React.Component { @@ -77,13 +78,12 @@ class AlertGroup extends React.Component { render() { const alerts = this.state.alerts; - const self = this; const children = alerts.map((a) => { if (!a.key) { seed++; a.key = String(seed); } - return ; + return { this.onEnd(a.key); }} />; }); const style = { position: 'fixed', diff --git a/examples/hide-todo.js b/examples/hide-todo.js index 43daf1b..ffdbbf5 100644 --- a/examples/hide-todo.js +++ b/examples/hide-todo.js @@ -1,16 +1,17 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import './assets/index.less'; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import Animate from 'rc-animate'; +import './assets/index.less'; class Todo extends React.Component { static propTypes = { children: PropTypes.any, end: PropTypes.func, onClick: PropTypes.func, + visible: PropTypes.bool, } static defaultProps = { @@ -66,7 +67,7 @@ class TodoList extends React.Component { { this.handleHide(i, item); }} > {item.content} diff --git a/examples/simple-animation.js b/examples/simple-animation.js index 303e098..bd8e8d6 100644 --- a/examples/simple-animation.js +++ b/examples/simple-animation.js @@ -1,27 +1,25 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import './assets/index.less'; import Animate from 'rc-animate'; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import velocity from 'velocity-animate'; +import './assets/index.less'; -class Box extends React.Component { - static propTypes = { - visible: PropTypes.bool, - } +const Box = () => { + const style = { + width: '200px', + display: this.props.visible ? 'block' : 'none', + height: '200px', + backgroundColor: 'red', + }; + return (
); +}; - render() { - const style = { - width: '200px', - display: this.props.visible ? 'block' : 'none', - height: '200px', - backgroundColor: 'red', - }; - return (
); - } -} +Box.propTypes = { + visible: PropTypes.bool, +}; class Demo extends React.Component { state = { @@ -108,14 +106,14 @@ class Demo extends React.Component {
  diff --git a/examples/simple-remove.js b/examples/simple-remove.js index 3b97447..1ecd0d3 100644 --- a/examples/simple-remove.js +++ b/examples/simple-remove.js @@ -1,9 +1,9 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import './assets/index.less'; import Animate from 'rc-animate'; import React from 'react'; import ReactDOM from 'react-dom'; +import './assets/index.less'; class Demo extends React.Component { state = { diff --git a/examples/simple.js b/examples/simple.js index 29e7d21..47e8c9c 100644 --- a/examples/simple.js +++ b/examples/simple.js @@ -1,10 +1,10 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import './assets/slow.less'; import Animate from 'rc-animate'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; +import './assets/slow.less'; const Div = (props) => { const { style, show, ...restProps } = props; @@ -43,14 +43,14 @@ class Demo extends Component {
  diff --git a/examples/todo-animation.js b/examples/todo-animation.js index c0f45d9..1fa0c56 100644 --- a/examples/todo-animation.js +++ b/examples/todo-animation.js @@ -1,11 +1,11 @@ /* eslint no-console:0, react/no-multi-comp:0, no-alert:0 */ -import './assets/index.less'; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import Animate from 'rc-animate'; import velocity from 'velocity-animate'; +import './assets/index.less'; class Todo extends React.Component { static propTypes = { @@ -94,7 +94,7 @@ class TodoList extends React.Component { handleAdd = () => { const newItems = - this.state.items.concat([prompt('Enter some text')]); + this.state.items.concat([prompt('Enter some text')]); // eslint-disable-line this.setState({ items: newItems }); } @@ -113,7 +113,7 @@ class TodoList extends React.Component { render() { const items = this.state.items.map((item, i) => { return ( - + { this.handleRemove(i); }}> {item} ); @@ -129,7 +129,7 @@ class TodoList extends React.Component { diff --git a/examples/todo.js b/examples/todo.js index 3f88f8f..a8e5fc4 100644 --- a/examples/todo.js +++ b/examples/todo.js @@ -1,10 +1,10 @@ /* eslint no-console:0, react/no-multi-comp:0, no-alert:0 */ -import './assets/index.less'; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import Animate from 'rc-animate'; +import './assets/index.less'; class Todo extends React.Component { static propTypes = { @@ -45,7 +45,7 @@ class TodoList extends React.Component { } handleAdd = () => { - const items = this.state.items.concat([prompt('Enter some text')]); + const items = this.state.items.concat([prompt('Enter some text')]); // eslint-disable-line this.setState({ items }); } @@ -58,7 +58,7 @@ class TodoList extends React.Component { render() { const items = this.state.items.map((item, i) => { return ( - + { this.handleRemove(i) }}> {item} ); diff --git a/examples/transitionAppear.js b/examples/transitionAppear.js index 95d9071..1f0a811 100644 --- a/examples/transitionAppear.js +++ b/examples/transitionAppear.js @@ -1,28 +1,26 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import './assets/slow.less'; import Animate from 'rc-animate'; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; +import './assets/slow.less'; -class Box extends React.Component { - static propTypes = { - visible: PropTypes.bool, - } +const Box = () => { + console.log('render', this.props.visible); + const style = { + display: this.props.visible ? 'block' : 'none', + marginTop: '20px', + width: '200px', + height: '200px', + backgroundColor: 'red', + }; + return (
); +}; - render() { - console.log('render', this.props.visible); - const style = { - display: this.props.visible ? 'block' : 'none', - marginTop: '20px', - width: '200px', - height: '200px', - backgroundColor: 'red', - }; - return (
); - } -} +Box.propTypes = { + visible: PropTypes.bool, +}; class Demo extends React.Component { state = { diff --git a/examples/transitionLeave.js b/examples/transitionLeave.js index a49d616..6cd1fa0 100644 --- a/examples/transitionLeave.js +++ b/examples/transitionLeave.js @@ -1,9 +1,9 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import './assets/index.less'; import Animate from 'rc-animate'; import React from 'react'; import ReactDOM from 'react-dom'; +import './assets/index.less'; class Demo extends React.Component { state = { diff --git a/package.json b/package.json index 9e3d1ff..ff02791 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "jquery": "^3.3.1", "pre-commit": "1.x", "rc-test": "6.x", - "rc-tools": "6.x", + "rc-tools": "8.x", "react": "^16.0.0", "react-dom": "^16.0.0", "velocity-animate": "~1.2.2" @@ -56,7 +56,8 @@ ], "dependencies": { "babel-runtime": "6.x", - "css-animation": "^1.3.2", - "prop-types": "15.x" + "classnames": "^2.2.5", + "prop-types": "15.x", + "react-lifecycles-compat": "^3.0.4" } } diff --git a/src/Animate.js b/src/Animate.js deleted file mode 100644 index 13ae9a6..0000000 --- a/src/Animate.js +++ /dev/null @@ -1,336 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - toArrayChildren, - mergeChildren, - findShownChildInChildrenByKey, - findChildInChildrenByKey, - isSameChildren, -} from './ChildrenUtils'; -import AnimateChild from './AnimateChild'; -const defaultKey = `rc_animate_${Date.now()}`; -import animUtil from './util'; - -function getChildrenFromProps(props) { - const children = props.children; - if (React.isValidElement(children)) { - if (!children.key) { - return React.cloneElement(children, { - key: defaultKey, - }); - } - } - return children; -} - -function noop() { -} - -export default class Animate extends React.Component { - static isAnimate = true; // eslint-disable-line - - static propTypes = { - component: PropTypes.any, - componentProps: PropTypes.object, - animation: PropTypes.object, - transitionName: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - ]), - transitionEnter: PropTypes.bool, - transitionAppear: PropTypes.bool, - exclusive: PropTypes.bool, - transitionLeave: PropTypes.bool, - onEnd: PropTypes.func, - onEnter: PropTypes.func, - onLeave: PropTypes.func, - onAppear: PropTypes.func, - showProp: PropTypes.string, - children: PropTypes.node, - } - - static defaultProps = { - animation: {}, - component: 'span', - componentProps: {}, - transitionEnter: true, - transitionLeave: true, - transitionAppear: false, - onEnd: noop, - onEnter: noop, - onLeave: noop, - onAppear: noop, - } - - constructor(props) { - super(props); - - this.currentlyAnimatingKeys = {}; - this.keysToEnter = []; - this.keysToLeave = []; - - this.state = { - children: toArrayChildren(getChildrenFromProps(props)), - }; - - this.childrenRefs = {}; - } - - componentDidMount() { - const showProp = this.props.showProp; - let children = this.state.children; - if (showProp) { - children = children.filter((child) => { - return !!child.props[showProp]; - }); - } - children.forEach((child) => { - if (child) { - this.performAppear(child.key); - } - }); - } - - componentWillReceiveProps(nextProps) { - this.nextProps = nextProps; - const nextChildren = toArrayChildren(getChildrenFromProps(nextProps)); - const props = this.props; - // exclusive needs immediate response - if (props.exclusive) { - Object.keys(this.currentlyAnimatingKeys).forEach((key) => { - this.stop(key); - }); - } - const showProp = props.showProp; - const currentlyAnimatingKeys = this.currentlyAnimatingKeys; - // last props children if exclusive - const currentChildren = props.exclusive ? - toArrayChildren(getChildrenFromProps(props)) : - this.state.children; - // in case destroy in showProp mode - let newChildren = []; - if (showProp) { - currentChildren.forEach((currentChild) => { - const nextChild = currentChild && findChildInChildrenByKey(nextChildren, currentChild.key); - let newChild; - if ((!nextChild || !nextChild.props[showProp]) && currentChild.props[showProp]) { - newChild = React.cloneElement(nextChild || currentChild, { - [showProp]: true, - }); - } else { - newChild = nextChild; - } - if (newChild) { - newChildren.push(newChild); - } - }); - nextChildren.forEach((nextChild) => { - if (!nextChild || !findChildInChildrenByKey(currentChildren, nextChild.key)) { - newChildren.push(nextChild); - } - }); - } else { - newChildren = mergeChildren( - currentChildren, - nextChildren - ); - } - - // need render to avoid update - this.setState({ - children: newChildren, - }); - - nextChildren.forEach((child) => { - const key = child && child.key; - if (child && currentlyAnimatingKeys[key]) { - return; - } - const hasPrev = child && findChildInChildrenByKey(currentChildren, key); - if (showProp) { - const showInNext = child.props[showProp]; - if (hasPrev) { - const showInNow = findShownChildInChildrenByKey(currentChildren, key, showProp); - if (!showInNow && showInNext) { - this.keysToEnter.push(key); - } - } else if (showInNext) { - this.keysToEnter.push(key); - } - } else if (!hasPrev) { - this.keysToEnter.push(key); - } - }); - - currentChildren.forEach((child) => { - const key = child && child.key; - if (child && currentlyAnimatingKeys[key]) { - return; - } - const hasNext = child && findChildInChildrenByKey(nextChildren, key); - if (showProp) { - const showInNow = child.props[showProp]; - if (hasNext) { - const showInNext = findShownChildInChildrenByKey(nextChildren, key, showProp); - if (!showInNext && showInNow) { - this.keysToLeave.push(key); - } - } else if (showInNow) { - this.keysToLeave.push(key); - } - } else if (!hasNext) { - this.keysToLeave.push(key); - } - }); - } - - componentDidUpdate() { - const keysToEnter = this.keysToEnter; - this.keysToEnter = []; - keysToEnter.forEach(this.performEnter); - const keysToLeave = this.keysToLeave; - this.keysToLeave = []; - keysToLeave.forEach(this.performLeave); - } - - performEnter = (key) => { - // may already remove by exclusive - if (this.childrenRefs[key]) { - this.currentlyAnimatingKeys[key] = true; - this.childrenRefs[key].componentWillEnter( - this.handleDoneAdding.bind(this, key, 'enter') - ); - } - } - - performAppear = (key) => { - if (this.childrenRefs[key]) { - this.currentlyAnimatingKeys[key] = true; - this.childrenRefs[key].componentWillAppear( - this.handleDoneAdding.bind(this, key, 'appear') - ); - } - } - - handleDoneAdding = (key, type) => { - const props = this.props; - delete this.currentlyAnimatingKeys[key]; - // if update on exclusive mode, skip check - if (props.exclusive && props !== this.nextProps) { - return; - } - const currentChildren = toArrayChildren(getChildrenFromProps(props)); - if (!this.isValidChildByKey(currentChildren, key)) { - // exclusive will not need this - this.performLeave(key); - } else { - if (type === 'appear') { - if (animUtil.allowAppearCallback(props)) { - props.onAppear(key); - props.onEnd(key, true); - } - } else { - if (animUtil.allowEnterCallback(props)) { - props.onEnter(key); - props.onEnd(key, true); - } - } - } - } - - performLeave = (key) => { - // may already remove by exclusive - if (this.childrenRefs[key]) { - this.currentlyAnimatingKeys[key] = true; - this.childrenRefs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key)); - } - } - - handleDoneLeaving = (key) => { - const props = this.props; - delete this.currentlyAnimatingKeys[key]; - // if update on exclusive mode, skip check - if (props.exclusive && props !== this.nextProps) { - return; - } - const currentChildren = toArrayChildren(getChildrenFromProps(props)); - // in case state change is too fast - if (this.isValidChildByKey(currentChildren, key)) { - this.performEnter(key); - } else { - const end = () => { - if (animUtil.allowLeaveCallback(props)) { - props.onLeave(key); - props.onEnd(key, false); - } - }; - if (!isSameChildren(this.state.children, - currentChildren, props.showProp)) { - this.setState({ - children: currentChildren, - }, end); - } else { - end(); - } - } - } - - isValidChildByKey(currentChildren, key) { - const showProp = this.props.showProp; - if (showProp) { - return findShownChildInChildrenByKey(currentChildren, key, showProp); - } - return findChildInChildrenByKey(currentChildren, key); - } - - stop(key) { - delete this.currentlyAnimatingKeys[key]; - const component = this.childrenRefs[key]; - if (component) { - component.stop(); - } - } - - render() { - const props = this.props; - this.nextProps = props; - const stateChildren = this.state.children; - let children = null; - if (stateChildren) { - children = stateChildren.map((child) => { - if (child === null || child === undefined) { - return child; - } - if (!child.key) { - throw new Error('must set key for children'); - } - return ( - this.childrenRefs[child.key] = node} - animation={props.animation} - transitionName={props.transitionName} - transitionEnter={props.transitionEnter} - transitionAppear={props.transitionAppear} - transitionLeave={props.transitionLeave} - > - {child} - - ); - }); - } - const Component = props.component; - if (Component) { - let passedProps = props; - if (typeof Component === 'string') { - passedProps = { - className: props.className, - style: props.style, - ...props.componentProps, - }; - } - return {children}; - } - return children[0] || null; - } -} diff --git a/src/Animate.jsx b/src/Animate.jsx new file mode 100644 index 0000000..6183bed --- /dev/null +++ b/src/Animate.jsx @@ -0,0 +1,123 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { polyfill } from 'react-lifecycles-compat'; + +import AnimateChild from './AnimateChild'; +import { cloneProps } from './util'; + +const defaultKey = `rc_animate_${Date.now()}`; +const clonePropList = ['children']; + +class Animate extends React.Component { + // [Legacy] Not sure usage + // commit: https://github.com/react-component/animate/commit/0a1cbfd647407498b10a8c6602a2dea80b42e324 + static isAnimate = true; // eslint-disable-line + + static propTypes = { + component: PropTypes.any, + componentProps: PropTypes.object, + animation: PropTypes.object, + transitionName: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]), + transitionEnter: PropTypes.bool, + transitionAppear: PropTypes.bool, + exclusive: PropTypes.bool, + transitionLeave: PropTypes.bool, + onEnd: PropTypes.func, + onEnter: PropTypes.func, + onLeave: PropTypes.func, + onAppear: PropTypes.func, + showProp: PropTypes.string, + children: PropTypes.node, + style: PropTypes.object, + className: PropTypes.string, + } + + static defaultProps = { + animation: {}, + component: 'span', + componentProps: {}, + transitionEnter: true, + transitionLeave: true, + transitionAppear: false, + } + + static getDerivedStateFromProps(nextProps, prevState) { + const { prevProps = {} } = prevState; + const newState = { + prevProps: cloneProps(nextProps, clonePropList), + }; + + function processState(propName, updater) { + if (prevProps[propName] !== nextProps[propName]) { + updater(nextProps[propName]); + return true; + } + return false; + } + + processState('children', (children) => { + newState.children = (React.Children.toArray(children) || []) + .filter(node => node); + }); + + return newState; + } + + state = { + children: [], + }; + + render() { + const { children } = this.state; + const { + component: Component, componentProps, + className, style, showProp, + } = this.props; + + + const $children = children.map((node) => { + if (children.length > 1 && !node.key) { + throw new Error('must set key for children'); + } + + return ( + + {node} + + ); + }); + + // Wrap with component + if (Component) { + let passedProps = this.props; + if (typeof Component === 'string') { + passedProps = { + className, + style, + ...componentProps, + }; + } + + return ( + + {$children} + + ); + } + + return $children[0] || null; + } +} + +polyfill(Animate); + +export default Animate; \ No newline at end of file diff --git a/src/AnimateChild.js b/src/AnimateChild.js deleted file mode 100644 index 7a16321..0000000 --- a/src/AnimateChild.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import cssAnimate, { isCssAnimationSupported } from 'css-animation'; -import animUtil from './util'; - -const transitionMap = { - enter: 'transitionEnter', - appear: 'transitionAppear', - leave: 'transitionLeave', -}; - -export default class AnimateChild extends React.Component { - static propTypes = { - children: PropTypes.any, - } - - componentWillUnmount() { - this.stop(); - } - - componentWillEnter(done) { - if (animUtil.isEnterSupported(this.props)) { - this.transition('enter', done); - } else { - done(); - } - } - - componentWillAppear(done) { - if (animUtil.isAppearSupported(this.props)) { - this.transition('appear', done); - } else { - done(); - } - } - - componentWillLeave(done) { - if (animUtil.isLeaveSupported(this.props)) { - this.transition('leave', done); - } else { - // always sync, do not interupt with react component life cycle - // update hidden -> animate hidden -> - // didUpdate -> animate leave -> unmount (if animate is none) - done(); - } - } - - transition(animationType, finishCallback) { - const node = ReactDOM.findDOMNode(this); - const props = this.props; - const transitionName = props.transitionName; - const nameIsObj = typeof transitionName === 'object'; - this.stop(); - const end = () => { - this.stopper = null; - finishCallback(); - }; - if ((isCssAnimationSupported || !props.animation[animationType]) && - transitionName && props[transitionMap[animationType]]) { - const name = nameIsObj ? transitionName[animationType] : `${transitionName}-${animationType}`; - let activeName = `${name}-active`; - if (nameIsObj && transitionName[`${animationType}Active`]) { - activeName = transitionName[`${animationType}Active`]; - } - this.stopper = cssAnimate(node, { - name, - active: activeName, - }, end); - } else { - this.stopper = props.animation[animationType](node, end); - } - } - - stop() { - const stopper = this.stopper; - if (stopper) { - this.stopper = null; - stopper.stop(); - } - } - - render() { - return this.props.children; - } -} diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx new file mode 100644 index 0000000..a7c7be7 --- /dev/null +++ b/src/AnimateChild.jsx @@ -0,0 +1,173 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { polyfill } from 'react-lifecycles-compat'; +import classNames from 'classnames'; + +import { cloneProps, getTransitionName, supportTransition } from './util'; + +const clonePropList = [ + 'show', + 'children', +]; + +/** + * AnimateChild only accept one child node. + */ + +class AnimateChild extends React.Component { + static propTypes = { + transitionName: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]), + transitionAppear: PropTypes.bool, + transitionEnter: PropTypes.bool, + transitionLeave: PropTypes.bool, + exclusive: PropTypes.bool, + showProp: PropTypes.string, + } + + static getDerivedStateFromProps(nextProps, prevState) { + const { + appear, enter, leave, + prevProps = {}, + } = prevState; + const { + transitionName, transitionAppear, transitionEnter, transitionLeave, + exclusive, + } = nextProps; + + const newState = { + prevProps: cloneProps(nextProps, clonePropList), + }; + + function processState(propName, updater) { + if (prevProps[propName] !== nextProps[propName]) { + if (updater) { + updater(nextProps[propName]); + } + return true; + } + return false; + } + + // Child update. Only set child. + processState('children', (child) => { + newState.child = child; + }); + + // Show update + processState('show', (show) => { + newState.transitionQueue = prevState.transitionQueue.slice(); + + function pushTransition(transition) { + if (exclusive) { + newState.transitionQueue = [transition]; + newState.transitionActive = false; + } else { + newState.transitionQueue.push(transition); + } + } + + if (show && !appear && transitionAppear) { + pushTransition({ + basic: getTransitionName(transitionName, 'appear'), + active: getTransitionName(transitionName, 'appear-active'), + }); + } else if (show && !enter && transitionEnter) { + pushTransition({ + basic: getTransitionName(transitionName, 'enter'), + active: getTransitionName(transitionName, 'enter-active'), + }); + } else if (!show && !leave && transitionLeave) { + pushTransition({ + basic: getTransitionName(transitionName, 'leave'), + active: getTransitionName(transitionName, 'leave-active'), + }); + } + }); + + return newState; + } + + constructor() { + super(); + + // Since React 16.3+ use static getDerivedStateFromProps. We need cache ourselves. + this._prevChild = null; + this._cachedChild = null; + } + + state = { + child: null, + transitionQueue: [], + transitionActive: false, + + appear: false, // TODO: handle this + enter: false, + leave: false, + } + + componentDidMount() { + this.onDomUpdated(); + } + + componentDidUpdate() { + this.onDomUpdated(); + } + + onDomUpdated = () => { + console.log('did update!!!', this.state); + const { transitionQueue, transitionActive } = this.state; + const transition = transitionQueue[0]; + + if (transition && !transitionActive) { + // requestAnimationFrame not support in IE 9- + // Use setTimeout instead + setTimeout(() => { + this.setState({ transitionActive: true }); + }); + } + }; + + onMotionEnd = () => { + console.log('onMotionEnd'); + } + + render() { + const { child, transitionQueue, transitionActive } = this.state; + const { showProp } = this.props; + const { className, onTransitionEnd, onAnimationEnd } = child.props || {}; + + // Class name + const transition = transitionQueue[0]; + const connectClassName = transition ? classNames( + className, + transition.basic, + transitionActive && transition.active, + ) : className; + + // Clone child + return React.cloneElement(child, { + className: connectClassName, + [showProp]: supportTransition ? !!transition : child.props[showProp], + + onTransitionEnd: (...args) => { + this.onMotionEnd(...args); + if (onTransitionEnd) { + onTransitionEnd(...args); + } + }, + onAnimationEnd: (...args) => { + this.onMotionEnd(...args); + if (onAnimationEnd) { + onAnimationEnd(...args); + } + }, + }); + } +} + +polyfill(AnimateChild); + +export default AnimateChild; \ No newline at end of file diff --git a/src/ChildrenUtils.js b/src/ChildrenUtils.js deleted file mode 100644 index 2fc371f..0000000 --- a/src/ChildrenUtils.js +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; - -export function toArrayChildren(children) { - const ret = []; - React.Children.forEach(children, (child) => { - ret.push(child); - }); - return ret; -} - -export function findChildInChildrenByKey(children, key) { - let ret = null; - if (children) { - children.forEach((child) => { - if (ret) { - return; - } - if (child && child.key === key) { - ret = child; - } - }); - } - return ret; -} - -export function findShownChildInChildrenByKey(children, key, showProp) { - let ret = null; - if (children) { - children.forEach((child) => { - if (child && child.key === key && child.props[showProp]) { - if (ret) { - throw new Error('two child with same key for children'); - } - ret = child; - } - }); - } - return ret; -} - -export function findHiddenChildInChildrenByKey(children, key, showProp) { - let found = 0; - if (children) { - children.forEach((child) => { - if (found) { - return; - } - found = child && child.key === key && !child.props[showProp]; - }); - } - return found; -} - -export function isSameChildren(c1, c2, showProp) { - let same = c1.length === c2.length; - if (same) { - c1.forEach((child, index) => { - const child2 = c2[index]; - if (child && child2) { - if ((child && !child2) || (!child && child2)) { - same = false; - } else if (child.key !== child2.key) { - same = false; - } else if (showProp && child.props[showProp] !== child2.props[showProp]) { - same = false; - } - } - }); - } - return same; -} - -export function mergeChildren(prev, next) { - let ret = []; - - // For each key of `next`, the list of keys to insert before that key in - // the combined list - const nextChildrenPending = {}; - let pendingChildren = []; - prev.forEach((child) => { - if (child && findChildInChildrenByKey(next, child.key)) { - if (pendingChildren.length) { - nextChildrenPending[child.key] = pendingChildren; - pendingChildren = []; - } - } else { - pendingChildren.push(child); - } - }); - - next.forEach((child) => { - if (child && nextChildrenPending.hasOwnProperty(child.key)) { - ret = ret.concat(nextChildrenPending[child.key]); - } - ret.push(child); - }); - - ret = ret.concat(pendingChildren); - - return ret; -} diff --git a/src/util.js b/src/util.js index d84f1e6..b646a45 100644 --- a/src/util.js +++ b/src/util.js @@ -1,21 +1,31 @@ -const util = { - isAppearSupported(props) { - return props.transitionName && props.transitionAppear || props.animation.appear; - }, - isEnterSupported(props) { - return props.transitionName && props.transitionEnter || props.animation.enter; - }, - isLeaveSupported(props) { - return props.transitionName && props.transitionLeave || props.animation.leave; - }, - allowAppearCallback(props) { - return props.transitionAppear || props.animation.appear; - }, - allowEnterCallback(props) { - return props.transitionEnter || props.animation.enter; - }, - allowLeaveCallback(props) { - return props.transitionLeave || props.animation.leave; - }, -}; -export default util; +function checkTransitionSupport() { + const dom = document.createElement('span'); + + const transitionList = [ + 'WebkitTransition', 'MozTransition', 'OTransition', 'transition', + ]; + + return transitionList.some(name => name in dom.style); +} + +export const supportTransition = checkTransitionSupport(); + +export function cloneProps(props, propList) { + const newProps = {}; + propList.forEach((prop) => { + if (prop in props) { + newProps[prop] = props[prop]; + } + }); + + return newProps; +} + +export function getTransitionName(transitionName, transitionType) { + if (typeof transitionName === 'object') { + return transitionName[transitionType]; + } + + return `${transitionName}-${transitionType}`; +} + diff --git a/tests/multiple.spec.js b/tests/multiple.spec.js index ebce032..998614a 100644 --- a/tests/multiple.spec.js +++ b/tests/multiple.spec.js @@ -1,12 +1,12 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import Animate from '../'; import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import TestUtils from 'react-dom/test-utils'; import expect from 'expect.js'; -import './index.spec.css'; import CssAnimation from 'css-animation'; +import Animate from '../'; +import './index.spec.css'; class Todo extends React.Component { static propTypes = { @@ -64,7 +64,7 @@ class TodoList extends React.Component { } return ( - + { this.handleRemove.bind(this, i); }}> {item} ); diff --git a/tests/single-animation.spec.js b/tests/single-animation.spec.js index 27823cf..b541f3d 100644 --- a/tests/single-animation.spec.js +++ b/tests/single-animation.spec.js @@ -1,9 +1,11 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import Animate from '../index'; import React from 'react'; -import './index.spec.css'; import $ from 'jquery'; +import Animate from '../index'; +import './index.spec.css'; + +import single from './single-common.spec'; function createClass(options) { return class extends React.Component { @@ -43,6 +45,4 @@ function createClass(options) { }; } -import single from './single-common.spec'; - single(createClass, 'animation'); diff --git a/tests/single-common.spec.js b/tests/single-common.spec.js index 77d2356..753be94 100644 --- a/tests/single-common.spec.js +++ b/tests/single-common.spec.js @@ -4,8 +4,8 @@ import expect from 'expect.js'; import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-dom/test-utils'; -import './index.spec.css'; import $ from 'jquery'; +import './index.spec.css'; export default function test(createClass, title) { function getOpacity(node) { @@ -51,17 +51,18 @@ export default function test(createClass, title) { remove: true, }); - const innerInstance = ReactDOM.render(, innerDiv); - expect(TestUtils.scryRenderedDOMComponentsWithTag(innerInstance, - 'span')[0]).not.to.be.ok(); - const child = TestUtils.findRenderedDOMComponentWithTag(innerInstance, 'div'); - expect(getOpacity(ReactDOM.findDOMNode(child))).not.to.be(1); - setTimeout(() => { - expect(getOpacity(ReactDOM.findDOMNode(child))).to.be(1); - ReactDOM.unmountComponentAtNode(innerDiv); - document.body.removeChild(innerDiv); - done(); - }, 900); + ReactDOM.render(, innerDiv, (innerInstance) => { + expect(TestUtils.scryRenderedDOMComponentsWithTag(innerInstance, + 'span')[0]).not.to.be.ok(); + const child = TestUtils.findRenderedDOMComponentWithTag(innerInstance, 'div'); + expect(getOpacity(ReactDOM.findDOMNode(child))).not.to.be(1); + setTimeout(() => { + expect(getOpacity(ReactDOM.findDOMNode(child))).to.be(1); + ReactDOM.unmountComponentAtNode(innerDiv); + document.body.removeChild(innerDiv); + done(); + }, 900); + }); }); }); @@ -76,7 +77,8 @@ export default function test(createClass, title) { describe('when toggle transitionEnter', () => { it('should remove children after transition', (done) => { if (window.callPhantom) { - return done(); + done(); + return; } instance.setState({ transitionEnter: false }); expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'div')[0]).to.be.ok(); diff --git a/tests/single.spec.js b/tests/single.spec.js index ceb666e..9bbc626 100644 --- a/tests/single.spec.js +++ b/tests/single.spec.js @@ -1,10 +1,13 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import Animate from '../index'; import React from 'react'; +import CssAnimation from 'css-animation'; +import Animate from '../index'; import './index.spec.css'; +import single from './single-common.spec'; + function createClass(options) { return class extends React.Component { state = { @@ -15,7 +18,6 @@ function createClass(options) { render() { return ( Date: Wed, 23 May 2018 10:31:17 +0800 Subject: [PATCH 02/41] support exclusive --- src/AnimateChild.jsx | 36 ++++++++++++++++++++++++++++++++---- src/util.js | 4 ++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index a7c7be7..520fc2a 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classNames from 'classnames'; @@ -7,6 +8,7 @@ import { cloneProps, getTransitionName, supportTransition } from './util'; const clonePropList = [ 'show', + 'exclusive', 'children', ]; @@ -87,6 +89,17 @@ class AnimateChild extends React.Component { } }); + // exclusive + processState('exclusive', (isExclusive) => { + if (isExclusive) { + const transitionQueue = newState.transitionQueue || prevState.transitionQueue; + newState.transitionQueue = transitionQueue.slice(-1); + if (transitionQueue.length !== 1) { + newState.transitionActive = false; + } + } + }); + return newState; } @@ -117,7 +130,6 @@ class AnimateChild extends React.Component { } onDomUpdated = () => { - console.log('did update!!!', this.state); const { transitionQueue, transitionActive } = this.state; const transition = transitionQueue[0]; @@ -130,10 +142,19 @@ class AnimateChild extends React.Component { } }; - onMotionEnd = () => { - console.log('onMotionEnd'); + onMotionEnd = ({ target }) => { + const { transitionQueue } = this.state; + if (!transitionQueue.length) return; + + const $ele = ReactDOM.findDOMNode(this); + if ($ele === target) { + this.setState({ + transitionQueue: transitionQueue.slice(1), + }); + } } + render() { const { child, transitionQueue, transitionActive } = this.state; const { showProp } = this.props; @@ -147,10 +168,17 @@ class AnimateChild extends React.Component { transitionActive && transition.active, ) : className; + let show = true; + if (supportTransition && transition) { + show = true; + } else { + show = child.props[showProp]; + } + // Clone child return React.cloneElement(child, { className: connectClassName, - [showProp]: supportTransition ? !!transition : child.props[showProp], + [showProp]: show, onTransitionEnd: (...args) => { this.onMotionEnd(...args); diff --git a/src/util.js b/src/util.js index b646a45..68b7b70 100644 --- a/src/util.js +++ b/src/util.js @@ -1,4 +1,8 @@ function checkTransitionSupport() { + if (typeof document === 'undefined') { + return false; + } + const dom = document.createElement('span'); const transitionList = [ From 7058a95e38dbdbe1ba370ed5ac8ead0f0e04a4cf Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 May 2018 10:34:12 +0800 Subject: [PATCH 03/41] not display animate when browser not support --- src/AnimateChild.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 520fc2a..44fed2c 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -162,7 +162,7 @@ class AnimateChild extends React.Component { // Class name const transition = transitionQueue[0]; - const connectClassName = transition ? classNames( + const connectClassName = (supportTransition && transition) ? classNames( className, transition.basic, transitionActive && transition.active, From 8a320b1a1dc9991be4eb4dbd41612b45c59547f3 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 May 2018 10:49:25 +0800 Subject: [PATCH 04/41] clean appear logic --- src/AnimateChild.jsx | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 44fed2c..afdd72d 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -30,10 +30,7 @@ class AnimateChild extends React.Component { } static getDerivedStateFromProps(nextProps, prevState) { - const { - appear, enter, leave, - prevProps = {}, - } = prevState; + const { appeared, prevProps = {} } = prevState; const { transitionName, transitionAppear, transitionEnter, transitionLeave, exclusive, @@ -71,17 +68,20 @@ class AnimateChild extends React.Component { } } - if (show && !appear && transitionAppear) { - pushTransition({ - basic: getTransitionName(transitionName, 'appear'), - active: getTransitionName(transitionName, 'appear-active'), - }); - } else if (show && !enter && transitionEnter) { + if (!appeared && show) { + newState.appeared = true; + if (transitionAppear) { + pushTransition({ + basic: getTransitionName(transitionName, 'appear'), + active: getTransitionName(transitionName, 'appear-active'), + }); + } + } else if (appeared && show && transitionEnter) { pushTransition({ basic: getTransitionName(transitionName, 'enter'), active: getTransitionName(transitionName, 'enter-active'), }); - } else if (!show && !leave && transitionLeave) { + } else if (appeared && !show && transitionLeave) { pushTransition({ basic: getTransitionName(transitionName, 'leave'), active: getTransitionName(transitionName, 'leave-active'), @@ -103,22 +103,12 @@ class AnimateChild extends React.Component { return newState; } - constructor() { - super(); - - // Since React 16.3+ use static getDerivedStateFromProps. We need cache ourselves. - this._prevChild = null; - this._cachedChild = null; - } - state = { child: null, transitionQueue: [], transitionActive: false, - appear: false, // TODO: handle this - enter: false, - leave: false, + appeared: false, } componentDidMount() { From 00ac61166fc84cda9dc5e6e6d38f0efa0b46e9c9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 May 2018 11:36:44 +0800 Subject: [PATCH 05/41] adjust appear logic --- examples/alert.js | 6 ++++-- src/Animate.jsx | 22 ++++++++++++++++--- src/AnimateChild.jsx | 50 ++++++++++++++++++++++---------------------- src/util.js | 43 +++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 30 deletions(-) diff --git a/examples/alert.js b/examples/alert.js index 359f650..7696430 100644 --- a/examples/alert.js +++ b/examples/alert.js @@ -18,7 +18,8 @@ class Alert extends React.Component { static defaultProps = { onEnd() {}, - time: 2000, + // time: 2000, + time: 2000000, type: 'success', } @@ -122,7 +123,8 @@ function alertFn(i) { } function onClick() { - for (let i = 0; i < 4; i++) { + // for (let i = 0; i < 4; i++) { + for (let i = 0; i < 1; i++) { setTimeout(alertFn(i), 1000 * i); } } diff --git a/src/Animate.jsx b/src/Animate.jsx index 6183bed..760743b 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import AnimateChild from './AnimateChild'; -import { cloneProps } from './util'; +import { cloneProps, mergeChildren } from './util'; const defaultKey = `rc_animate_${Date.now()}`; const clonePropList = ['children']; @@ -59,19 +59,32 @@ class Animate extends React.Component { } processState('children', (children) => { + const prevChildren = prevState.children; newState.children = (React.Children.toArray(children) || []) .filter(node => node); + + // Merge prev children to keep the animation + newState.children = mergeChildren(prevChildren, newState.children); + console.log('children update:', newState); }); return newState; } state = { + appeared: true, children: [], }; + componentDidMount() { + // Next animation update will not rigger appear + setTimeout(() => { + this.setState({ appeared: false }); + }); + } + render() { - const { children } = this.state; + const { appeared, children } = this.state; const { component: Component, componentProps, className, style, showProp, @@ -83,10 +96,13 @@ class Animate extends React.Component { throw new Error('must set key for children'); } + const show = showProp ? node.props[showProp] : true; + return ( { newState.child = child; }); - // Show update - processState('show', (show) => { - newState.transitionQueue = prevState.transitionQueue.slice(); - - function pushTransition(transition) { - if (exclusive) { - newState.transitionQueue = [transition]; - newState.transitionActive = false; - } else { - newState.transitionQueue.push(transition); - } + processState('appeared', (isAppeared) => { + if (isAppeared && transitionAppear) { + pushTransition({ + basic: getTransitionName(transitionName, 'appear'), + active: getTransitionName(transitionName, 'appear-active'), + }); } + }); - if (!appeared && show) { - newState.appeared = true; - if (transitionAppear) { - pushTransition({ - basic: getTransitionName(transitionName, 'appear'), - active: getTransitionName(transitionName, 'appear-active'), - }); - } - } else if (appeared && show && transitionEnter) { + // Show update + processState('show', (show) => { + if (!appeared && show && transitionEnter) { pushTransition({ basic: getTransitionName(transitionName, 'enter'), active: getTransitionName(transitionName, 'enter-active'), }); - } else if (appeared && !show && transitionLeave) { + } else if (!appeared && !show && transitionLeave) { pushTransition({ basic: getTransitionName(transitionName, 'leave'), active: getTransitionName(transitionName, 'leave-active'), @@ -107,8 +109,6 @@ class AnimateChild extends React.Component { child: null, transitionQueue: [], transitionActive: false, - - appeared: false, } componentDidMount() { diff --git a/src/util.js b/src/util.js index 68b7b70..3ca858e 100644 --- a/src/util.js +++ b/src/util.js @@ -1,3 +1,6 @@ +import React from 'react'; + +// ================== Browser ================== function checkTransitionSupport() { if (typeof document === 'undefined') { return false; @@ -14,6 +17,46 @@ function checkTransitionSupport() { export const supportTransition = checkTransitionSupport(); +// =================== Node ==================== +/** + * [Legacy] Find the same children in both prev & next list. + * Insert not find one before the find one, otherwise in the end. For example: + * - prev: [1,2,3] + * - next: [2,4] + * -> [1,2,4,3] + */ +export function mergeChildren(prev, next) { + const prevList = React.Children.toArray(prev) || []; + const nextList = React.Children.toArray(next) || []; + let mergeList = []; + const nextChildrenMap = {}; + let missMatchChildrenList = []; + + // Fill matched prev node into next node map + prevList.forEach((prevNode) => { + if (prevNode && nextList.some(({ key }) => key === prevNode.key)) { + if (missMatchChildrenList.length) { + nextChildrenMap[prevNode.key] = missMatchChildrenList; + missMatchChildrenList = []; + } else { + missMatchChildrenList.push(prevNode); + } + } + }); + + // Insert prev node before the matched next node + nextList.forEach((nextNode) => { + if (nextNode && nextChildrenMap[nextNode.key]) { + mergeList = mergeList.concat(nextChildrenMap[nextNode.key]); + } + mergeList.push(nextNode); + }); + + mergeList = mergeList.concat(missMatchChildrenList); + + return mergeList; +} + export function cloneProps(props, propList) { const newProps = {}; propList.forEach((prop) => { From 0b568a2b496adcd02329dafe6d19c6e80a97eea3 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 May 2018 15:39:43 +0800 Subject: [PATCH 06/41] use native event listener --- examples/alert.js | 6 +-- package.json | 3 ++ src/Animate.jsx | 58 ++++++++++++++++------ src/AnimateChild.jsx | 111 ++++++++++++++++++++++++++++++++++--------- src/util.js | 80 ++++++++++++++++++++++++------- 5 files changed, 201 insertions(+), 57 deletions(-) diff --git a/examples/alert.js b/examples/alert.js index 7696430..359f650 100644 --- a/examples/alert.js +++ b/examples/alert.js @@ -18,8 +18,7 @@ class Alert extends React.Component { static defaultProps = { onEnd() {}, - // time: 2000, - time: 2000000, + time: 2000, type: 'success', } @@ -123,8 +122,7 @@ function alertFn(i) { } function onClick() { - // for (let i = 0; i < 4; i++) { - for (let i = 0; i < 1; i++) { + for (let i = 0; i < 4; i++) { setTimeout(alertFn(i), 1000 * i); } } diff --git a/package.json b/package.json index ff02791..cfe078c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,10 @@ "dependencies": { "babel-runtime": "6.x", "classnames": "^2.2.5", + "component-classes": "^1.2.6", + "fbjs": "^0.8.16", "prop-types": "15.x", + "rc-util": "^4.5.0", "react-lifecycles-compat": "^3.0.4" } } diff --git a/src/Animate.jsx b/src/Animate.jsx index 760743b..21bf098 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; +import toArray from 'rc-util/lib/Children/toArray'; import AnimateChild from './AnimateChild'; import { cloneProps, mergeChildren } from './util'; @@ -59,13 +60,16 @@ class Animate extends React.Component { } processState('children', (children) => { - const prevChildren = prevState.children; - newState.children = (React.Children.toArray(children) || []) - .filter(node => node); + const prevChildren = prevState.mergedChildren; + const currentChildren = toArray(children).filter(node => node); // Merge prev children to keep the animation - newState.children = mergeChildren(prevChildren, newState.children); - console.log('children update:', newState); + newState.mergedChildren = mergeChildren(prevChildren, currentChildren); + const toKey = ({ key }) => key; + console.log('children update - prev:', prevChildren.map(toKey)); + console.log('children update - current:', currentChildren.map(toKey)); + console.log('children update - merge:', newState.mergedChildren.map(toKey)); + console.log('-----------------------'); }); return newState; @@ -73,30 +77,51 @@ class Animate extends React.Component { state = { appeared: true, - children: [], + mergedChildren: [], }; componentDidMount() { - // Next animation update will not rigger appear - setTimeout(() => { - this.setState({ appeared: false }); - }); + // No need to re-render + this.state.appeared = false; } + onChildLeaved = (key) => { + // Remove child which not exist anymore + if (!this.hasChild(key)) { + const { mergedChildren } = this.state; + this.setState({ + mergedChildren: mergedChildren.filter(node => node.key !== key), + }); + } + }; + + hasChild = (key) => { + const { children } = this.props; + + return toArray(children).some(node => node.key === key); + }; + render() { - const { appeared, children } = this.state; + const { appeared, mergedChildren } = this.state; const { component: Component, componentProps, className, style, showProp, } = this.props; - const $children = children.map((node) => { - if (children.length > 1 && !node.key) { + const $children = mergedChildren.map((node) => { + if (mergedChildren.length > 1 && !node.key) { throw new Error('must set key for children'); } - const show = showProp ? node.props[showProp] : true; + let show = true; + if (showProp) { + show = node.props[showProp]; + } else if (!this.hasChild(node.key)) { + show = false; + } + + const key = node.key || defaultKey; return ( {node} diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index ebad5a8..84e7b04 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -3,8 +3,12 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classNames from 'classnames'; +import classes from 'component-classes'; -import { cloneProps, getTransitionName, supportTransition } from './util'; +import { + cloneProps, getTransitionName, + supportTransition, animationEndName, transitionEndName, +} from './util'; const clonePropList = [ 'appeared', @@ -29,6 +33,9 @@ class AnimateChild extends React.Component { exclusive: PropTypes.bool, appeared: PropTypes.bool, showProp: PropTypes.string, + + animateKey: PropTypes.any, + onChildLeaved: PropTypes.func, } static getDerivedStateFromProps(nextProps, prevState) { @@ -53,6 +60,8 @@ class AnimateChild extends React.Component { } function pushTransition(transition) { + if (!supportTransition) return; + newState.transitionQueue = newState.transitionQueue || prevState.transitionQueue; if (exclusive) { newState.transitionQueue = [transition]; @@ -70,6 +79,7 @@ class AnimateChild extends React.Component { processState('appeared', (isAppeared) => { if (isAppeared && transitionAppear) { pushTransition({ + type: 'appear', basic: getTransitionName(transitionName, 'appear'), active: getTransitionName(transitionName, 'appear-active'), }); @@ -80,20 +90,27 @@ class AnimateChild extends React.Component { processState('show', (show) => { if (!appeared && show && transitionEnter) { pushTransition({ + type: 'enter', basic: getTransitionName(transitionName, 'enter'), active: getTransitionName(transitionName, 'enter-active'), }); } else if (!appeared && !show && transitionLeave) { - pushTransition({ - basic: getTransitionName(transitionName, 'leave'), - active: getTransitionName(transitionName, 'leave-active'), - }); + if (!supportTransition || !transitionLeave) { + // Call leave directly if not support or not set leave + nextProps.onChildLeaved(nextProps.animateKey); + } else { + pushTransition({ + type: 'leave', + basic: getTransitionName(transitionName, 'leave'), + active: getTransitionName(transitionName, 'leave-active'), + }); + } } }); // exclusive processState('exclusive', (isExclusive) => { - if (isExclusive) { + if (supportTransition && isExclusive) { const transitionQueue = newState.transitionQueue || prevState.transitionQueue; newState.transitionQueue = transitionQueue.slice(-1); if (transitionQueue.length !== 1) { @@ -105,6 +122,15 @@ class AnimateChild extends React.Component { return newState; } + constructor() { + super(); + + // [Legacy] Since old code addListener on the element. + // To avoid break the behaviour that component not handle animation/transition + // also can handle the animate, let keep the logic. + this.$prevEle = null; + } + state = { child: null, transitionQueue: [], @@ -119,10 +145,37 @@ class AnimateChild extends React.Component { this.onDomUpdated(); } + componentWillUnmount() { + this.cleanDomEvent(); + } + onDomUpdated = () => { const { transitionQueue, transitionActive } = this.state; const transition = transitionQueue[0]; + // [Legacy] Since origin code use js to set `className`. + // This caused that any component without support `className` can be forced set. + // Let's keep the logic. + const $ele = ReactDOM.findDOMNode(this); + if (transition && $ele) { + const nodeClasses = classes($ele); + nodeClasses.add(transition.basic); + + if (transitionActive) { + nodeClasses.add(transition.active); + } + } + + // [Legacy] Add animation/transition event by dom level + if (supportTransition && this.$prevEle !== $ele) { + this.cleanDomEvent(); + + this.$prevEle = $ele; + this.$prevEle.addEventListener(animationEndName, this.onMotionEnd); + this.$prevEle.addEventListener(transitionEndName, this.onMotionEnd); + } + + // Update transition active class if (transition && !transitionActive) { // requestAnimationFrame not support in IE 9- // Use setTimeout instead @@ -138,17 +191,39 @@ class AnimateChild extends React.Component { const $ele = ReactDOM.findDOMNode(this); if ($ele === target) { + const { onChildLeaved, animateKey } = this.props; + const transition = transitionQueue[0]; + + // Update transition queue this.setState({ transitionQueue: transitionQueue.slice(1), }); + + // [Legacy] Same as above, we need call js to remove the class + if (transition) { + const nodeClasses = classes($ele); + nodeClasses.remove(transition.basic); + nodeClasses.remove(transition.active); + } + + // Trigger parent event + if (transition.type === 'leave') { + onChildLeaved(animateKey); + } } } + cleanDomEvent = () => { + if (this.$prevEle && supportTransition) { + this.$prevEle.removeEventListener(animationEndName, this.onMotionEnd); + this.$prevEle.removeEventListener(transitionEndName, this.onMotionEnd); + } + }; render() { const { child, transitionQueue, transitionActive } = this.state; const { showProp } = this.props; - const { className, onTransitionEnd, onAnimationEnd } = child.props || {}; + const { className } = child.props || {}; // Class name const transition = transitionQueue[0]; @@ -166,23 +241,15 @@ class AnimateChild extends React.Component { } // Clone child - return React.cloneElement(child, { + const newChildProps = { className: connectClassName, - [showProp]: show, + }; - onTransitionEnd: (...args) => { - this.onMotionEnd(...args); - if (onTransitionEnd) { - onTransitionEnd(...args); - } - }, - onAnimationEnd: (...args) => { - this.onMotionEnd(...args); - if (onAnimationEnd) { - onAnimationEnd(...args); - } - }, - }); + if (showProp) { + newChildProps[showProp] = show; + } + + return React.cloneElement(child, newChildProps); } } diff --git a/src/util.js b/src/util.js index 3ca858e..959379a 100644 --- a/src/util.js +++ b/src/util.js @@ -1,23 +1,62 @@ -import React from 'react'; +import toArray from 'rc-util/lib/Children/toArray'; +import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; -// ================== Browser ================== -function checkTransitionSupport() { - if (typeof document === 'undefined') { - return false; +// ================= Transition ================= +// Event wrapper. Copy from react source code +function makePrefixMap(styleProp, eventName) { + const prefixes = {}; + + prefixes[styleProp.toLowerCase()] = eventName.toLowerCase(); + prefixes[`Webkit${styleProp}`] = `webkit${eventName}`; + prefixes[`Moz${styleProp}`] = `moz${eventName}`; + prefixes[`ms${styleProp}`] = `MS${eventName}`; + prefixes[`O${styleProp}`] = `o${eventName.toLowerCase()}`; + + return prefixes; +} + +const vendorPrefixes = { + animationend: makePrefixMap('Animation', 'AnimationEnd'), + transitionend: makePrefixMap('Transition', 'TransitionEnd'), +}; + +let style = {}; + +if (canUseDOM) { + style = document.createElement('div').style; + + if (!('AnimationEvent' in window)) { + delete vendorPrefixes.animationend.animation; + } + + if (!('TransitionEvent' in window)) { + delete vendorPrefixes.transitionend.transition; } +} - const dom = document.createElement('span'); +const prefixedEventNames = {}; - const transitionList = [ - 'WebkitTransition', 'MozTransition', 'OTransition', 'transition', - ]; +function getVendorPrefixedEventName(eventName) { + const prefixMap = vendorPrefixes[eventName]; + + const stylePropList = Object.keys(prefixMap); + const len = stylePropList.length; + for (let i = 0; i < len; i += 1) { + const styleProp = stylePropList[i]; + if (Object.prototype.hasOwnProperty.call(prefixMap, styleProp) && styleProp in style) { + prefixedEventNames[eventName] = prefixMap[styleProp]; + return prefixedEventNames[eventName]; + } + } - return transitionList.some(name => name in dom.style); + return ''; } -export const supportTransition = checkTransitionSupport(); +export const animationEndName = getVendorPrefixedEventName('animationend'); +export const transitionEndName = getVendorPrefixedEventName('transitionend'); +export const supportTransition = !!(animationEndName && transitionEndName); -// =================== Node ==================== +// ==================== Node ==================== /** * [Legacy] Find the same children in both prev & next list. * Insert not find one before the find one, otherwise in the end. For example: @@ -26,8 +65,17 @@ export const supportTransition = checkTransitionSupport(); * -> [1,2,4,3] */ export function mergeChildren(prev, next) { - const prevList = React.Children.toArray(prev) || []; - const nextList = React.Children.toArray(next) || []; + const prevList = toArray(prev); + const nextList = toArray(next); + + // Skip if is single children + if ( + prevList.length === 1 && nextList.length === 1 && + prevList[0].key === nextList[0].key + ) { + return nextList; + } + let mergeList = []; const nextChildrenMap = {}; let missMatchChildrenList = []; @@ -38,9 +86,9 @@ export function mergeChildren(prev, next) { if (missMatchChildrenList.length) { nextChildrenMap[prevNode.key] = missMatchChildrenList; missMatchChildrenList = []; - } else { - missMatchChildrenList.push(prevNode); } + } else { + missMatchChildrenList.push(prevNode); } }); From 07d38194e71fd6ede3fa764ff3f19304e4ae9e37 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 May 2018 16:00:01 +0800 Subject: [PATCH 07/41] fix example --- examples/simple-animation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simple-animation.js b/examples/simple-animation.js index bd8e8d6..e34cb60 100644 --- a/examples/simple-animation.js +++ b/examples/simple-animation.js @@ -7,10 +7,10 @@ import ReactDOM from 'react-dom'; import velocity from 'velocity-animate'; import './assets/index.less'; -const Box = () => { +const Box = (props) => { const style = { width: '200px', - display: this.props.visible ? 'block' : 'none', + display: props.visible ? 'block' : 'none', height: '200px', backgroundColor: 'red', }; From 71563380a0ef49899a9f4f16589eb6ad36bfe252 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 May 2018 20:52:59 +0800 Subject: [PATCH 08/41] keep the transition --- src/AnimateChild.jsx | 67 +++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 84e7b04..8ec7a2b 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -36,6 +36,11 @@ class AnimateChild extends React.Component { animateKey: PropTypes.any, onChildLeaved: PropTypes.func, + + // Customize event handler + onChildAppear: PropTypes.func, + onChildEnter: PropTypes.func, + onChildLeave: PropTypes.func, } static getDerivedStateFromProps(nextProps, prevState) { @@ -60,8 +65,6 @@ class AnimateChild extends React.Component { } function pushTransition(transition) { - if (!supportTransition) return; - newState.transitionQueue = newState.transitionQueue || prevState.transitionQueue; if (exclusive) { newState.transitionQueue = [transition]; @@ -129,6 +132,9 @@ class AnimateChild extends React.Component { // To avoid break the behaviour that component not handle animation/transition // also can handle the animate, let keep the logic. this.$prevEle = null; + + // Check if transition first come + this.currentTransition = null; } state = { @@ -151,37 +157,58 @@ class AnimateChild extends React.Component { onDomUpdated = () => { const { transitionQueue, transitionActive } = this.state; + const { animateKey, onChildAppear, onChildEnter, onChildLeave } = this.props; + const transition = transitionQueue[0]; - // [Legacy] Since origin code use js to set `className`. - // This caused that any component without support `className` can be forced set. - // Let's keep the logic. const $ele = ReactDOM.findDOMNode(this); + if (transition && $ele) { + // [Legacy] Since origin code use js to set `className`. + // This caused that any component without support `className` can be forced set. + // Let's keep the logic. const nodeClasses = classes($ele); nodeClasses.add(transition.basic); if (transitionActive) { nodeClasses.add(transition.active); } - } - // [Legacy] Add animation/transition event by dom level - if (supportTransition && this.$prevEle !== $ele) { - this.cleanDomEvent(); + // [Legacy] Add animation/transition event by dom level + if (supportTransition && this.$prevEle !== $ele) { + this.cleanDomEvent(); - this.$prevEle = $ele; - this.$prevEle.addEventListener(animationEndName, this.onMotionEnd); - this.$prevEle.addEventListener(transitionEndName, this.onMotionEnd); - } + this.$prevEle = $ele; + this.$prevEle.addEventListener(animationEndName, this.onMotionEnd); + this.$prevEle.addEventListener(transitionEndName, this.onMotionEnd); + } - // Update transition active class - if (transition && !transitionActive) { - // requestAnimationFrame not support in IE 9- - // Use setTimeout instead - setTimeout(() => { - this.setState({ transitionActive: true }); - }); + // Check if transition update + if (this.currentTransition !== transition) { + this.currentTransition = transition; + + if (transition.type === 'appear' && onChildAppear) { + onChildAppear(animateKey, $ele); + } else if (transition.type === 'enter' && onChildEnter) { + onChildEnter(animateKey, $ele); + } else if (transition.type === 'leave' && onChildLeave) { + onChildLeave(animateKey, $ele); + } + } + + // Update transition active class + if (!transitionActive) { + // requestAnimationFrame not support in IE 9- + // Use setTimeout instead + setTimeout(() => { + this.setState({transitionActive: true}); + }, 0); + } + + // Call onMotionEnd directly + if (!supportTransition) { + this.onMotionEnd({ target: $ele }); + } } }; From db3e211e05c8652c20d8e59e15067d7080359a35 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 24 May 2018 20:16:47 +0800 Subject: [PATCH 09/41] support animation --- src/Animate.jsx | 25 +++++++++++++++++++++++++ src/AnimateChild.jsx | 34 +++++++++++++++++++++++++--------- src/util.js | 2 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/Animate.jsx b/src/Animate.jsx index 21bf098..f2463c0 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -95,6 +95,27 @@ class Animate extends React.Component { } }; + onAnimationAction = (func, $ele, childCallback) => { + if (func) { + const animation = func($ele, childCallback); + return animation || {}; + } + return null; + }; + + onChildAppear = (key, $ele, childCallback) => { + const { appear } = this.props.animation; + return this.onAnimationAction(appear, $ele, childCallback); + }; + onChildEnter = (key, $ele, childCallback) => { + const { enter } = this.props.animation; + return this.onAnimationAction(enter, $ele, childCallback); + }; + onChildLeave = (key, $ele, childCallback) => { + const { leave } = this.props.animation; + return this.onAnimationAction(leave, $ele, childCallback); + }; + hasChild = (key) => { const { children } = this.props; @@ -134,6 +155,10 @@ class Animate extends React.Component { animateKey={key} onChildLeaved={this.onChildLeaved} + + onChildAppear={this.onChildAppear} + onChildEnter={this.onChildEnter} + onChildLeave={this.onChildLeave} > {node} diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 8ec7a2b..c420cd7 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -133,8 +133,10 @@ class AnimateChild extends React.Component { // also can handle the animate, let keep the logic. this.$prevEle = null; - // Check if transition first come + // Keep the current transition this.currentTransition = null; + // Keep external handler + this.currentTransitionHanlder = null; } state = { @@ -187,13 +189,21 @@ class AnimateChild extends React.Component { if (this.currentTransition !== transition) { this.currentTransition = transition; - if (transition.type === 'appear' && onChildAppear) { - onChildAppear(animateKey, $ele); - } else if (transition.type === 'enter' && onChildEnter) { - onChildEnter(animateKey, $ele); - } else if (transition.type === 'leave' && onChildLeave) { - onChildLeave(animateKey, $ele); - } + // Trigger customize animation + const mapAnimation = (type, func) => { + if (transition.type === type && func) { + const handler = func(animateKey, $ele, () => { + this.onMotionEnd({ target: $ele }); + }); + if (handler) { + this.currentTransitionHanlder = handler; + } + } + }; + + mapAnimation('appear', onChildAppear); + mapAnimation('enter', onChildEnter); + mapAnimation('leave', onChildLeave); } // Update transition active class @@ -206,7 +216,7 @@ class AnimateChild extends React.Component { } // Call onMotionEnd directly - if (!supportTransition) { + if (!supportTransition && !this.currentTransitionHanlder) { this.onMotionEnd({ target: $ele }); } } @@ -216,6 +226,12 @@ class AnimateChild extends React.Component { const { transitionQueue } = this.state; if (!transitionQueue.length) return; + // Remove the handler + if (this.currentTransitionHanlder && this.currentTransitionHanlder.stop) { + this.currentTransitionHanlder.stop(); + } + this.currentTransitionHanlder = null; + const $ele = ReactDOM.findDOMNode(this); if ($ele === target) { const { onChildLeaved, animateKey } = this.props; diff --git a/src/util.js b/src/util.js index 959379a..b7e1821 100644 --- a/src/util.js +++ b/src/util.js @@ -116,7 +116,7 @@ export function cloneProps(props, propList) { return newProps; } -export function getTransitionName(transitionName, transitionType) { +export function getTransitionName(transitionName = '', transitionType) { if (typeof transitionName === 'object') { return transitionName[transitionType]; } From f4da5671016e5121935e18ff1e649bd96b665778 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 24 May 2018 20:27:34 +0800 Subject: [PATCH 10/41] put animate handler into state --- src/AnimateChild.jsx | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index c420cd7..809b60c 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -69,6 +69,12 @@ class AnimateChild extends React.Component { if (exclusive) { newState.transitionQueue = [transition]; newState.transitionActive = false; + + // Remove the handler + if (prevState.currentTransitionHandler && prevState.currentTransitionHandler.stop) { + prevState.currentTransitionHandler.stop(); + } + newState.currentTransitionHandler = null } else { newState.transitionQueue.push(transition); } @@ -135,14 +141,15 @@ class AnimateChild extends React.Component { // Keep the current transition this.currentTransition = null; - // Keep external handler - this.currentTransitionHanlder = null; } state = { child: null, transitionQueue: [], transitionActive: false, + + // Customize animation handler + currentTransitionHandler: null, } componentDidMount() { @@ -186,6 +193,7 @@ class AnimateChild extends React.Component { } // Check if transition update + let hasHandler = false; if (this.currentTransition !== transition) { this.currentTransition = transition; @@ -196,7 +204,10 @@ class AnimateChild extends React.Component { this.onMotionEnd({ target: $ele }); }); if (handler) { - this.currentTransitionHanlder = handler; + this.setState({ + currentTransitionHandler: handler, + }); + hasHandler = true; } } }; @@ -216,30 +227,30 @@ class AnimateChild extends React.Component { } // Call onMotionEnd directly - if (!supportTransition && !this.currentTransitionHanlder) { + if (!supportTransition && !hasHandler) { this.onMotionEnd({ target: $ele }); } } }; onMotionEnd = ({ target }) => { - const { transitionQueue } = this.state; + const { transitionQueue, currentTransitionHandler } = this.state; if (!transitionQueue.length) return; - // Remove the handler - if (this.currentTransitionHanlder && this.currentTransitionHanlder.stop) { - this.currentTransitionHanlder.stop(); - } - this.currentTransitionHanlder = null; - const $ele = ReactDOM.findDOMNode(this); if ($ele === target) { const { onChildLeaved, animateKey } = this.props; const transition = transitionQueue[0]; + // Remove the handler + if (currentTransitionHandler && currentTransitionHandler.stop) { + currentTransitionHandler.stop(); + } + // Update transition queue this.setState({ transitionQueue: transitionQueue.slice(1), + currentTransitionHandler: null, }); // [Legacy] Same as above, we need call js to remove the class From aaf256943bcecb3782d81bf713c94a2ad4b402ff Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 25 May 2018 10:24:51 +0800 Subject: [PATCH 11/41] sync logic in didMount --- examples/simple-animation.js | 2 ++ src/Animate.jsx | 10 +++++----- src/AnimateChild.jsx | 29 ++++++++++++++--------------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/examples/simple-animation.js b/examples/simple-animation.js index e34cb60..7ca8059 100644 --- a/examples/simple-animation.js +++ b/examples/simple-animation.js @@ -52,6 +52,7 @@ class Demo extends React.Component { }); return { stop() { + console.error('enter stop!'); velocity(node, 'finish'); // velocity complete is async complete(); @@ -77,6 +78,7 @@ class Demo extends React.Component { }); return { stop() { + console.error('leave stop!'); velocity(node, 'finish'); // velocity complete is async complete(); diff --git a/src/Animate.jsx b/src/Animate.jsx index f2463c0..6d7973d 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -65,11 +65,11 @@ class Animate extends React.Component { // Merge prev children to keep the animation newState.mergedChildren = mergeChildren(prevChildren, currentChildren); - const toKey = ({ key }) => key; - console.log('children update - prev:', prevChildren.map(toKey)); - console.log('children update - current:', currentChildren.map(toKey)); - console.log('children update - merge:', newState.mergedChildren.map(toKey)); - console.log('-----------------------'); + // const toKey = ({ key }) => key; + // console.log('children update - prev:', prevChildren.map(toKey)); + // console.log('children update - current:', currentChildren.map(toKey)); + // console.log('children update - merge:', newState.mergedChildren.map(toKey)); + // console.log('-----------------------'); }); return newState; diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 809b60c..c33a4b5 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -70,10 +70,6 @@ class AnimateChild extends React.Component { newState.transitionQueue = [transition]; newState.transitionActive = false; - // Remove the handler - if (prevState.currentTransitionHandler && prevState.currentTransitionHandler.stop) { - prevState.currentTransitionHandler.stop(); - } newState.currentTransitionHandler = null } else { newState.transitionQueue.push(transition); @@ -153,25 +149,33 @@ class AnimateChild extends React.Component { } componentDidMount() { - this.onDomUpdated(); + this.onDomUpdated({}, {}); } - componentDidUpdate() { - this.onDomUpdated(); + componentDidUpdate(prevProps, prevState) { + this.onDomUpdated(prevProps, prevState); } componentWillUnmount() { this.cleanDomEvent(); } - onDomUpdated = () => { - const { transitionQueue, transitionActive } = this.state; + onDomUpdated = (prevProps, prevState) => { + const { transitionQueue, transitionActive, currentTransitionHandler } = this.state; const { animateKey, onChildAppear, onChildEnter, onChildLeave } = this.props; const transition = transitionQueue[0]; const $ele = ReactDOM.findDOMNode(this); + // Clean up if currentTransitionHandler set to null + if ( + prevState.currentTransitionHandler && + prevState.currentTransitionHandler !== currentTransitionHandler + ) { + prevState.currentTransitionHandler.stop(); + } + if (transition && $ele) { // [Legacy] Since origin code use js to set `className`. // This caused that any component without support `className` can be forced set. @@ -234,7 +238,7 @@ class AnimateChild extends React.Component { }; onMotionEnd = ({ target }) => { - const { transitionQueue, currentTransitionHandler } = this.state; + const { transitionQueue } = this.state; if (!transitionQueue.length) return; const $ele = ReactDOM.findDOMNode(this); @@ -242,11 +246,6 @@ class AnimateChild extends React.Component { const { onChildLeaved, animateKey } = this.props; const transition = transitionQueue[0]; - // Remove the handler - if (currentTransitionHandler && currentTransitionHandler.stop) { - currentTransitionHandler.stop(); - } - // Update transition queue this.setState({ transitionQueue: transitionQueue.slice(1), From 25fc778d259b889bbcce27bbbc52eb2c81ff5265 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 25 May 2018 11:55:59 +0800 Subject: [PATCH 12/41] update func name --- src/Animate.jsx | 16 ++++++++-------- src/AnimateChild.jsx | 23 ++++++++++++++++------- src/util.js | 4 +++- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/Animate.jsx b/src/Animate.jsx index 6d7973d..cf92dfe 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -95,25 +95,25 @@ class Animate extends React.Component { } }; - onAnimationAction = (func, $ele, childCallback) => { + onAnimationAction = (func, $ele, animateDone) => { if (func) { - const animation = func($ele, childCallback); + const animation = func($ele, animateDone); return animation || {}; } return null; }; - onChildAppear = (key, $ele, childCallback) => { + onChildAppear = (key, $ele, animateDone) => { const { appear } = this.props.animation; - return this.onAnimationAction(appear, $ele, childCallback); + return this.onAnimationAction(appear, $ele, animateDone); }; - onChildEnter = (key, $ele, childCallback) => { + onChildEnter = (key, $ele, animateDone) => { const { enter } = this.props.animation; - return this.onAnimationAction(enter, $ele, childCallback); + return this.onAnimationAction(enter, $ele, animateDone); }; - onChildLeave = (key, $ele, childCallback) => { + onChildLeave = (key, $ele, animateDone) => { const { leave } = this.props.animation; - return this.onAnimationAction(leave, $ele, childCallback); + return this.onAnimationAction(leave, $ele, animateDone); }; hasChild = (key) => { diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index c33a4b5..377e1e5 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -171,7 +171,8 @@ class AnimateChild extends React.Component { // Clean up if currentTransitionHandler set to null if ( prevState.currentTransitionHandler && - prevState.currentTransitionHandler !== currentTransitionHandler + prevState.currentTransitionHandler !== currentTransitionHandler && + !currentTransitionHandler ) { prevState.currentTransitionHandler.stop(); } @@ -181,10 +182,10 @@ class AnimateChild extends React.Component { // This caused that any component without support `className` can be forced set. // Let's keep the logic. const nodeClasses = classes($ele); - nodeClasses.add(transition.basic); + if (transition.basic) nodeClasses.add(transition.basic); if (transitionActive) { - nodeClasses.add(transition.active); + if (transition.active) nodeClasses.add(transition.active); } // [Legacy] Add animation/transition event by dom level @@ -238,8 +239,15 @@ class AnimateChild extends React.Component { }; onMotionEnd = ({ target }) => { - const { transitionQueue } = this.state; - if (!transitionQueue.length) return; + const { transitionQueue, currentTransitionHandler } = this.state; + if (!transitionQueue.length) { + if (currentTransitionHandler) { + this.setState({ + currentTransitionHandler: null, + }); + } + return; + } const $ele = ReactDOM.findDOMNode(this); if ($ele === target) { @@ -255,8 +263,8 @@ class AnimateChild extends React.Component { // [Legacy] Same as above, we need call js to remove the class if (transition) { const nodeClasses = classes($ele); - nodeClasses.remove(transition.basic); - nodeClasses.remove(transition.active); + if (transition.basic) nodeClasses.remove(transition.basic); + if (transition.active) nodeClasses.remove(transition.active); } // Trigger parent event @@ -292,6 +300,7 @@ class AnimateChild extends React.Component { } else { show = child.props[showProp]; } + console.log('[Child]', !!supportTransition, !!transition); // Clone child const newChildProps = { diff --git a/src/util.js b/src/util.js index b7e1821..1f83bbf 100644 --- a/src/util.js +++ b/src/util.js @@ -116,7 +116,9 @@ export function cloneProps(props, propList) { return newProps; } -export function getTransitionName(transitionName = '', transitionType) { +export function getTransitionName(transitionName, transitionType) { + if (!transitionName) return null; + if (typeof transitionName === 'object') { return transitionName[transitionType]; } From 2761c29ede3b48689c63189b41afd54533971726 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 25 May 2018 11:58:38 +0800 Subject: [PATCH 13/41] support customize handler display --- examples/simple-animation.js | 2 -- package.json | 2 +- src/AnimateChild.jsx | 10 +++++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/simple-animation.js b/examples/simple-animation.js index 7ca8059..e34cb60 100644 --- a/examples/simple-animation.js +++ b/examples/simple-animation.js @@ -52,7 +52,6 @@ class Demo extends React.Component { }); return { stop() { - console.error('enter stop!'); velocity(node, 'finish'); // velocity complete is async complete(); @@ -78,7 +77,6 @@ class Demo extends React.Component { }); return { stop() { - console.error('leave stop!'); velocity(node, 'finish'); // velocity complete is async complete(); diff --git a/package.json b/package.json index cfe078c..c5d36d7 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "rc-tools": "8.x", "react": "^16.0.0", "react-dom": "^16.0.0", - "velocity-animate": "~1.2.2" + "velocity-animate": "~1.5.1" }, "pre-commit": [ "lint" diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 377e1e5..a161a13 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -282,7 +282,7 @@ class AnimateChild extends React.Component { }; render() { - const { child, transitionQueue, transitionActive } = this.state; + const { child, transitionQueue, transitionActive, currentTransitionHandler } = this.state; const { showProp } = this.props; const { className } = child.props || {}; @@ -295,12 +295,16 @@ class AnimateChild extends React.Component { ) : className; let show = true; - if (supportTransition && transition) { + + // Keep show when is in transition or has customize animate + if ( + (supportTransition && transition) || + currentTransitionHandler + ) { show = true; } else { show = child.props[showProp]; } - console.log('[Child]', !!supportTransition, !!transition); // Clone child const newChildProps = { From 1e1707892278df97b2f14346bd9c787a183a6bb3 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 25 May 2018 12:06:25 +0800 Subject: [PATCH 14/41] adjust animate show logic --- src/Animate.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Animate.jsx b/src/Animate.jsx index cf92dfe..8b629a2 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -136,10 +136,11 @@ class Animate extends React.Component { } let show = true; - if (showProp) { - show = node.props[showProp]; - } else if (!this.hasChild(node.key)) { + + if (!this.hasChild(node.key)) { show = false; + } else if (showProp) { + show = node.props[showProp]; } const key = node.key || defaultKey; From 6267a5f84b723400c9ff3ba2d11864d17c16caca Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 25 May 2018 13:41:22 +0800 Subject: [PATCH 15/41] use old velocity --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5d36d7..cfe078c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "rc-tools": "8.x", "react": "^16.0.0", "react-dom": "^16.0.0", - "velocity-animate": "~1.5.1" + "velocity-animate": "~1.2.2" }, "pre-commit": [ "lint" From 3bc933dd37b81053f17526323ad2c0564fcce21e Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 25 May 2018 14:14:29 +0800 Subject: [PATCH 16/41] adjust logic of leave --- examples/transitionAppear.js | 8 ++++---- src/Animate.jsx | 2 +- src/AnimateChild.jsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/transitionAppear.js b/examples/transitionAppear.js index 1f0a811..086d05b 100644 --- a/examples/transitionAppear.js +++ b/examples/transitionAppear.js @@ -6,16 +6,16 @@ import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import './assets/slow.less'; -const Box = () => { - console.log('render', this.props.visible); +const Box = (props) => { + console.log('render', props.visible); const style = { - display: this.props.visible ? 'block' : 'none', + display: props.visible ? 'block' : 'none', marginTop: '20px', width: '200px', height: '200px', backgroundColor: 'red', }; - return (
); + return (
); }; Box.propTypes = { diff --git a/src/Animate.jsx b/src/Animate.jsx index 8b629a2..4d7b100 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -154,7 +154,7 @@ class Animate extends React.Component { style={node.props.style} key={key} - animateKey={key} + animateKey={node.key} // Keep trans origin key onChildLeaved={this.onChildLeaved} onChildAppear={this.onChildAppear} diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index a161a13..aedd506 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -99,7 +99,7 @@ class AnimateChild extends React.Component { basic: getTransitionName(transitionName, 'enter'), active: getTransitionName(transitionName, 'enter-active'), }); - } else if (!appeared && !show && transitionLeave) { + } else if (!appeared && !show) { if (!supportTransition || !transitionLeave) { // Call leave directly if not support or not set leave nextProps.onChildLeaved(nextProps.animateKey); From 92a67fb57a3b39afe106ffdbcddd3d5094daa222 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 25 May 2018 15:38:37 +0800 Subject: [PATCH 17/41] adjust leave logic --- src/Animate.jsx | 12 +++++++++++- src/AnimateChild.jsx | 18 ++++++++++++------ tests/multiple.spec.js | 4 ++-- tests/single-common.spec.js | 5 +++-- tests/single.spec.js | 6 ++++-- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/Animate.jsx b/src/Animate.jsx index 4d7b100..d8c2363 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -50,6 +50,7 @@ class Animate extends React.Component { const newState = { prevProps: cloneProps(nextProps, clonePropList), }; + const { showProp } = nextProps; function processState(propName, updater) { if (prevProps[propName] !== nextProps[propName]) { @@ -60,8 +61,17 @@ class Animate extends React.Component { } processState('children', (children) => { - const prevChildren = prevState.mergedChildren; const currentChildren = toArray(children).filter(node => node); + const prevChildren = prevState.mergedChildren.filter((node) => { + // Remove prev child if not show anymore + if ( + currentChildren.every(({ key }) => key !== node.key) && + showProp && !node.props[showProp] + ) { + return false; + } + return true; + }); // Merge prev children to keep the animation newState.mergedChildren = mergeChildren(prevChildren, currentChildren); diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index aedd506..dd15eb8 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -100,7 +100,7 @@ class AnimateChild extends React.Component { active: getTransitionName(transitionName, 'enter-active'), }); } else if (!appeared && !show) { - if (!supportTransition || !transitionLeave) { + if (!transitionLeave) { // Call leave directly if not support or not set leave nextProps.onChildLeaved(nextProps.animateKey); } else { @@ -108,6 +108,9 @@ class AnimateChild extends React.Component { type: 'leave', basic: getTransitionName(transitionName, 'leave'), active: getTransitionName(transitionName, 'leave-active'), + postAction: () => { + nextProps.onChildLeaved(nextProps.animateKey); + }, }); } } @@ -115,7 +118,7 @@ class AnimateChild extends React.Component { // exclusive processState('exclusive', (isExclusive) => { - if (supportTransition && isExclusive) { + if (isExclusive) { const transitionQueue = newState.transitionQueue || prevState.transitionQueue; newState.transitionQueue = transitionQueue.slice(-1); if (transitionQueue.length !== 1) { @@ -157,6 +160,7 @@ class AnimateChild extends React.Component { } componentWillUnmount() { + this._destroy = true; this.cleanDomEvent(); } @@ -255,10 +259,12 @@ class AnimateChild extends React.Component { const transition = transitionQueue[0]; // Update transition queue - this.setState({ - transitionQueue: transitionQueue.slice(1), - currentTransitionHandler: null, - }); + if (!this._destroy) { + this.setState({ + transitionQueue: transitionQueue.slice(1), + currentTransitionHandler: null, + }); + } // [Legacy] Same as above, we need call js to remove the class if (transition) { diff --git a/tests/multiple.spec.js b/tests/multiple.spec.js index 998614a..41566c8 100644 --- a/tests/multiple.spec.js +++ b/tests/multiple.spec.js @@ -4,8 +4,8 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import TestUtils from 'react-dom/test-utils'; import expect from 'expect.js'; -import CssAnimation from 'css-animation'; import Animate from '../'; +import { supportTransition } from '../src/util'; import './index.spec.css'; class Todo extends React.Component { @@ -104,7 +104,7 @@ describe('Animate', () => { expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); }); - if (!CssAnimation.isCssAnimationSupported) { + if (!supportTransition) { return; } diff --git a/tests/single-common.spec.js b/tests/single-common.spec.js index 753be94..9e8e024 100644 --- a/tests/single-common.spec.js +++ b/tests/single-common.spec.js @@ -34,14 +34,14 @@ export default function test(createClass, title) { } }); - describe('when transitionAppear', () => { + describe.only('when transitionAppear', () => { it('should render children', () => { expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'span')[0]).not.to.be.ok(); const child = TestUtils.findRenderedDOMComponentWithTag(instance, 'div'); expect(getOpacity(ReactDOM.findDOMNode(child))).to.be(1); }); - it('should anim children', (done) => { + it.only('should anim children', (done) => { const innerDiv = document.createElement('div'); document.body.appendChild(innerDiv); const Component = createClass({ @@ -52,6 +52,7 @@ export default function test(createClass, title) { }); ReactDOM.render(, innerDiv, (innerInstance) => { + console.log('>>>', innerInstance); expect(TestUtils.scryRenderedDOMComponentsWithTag(innerInstance, 'span')[0]).not.to.be.ok(); const child = TestUtils.findRenderedDOMComponentWithTag(innerInstance, 'div'); diff --git a/tests/single.spec.js b/tests/single.spec.js index 9bbc626..06fc374 100644 --- a/tests/single.spec.js +++ b/tests/single.spec.js @@ -1,8 +1,8 @@ /* eslint no-console:0, react/no-multi-comp:0 */ import React from 'react'; -import CssAnimation from 'css-animation'; import Animate from '../index'; +import { supportTransition } from '../src/util'; import './index.spec.css'; @@ -16,6 +16,7 @@ function createClass(options) { } render() { + console.log('!!!!!!!!!!!', options.remove && !this.state.transitionEnter); return ( Date: Fri, 25 May 2018 16:03:23 +0800 Subject: [PATCH 18/41] fix in IE --- src/AnimateChild.jsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index dd15eb8..97928b3 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -224,20 +224,20 @@ class AnimateChild extends React.Component { mapAnimation('appear', onChildAppear); mapAnimation('enter', onChildEnter); mapAnimation('leave', onChildLeave); - } - // Update transition active class - if (!transitionActive) { - // requestAnimationFrame not support in IE 9- - // Use setTimeout instead - setTimeout(() => { - this.setState({transitionActive: true}); - }, 0); - } + // Update transition active class + if (!transitionActive) { + // requestAnimationFrame not support in IE 9- + // Use setTimeout instead + setTimeout(() => { + this.setState({transitionActive: true}); + }, 0); + } - // Call onMotionEnd directly - if (!supportTransition && !hasHandler) { - this.onMotionEnd({ target: $ele }); + // Call onMotionEnd directly + if (!supportTransition && !hasHandler) { + this.onMotionEnd({ target: $ele }); + } } } }; From 4a5eb53805651c829205858450b4efe2c9b6e27b Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 28 May 2018 11:53:38 +0800 Subject: [PATCH 19/41] redesign the animate logic --- examples/hide-todo.js | 3 +- package.json | 1 + src/Animate.jsx | 28 +--- src/AnimateChild.jsx | 316 +++++++++++++++++++++--------------------- 4 files changed, 166 insertions(+), 182 deletions(-) diff --git a/examples/hide-todo.js b/examples/hide-todo.js index ffdbbf5..fbe040f 100644 --- a/examples/hide-todo.js +++ b/examples/hide-todo.js @@ -49,7 +49,8 @@ class TodoList extends React.Component { { content: 'hello', visible: true }, { content: 'world', visible: true }, { content: 'click', visible: true }, - { content: 'me', visible: true }], + { content: 'me', visible: true }, + ], } handleHide = (i, item) => { diff --git a/package.json b/package.json index cfe078c..fd318fc 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "component-classes": "^1.2.6", "fbjs": "^0.8.16", "prop-types": "15.x", + "raf": "^3.4.0", "rc-util": "^4.5.0", "react-lifecycles-compat": "^3.0.4" } diff --git a/src/Animate.jsx b/src/Animate.jsx index d8c2363..a56d32f 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -105,27 +105,6 @@ class Animate extends React.Component { } }; - onAnimationAction = (func, $ele, animateDone) => { - if (func) { - const animation = func($ele, animateDone); - return animation || {}; - } - return null; - }; - - onChildAppear = (key, $ele, animateDone) => { - const { appear } = this.props.animation; - return this.onAnimationAction(appear, $ele, animateDone); - }; - onChildEnter = (key, $ele, animateDone) => { - const { enter } = this.props.animation; - return this.onAnimationAction(enter, $ele, animateDone); - }; - onChildLeave = (key, $ele, animateDone) => { - const { leave } = this.props.animation; - return this.onAnimationAction(leave, $ele, animateDone); - }; - hasChild = (key) => { const { children } = this.props; @@ -136,7 +115,7 @@ class Animate extends React.Component { const { appeared, mergedChildren } = this.state; const { component: Component, componentProps, - className, style, showProp, + className, style, showProp, animation, } = this.props; @@ -165,11 +144,8 @@ class Animate extends React.Component { key={key} animateKey={node.key} // Keep trans origin key + animation={animation} onChildLeaved={this.onChildLeaved} - - onChildAppear={this.onChildAppear} - onChildEnter={this.onChildEnter} - onChildLeave={this.onChildLeave} > {node} diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 97928b3..4c21bcf 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classNames from 'classnames'; import classes from 'component-classes'; +import raf from 'raf'; import { cloneProps, getTransitionName, @@ -15,6 +16,7 @@ const clonePropList = [ 'show', 'exclusive', 'children', + 'animation', ]; /** @@ -35,20 +37,13 @@ class AnimateChild extends React.Component { showProp: PropTypes.string, animateKey: PropTypes.any, + animation: PropTypes.object, onChildLeaved: PropTypes.func, - - // Customize event handler - onChildAppear: PropTypes.func, - onChildEnter: PropTypes.func, - onChildLeave: PropTypes.func, } static getDerivedStateFromProps(nextProps, prevState) { const { prevProps = {} } = prevState; - const { - transitionName, transitionAppear, transitionEnter, transitionLeave, - exclusive, appeared, - } = nextProps; + const { appeared } = nextProps; const newState = { prevProps: cloneProps(nextProps, clonePropList), @@ -64,16 +59,9 @@ class AnimateChild extends React.Component { return false; } - function pushTransition(transition) { - newState.transitionQueue = newState.transitionQueue || prevState.transitionQueue; - if (exclusive) { - newState.transitionQueue = [transition]; - newState.transitionActive = false; - - newState.currentTransitionHandler = null - } else { - newState.transitionQueue.push(transition); - } + function pushEvent(eventType) { + newState.eventQueue = newState.eventQueue || prevState.eventQueue; + newState.eventQueue.push(eventType); } // Child update. Only set child. @@ -82,36 +70,18 @@ class AnimateChild extends React.Component { }); processState('appeared', (isAppeared) => { - if (isAppeared && transitionAppear) { - pushTransition({ - type: 'appear', - basic: getTransitionName(transitionName, 'appear'), - active: getTransitionName(transitionName, 'appear-active'), - }); + if (isAppeared) { + pushEvent('appear'); } }); // Show update processState('show', (show) => { - if (!appeared && show && transitionEnter) { - pushTransition({ - type: 'enter', - basic: getTransitionName(transitionName, 'enter'), - active: getTransitionName(transitionName, 'enter-active'), - }); - } else if (!appeared && !show) { - if (!transitionLeave) { - // Call leave directly if not support or not set leave - nextProps.onChildLeaved(nextProps.animateKey); + if (!appeared) { + if (show) { + pushEvent('enter'); } else { - pushTransition({ - type: 'leave', - basic: getTransitionName(transitionName, 'leave'), - active: getTransitionName(transitionName, 'leave-active'), - postAction: () => { - nextProps.onChildLeaved(nextProps.animateKey); - }, - }); + pushEvent('leave'); } } }); @@ -119,11 +89,7 @@ class AnimateChild extends React.Component { // exclusive processState('exclusive', (isExclusive) => { if (isExclusive) { - const transitionQueue = newState.transitionQueue || prevState.transitionQueue; - newState.transitionQueue = transitionQueue.slice(-1); - if (transitionQueue.length !== 1) { - newState.transitionActive = false; - } + // TODO: clean the queue } }); @@ -138,17 +104,14 @@ class AnimateChild extends React.Component { // also can handle the animate, let keep the logic. this.$prevEle = null; - // Keep the current transition - this.currentTransition = null; + this.currentEvent = null; } state = { child: null, - transitionQueue: [], - transitionActive: false, - // Customize animation handler - currentTransitionHandler: null, + eventQueue: [], + eventActive: false, } componentDidMount() { @@ -164,121 +127,168 @@ class AnimateChild extends React.Component { this.cleanDomEvent(); } - onDomUpdated = (prevProps, prevState) => { - const { transitionQueue, transitionActive, currentTransitionHandler } = this.state; - const { animateKey, onChildAppear, onChildEnter, onChildLeave } = this.props; - - const transition = transitionQueue[0]; + onDomUpdated = () => { + const { eventActive } = this.state; + const { + transitionName, animation, + } = this.props; const $ele = ReactDOM.findDOMNode(this); - // Clean up if currentTransitionHandler set to null - if ( - prevState.currentTransitionHandler && - prevState.currentTransitionHandler !== currentTransitionHandler && - !currentTransitionHandler - ) { - prevState.currentTransitionHandler.stop(); + // Skip if dom element not ready + if (!$ele) return; + + // [Legacy] Add animation/transition event by dom level + if (supportTransition && this.$prevEle !== $ele) { + this.cleanDomEvent(); + + this.$prevEle = $ele; + this.$prevEle.addEventListener(animationEndName, this.onMotionEnd); + this.$prevEle.addEventListener(transitionEndName, this.onMotionEnd); } - if (transition && $ele) { - // [Legacy] Since origin code use js to set `className`. - // This caused that any component without support `className` can be forced set. - // Let's keep the logic. - const nodeClasses = classes($ele); - if (transition.basic) nodeClasses.add(transition.basic); + const currentEvent = this.getCurrentEvent(); + if (!currentEvent) return; - if (transitionActive) { - if (transition.active) nodeClasses.add(transition.active); - } + const { eventType } = currentEvent; + + // [Legacy] Since origin code use js to set `className`. + // This caused that any component without support `className` can be forced set. + // Let's keep the logic. + function legacyAppendClass() { + if (!supportTransition) return; - // [Legacy] Add animation/transition event by dom level - if (supportTransition && this.$prevEle !== $ele) { - this.cleanDomEvent(); + const nodeClasses = classes($ele); + const basicClassName = getTransitionName(transitionName, eventType); + if (basicClassName) nodeClasses.add(basicClassName); - this.$prevEle = $ele; - this.$prevEle.addEventListener(animationEndName, this.onMotionEnd); - this.$prevEle.addEventListener(transitionEndName, this.onMotionEnd); + if (eventActive) { + const activeClassName = getTransitionName(transitionName, `${eventType}-active`); + if (activeClassName) nodeClasses.add(activeClassName); } - // Check if transition update - let hasHandler = false; - if (this.currentTransition !== transition) { - this.currentTransition = transition; - - // Trigger customize animation - const mapAnimation = (type, func) => { - if (transition.type === type && func) { - const handler = func(animateKey, $ele, () => { - this.onMotionEnd({ target: $ele }); - }); - if (handler) { - this.setState({ - currentTransitionHandler: handler, - }); - hasHandler = true; - } - } - }; + } - mapAnimation('appear', onChildAppear); - mapAnimation('enter', onChildEnter); - mapAnimation('leave', onChildLeave); - - // Update transition active class - if (!transitionActive) { - // requestAnimationFrame not support in IE 9- - // Use setTimeout instead - setTimeout(() => { - this.setState({transitionActive: true}); - }, 0); - } + if (this.currentEvent && this.currentEvent.type === eventType) { + legacyAppendClass(); + return; + } - // Call onMotionEnd directly - if (!supportTransition && !hasHandler) { - this.onMotionEnd({ target: $ele }); - } - } + // Clean up last event environment + if (this.currentEvent && this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { + this.currentEvent.animateObj.stop(); } - }; - onMotionEnd = ({ target }) => { - const { transitionQueue, currentTransitionHandler } = this.state; - if (!transitionQueue.length) { - if (currentTransitionHandler) { - this.setState({ - currentTransitionHandler: null, + // New event come + this.currentEvent = { + type: eventType, + }; + + const animationHandler = (animation || {})[eventType]; + // =============== Check if has customize animation =============== + if (animationHandler) { + this.currentEvent.animateObj = animationHandler($ele, () => { + this.onMotionEnd({ target: $ele }); + }); + + // ==================== Use transition instead ==================== + } else if (supportTransition) { + legacyAppendClass(); + if (!eventActive) { + // Trigger `eventActive` in next frame + raf(() => { + if (this.currentEvent && this.currentEvent.type === eventType) { + this.setState({ eventActive: true }); + } }); } - return; + + // ======================= Just next action ======================= + } else { + this.onMotionEnd({ target: $ele }); } + } + + onMotionEnd = ({ target }) => { + const { transitionName, onChildLeaved, animateKey } = this.props; + const currentEvent = this.getCurrentEvent(); + if (!currentEvent) return; + + const { restQueue } = currentEvent; const $ele = ReactDOM.findDOMNode(this); - if ($ele === target) { - const { onChildLeaved, animateKey } = this.props; - const transition = transitionQueue[0]; - - // Update transition queue - if (!this._destroy) { - this.setState({ - transitionQueue: transitionQueue.slice(1), - currentTransitionHandler: null, - }); - } + if (!this.currentEvent || $ele !== target) return; - // [Legacy] Same as above, we need call js to remove the class - if (transition) { - const nodeClasses = classes($ele); - if (transition.basic) nodeClasses.remove(transition.basic); - if (transition.active) nodeClasses.remove(transition.active); + if (this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { + this.currentEvent.animateObj.stop(); + } + + // [Legacy] Same as above, we need call js to remove the class + if (supportTransition) { + const basicClassName = getTransitionName(transitionName, this.currentEvent.type); + const activeClassName = getTransitionName(transitionName, `${this.currentEvent.type}-active`); + + const nodeClasses = classes($ele); + if (basicClassName) nodeClasses.remove(basicClassName); + if (activeClassName) nodeClasses.remove(activeClassName); + } + + // Additional process the leave event + if (this.currentEvent.type === 'leave') { + onChildLeaved(animateKey); + } + + this.currentEvent = null; + + // Next queue + if (!this._destroy) { + this.setState({ + eventQueue: restQueue, + eventActive: false, + }); + } + }; + + getCurrentEvent = () => { + const { eventQueue = [] } = this.state; + const { + animation, exclusive, + transitionAppear, transitionEnter, transitionLeave, + } = this.props; + + function hasEventHandler(eventType) { + return (eventType === 'appear' && (transitionAppear || animation.appear)) || + (eventType === 'enter' && (transitionEnter || animation.enter)) || + (eventType === 'leave' && (transitionLeave || animation.leave)); + } + + // If is exclusive, only check the last event + if (exclusive) { + const eventType = eventQueue[eventQueue.length - 1]; + if (hasEventHandler(eventType)) { + return { + eventType, + restQueue: [], + }; } + return null; + } - // Trigger parent event - if (transition.type === 'leave') { - onChildLeaved(animateKey); + // Loop check the queue until find match + let cloneQueue = eventQueue.slice(); + while (cloneQueue.length) { + const [eventType, ...restQueue] = cloneQueue; + if (hasEventHandler(eventType)) { + return { + eventType, + restQueue, + } } + cloneQueue = restQueue; } - } + + return null; + }; cleanDomEvent = () => { if (this.$prevEle && supportTransition) { @@ -288,25 +298,21 @@ class AnimateChild extends React.Component { }; render() { - const { child, transitionQueue, transitionActive, currentTransitionHandler } = this.state; - const { showProp } = this.props; + const { child, eventActive } = this.state; + const { showProp, transitionName } = this.props; const { className } = child.props || {}; // Class name - const transition = transitionQueue[0]; - const connectClassName = (supportTransition && transition) ? classNames( + const connectClassName = (supportTransition && this.currentEvent) ? classNames( className, - transition.basic, - transitionActive && transition.active, + getTransitionName(transitionName, this.currentEvent.type), + eventActive && getTransitionName(transitionName, `${this.currentEvent.type}-active`), ) : className; let show = true; // Keep show when is in transition or has customize animate - if ( - (supportTransition && transition) || - currentTransitionHandler - ) { + if (supportTransition && this.currentEvent) { show = true; } else { show = child.props[showProp]; From 45888983f9b5e09a0f78de13da472258154862dd Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 28 May 2018 12:23:40 +0800 Subject: [PATCH 20/41] trigger onEvent --- src/Animate.jsx | 3 +-- src/AnimateChild.jsx | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Animate.jsx b/src/Animate.jsx index a56d32f..b310f1b 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -115,7 +115,7 @@ class Animate extends React.Component { const { appeared, mergedChildren } = this.state; const { component: Component, componentProps, - className, style, showProp, animation, + className, style, showProp, } = this.props; @@ -144,7 +144,6 @@ class Animate extends React.Component { key={key} animateKey={node.key} // Keep trans origin key - animation={animation} onChildLeaved={this.onChildLeaved} > {node} diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 4c21bcf..3f03b6c 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -39,6 +39,11 @@ class AnimateChild extends React.Component { animateKey: PropTypes.any, animation: PropTypes.object, onChildLeaved: PropTypes.func, + + onEnd: PropTypes.func, + onAppear: PropTypes.func, + onEnter: PropTypes.func, + onLeave: PropTypes.func, } static getDerivedStateFromProps(nextProps, prevState) { @@ -210,7 +215,10 @@ class AnimateChild extends React.Component { } onMotionEnd = ({ target }) => { - const { transitionName, onChildLeaved, animateKey } = this.props; + const { + transitionName, onChildLeaved, animateKey, + onAppear, onEnter, onLeave, onEnd, + } = this.props; const currentEvent = this.getCurrentEvent(); if (!currentEvent) return; @@ -238,6 +246,22 @@ class AnimateChild extends React.Component { onChildLeaved(animateKey); } + // [Legacy] Trigger on event when it's last event + if (!restQueue.length) { + if (this.currentEvent.type === 'appear' && onAppear) { + onAppear(animateKey); + } else if (this.currentEvent.type === 'enter' && onEnd) { + onEnter(animateKey); + } else if (this.currentEvent.type === 'leave' && onLeave) { + onLeave(animateKey); + } + + if (onEnd) { + // OnEnd(key, isShow) + onEnd(animateKey, this.currentEvent.type !== 'leave'); + } + } + this.currentEvent = null; // Next queue From 7c7348d5e51b3b960f7f575ecab92c635e7cefda Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 28 May 2018 12:42:20 +0800 Subject: [PATCH 21/41] addtional leave trigger when transitionLeave is false --- src/AnimateChild.jsx | 48 ++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 3f03b6c..9948391 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -135,7 +135,7 @@ class AnimateChild extends React.Component { onDomUpdated = () => { const { eventActive } = this.state; const { - transitionName, animation, + transitionName, animation, onChildLeaved, animateKey, } = this.props; const $ele = ReactDOM.findDOMNode(this); @@ -153,7 +153,13 @@ class AnimateChild extends React.Component { } const currentEvent = this.getCurrentEvent(); - if (!currentEvent) return; + if (currentEvent.empty) { + // Additional process the leave event + if (currentEvent.lastEventType === 'leave') { + onChildLeaved(animateKey); + } + return; + } const { eventType } = currentEvent; @@ -220,7 +226,7 @@ class AnimateChild extends React.Component { onAppear, onEnter, onLeave, onEnd, } = this.props; const currentEvent = this.getCurrentEvent(); - if (!currentEvent) return; + if (currentEvent.empty) return; const { restQueue } = currentEvent; @@ -286,32 +292,40 @@ class AnimateChild extends React.Component { (eventType === 'leave' && (transitionLeave || animation.leave)); } + let event = null; // If is exclusive, only check the last event if (exclusive) { const eventType = eventQueue[eventQueue.length - 1]; if (hasEventHandler(eventType)) { - return { + event = { eventType, restQueue: [], }; } - return null; - } - - // Loop check the queue until find match - let cloneQueue = eventQueue.slice(); - while (cloneQueue.length) { - const [eventType, ...restQueue] = cloneQueue; - if (hasEventHandler(eventType)) { - return { - eventType, - restQueue, + } else { + // Loop check the queue until find match + let cloneQueue = eventQueue.slice(); + while (cloneQueue.length) { + const [eventType, ...restQueue] = cloneQueue; + if (hasEventHandler(eventType)) { + event = { + eventType, + restQueue, + }; + break; } + cloneQueue = restQueue; } - cloneQueue = restQueue; } - return null; + if (!event) { + event = { + empty: true, + lastEventType: eventQueue[eventQueue.length - 1], + }; + } + + return event; }; cleanDomEvent = () => { From 4de2060a1511e983e5eecb8d5d91b75ffc231b09 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 28 May 2018 14:17:36 +0800 Subject: [PATCH 22/41] clean up when new event come --- src/AnimateChild.jsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 9948391..839907f 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -162,6 +162,7 @@ class AnimateChild extends React.Component { } const { eventType } = currentEvent; + const nodeClasses = classes($ele); // [Legacy] Since origin code use js to set `className`. // This caused that any component without support `className` can be forced set. @@ -169,8 +170,7 @@ class AnimateChild extends React.Component { function legacyAppendClass() { if (!supportTransition) return; - const nodeClasses = classes($ele); - const basicClassName = getTransitionName(transitionName, eventType); + const basicClassName = getTransitionName(transitionName, `${eventType}`); if (basicClassName) nodeClasses.add(basicClassName); if (eventActive) { @@ -190,6 +190,14 @@ class AnimateChild extends React.Component { this.currentEvent.animateObj.stop(); } + // Clean up last transition class + if (this.currentEvent) { + const basicClassName = getTransitionName(transitionName, `${this.currentEvent.type}`); + const activeClassName = getTransitionName(transitionName, `${this.currentEvent.type}-active`); + if (basicClassName) nodeClasses.remove(basicClassName); + if (activeClassName) nodeClasses.remove(activeClassName); + } + // New event come this.currentEvent = { type: eventType, From e7368908f3e1b5f061d885405a28e7be099ea8a1 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 28 May 2018 17:30:13 +0800 Subject: [PATCH 23/41] event will only fire once --- src/AnimateChild.jsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 839907f..bfcc4bf 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -65,8 +65,16 @@ class AnimateChild extends React.Component { } function pushEvent(eventType) { - newState.eventQueue = newState.eventQueue || prevState.eventQueue; - newState.eventQueue.push(eventType); + let eventQueue = newState.eventQueue || prevState.eventQueue.slice(); + const matchIndex = eventQueue.findIndex(type => type === eventType); + + // Clean the rest event if eventType match + if (matchIndex !== -1) { + eventQueue = eventQueue.slice(0, matchIndex); + } + + eventQueue.push(eventType); + newState.eventQueue = eventQueue; } // Child update. Only set child. @@ -91,13 +99,6 @@ class AnimateChild extends React.Component { } }); - // exclusive - processState('exclusive', (isExclusive) => { - if (isExclusive) { - // TODO: clean the queue - } - }); - return newState; } From 8fa534de468ed2640c2f609816e36a12063dc94b Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 29 May 2018 11:54:15 +0800 Subject: [PATCH 24/41] fit for es5 --- src/Animate.jsx | 2 +- src/AnimateChild.jsx | 2 +- tests/multiple.spec.js | 181 ----------------------------------------- 3 files changed, 2 insertions(+), 183 deletions(-) delete mode 100644 tests/multiple.spec.js diff --git a/src/Animate.jsx b/src/Animate.jsx index b310f1b..cdd1049 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -108,7 +108,7 @@ class Animate extends React.Component { hasChild = (key) => { const { children } = this.props; - return toArray(children).some(node => node.key === key); + return toArray(children).some(node => node && node.key === key); }; render() { diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index bfcc4bf..3a23ae7 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -66,7 +66,7 @@ class AnimateChild extends React.Component { function pushEvent(eventType) { let eventQueue = newState.eventQueue || prevState.eventQueue.slice(); - const matchIndex = eventQueue.findIndex(type => type === eventType); + const matchIndex = eventQueue.indexOf(eventType); // Clean the rest event if eventType match if (matchIndex !== -1) { diff --git a/tests/multiple.spec.js b/tests/multiple.spec.js deleted file mode 100644 index 41566c8..0000000 --- a/tests/multiple.spec.js +++ /dev/null @@ -1,181 +0,0 @@ -/* eslint no-console:0, react/no-multi-comp:0 */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import TestUtils from 'react-dom/test-utils'; -import expect from 'expect.js'; -import Animate from '../'; -import { supportTransition } from '../src/util'; -import './index.spec.css'; - -class Todo extends React.Component { - static propTypes = { - end: PropTypes.func, - onClick: PropTypes.func, - } - - static defaultProps = { - end() {}, - } - - componentWillUnmount() { - this.props.end(); - } - - render() { - const props = this.props; - return ( -
- {props.children} -
- ); - } -} - -class TodoList extends React.Component { - state = { - items: ['hello', 'world', 'click', 'me'], - } - - handleAdd = (item) => { - const newItems = - this.state.items.concat(item); - this.setState({ items: newItems }); - } - - handleRemove = (i) => { - const newItems = this.state.items; - newItems.splice(i, 1); - this.setState({ items: newItems }); - } - - insertUndefined = (i) => { - const newItems = this.state.items; - newItems.splice(i, 1, undefined); - this.setState({ items: newItems }); - } - - render() { - const items = this.state.items.map((item, i) => { - // Allow testing of null/undefined values by just passing them directly to - // Animate instead of wrapping them with a Todo component - if (item === null || item === undefined) { - return item; - } - - return ( - { this.handleRemove.bind(this, i); }}> - {item} - - ); - }); - return ( -
- - {items} - -
- ); - } -} - -describe('Animate', () => { - let list; - const container = document.createElement('div'); - document.body.appendChild(container); - - beforeEach((done) => { - ReactDOM.render(, container, function init() { - list = this; - done(); - }); - }); - - afterEach(() => { - try { - ReactDOM.unmountComponentAtNode(container); - } catch (e) { - console.log(e); - container.innerHTML = ''; - } - }); - - it('create works', () => { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); - }); - - if (!supportTransition) { - return; - } - - it('transitionLeave works', function t2(done) { - this.timeout(5999); - list.handleRemove(0); - setTimeout(() => { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); - if (!window.callPhantom) { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) - .to.contain('example-leave'); - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) - .to.contain('example-leave-active'); - } - }, 100); - setTimeout(() => { - if (!window.callPhantom) { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(3); - } - done(); - }, 1000); - }); - - it('transitionLeave works', function t1(done) { - this.timeout(5999); - list.handleAdd(Date.now()); - setTimeout(() => { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(5); - if (!window.callPhantom) { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) - .to.contain('example-enter'); - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) - .to.contain('example-enter-active'); - } - }, 100); - setTimeout(() => { - if (!window.callPhantom) { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(5); - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) - .not.to.contain('example-enter'); - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) - .not.to.contain('example-enter-active'); - } - done(); - }, 1000); - }); - - it('does not generate an error when a null or undefined child is present', () => { - list.handleAdd(null); - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); - list.handleAdd(undefined); - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); - }); - - it('transitionLeave works when a child becomes undefined', function t4(done) { - this.timeout(5999); - list.insertUndefined(0); - setTimeout(() => { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); - if (!window.callPhantom) { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) - .to.contain('example-leave'); - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) - .to.contain('example-leave-active'); - } - }, 100); - setTimeout(() => { - if (!window.callPhantom) { - expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(3); - } - done(); - }, 1000); - }); -}); From 16053da846b4994902f9f5d24c08af5dab964cfd Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 29 May 2018 12:28:31 +0800 Subject: [PATCH 25/41] clean up --- tests/multiple.spec.js | 181 +++++++++++++++++++++++++++++++++ tests/single-animation.spec.js | 4 +- tests/single-common.spec.js | 30 +++--- tests/single.spec.js | 5 +- 4 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 tests/multiple.spec.js diff --git a/tests/multiple.spec.js b/tests/multiple.spec.js new file mode 100644 index 0000000..a577265 --- /dev/null +++ b/tests/multiple.spec.js @@ -0,0 +1,181 @@ +/* eslint no-console:0, react/no-multi-comp:0 */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import TestUtils from 'react-dom/test-utils'; +import expect from 'expect.js'; +import Animate from '../'; +import { supportTransition } from '../src/util'; +import './index.spec.css'; + +class Todo extends React.Component { + static propTypes = { + end: PropTypes.func, + onClick: PropTypes.func, + } + + static defaultProps = { + end() {}, + } + + componentWillUnmount() { + this.props.end(); + } + + render() { + const props = this.props; + return ( +
+ {props.children} +
+ ); + } +} + +class TodoList extends React.Component { + state = { + items: ['hello', 'world', 'click', 'me'], + } + + handleAdd = (item) => { + const newItems = + this.state.items.concat(item); + this.setState({ items: newItems }); + } + + handleRemove = (i) => { + const newItems = this.state.items; + newItems.splice(i, 1); + this.setState({ items: newItems }); + } + + insertUndefined = (i) => { + const newItems = this.state.items; + newItems.splice(i, 1, undefined); + this.setState({ items: newItems }); + } + + render() { + const items = this.state.items.map((item, i) => { + // Allow testing of null/undefined values by just passing them directly to + // Animate instead of wrapping them with a Todo component + if (item === null || item === undefined) { + return item; + } + + return ( + { this.handleRemove(i); }}> + {item} + + ); + }); + return ( +
+ + {items} + +
+ ); + } +} + +describe('Animate', () => { + let list; + const container = document.createElement('div'); + document.body.appendChild(container); + + beforeEach((done) => { + ReactDOM.render(, container, function init() { + list = this; + done(); + }); + }); + + afterEach(() => { + try { + ReactDOM.unmountComponentAtNode(container); + } catch (e) { + console.log(e); + container.innerHTML = ''; + } + }); + + it('create works', () => { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); + }); + + if (!supportTransition) { + return; + } + + it('transitionLeave works', function t2(done) { + this.timeout(5999); + list.handleRemove(0); + setTimeout(() => { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); + if (!window.callPhantom) { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) + .to.contain('example-leave'); + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) + .to.contain('example-leave-active'); + } + }, 100); + setTimeout(() => { + if (!window.callPhantom) { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(3); + } + done(); + }, 1000); + }); + + it('transitionLeave works', function t1(done) { + this.timeout(5999); + list.handleAdd(Date.now()); + setTimeout(() => { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(5); + if (!window.callPhantom) { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) + .to.contain('example-enter'); + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) + .to.contain('example-enter-active'); + } + }, 100); + setTimeout(() => { + if (!window.callPhantom) { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(5); + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) + .not.to.contain('example-enter'); + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].className) + .not.to.contain('example-enter-active'); + } + done(); + }, 1000); + }); + + it('does not generate an error when a null or undefined child is present', () => { + list.handleAdd(null); + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); + list.handleAdd(undefined); + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); + }); + + it('transitionLeave works when a child becomes undefined', function t4(done) { + this.timeout(5999); + list.insertUndefined(0); + setTimeout(() => { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); + if (!window.callPhantom) { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) + .to.contain('example-leave'); + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].className) + .to.contain('example-leave-active'); + } + }, 100); + setTimeout(() => { + if (!window.callPhantom) { + expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(3); + } + done(); + }, 1000); + }); +}); diff --git a/tests/single-animation.spec.js b/tests/single-animation.spec.js index b541f3d..fbee97f 100644 --- a/tests/single-animation.spec.js +++ b/tests/single-animation.spec.js @@ -3,9 +3,8 @@ import React from 'react'; import $ from 'jquery'; import Animate from '../index'; -import './index.spec.css'; - import single from './single-common.spec'; +import './index.spec.css'; function createClass(options) { return class extends React.Component { @@ -45,4 +44,5 @@ function createClass(options) { }; } + single(createClass, 'animation'); diff --git a/tests/single-common.spec.js b/tests/single-common.spec.js index 9e8e024..0f95ebf 100644 --- a/tests/single-common.spec.js +++ b/tests/single-common.spec.js @@ -1,4 +1,4 @@ -/* eslint no-console:0, react/no-multi-comp:0 */ +/* eslint no-console:0, react/no-multi-comp:0, react/no-render-return-value:0 */ import expect from 'expect.js'; import React from 'react'; @@ -34,14 +34,14 @@ export default function test(createClass, title) { } }); - describe.only('when transitionAppear', () => { + describe('when transitionAppear', () => { it('should render children', () => { expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'span')[0]).not.to.be.ok(); const child = TestUtils.findRenderedDOMComponentWithTag(instance, 'div'); expect(getOpacity(ReactDOM.findDOMNode(child))).to.be(1); }); - it.only('should anim children', (done) => { + it('should anim children', (done) => { const innerDiv = document.createElement('div'); document.body.appendChild(innerDiv); const Component = createClass({ @@ -51,19 +51,17 @@ export default function test(createClass, title) { remove: true, }); - ReactDOM.render(, innerDiv, (innerInstance) => { - console.log('>>>', innerInstance); - expect(TestUtils.scryRenderedDOMComponentsWithTag(innerInstance, - 'span')[0]).not.to.be.ok(); - const child = TestUtils.findRenderedDOMComponentWithTag(innerInstance, 'div'); - expect(getOpacity(ReactDOM.findDOMNode(child))).not.to.be(1); - setTimeout(() => { - expect(getOpacity(ReactDOM.findDOMNode(child))).to.be(1); - ReactDOM.unmountComponentAtNode(innerDiv); - document.body.removeChild(innerDiv); - done(); - }, 900); - }); + const innerInstance = ReactDOM.render(, innerDiv); + expect(TestUtils.scryRenderedDOMComponentsWithTag(innerInstance, + 'span')[0]).not.to.be.ok(); + const child = TestUtils.findRenderedDOMComponentWithTag(innerInstance, 'div'); + expect(getOpacity(ReactDOM.findDOMNode(child))).not.to.be(1); + setTimeout(() => { + expect(getOpacity(ReactDOM.findDOMNode(child))).to.be(1); + ReactDOM.unmountComponentAtNode(innerDiv); + document.body.removeChild(innerDiv); + done(); + }, 900); }); }); diff --git a/tests/single.spec.js b/tests/single.spec.js index 06fc374..43302ab 100644 --- a/tests/single.spec.js +++ b/tests/single.spec.js @@ -3,11 +3,10 @@ import React from 'react'; import Animate from '../index'; import { supportTransition } from '../src/util'; +import single from './single-common.spec'; import './index.spec.css'; -import single from './single-common.spec'; - function createClass(options) { return class extends React.Component { state = { @@ -16,9 +15,9 @@ function createClass(options) { } render() { - console.log('!!!!!!!!!!!', options.remove && !this.state.transitionEnter); return ( Date: Tue, 29 May 2018 16:35:24 +0800 Subject: [PATCH 26/41] add additional check if transition event not fire --- src/Animate.jsx | 5 ----- src/AnimateChild.jsx | 27 ++++++++++++++++++++++++++- src/util.js | 20 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/Animate.jsx b/src/Animate.jsx index cdd1049..a84ff47 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -75,11 +75,6 @@ class Animate extends React.Component { // Merge prev children to keep the animation newState.mergedChildren = mergeChildren(prevChildren, currentChildren); - // const toKey = ({ key }) => key; - // console.log('children update - prev:', prevChildren.map(toKey)); - // console.log('children update - current:', currentChildren.map(toKey)); - // console.log('children update - merge:', newState.mergedChildren.map(toKey)); - // console.log('-----------------------'); }); return newState; diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 3a23ae7..0a1c731 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -7,6 +7,7 @@ import classes from 'component-classes'; import raf from 'raf'; import { + getStyleValue, cloneProps, getTransitionName, supportTransition, animationEndName, transitionEndName, } from './util'; @@ -111,6 +112,7 @@ class AnimateChild extends React.Component { this.$prevEle = null; this.currentEvent = null; + this.timeout = null; } state = { @@ -129,6 +131,7 @@ class AnimateChild extends React.Component { } componentWillUnmount() { + clearTimeout(this.timeout); this._destroy = true; this.cleanDomEvent(); } @@ -186,6 +189,9 @@ class AnimateChild extends React.Component { return; } + // Clear timeout for legacy check + clearTimeout(this.timeout); + // Clean up last event environment if (this.currentEvent && this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { this.currentEvent.animateObj.stop(); @@ -218,7 +224,23 @@ class AnimateChild extends React.Component { // Trigger `eventActive` in next frame raf(() => { if (this.currentEvent && this.currentEvent.type === eventType) { - this.setState({ eventActive: true }); + this.setState({ eventActive: true }, () => { + // [Legacy] Handle timeout if browser transition event not handle + const transitionDelay = getStyleValue($ele, 'transition-delay') || 0; + const transitionDuration = getStyleValue($ele, 'transition-duration') || 0; + const animationDelay = getStyleValue($ele, 'animation-delay') || 0; + const animationDuration = getStyleValue($ele, 'animation-duration') || 0; + const totalTime = Math.max( + transitionDuration + transitionDelay, + animationDuration + animationDelay + ); + + if (totalTime) { + this.timeout = setTimeout(() => { + this.onMotionEnd({ target: $ele }); + }, totalTime * 1000); + } + }); } }); } @@ -237,6 +259,9 @@ class AnimateChild extends React.Component { const currentEvent = this.getCurrentEvent(); if (currentEvent.empty) return; + // Clear timeout for legacy check + clearTimeout(this.timeout); + const { restQueue } = currentEvent; const $ele = ReactDOM.findDOMNode(this); diff --git a/src/util.js b/src/util.js index 1f83bbf..9de309b 100644 --- a/src/util.js +++ b/src/util.js @@ -1,6 +1,26 @@ import toArray from 'rc-util/lib/Children/toArray'; import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; +// =================== Style ==================== +const stylePrefixes = ['-webkit-', '-moz-', '-o-', 'ms-', '']; + +export function getStyleProperty(node, name) { + // old ff need null, https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle + const style = window.getComputedStyle(node, null); + let ret = ''; + for (let i = 0; i < stylePrefixes.length; i++) { + ret = style.getPropertyValue(stylePrefixes[i] + name); + if (ret) { + break; + } + } + return ret; +} + +export function getStyleValue(node, name) { + return parseFloat(getStyleProperty(node, name)); +} + // ================= Transition ================= // Event wrapper. Copy from react source code function makePrefixMap(styleProp, eventName) { From 10231c5fe6dad9a8f7ed0555bd3365807261faac Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 11:57:48 +0800 Subject: [PATCH 27/41] add key match test case --- package.json | 2 +- src/Animate.jsx | 5 +++-- tests/basic.spec.js | 41 +++++++++++++++++++++++++++++++++++++++++ tests/index.js | 1 + 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 tests/basic.spec.js diff --git a/package.json b/package.json index fd318fc..c26d349 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "pre-commit": "1.x", "rc-test": "6.x", "rc-tools": "8.x", - "react": "^16.0.0", + "react": "^16.3.0", "react-dom": "^16.0.0", "velocity-animate": "~1.2.2" }, diff --git a/src/Animate.jsx b/src/Animate.jsx index a84ff47..f437c7f 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import toArray from 'rc-util/lib/Children/toArray'; +import warning from 'fbjs/lib/warning'; import AnimateChild from './AnimateChild'; import { cloneProps, mergeChildren } from './util'; @@ -113,10 +114,10 @@ class Animate extends React.Component { className, style, showProp, } = this.props; - const $children = mergedChildren.map((node) => { if (mergedChildren.length > 1 && !node.key) { - throw new Error('must set key for children'); + warning(false, 'must set key for children'); + return null; } let show = true; diff --git a/tests/basic.spec.js b/tests/basic.spec.js new file mode 100644 index 0000000..ed2b692 --- /dev/null +++ b/tests/basic.spec.js @@ -0,0 +1,41 @@ +/* eslint react/no-render-return-value:0 */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-dom/test-utils'; +import expect from 'expect.js'; +import Animate from '../'; + +describe('basic', () => { + let div; + beforeEach(() => { + div = document.createElement('div'); + }); + + afterEach(() => { + try { + ReactDOM.unmountComponentAtNode(div); + document.body.removeChild(div); + } catch (e) { + // Do nothing + } + }); + + it('exception if children has key', () => { + const instance = ReactDOM.render( + +
  • +
  • + , div); + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(2); + }); + + it('exception if children without key', () => { + const instance = ReactDOM.render( + +
  • +
  • + , div); + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(0); + }); +}); diff --git a/tests/index.js b/tests/index.js index e65f2f6..dd6d38c 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,5 +1,6 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; +import './basic.spec'; import './single.spec'; import './single-animation.spec'; import './multiple.spec'; From 73d7caf32939d7cd1ce41515bbdd950c7d612c4c Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 15:22:04 +0800 Subject: [PATCH 28/41] add util related test case --- src/util.js | 57 ++++++++++++++++++++++++++++----------------- tests/basic.spec.js | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 21 deletions(-) diff --git a/src/util.js b/src/util.js index 9de309b..972f4a6 100644 --- a/src/util.js +++ b/src/util.js @@ -35,37 +35,51 @@ function makePrefixMap(styleProp, eventName) { return prefixes; } -const vendorPrefixes = { - animationend: makePrefixMap('Animation', 'AnimationEnd'), - transitionend: makePrefixMap('Transition', 'TransitionEnd'), -}; +export function getVendorPrefixes(domSupport, win) { + const prefixes = { + animationend: makePrefixMap('Animation', 'AnimationEnd'), + transitionend: makePrefixMap('Transition', 'TransitionEnd'), + }; + + if (domSupport) { + if (!('AnimationEvent' in win)) { + delete prefixes.animationend.animation; + } + + if (!('TransitionEvent' in win)) { + delete prefixes.transitionend.transition; + } + } + + return prefixes; +} + +const vendorPrefixes = getVendorPrefixes(canUseDOM, window); let style = {}; if (canUseDOM) { style = document.createElement('div').style; - - if (!('AnimationEvent' in window)) { - delete vendorPrefixes.animationend.animation; - } - - if (!('TransitionEvent' in window)) { - delete vendorPrefixes.transitionend.transition; - } } const prefixedEventNames = {}; -function getVendorPrefixedEventName(eventName) { +export function getVendorPrefixedEventName(eventName) { + if (prefixedEventNames[eventName]) { + return prefixedEventNames[eventName]; + } + const prefixMap = vendorPrefixes[eventName]; - const stylePropList = Object.keys(prefixMap); - const len = stylePropList.length; - for (let i = 0; i < len; i += 1) { - const styleProp = stylePropList[i]; - if (Object.prototype.hasOwnProperty.call(prefixMap, styleProp) && styleProp in style) { - prefixedEventNames[eventName] = prefixMap[styleProp]; - return prefixedEventNames[eventName]; + if (prefixMap) { + const stylePropList = Object.keys(prefixMap); + const len = stylePropList.length; + for (let i = 0; i < len; i += 1) { + const styleProp = stylePropList[i]; + if (Object.prototype.hasOwnProperty.call(prefixMap, styleProp) && styleProp in style) { + prefixedEventNames[eventName] = prefixMap[styleProp]; + return prefixedEventNames[eventName]; + } } } @@ -140,7 +154,8 @@ export function getTransitionName(transitionName, transitionType) { if (!transitionName) return null; if (typeof transitionName === 'object') { - return transitionName[transitionType]; + const type = transitionType.replace(/-\w/g, (match) => match[1].toUpperCase()); + return transitionName[type]; } return `${transitionName}-${transitionType}`; diff --git a/tests/basic.spec.js b/tests/basic.spec.js index ed2b692..913db75 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -5,6 +5,7 @@ import ReactDOM from 'react-dom'; import TestUtils from 'react-dom/test-utils'; import expect from 'expect.js'; import Animate from '../'; +import { getVendorPrefixes, getVendorPrefixedEventName, transitionEndName } from '../src/util'; describe('basic', () => { let div; @@ -38,4 +39,45 @@ describe('basic', () => { , div); expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(0); }); + + it('transitionName is an object', (done) => { + const transitionName = { + appear: 'trans-appear', + appearActive: 'trans-appear-active', + }; + + const instance = ReactDOM.render( + +
    + , div); + + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'div')[0].className) + .to.contain('trans-appear'); + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'div')[0].className) + .not.to.contain('trans-appear-active'); + + setTimeout(() => { + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'div')[0].className) + .to.contain('trans-appear'); + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'div')[0].className) + .to.contain('trans-appear-active'); + done(); + }, 0); + }); +}); + +describe('util', () => { + it('getVendorPrefixes without window support', () => { + const prefix = getVendorPrefixes(true, {}); + expect(prefix.animationend.animation).to.be(undefined); + expect(prefix.transitionend.transition).to.be(undefined); + }); + + it('getVendorPrefixedEventName cache check', () => { + expect(getVendorPrefixedEventName('transitionend')).to.be(transitionEndName); + }); + + it('getVendorPrefixedEventName not exist', () => { + expect(getVendorPrefixedEventName('NotExist')).to.be(''); + }); }); From ec375c31317d8c624d78da40b455a627434a5588 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 18:14:50 +0800 Subject: [PATCH 29/41] add children remove test case --- src/AnimateChild.jsx | 6 +++-- tests/basic.spec.js | 54 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 0a1c731..bcaa57d 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -235,7 +235,7 @@ class AnimateChild extends React.Component { animationDuration + animationDelay ); - if (totalTime) { + if (totalTime >= 0) { this.timeout = setTimeout(() => { this.onMotionEnd({ target: $ele }); }, totalTime * 1000); @@ -374,6 +374,8 @@ class AnimateChild extends React.Component { const { showProp, transitionName } = this.props; const { className } = child.props || {}; + const currentEvent = this.getCurrentEvent(); + // Class name const connectClassName = (supportTransition && this.currentEvent) ? classNames( className, @@ -384,7 +386,7 @@ class AnimateChild extends React.Component { let show = true; // Keep show when is in transition or has customize animate - if (supportTransition && this.currentEvent) { + if (supportTransition && !currentEvent.empty) { show = true; } else { show = child.props[showProp]; diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 913db75..c26d63e 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -1,7 +1,8 @@ -/* eslint react/no-render-return-value:0 */ +/* eslint react/no-render-return-value:0, react/prefer-stateless-function:0, react/no-multi-comp:0 */ import React from 'react'; import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; import TestUtils from 'react-dom/test-utils'; import expect from 'expect.js'; import Animate from '../'; @@ -16,7 +17,6 @@ describe('basic', () => { afterEach(() => { try { ReactDOM.unmountComponentAtNode(div); - document.body.removeChild(div); } catch (e) { // Do nothing } @@ -64,6 +64,56 @@ describe('basic', () => { done(); }, 0); }); + + it('clean up when hidden children removed', (done) => { + // Stateless component not work for `scryRenderedComponentsWithType` + class LI extends React.Component { + render() { + const {show} = this.props; + return ( + show ?
  • : null + ) + } + } + + LI.propTypes = { + show: PropTypes.bool, + }; + + class Wrapper extends React.Component { + state = { + show: true, + propShow: true, + }; + + render() { + const { show, propShow } = this.state; + return ( + + {show &&
  • } + + ); + } + } + + const instance = ReactDOM.render(, div); + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(1); + + instance.setState({ propShow: false }); + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(1); + + setTimeout(() => { + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(0); + expect(TestUtils.scryRenderedComponentsWithType(instance, LI).length).to.be(1); + + instance.setState({ show: false }); + setTimeout(() => { + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(0); + expect(TestUtils.scryRenderedComponentsWithType(instance, LI).length).to.be(0); + done(); + }, 100); + }, 100); + }); }); describe('util', () => { From f7354ed53624b6fdfc12e97fcc453b7cf7044278 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 18:27:23 +0800 Subject: [PATCH 30/41] add event dedup test --- src/AnimateChild.jsx | 2 +- tests/basic.spec.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index bcaa57d..40d1215 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -223,7 +223,7 @@ class AnimateChild extends React.Component { if (!eventActive) { // Trigger `eventActive` in next frame raf(() => { - if (this.currentEvent && this.currentEvent.type === eventType) { + if (this.currentEvent && this.currentEvent.type === eventType && !this._destroy) { this.setState({ eventActive: true }, () => { // [Legacy] Handle timeout if browser transition event not handle const transitionDelay = getStyleValue($ele, 'transition-delay') || 0; diff --git a/tests/basic.spec.js b/tests/basic.spec.js index c26d63e..153a0e9 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -6,8 +6,11 @@ import PropTypes from 'prop-types'; import TestUtils from 'react-dom/test-utils'; import expect from 'expect.js'; import Animate from '../'; +import AnimateChild from '../src/AnimateChild'; import { getVendorPrefixes, getVendorPrefixedEventName, transitionEndName } from '../src/util'; +import './index.spec.css'; + describe('basic', () => { let div; beforeEach(() => { @@ -114,6 +117,40 @@ describe('basic', () => { }, 100); }, 100); }); + + it('de-dup event', (done) => { + class Wrapper extends React.Component { + state = { show: false }; + + render() { + return ( + + {this.state.show &&
    } + + ); + } + } + + const instance = ReactDOM.render(, div); + + instance.setState({ show: true }, () => { + // Enter + const child = TestUtils.findRenderedComponentWithType(instance, AnimateChild); + expect(child.state.eventQueue).to.eql(['enter']); + + instance.setState({ show: false }, () => { + // Leave + expect(child.state.eventQueue).to.eql(['enter', 'leave']); + + instance.setState({ show: true }, () => { + // Enter again, clean the leave in queue + expect(child.state.eventQueue).to.eql(['enter']); + + done(); + }); + }); + }); + }); }); describe('util', () => { From b114904e609f988a0082ae556db1ad5fb091aede Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 18:45:35 +0800 Subject: [PATCH 31/41] add exclusive test case --- tests/basic.spec.js | 47 ++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 153a0e9..2cb33f4 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -118,34 +118,55 @@ describe('basic', () => { }, 100); }); - it('de-dup event', (done) => { + describe('de-dup event', () => { class Wrapper extends React.Component { + static propTypes = { + exclusive: PropTypes.bool, + }; state = { show: false }; render() { return ( - + {this.state.show &&
    } ); } } - const instance = ReactDOM.render(, div); + it('without exclusive', (done) => { + const instance = ReactDOM.render(, div); + + instance.setState({ show: true }, () => { + // Enter + const child = TestUtils.findRenderedComponentWithType(instance, AnimateChild); + expect(child.state.eventQueue).to.eql(['enter']); + + instance.setState({ show: false }, () => { + // Leave + expect(child.state.eventQueue).to.eql(['enter', 'leave']); - instance.setState({ show: true }, () => { - // Enter - const child = TestUtils.findRenderedComponentWithType(instance, AnimateChild); - expect(child.state.eventQueue).to.eql(['enter']); + instance.setState({ show: true }, () => { + // Enter again, clean the leave in queue + expect(child.state.eventQueue).to.eql(['enter']); + + done(); + }); + }); + }); + }); - instance.setState({ show: false }, () => { - // Leave - expect(child.state.eventQueue).to.eql(['enter', 'leave']); + it('exclusive', (done) => { + const instance = ReactDOM.render(, div); - instance.setState({ show: true }, () => { - // Enter again, clean the leave in queue - expect(child.state.eventQueue).to.eql(['enter']); + instance.setState({ show: true }, () => { + // Enter + const child = TestUtils.findRenderedComponentWithType(instance, AnimateChild); + instance.setState({ show: false }, () => { + // Leave, exclusive only get the latest one + const currentEvent = child.getCurrentEvent(); + expect(currentEvent.eventType).to.eql('leave'); done(); }); }); From c1b3b158f99302a4ac0ef716a887dd7031b70cc2 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 18:46:02 +0800 Subject: [PATCH 32/41] add exclusive test case --- tests/basic.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 2cb33f4..3271cd3 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -120,9 +120,6 @@ describe('basic', () => { describe('de-dup event', () => { class Wrapper extends React.Component { - static propTypes = { - exclusive: PropTypes.bool, - }; state = { show: false }; render() { @@ -133,6 +130,9 @@ describe('basic', () => { ); } } + Wrapper.propTypes = { + exclusive: PropTypes.bool, + }; it('without exclusive', (done) => { const instance = ReactDOM.render(, div); From bf3833b3d9413ed4bc3661880877e5f299275be8 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 19:40:41 +0800 Subject: [PATCH 33/41] trigger remove when transitionLeave i false --- tests/basic.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 3271cd3..0fb481a 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -172,6 +172,25 @@ describe('basic', () => { }); }); }); + + it('remove child when transitionLeave is false', () => { + class Wrapper extends React.Component { + state = { show: true }; + + render() { + return ( + + {this.state.show &&
    } + + ); + } + } + + const instance = ReactDOM.render(, div); + instance.setState({ show: false }); + + expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(0); + }); }); describe('util', () => { From cf9306186e34665fec50a91af9b0af9d23da99fb Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 20:07:26 +0800 Subject: [PATCH 34/41] add animation clean up test --- tests/basic.spec.js | 79 +++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 0fb481a..95b1533 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -12,6 +12,23 @@ import { getVendorPrefixes, getVendorPrefixedEventName, transitionEndName } from import './index.spec.css'; describe('basic', () => { + class SimpleWrapper extends React.Component { + state = { show: this.props.show || false }; + + render() { + const { ...props } = this.props; + delete props.show; + return ( + + {this.state.show &&
    } + + ); + } + } + SimpleWrapper.propTypes = { + show: PropTypes.bool, + }; + let div; beforeEach(() => { div = document.createElement('div'); @@ -83,7 +100,7 @@ describe('basic', () => { show: PropTypes.bool, }; - class Wrapper extends React.Component { + class UL extends React.Component { state = { show: true, propShow: true, @@ -99,7 +116,7 @@ describe('basic', () => { } } - const instance = ReactDOM.render(, div); + const instance = ReactDOM.render(
      , div); expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(1); instance.setState({ propShow: false }); @@ -119,23 +136,8 @@ describe('basic', () => { }); describe('de-dup event', () => { - class Wrapper extends React.Component { - state = { show: false }; - - render() { - return ( - - {this.state.show &&
      } - - ); - } - } - Wrapper.propTypes = { - exclusive: PropTypes.bool, - }; - it('without exclusive', (done) => { - const instance = ReactDOM.render(, div); + const instance = ReactDOM.render(, div); instance.setState({ show: true }, () => { // Enter @@ -157,7 +159,7 @@ describe('basic', () => { }); it('exclusive', (done) => { - const instance = ReactDOM.render(, div); + const instance = ReactDOM.render(, div); instance.setState({ show: true }, () => { // Enter @@ -174,23 +176,36 @@ describe('basic', () => { }); it('remove child when transitionLeave is false', () => { - class Wrapper extends React.Component { - state = { show: true }; - - render() { - return ( - - {this.state.show &&
      } - - ); - } - } - - const instance = ReactDOM.render(, div); + const instance = ReactDOM.render(, div); instance.setState({ show: false }); expect(TestUtils.scryRenderedDOMComponentsWithTag(instance, 'li').length).to.be(0); }); + + it('clean up animation when exclusive item remove', (done) => { + let stopCalled = false; + const animation = { + enter() { + return { + stop() { + stopCalled = true; + }, + }; + }, + }; + const instance = ReactDOM.render( + , div); + instance.setState({ show: true }, () => { + instance.setState({ show: false }, () => { + expect(stopCalled).to.be.ok(); + done(); + }); + }); + }); }); describe('util', () => { From e99a00345270bca54a9f9efdd098b5090f18e8ce Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 20:11:57 +0800 Subject: [PATCH 35/41] add wrapper for test usage --- src/AnimateChild.jsx | 619 ++++++++++++++++++++++--------------------- 1 file changed, 312 insertions(+), 307 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 40d1215..f7de281 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -22,389 +22,394 @@ const clonePropList = [ /** * AnimateChild only accept one child node. + * `transitionSupport` is used for none transition test case. + * Default we use browser transition event support check. */ +export function genAnimateChild(transitionSupport) { + class AnimateChild extends React.Component { + static propTypes = { + transitionName: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]), + transitionAppear: PropTypes.bool, + transitionEnter: PropTypes.bool, + transitionLeave: PropTypes.bool, + exclusive: PropTypes.bool, + appeared: PropTypes.bool, + showProp: PropTypes.string, + + animateKey: PropTypes.any, + animation: PropTypes.object, + onChildLeaved: PropTypes.func, + + onEnd: PropTypes.func, + onAppear: PropTypes.func, + onEnter: PropTypes.func, + onLeave: PropTypes.func, + } -class AnimateChild extends React.Component { - static propTypes = { - transitionName: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - ]), - transitionAppear: PropTypes.bool, - transitionEnter: PropTypes.bool, - transitionLeave: PropTypes.bool, - exclusive: PropTypes.bool, - appeared: PropTypes.bool, - showProp: PropTypes.string, - - animateKey: PropTypes.any, - animation: PropTypes.object, - onChildLeaved: PropTypes.func, - - onEnd: PropTypes.func, - onAppear: PropTypes.func, - onEnter: PropTypes.func, - onLeave: PropTypes.func, - } - - static getDerivedStateFromProps(nextProps, prevState) { - const { prevProps = {} } = prevState; - const { appeared } = nextProps; + static getDerivedStateFromProps(nextProps, prevState) { + const { prevProps = {} } = prevState; + const { appeared } = nextProps; - const newState = { - prevProps: cloneProps(nextProps, clonePropList), - }; + const newState = { + prevProps: cloneProps(nextProps, clonePropList), + }; - function processState(propName, updater) { - if (prevProps[propName] !== nextProps[propName]) { - if (updater) { - updater(nextProps[propName]); + function processState(propName, updater) { + if (prevProps[propName] !== nextProps[propName]) { + if (updater) { + updater(nextProps[propName]); + } + return true; } - return true; + return false; } - return false; - } - function pushEvent(eventType) { - let eventQueue = newState.eventQueue || prevState.eventQueue.slice(); - const matchIndex = eventQueue.indexOf(eventType); + function pushEvent(eventType) { + let eventQueue = newState.eventQueue || prevState.eventQueue.slice(); + const matchIndex = eventQueue.indexOf(eventType); - // Clean the rest event if eventType match - if (matchIndex !== -1) { - eventQueue = eventQueue.slice(0, matchIndex); + // Clean the rest event if eventType match + if (matchIndex !== -1) { + eventQueue = eventQueue.slice(0, matchIndex); + } + + eventQueue.push(eventType); + newState.eventQueue = eventQueue; } - eventQueue.push(eventType); - newState.eventQueue = eventQueue; - } + // Child update. Only set child. + processState('children', (child) => { + newState.child = child; + }); - // Child update. Only set child. - processState('children', (child) => { - newState.child = child; - }); + processState('appeared', (isAppeared) => { + if (isAppeared) { + pushEvent('appear'); + } + }); - processState('appeared', (isAppeared) => { - if (isAppeared) { - pushEvent('appear'); - } - }); - - // Show update - processState('show', (show) => { - if (!appeared) { - if (show) { - pushEvent('enter'); - } else { - pushEvent('leave'); + // Show update + processState('show', (show) => { + if (!appeared) { + if (show) { + pushEvent('enter'); + } else { + pushEvent('leave'); + } } - } - }); + }); - return newState; - } + return newState; + } - constructor() { - super(); + constructor() { + super(); - // [Legacy] Since old code addListener on the element. - // To avoid break the behaviour that component not handle animation/transition - // also can handle the animate, let keep the logic. - this.$prevEle = null; + // [Legacy] Since old code addListener on the element. + // To avoid break the behaviour that component not handle animation/transition + // also can handle the animate, let keep the logic. + this.$prevEle = null; - this.currentEvent = null; - this.timeout = null; - } + this.currentEvent = null; + this.timeout = null; + } - state = { - child: null, + state = { + child: null, - eventQueue: [], - eventActive: false, - } + eventQueue: [], + eventActive: false, + } - componentDidMount() { - this.onDomUpdated({}, {}); - } + componentDidMount() { + this.onDomUpdated({}, {}); + } - componentDidUpdate(prevProps, prevState) { - this.onDomUpdated(prevProps, prevState); - } + componentDidUpdate(prevProps, prevState) { + this.onDomUpdated(prevProps, prevState); + } - componentWillUnmount() { - clearTimeout(this.timeout); - this._destroy = true; - this.cleanDomEvent(); - } + componentWillUnmount() { + clearTimeout(this.timeout); + this._destroy = true; + this.cleanDomEvent(); + } - onDomUpdated = () => { - const { eventActive } = this.state; - const { - transitionName, animation, onChildLeaved, animateKey, - } = this.props; + onDomUpdated = () => { + const { eventActive } = this.state; + const { + transitionName, animation, onChildLeaved, animateKey, + } = this.props; - const $ele = ReactDOM.findDOMNode(this); + const $ele = ReactDOM.findDOMNode(this); - // Skip if dom element not ready - if (!$ele) return; + // Skip if dom element not ready + if (!$ele) return; - // [Legacy] Add animation/transition event by dom level - if (supportTransition && this.$prevEle !== $ele) { - this.cleanDomEvent(); + // [Legacy] Add animation/transition event by dom level + if (transitionSupport && this.$prevEle !== $ele) { + this.cleanDomEvent(); - this.$prevEle = $ele; - this.$prevEle.addEventListener(animationEndName, this.onMotionEnd); - this.$prevEle.addEventListener(transitionEndName, this.onMotionEnd); - } + this.$prevEle = $ele; + this.$prevEle.addEventListener(animationEndName, this.onMotionEnd); + this.$prevEle.addEventListener(transitionEndName, this.onMotionEnd); + } - const currentEvent = this.getCurrentEvent(); - if (currentEvent.empty) { - // Additional process the leave event - if (currentEvent.lastEventType === 'leave') { - onChildLeaved(animateKey); + const currentEvent = this.getCurrentEvent(); + if (currentEvent.empty) { + // Additional process the leave event + if (currentEvent.lastEventType === 'leave') { + onChildLeaved(animateKey); + } + return; } - return; - } - const { eventType } = currentEvent; - const nodeClasses = classes($ele); + const { eventType } = currentEvent; + const nodeClasses = classes($ele); - // [Legacy] Since origin code use js to set `className`. - // This caused that any component without support `className` can be forced set. - // Let's keep the logic. - function legacyAppendClass() { - if (!supportTransition) return; + // [Legacy] Since origin code use js to set `className`. + // This caused that any component without support `className` can be forced set. + // Let's keep the logic. + function legacyAppendClass() { + if (!transitionSupport) return; - const basicClassName = getTransitionName(transitionName, `${eventType}`); - if (basicClassName) nodeClasses.add(basicClassName); + const basicClassName = getTransitionName(transitionName, `${eventType}`); + if (basicClassName) nodeClasses.add(basicClassName); + + if (eventActive) { + const activeClassName = getTransitionName(transitionName, `${eventType}-active`); + if (activeClassName) nodeClasses.add(activeClassName); + } - if (eventActive) { - const activeClassName = getTransitionName(transitionName, `${eventType}-active`); - if (activeClassName) nodeClasses.add(activeClassName); } - } + if (this.currentEvent && this.currentEvent.type === eventType) { + legacyAppendClass(); + return; + } - if (this.currentEvent && this.currentEvent.type === eventType) { - legacyAppendClass(); - return; - } + // Clear timeout for legacy check + clearTimeout(this.timeout); - // Clear timeout for legacy check - clearTimeout(this.timeout); + // Clean up last event environment + if (this.currentEvent && this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { + this.currentEvent.animateObj.stop(); + } - // Clean up last event environment - if (this.currentEvent && this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { - this.currentEvent.animateObj.stop(); - } + // Clean up last transition class + if (this.currentEvent) { + const basicClassName = getTransitionName(transitionName, `${this.currentEvent.type}`); + const activeClassName = getTransitionName(transitionName, `${this.currentEvent.type}-active`); + if (basicClassName) nodeClasses.remove(basicClassName); + if (activeClassName) nodeClasses.remove(activeClassName); + } - // Clean up last transition class - if (this.currentEvent) { - const basicClassName = getTransitionName(transitionName, `${this.currentEvent.type}`); - const activeClassName = getTransitionName(transitionName, `${this.currentEvent.type}-active`); - if (basicClassName) nodeClasses.remove(basicClassName); - if (activeClassName) nodeClasses.remove(activeClassName); - } + // New event come + this.currentEvent = { + type: eventType, + }; - // New event come - this.currentEvent = { - type: eventType, - }; + const animationHandler = (animation || {})[eventType]; + // =============== Check if has customize animation =============== + if (animationHandler) { + this.currentEvent.animateObj = animationHandler($ele, () => { + this.onMotionEnd({ target: $ele }); + }); - const animationHandler = (animation || {})[eventType]; - // =============== Check if has customize animation =============== - if (animationHandler) { - this.currentEvent.animateObj = animationHandler($ele, () => { - this.onMotionEnd({ target: $ele }); - }); + // ==================== Use transition instead ==================== + } else if (transitionSupport) { + legacyAppendClass(); + if (!eventActive) { + // Trigger `eventActive` in next frame + raf(() => { + if (this.currentEvent && this.currentEvent.type === eventType && !this._destroy) { + this.setState({ eventActive: true }, () => { + // [Legacy] Handle timeout if browser transition event not handle + const transitionDelay = getStyleValue($ele, 'transition-delay') || 0; + const transitionDuration = getStyleValue($ele, 'transition-duration') || 0; + const animationDelay = getStyleValue($ele, 'animation-delay') || 0; + const animationDuration = getStyleValue($ele, 'animation-duration') || 0; + const totalTime = Math.max( + transitionDuration + transitionDelay, + animationDuration + animationDelay + ); + + if (totalTime >= 0) { + this.timeout = setTimeout(() => { + this.onMotionEnd({ target: $ele }); + }, totalTime * 1000); + } + }); + } + }); + } - // ==================== Use transition instead ==================== - } else if (supportTransition) { - legacyAppendClass(); - if (!eventActive) { - // Trigger `eventActive` in next frame - raf(() => { - if (this.currentEvent && this.currentEvent.type === eventType && !this._destroy) { - this.setState({ eventActive: true }, () => { - // [Legacy] Handle timeout if browser transition event not handle - const transitionDelay = getStyleValue($ele, 'transition-delay') || 0; - const transitionDuration = getStyleValue($ele, 'transition-duration') || 0; - const animationDelay = getStyleValue($ele, 'animation-delay') || 0; - const animationDuration = getStyleValue($ele, 'animation-duration') || 0; - const totalTime = Math.max( - transitionDuration + transitionDelay, - animationDuration + animationDelay - ); - - if (totalTime >= 0) { - this.timeout = setTimeout(() => { - this.onMotionEnd({ target: $ele }); - }, totalTime * 1000); - } - }); - } - }); + // ======================= Just next action ======================= + } else { + this.onMotionEnd({ target: $ele }); } - - // ======================= Just next action ======================= - } else { - this.onMotionEnd({ target: $ele }); } - } - onMotionEnd = ({ target }) => { - const { - transitionName, onChildLeaved, animateKey, - onAppear, onEnter, onLeave, onEnd, - } = this.props; - const currentEvent = this.getCurrentEvent(); - if (currentEvent.empty) return; + onMotionEnd = ({ target }) => { + const { + transitionName, onChildLeaved, animateKey, + onAppear, onEnter, onLeave, onEnd, + } = this.props; + const currentEvent = this.getCurrentEvent(); + if (currentEvent.empty) return; - // Clear timeout for legacy check - clearTimeout(this.timeout); + // Clear timeout for legacy check + clearTimeout(this.timeout); - const { restQueue } = currentEvent; + const { restQueue } = currentEvent; - const $ele = ReactDOM.findDOMNode(this); - if (!this.currentEvent || $ele !== target) return; + const $ele = ReactDOM.findDOMNode(this); + if (!this.currentEvent || $ele !== target) return; - if (this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { - this.currentEvent.animateObj.stop(); - } - - // [Legacy] Same as above, we need call js to remove the class - if (supportTransition) { - const basicClassName = getTransitionName(transitionName, this.currentEvent.type); - const activeClassName = getTransitionName(transitionName, `${this.currentEvent.type}-active`); + if (this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { + this.currentEvent.animateObj.stop(); + } - const nodeClasses = classes($ele); - if (basicClassName) nodeClasses.remove(basicClassName); - if (activeClassName) nodeClasses.remove(activeClassName); - } + // [Legacy] Same as above, we need call js to remove the class + if (transitionSupport) { + const basicClassName = getTransitionName(transitionName, this.currentEvent.type); + const activeClassName = getTransitionName(transitionName, `${this.currentEvent.type}-active`); - // Additional process the leave event - if (this.currentEvent.type === 'leave') { - onChildLeaved(animateKey); - } + const nodeClasses = classes($ele); + if (basicClassName) nodeClasses.remove(basicClassName); + if (activeClassName) nodeClasses.remove(activeClassName); + } - // [Legacy] Trigger on event when it's last event - if (!restQueue.length) { - if (this.currentEvent.type === 'appear' && onAppear) { - onAppear(animateKey); - } else if (this.currentEvent.type === 'enter' && onEnd) { - onEnter(animateKey); - } else if (this.currentEvent.type === 'leave' && onLeave) { - onLeave(animateKey); + // Additional process the leave event + if (this.currentEvent.type === 'leave') { + onChildLeaved(animateKey); } - if (onEnd) { - // OnEnd(key, isShow) - onEnd(animateKey, this.currentEvent.type !== 'leave'); + // [Legacy] Trigger on event when it's last event + if (!restQueue.length) { + if (this.currentEvent.type === 'appear' && onAppear) { + onAppear(animateKey); + } else if (this.currentEvent.type === 'enter' && onEnd) { + onEnter(animateKey); + } else if (this.currentEvent.type === 'leave' && onLeave) { + onLeave(animateKey); + } + + if (onEnd) { + // OnEnd(key, isShow) + onEnd(animateKey, this.currentEvent.type !== 'leave'); + } } - } - this.currentEvent = null; + this.currentEvent = null; - // Next queue - if (!this._destroy) { - this.setState({ - eventQueue: restQueue, - eventActive: false, - }); - } - }; - - getCurrentEvent = () => { - const { eventQueue = [] } = this.state; - const { - animation, exclusive, - transitionAppear, transitionEnter, transitionLeave, - } = this.props; - - function hasEventHandler(eventType) { - return (eventType === 'appear' && (transitionAppear || animation.appear)) || - (eventType === 'enter' && (transitionEnter || animation.enter)) || - (eventType === 'leave' && (transitionLeave || animation.leave)); - } + // Next queue + if (!this._destroy) { + this.setState({ + eventQueue: restQueue, + eventActive: false, + }); + } + }; - let event = null; - // If is exclusive, only check the last event - if (exclusive) { - const eventType = eventQueue[eventQueue.length - 1]; - if (hasEventHandler(eventType)) { - event = { - eventType, - restQueue: [], - }; + getCurrentEvent = () => { + const { eventQueue = [] } = this.state; + const { + animation, exclusive, + transitionAppear, transitionEnter, transitionLeave, + } = this.props; + + function hasEventHandler(eventType) { + return (eventType === 'appear' && (transitionAppear || animation.appear)) || + (eventType === 'enter' && (transitionEnter || animation.enter)) || + (eventType === 'leave' && (transitionLeave || animation.leave)); } - } else { - // Loop check the queue until find match - let cloneQueue = eventQueue.slice(); - while (cloneQueue.length) { - const [eventType, ...restQueue] = cloneQueue; + + let event = null; + // If is exclusive, only check the last event + if (exclusive) { + const eventType = eventQueue[eventQueue.length - 1]; if (hasEventHandler(eventType)) { event = { eventType, - restQueue, + restQueue: [], }; - break; } - cloneQueue = restQueue; + } else { + // Loop check the queue until find match + let cloneQueue = eventQueue.slice(); + while (cloneQueue.length) { + const [eventType, ...restQueue] = cloneQueue; + if (hasEventHandler(eventType)) { + event = { + eventType, + restQueue, + }; + break; + } + cloneQueue = restQueue; + } } - } - if (!event) { - event = { - empty: true, - lastEventType: eventQueue[eventQueue.length - 1], - }; - } + if (!event) { + event = { + empty: true, + lastEventType: eventQueue[eventQueue.length - 1], + }; + } - return event; - }; + return event; + }; - cleanDomEvent = () => { - if (this.$prevEle && supportTransition) { - this.$prevEle.removeEventListener(animationEndName, this.onMotionEnd); - this.$prevEle.removeEventListener(transitionEndName, this.onMotionEnd); - } - }; + cleanDomEvent = () => { + if (this.$prevEle && transitionSupport) { + this.$prevEle.removeEventListener(animationEndName, this.onMotionEnd); + this.$prevEle.removeEventListener(transitionEndName, this.onMotionEnd); + } + }; - render() { - const { child, eventActive } = this.state; - const { showProp, transitionName } = this.props; - const { className } = child.props || {}; + render() { + const { child, eventActive } = this.state; + const { showProp, transitionName } = this.props; + const { className } = child.props || {}; - const currentEvent = this.getCurrentEvent(); + const currentEvent = this.getCurrentEvent(); - // Class name - const connectClassName = (supportTransition && this.currentEvent) ? classNames( - className, - getTransitionName(transitionName, this.currentEvent.type), - eventActive && getTransitionName(transitionName, `${this.currentEvent.type}-active`), - ) : className; + // Class name + const connectClassName = (transitionSupport && this.currentEvent) ? classNames( + className, + getTransitionName(transitionName, this.currentEvent.type), + eventActive && getTransitionName(transitionName, `${this.currentEvent.type}-active`), + ) : className; - let show = true; + let show = true; - // Keep show when is in transition or has customize animate - if (supportTransition && !currentEvent.empty) { - show = true; - } else { - show = child.props[showProp]; - } + // Keep show when is in transition or has customize animate + if (transitionSupport && !currentEvent.empty) { + show = true; + } else { + show = child.props[showProp]; + } - // Clone child - const newChildProps = { - className: connectClassName, - }; + // Clone child + const newChildProps = { + className: connectClassName, + }; - if (showProp) { - newChildProps[showProp] = show; - } + if (showProp) { + newChildProps[showProp] = show; + } - return React.cloneElement(child, newChildProps); + return React.cloneElement(child, newChildProps); + } } -} -polyfill(AnimateChild); + polyfill(AnimateChild); + + return AnimateChild; +} -export default AnimateChild; \ No newline at end of file +export default genAnimateChild(supportTransition); \ No newline at end of file From 7d092fde9e2ca2ebfa35d24b74a96ec2942ef1f8 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 20:53:53 +0800 Subject: [PATCH 36/41] onXXX event test case --- src/Animate.jsx | 285 +++++++++++++++++++----------------- src/AnimateChild.jsx | 2 +- tests/basic.spec.js | 80 ++++++++++ tests/index.js | 1 + tests/no.transition.spec.js | 60 ++++++++ 5 files changed, 289 insertions(+), 139 deletions(-) create mode 100644 tests/no.transition.spec.js diff --git a/src/Animate.jsx b/src/Animate.jsx index f437c7f..932ce89 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -10,165 +10,174 @@ import { cloneProps, mergeChildren } from './util'; const defaultKey = `rc_animate_${Date.now()}`; const clonePropList = ['children']; -class Animate extends React.Component { - // [Legacy] Not sure usage - // commit: https://github.com/react-component/animate/commit/0a1cbfd647407498b10a8c6602a2dea80b42e324 - static isAnimate = true; // eslint-disable-line - - static propTypes = { - component: PropTypes.any, - componentProps: PropTypes.object, - animation: PropTypes.object, - transitionName: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - ]), - transitionEnter: PropTypes.bool, - transitionAppear: PropTypes.bool, - exclusive: PropTypes.bool, - transitionLeave: PropTypes.bool, - onEnd: PropTypes.func, - onEnter: PropTypes.func, - onLeave: PropTypes.func, - onAppear: PropTypes.func, - showProp: PropTypes.string, - children: PropTypes.node, - style: PropTypes.object, - className: PropTypes.string, - } - - static defaultProps = { - animation: {}, - component: 'span', - componentProps: {}, - transitionEnter: true, - transitionLeave: true, - transitionAppear: false, - } - static getDerivedStateFromProps(nextProps, prevState) { - const { prevProps = {} } = prevState; - const newState = { - prevProps: cloneProps(nextProps, clonePropList), - }; - const { showProp } = nextProps; +/** + * Default use `AnimateChild` as component. + * Here can also pass customize `ChildComponent` for test usage. + */ +export function genAnimate(ChildComponent) { + class Animate extends React.Component { + // [Legacy] Not sure usage + // commit: https://github.com/react-component/animate/commit/0a1cbfd647407498b10a8c6602a2dea80b42e324 + static isAnimate = true; // eslint-disable-line + + static propTypes = { + component: PropTypes.any, + componentProps: PropTypes.object, + animation: PropTypes.object, + transitionName: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]), + transitionEnter: PropTypes.bool, + transitionAppear: PropTypes.bool, + exclusive: PropTypes.bool, + transitionLeave: PropTypes.bool, + onEnd: PropTypes.func, + onEnter: PropTypes.func, + onLeave: PropTypes.func, + onAppear: PropTypes.func, + showProp: PropTypes.string, + children: PropTypes.node, + style: PropTypes.object, + className: PropTypes.string, + } - function processState(propName, updater) { - if (prevProps[propName] !== nextProps[propName]) { - updater(nextProps[propName]); - return true; - } - return false; + static defaultProps = { + animation: {}, + component: 'span', + componentProps: {}, + transitionEnter: true, + transitionLeave: true, + transitionAppear: false, } - processState('children', (children) => { - const currentChildren = toArray(children).filter(node => node); - const prevChildren = prevState.mergedChildren.filter((node) => { - // Remove prev child if not show anymore - if ( - currentChildren.every(({ key }) => key !== node.key) && - showProp && !node.props[showProp] - ) { - return false; + static getDerivedStateFromProps(nextProps, prevState) { + const { prevProps = {} } = prevState; + const newState = { + prevProps: cloneProps(nextProps, clonePropList), + }; + const { showProp } = nextProps; + + function processState(propName, updater) { + if (prevProps[propName] !== nextProps[propName]) { + updater(nextProps[propName]); + return true; } - return true; + return false; + } + + processState('children', (children) => { + const currentChildren = toArray(children).filter(node => node); + const prevChildren = prevState.mergedChildren.filter((node) => { + // Remove prev child if not show anymore + if ( + currentChildren.every(({ key }) => key !== node.key) && + showProp && !node.props[showProp] + ) { + return false; + } + return true; + }); + + // Merge prev children to keep the animation + newState.mergedChildren = mergeChildren(prevChildren, currentChildren); }); - // Merge prev children to keep the animation - newState.mergedChildren = mergeChildren(prevChildren, currentChildren); - }); + return newState; + } - return newState; - } + state = { + appeared: true, + mergedChildren: [], + }; - state = { - appeared: true, - mergedChildren: [], - }; + componentDidMount() { + // No need to re-render + this.state.appeared = false; + } - componentDidMount() { - // No need to re-render - this.state.appeared = false; - } + onChildLeaved = (key) => { + // Remove child which not exist anymore + if (!this.hasChild(key)) { + const { mergedChildren } = this.state; + this.setState({ + mergedChildren: mergedChildren.filter(node => node.key !== key), + }); + } + }; - onChildLeaved = (key) => { - // Remove child which not exist anymore - if (!this.hasChild(key)) { - const { mergedChildren } = this.state; - this.setState({ - mergedChildren: mergedChildren.filter(node => node.key !== key), - }); - } - }; + hasChild = (key) => { + const { children } = this.props; - hasChild = (key) => { - const { children } = this.props; + return toArray(children).some(node => node && node.key === key); + }; - return toArray(children).some(node => node && node.key === key); - }; + render() { + const { appeared, mergedChildren } = this.state; + const { + component: Component, componentProps, + className, style, showProp, + } = this.props; + + const $children = mergedChildren.map((node) => { + if (mergedChildren.length > 1 && !node.key) { + warning(false, 'must set key for children'); + return null; + } - render() { - const { appeared, mergedChildren } = this.state; - const { - component: Component, componentProps, - className, style, showProp, - } = this.props; + let show = true; - const $children = mergedChildren.map((node) => { - if (mergedChildren.length > 1 && !node.key) { - warning(false, 'must set key for children'); - return null; - } + if (!this.hasChild(node.key)) { + show = false; + } else if (showProp) { + show = node.props[showProp]; + } - let show = true; + const key = node.key || defaultKey; + + return ( + + {node} + + ); + }); - if (!this.hasChild(node.key)) { - show = false; - } else if (showProp) { - show = node.props[showProp]; - } + // Wrap with component + if (Component) { + let passedProps = this.props; + if (typeof Component === 'string') { + passedProps = { + className, + style, + ...componentProps, + }; + } - const key = node.key || defaultKey; - - return ( - - {node} - - ); - }); - - // Wrap with component - if (Component) { - let passedProps = this.props; - if (typeof Component === 'string') { - passedProps = { - className, - style, - ...componentProps, - }; + return ( + + {$children} + + ); } - return ( - - {$children} - - ); + return $children[0] || null; } - - return $children[0] || null; } -} -polyfill(Animate); + polyfill(Animate); + + return Animate; +} -export default Animate; \ No newline at end of file +export default genAnimate(AnimateChild); \ No newline at end of file diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index f7de281..dddd2f5 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -292,7 +292,7 @@ export function genAnimateChild(transitionSupport) { if (!restQueue.length) { if (this.currentEvent.type === 'appear' && onAppear) { onAppear(animateKey); - } else if (this.currentEvent.type === 'enter' && onEnd) { + } else if (this.currentEvent.type === 'enter' && onEnter) { onEnter(animateKey); } else if (this.currentEvent.type === 'leave' && onLeave) { onLeave(animateKey); diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 95b1533..94f05de 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -85,6 +85,86 @@ describe('basic', () => { }, 0); }); + it('transition last callback', (done) => { + function createOnCalled(type) { + function callback() { + callback.times += 1; + } + callback.type = type; + callback.times = 0; + + return callback; + } + + const onAppear = createOnCalled('appear'); + const onEnter = createOnCalled('enter'); + const onLeave = createOnCalled('leave'); + const onEnd = createOnCalled('end'); + + const instance = ReactDOM.render(, div); + + // What event state change, this will only trigger once when final transition finished + setTimeout(() => { + expect(onAppear.times).to.be(1); + expect(onEnter.times).to.be(0); + expect(onLeave.times).to.be(0); + expect(onEnd.times).to.be(1); + + instance.setState({ show: false }, () => { + expect(onAppear.times).to.be(1); + expect(onEnter.times).to.be(0); + expect(onLeave.times).to.be(0); + expect(onEnd.times).to.be(1); + + setTimeout(() => { + expect(onAppear.times).to.be(1); + expect(onEnter.times).to.be(0); + expect(onLeave.times).to.be(1); + expect(onEnd.times).to.be(2); + + instance.setState({ show: true }, () => { + const child = TestUtils.findRenderedComponentWithType(instance, AnimateChild); + + instance.setState({ show: false }, () => { + expect(child.state.eventQueue).to.eql(['enter', 'leave']); + + instance.setState({ show: true }, () => { + expect(onAppear.times).to.be(1); + expect(onEnter.times).to.be(0); + expect(onLeave.times).to.be(1); + expect(onEnd.times).to.be(2); + + expect(child.state.eventQueue).to.eql(['enter']); + + setTimeout(() => { + expect(child.state.eventQueue).to.eql([]); + + expect(onAppear.times).to.be(1); + expect(onEnter.times).to.be(1); + expect(onLeave.times).to.be(1); + expect(onEnd.times).to.be(3); + + done(); + }, 100); + }); + }); + }); + }, 100); + }); + + + }, 100); + + + }); + it('clean up when hidden children removed', (done) => { // Stateless component not work for `scryRenderedComponentsWithType` class LI extends React.Component { diff --git a/tests/index.js b/tests/index.js index dd6d38c..6ec1dac 100644 --- a/tests/index.js +++ b/tests/index.js @@ -4,3 +4,4 @@ import './basic.spec'; import './single.spec'; import './single-animation.spec'; import './multiple.spec'; +import './no.transition.spec'; diff --git a/tests/no.transition.spec.js b/tests/no.transition.spec.js new file mode 100644 index 0000000..656d45a --- /dev/null +++ b/tests/no.transition.spec.js @@ -0,0 +1,60 @@ +/* eslint react/no-render-return-value:0, react/prefer-stateless-function:0, react/no-multi-comp:0 */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import TestUtils from 'react-dom/test-utils'; +import expect from 'expect.js'; +import { genAnimate } from '../src/Animate'; +import { genAnimateChild } from '../src/AnimateChild'; + + +describe('no transition', () => { + const AnimateChild = genAnimateChild(false); + const Animate = genAnimate(AnimateChild); + + class SimpleWrapper extends React.Component { + state = { show: this.props.show || false }; + + render() { + const { ...props } = this.props; + delete props.show; + return ( + + {this.state.show &&
      } + + ); + } + } + SimpleWrapper.propTypes = { + show: PropTypes.bool, + }; + + let div; + beforeEach(() => { + div = document.createElement('div'); + }); + + afterEach(() => { + try { + ReactDOM.unmountComponentAtNode(div); + } catch (e) { + // Do nothing + } + }); + + + it('event queue', (done) => { + const instance = ReactDOM.render(, div); + + instance.setState({ show: true }, () => { + const child = TestUtils.findRenderedComponentWithType(instance, AnimateChild); + expect(child.state.eventQueue).to.eql(['enter']); + + setTimeout(() => { + expect(child.state.eventQueue).to.eql([]); + done(); + }, 100); + }); + }); +}); From 1f5a5a1a911c12ae963031d7a7a527b3fd813f7f Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 30 May 2018 22:15:23 +0800 Subject: [PATCH 37/41] keep show when animate object exist --- src/AnimateChild.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index dddd2f5..3fed354 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -388,7 +388,10 @@ export function genAnimateChild(transitionSupport) { let show = true; // Keep show when is in transition or has customize animate - if (transitionSupport && !currentEvent.empty) { + if (transitionSupport && ( + !currentEvent.empty || + (this.currentEvent && this.currentEvent.animateObj) + )) { show = true; } else { show = child.props[showProp]; From ef63427a4235a113de51eaaa51c9681b5a0d6703 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 1 Jun 2018 14:09:15 +0800 Subject: [PATCH 38/41] fix coverage in react 16.4 --- src/AnimateChild.jsx | 6 +++--- tests/basic.spec.js | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 3fed354..ae7000c 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -274,7 +274,7 @@ export function genAnimateChild(transitionSupport) { } // [Legacy] Same as above, we need call js to remove the class - if (transitionSupport) { + if (transitionSupport && this.currentEvent) { const basicClassName = getTransitionName(transitionName, this.currentEvent.type); const activeClassName = getTransitionName(transitionName, `${this.currentEvent.type}-active`); @@ -284,12 +284,12 @@ export function genAnimateChild(transitionSupport) { } // Additional process the leave event - if (this.currentEvent.type === 'leave') { + if (this.currentEvent && this.currentEvent.type === 'leave') { onChildLeaved(animateKey); } // [Legacy] Trigger on event when it's last event - if (!restQueue.length) { + if (this.currentEvent && !restQueue.length) { if (this.currentEvent.type === 'appear' && onAppear) { onAppear(animateKey); } else if (this.currentEvent.type === 'enter' && onEnter) { diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 94f05de..cb97eee 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -5,9 +5,9 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import TestUtils from 'react-dom/test-utils'; import expect from 'expect.js'; -import Animate from '../'; +import Animate from '../src/Animate'; import AnimateChild from '../src/AnimateChild'; -import { getVendorPrefixes, getVendorPrefixedEventName, transitionEndName } from '../src/util'; +import { getVendorPrefixes, getVendorPrefixedEventName, transitionEndName, mergeChildren } from '../src/util'; import './index.spec.css'; @@ -302,4 +302,32 @@ describe('util', () => { it('getVendorPrefixedEventName not exist', () => { expect(getVendorPrefixedEventName('NotExist')).to.be(''); }); + + describe('mergeChildren', () => { + const gen = (key) => ( + {key} + ); + const flatten = (list) => list.map(p => p.key); + + it('prepend', () => { + const prev = [gen(2), gen(4)]; + const next = [gen(1), gen(2)]; + const merge = mergeChildren(prev, next); + expect(flatten(merge)).to.eql([1, 2, 4]); + }); + + it('append', () => { + const prev = [gen(1), gen(3)]; + const next = [gen(5), gen(6)]; + const merge = mergeChildren(prev, next); + expect(flatten(merge)).to.eql([5, 6, 1, 3]); + }); + + it('mixed', () => { + const prev = [gen(1), gen(3), gen(5)]; + const next = [gen(2), gen(3), gen(6)]; + const merge = mergeChildren(prev, next); + expect(flatten(merge)).to.eql([2, 1, 3, 6, 5]); + }); + }); }); From ae673fa68068510944e0a6c4d8de28311b74cc78 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 6 Jun 2018 16:42:03 +0800 Subject: [PATCH 39/41] add get dom element func --- src/AnimateChild.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index ae7000c..7430a83 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -144,7 +144,7 @@ export function genAnimateChild(transitionSupport) { transitionName, animation, onChildLeaved, animateKey, } = this.props; - const $ele = ReactDOM.findDOMNode(this); + const $ele = this.getDomElement(); // Skip if dom element not ready if (!$ele) return; @@ -266,7 +266,7 @@ export function genAnimateChild(transitionSupport) { const { restQueue } = currentEvent; - const $ele = ReactDOM.findDOMNode(this); + const $ele = this.getDomElement(); if (!this.currentEvent || $ele !== target) return; if (this.currentEvent.animateObj && this.currentEvent.animateObj.stop) { @@ -315,6 +315,11 @@ export function genAnimateChild(transitionSupport) { } }; + getDomElement = () => { + if (this._destroy) return null; + return ReactDOM.findDOMNode(this); + }; + getCurrentEvent = () => { const { eventQueue = [] } = this.state; const { From 125bbe014dc824545477261d5d50a77d6f479c44 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 11 Jun 2018 09:51:02 +0800 Subject: [PATCH 40/41] fix for new lint --- src/Animate.jsx | 10 +++++----- src/AnimateChild.jsx | 44 ++++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Animate.jsx b/src/Animate.jsx index 932ce89..a62c178 100644 --- a/src/Animate.jsx +++ b/src/Animate.jsx @@ -52,6 +52,11 @@ export function genAnimate(ChildComponent) { transitionAppear: false, } + state = { + appeared: true, + mergedChildren: [], + }; + static getDerivedStateFromProps(nextProps, prevState) { const { prevProps = {} } = prevState; const newState = { @@ -87,11 +92,6 @@ export function genAnimate(ChildComponent) { return newState; } - state = { - appeared: true, - mergedChildren: [], - }; - componentDidMount() { // No need to re-render this.state.appeared = false; diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 7430a83..9f9d503 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -49,6 +49,25 @@ export function genAnimateChild(transitionSupport) { onLeave: PropTypes.func, } + constructor() { + super(); + + // [Legacy] Since old code addListener on the element. + // To avoid break the behaviour that component not handle animation/transition + // also can handle the animate, let keep the logic. + this.$prevEle = null; + + this.currentEvent = null; + this.timeout = null; + } + + state = { + child: null, + + eventQueue: [], + eventActive: false, + } + static getDerivedStateFromProps(nextProps, prevState) { const { prevProps = {} } = prevState; const { appeared } = nextProps; @@ -105,31 +124,12 @@ export function genAnimateChild(transitionSupport) { return newState; } - constructor() { - super(); - - // [Legacy] Since old code addListener on the element. - // To avoid break the behaviour that component not handle animation/transition - // also can handle the animate, let keep the logic. - this.$prevEle = null; - - this.currentEvent = null; - this.timeout = null; - } - - state = { - child: null, - - eventQueue: [], - eventActive: false, - } - componentDidMount() { - this.onDomUpdated({}, {}); + this.onDomUpdated(); } - componentDidUpdate(prevProps, prevState) { - this.onDomUpdated(prevProps, prevState); + componentDidUpdate() { + this.onDomUpdated(); } componentWillUnmount() { From c9840d53cdfbeb73bc56a1faf4f7ff4394018b6e Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 11 Jun 2018 10:57:25 +0800 Subject: [PATCH 41/41] skip event if animateObj is empty --- src/AnimateChild.jsx | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/AnimateChild.jsx b/src/AnimateChild.jsx index 9f9d503..2c7668d 100644 --- a/src/AnimateChild.jsx +++ b/src/AnimateChild.jsx @@ -167,7 +167,7 @@ export function genAnimateChild(transitionSupport) { return; } - const { eventType } = currentEvent; + const { eventType, restQueue } = currentEvent; const nodeClasses = classes($ele); // [Legacy] Since origin code use js to set `className`. @@ -219,6 +219,11 @@ export function genAnimateChild(transitionSupport) { this.onMotionEnd({ target: $ele }); }); + // Do next step if not animate object provided + if (!this.currentEvent.animateObj) { + this.nextEvent(restQueue); + } + // ==================== Use transition instead ==================== } else if (transitionSupport) { legacyAppendClass(); @@ -307,12 +312,7 @@ export function genAnimateChild(transitionSupport) { this.currentEvent = null; // Next queue - if (!this._destroy) { - this.setState({ - eventQueue: restQueue, - eventActive: false, - }); - } + this.nextEvent(restQueue); }; getDomElement = () => { @@ -369,6 +369,16 @@ export function genAnimateChild(transitionSupport) { return event; }; + nextEvent = (restQueue) => { + // Next queue + if (!this._destroy) { + this.setState({ + eventQueue: restQueue, + eventActive: false, + }); + } + }; + cleanDomEvent = () => { if (this.$prevEle && transitionSupport) { this.$prevEle.removeEventListener(animationEndName, this.onMotionEnd);