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..fbe040f 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 = { @@ -48,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) => { @@ -66,7 +68,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..e34cb60 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 = (props) => { + const style = { + width: '200px', + display: 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..086d05b 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 = (props) => { + console.log('render', props.visible); + const style = { + display: 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..c26d349 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "jquery": "^3.3.1", "pre-commit": "1.x", "rc-test": "6.x", - "rc-tools": "6.x", - "react": "^16.0.0", + "rc-tools": "8.x", + "react": "^16.3.0", "react-dom": "^16.0.0", "velocity-animate": "~1.2.2" }, @@ -56,7 +56,12 @@ ], "dependencies": { "babel-runtime": "6.x", - "css-animation": "^1.3.2", - "prop-types": "15.x" + "classnames": "^2.2.5", + "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.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..a62c178 --- /dev/null +++ b/src/Animate.jsx @@ -0,0 +1,183 @@ +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'; + +const defaultKey = `rc_animate_${Date.now()}`; +const clonePropList = ['children']; + + +/** + * 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, + } + + static defaultProps = { + animation: {}, + component: 'span', + componentProps: {}, + transitionEnter: true, + transitionLeave: true, + transitionAppear: false, + } + + state = { + appeared: true, + mergedChildren: [], + }; + + 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 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); + }); + + return newState; + } + + 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), + }); + } + }; + + hasChild = (key) => { + const { children } = this.props; + + 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; + } + + let show = true; + + if (!this.hasChild(node.key)) { + show = false; + } else if (showProp) { + show = node.props[showProp]; + } + + 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[0] || null; + } + } + + polyfill(Animate); + + return Animate; +} + +export default genAnimate(AnimateChild); \ 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..2c7668d --- /dev/null +++ b/src/AnimateChild.jsx @@ -0,0 +1,433 @@ +import React from 'react'; +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 raf from 'raf'; + +import { + getStyleValue, + cloneProps, getTransitionName, + supportTransition, animationEndName, transitionEndName, +} from './util'; + +const clonePropList = [ + 'appeared', + 'show', + 'exclusive', + 'children', + 'animation', +]; + +/** + * 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, + } + + 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; + + const newState = { + prevProps: cloneProps(nextProps, clonePropList), + }; + + function processState(propName, updater) { + if (prevProps[propName] !== nextProps[propName]) { + if (updater) { + updater(nextProps[propName]); + } + return true; + } + return false; + } + + 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); + } + + eventQueue.push(eventType); + newState.eventQueue = eventQueue; + } + + // Child update. Only set child. + processState('children', (child) => { + newState.child = child; + }); + + processState('appeared', (isAppeared) => { + if (isAppeared) { + pushEvent('appear'); + } + }); + + // Show update + processState('show', (show) => { + if (!appeared) { + if (show) { + pushEvent('enter'); + } else { + pushEvent('leave'); + } + } + }); + + return newState; + } + + componentDidMount() { + this.onDomUpdated(); + } + + componentDidUpdate() { + this.onDomUpdated(); + } + + componentWillUnmount() { + clearTimeout(this.timeout); + this._destroy = true; + this.cleanDomEvent(); + } + + onDomUpdated = () => { + const { eventActive } = this.state; + const { + transitionName, animation, onChildLeaved, animateKey, + } = this.props; + + const $ele = this.getDomElement(); + + // Skip if dom element not ready + if (!$ele) return; + + // [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); + } + + const currentEvent = this.getCurrentEvent(); + if (currentEvent.empty) { + // Additional process the leave event + if (currentEvent.lastEventType === 'leave') { + onChildLeaved(animateKey); + } + return; + } + + const { eventType, restQueue } = 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 (!transitionSupport) return; + + const basicClassName = getTransitionName(transitionName, `${eventType}`); + if (basicClassName) nodeClasses.add(basicClassName); + + if (eventActive) { + const activeClassName = getTransitionName(transitionName, `${eventType}-active`); + if (activeClassName) nodeClasses.add(activeClassName); + } + + } + + if (this.currentEvent && this.currentEvent.type === eventType) { + legacyAppendClass(); + 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(); + } + + // 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, + }; + + const animationHandler = (animation || {})[eventType]; + // =============== Check if has customize animation =============== + if (animationHandler) { + this.currentEvent.animateObj = animationHandler($ele, () => { + 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(); + 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 }); + } + } + + 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); + + const { restQueue } = currentEvent; + + const $ele = this.getDomElement(); + 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 (transitionSupport && this.currentEvent) { + 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 && this.currentEvent.type === 'leave') { + onChildLeaved(animateKey); + } + + // [Legacy] Trigger on event when it's last event + if (this.currentEvent && !restQueue.length) { + if (this.currentEvent.type === 'appear' && onAppear) { + onAppear(animateKey); + } else if (this.currentEvent.type === 'enter' && onEnter) { + 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 + this.nextEvent(restQueue); + }; + + getDomElement = () => { + if (this._destroy) return null; + return ReactDOM.findDOMNode(this); + }; + + 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)); + } + + 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: [], + }; + } + } 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], + }; + } + + 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); + this.$prevEle.removeEventListener(transitionEndName, this.onMotionEnd); + } + }; + + render() { + const { child, eventActive } = this.state; + const { showProp, transitionName } = this.props; + const { className } = child.props || {}; + + const currentEvent = this.getCurrentEvent(); + + // 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; + + // Keep show when is in transition or has customize animate + if (transitionSupport && ( + !currentEvent.empty || + (this.currentEvent && this.currentEvent.animateObj) + )) { + show = true; + } else { + show = child.props[showProp]; + } + + // Clone child + const newChildProps = { + className: connectClassName, + }; + + if (showProp) { + newChildProps[showProp] = show; + } + + return React.cloneElement(child, newChildProps); + } + } + + polyfill(AnimateChild); + + return AnimateChild; +} + +export default genAnimateChild(supportTransition); \ 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..972f4a6 100644 --- a/src/util.js +++ b/src/util.js @@ -1,21 +1,163 @@ -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; +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) { + 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; +} + +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; +} + +const prefixedEventNames = {}; + +export function getVendorPrefixedEventName(eventName) { + if (prefixedEventNames[eventName]) { + return prefixedEventNames[eventName]; + } + + const prefixMap = vendorPrefixes[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]; + } + } + } + + return ''; +} + +export const animationEndName = getVendorPrefixedEventName('animationend'); +export const transitionEndName = getVendorPrefixedEventName('transitionend'); +export const supportTransition = !!(animationEndName && transitionEndName); + +// ==================== 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 = 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 = []; + + // 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) => { + if (prop in props) { + newProps[prop] = props[prop]; + } + }); + + return newProps; +} + +export function getTransitionName(transitionName, transitionType) { + if (!transitionName) return null; + + if (typeof transitionName === 'object') { + 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 new file mode 100644 index 0000000..cb97eee --- /dev/null +++ b/tests/basic.spec.js @@ -0,0 +1,333 @@ +/* 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 '../src/Animate'; +import AnimateChild from '../src/AnimateChild'; +import { getVendorPrefixes, getVendorPrefixedEventName, transitionEndName, mergeChildren } from '../src/util'; + +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'); + }); + + afterEach(() => { + try { + ReactDOM.unmountComponentAtNode(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); + }); + + 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); + }); + + 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 { + render() { + const {show} = this.props; + return ( + show ?
  • : null + ) + } + } + + LI.propTypes = { + show: PropTypes.bool, + }; + + class UL 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('de-dup event', () => { + 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 again, clean the leave in queue + expect(child.state.eventQueue).to.eql(['enter']); + + done(); + }); + }); + }); + }); + + it('exclusive', (done) => { + const instance = ReactDOM.render(, div); + + 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(); + }); + }); + }); + }); + + it('remove child when transitionLeave is false', () => { + 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', () => { + 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(''); + }); + + 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]); + }); + }); +}); diff --git a/tests/index.js b/tests/index.js index e65f2f6..6ec1dac 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,5 +1,7 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; +import './basic.spec'; import './single.spec'; import './single-animation.spec'; import './multiple.spec'; +import './no.transition.spec'; diff --git a/tests/multiple.spec.js b/tests/multiple.spec.js index ebce032..a577265 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 Animate from '../'; +import { supportTransition } from '../src/util'; import './index.spec.css'; -import CssAnimation from 'css-animation'; class Todo extends React.Component { static propTypes = { @@ -64,7 +64,7 @@ class TodoList extends React.Component { } return ( - + { this.handleRemove(i); }}> {item} ); @@ -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/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); + }); + }); +}); diff --git a/tests/single-animation.spec.js b/tests/single-animation.spec.js index 27823cf..fbee97f 100644 --- a/tests/single-animation.spec.js +++ b/tests/single-animation.spec.js @@ -1,9 +1,10 @@ /* 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 single from './single-common.spec'; +import './index.spec.css'; function createClass(options) { return class extends React.Component { @@ -43,6 +44,5 @@ 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..0f95ebf 100644 --- a/tests/single-common.spec.js +++ b/tests/single-common.spec.js @@ -1,11 +1,11 @@ -/* 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'; 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) { @@ -76,7 +76,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..43302ab 100644 --- a/tests/single.spec.js +++ b/tests/single.spec.js @@ -1,7 +1,9 @@ /* eslint no-console:0, react/no-multi-comp:0 */ -import Animate from '../index'; import React from 'react'; +import Animate from '../index'; +import { supportTransition } from '../src/util'; +import single from './single-common.spec'; import './index.spec.css'; @@ -15,7 +17,7 @@ function createClass(options) { render() { return (