From bc350376395f06e4d03617b1573a36ab35fe5cb8 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 26 Jul 2018 12:10:55 +0800 Subject: [PATCH 01/10] add CSSTransition --- examples/CSSTransition.html | 0 examples/CSSTransition.js | 54 ++++++++++++++ examples/CSSTransition.less | 26 +++++++ index.js | 1 + package.json | 2 +- src/CSSTransition.js | 141 ++++++++++++++++++++++++++++++++++++ 6 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 examples/CSSTransition.html create mode 100644 examples/CSSTransition.js create mode 100644 examples/CSSTransition.less create mode 100644 src/CSSTransition.js diff --git a/examples/CSSTransition.html b/examples/CSSTransition.html new file mode 100644 index 0000000..e69de29 diff --git a/examples/CSSTransition.js b/examples/CSSTransition.js new file mode 100644 index 0000000..448c910 --- /dev/null +++ b/examples/CSSTransition.js @@ -0,0 +1,54 @@ +/* eslint no-console:0, react/no-multi-comp:0 */ + +import React from 'react'; +// import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import { CSSTransition } from 'rc-animate'; +import classNames from 'classnames'; +import './CSSTransition.less'; + +class Demo extends React.Component { + state = { + show: true, + }; + + onTrigger = () => { + this.setState({ + show: !this.state.show, + }); + }; + + onAppearStart = () => ({ height: 0 }); + + onLeaveActive = () => ({ height: 0 }); + + render() { + const { show } = this.state; + + return ( +
+ + + {({ style, className }) => { + return ( +
+ 666 +
+ ); + }} +
+
+ ); + } +} + +ReactDOM.render(, document.getElementById('__react-content')); diff --git a/examples/CSSTransition.less b/examples/CSSTransition.less new file mode 100644 index 0000000..0679986 --- /dev/null +++ b/examples/CSSTransition.less @@ -0,0 +1,26 @@ +.demo-block { + display: block; + height: 300px; + width: 300px; + background: red; +} + +.transition { + transition: all 1s; + + &.transition-appear { + opacity: 0; + } + + &.transition-appear-active { + opacity: 1; + } + + &.transition-leave-active { + opacity: 1; + } + + &.transition-leave { + opacity: 0; + } +} diff --git a/index.js b/index.js index a1d1242..1c7f276 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ // do not modify this file import Animate from './src/Animate'; +export CSSTransition from './src/CSSTransition'; export default Animate; diff --git a/package.json b/package.json index 8ec60ca..1d066e3 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "prepublish": "rc-tools run guard" }, "devDependencies": { + "babel-runtime": "6.x", "core-js": "^2.5.1", "expect.js": "0.3.x", "jquery": "^3.3.1", @@ -55,7 +56,6 @@ "lint" ], "dependencies": { - "babel-runtime": "6.x", "classnames": "^2.2.5", "component-classes": "^1.2.6", "fbjs": "^0.8.16", diff --git a/src/CSSTransition.js b/src/CSSTransition.js new file mode 100644 index 0000000..8f83eee --- /dev/null +++ b/src/CSSTransition.js @@ -0,0 +1,141 @@ +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 raf from 'raf'; +import { getTransitionName } from './util'; + +const STATUS_NONE = 'none'; +const STATUS_APPEAR = 'appear'; +const STATUS_APPEAR_ACTIVE = 'appear-active'; +const STATUS_ENTER = 'enter'; +const STATUS_ENTER_ACTIVE = 'enter-active'; +const STATUS_LEAVE = 'leave'; +const STATUS_LEAVE_ACTIVE = 'leave-active'; + +class CSSTransition extends React.Component { + static propTypes = { + visible: PropTypes.bool, + children: PropTypes.func, + transitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + transitionAppear: PropTypes.bool, + transitionEnter: PropTypes.bool, + transitionLeave: PropTypes.bool, + onAppearStart: PropTypes.func, + onAppearActive: PropTypes.func, + onEnterStart: PropTypes.func, + onEnterActive: PropTypes.func, + onLeaveStart: PropTypes.func, + onLeaveActive: PropTypes.func, + }; + + static defaultProps = { + transitionEnter: true, + transitionAppear: true, + transitionLeave: true, + }; + + state = { + status: STATUS_NONE, + newStatus: false, + statusStyle: null, + }; + + static getDerivedStateFromProps(props, { prevProps }) { + const { visible } = props; + const newState = { + prevProps: props, + }; + + // Appear + if (!prevProps && visible) { + newState.status = STATUS_APPEAR; + newState.newStatus = true; + } + + // Enter + if (prevProps && !prevProps.visible && visible) { + newState.status = STATUS_ENTER; + newState.newStatus = true; + } + + // Leave + if (prevProps && prevProps.visible && !visible) { + newState.status = STATUS_LEAVE; + newState.newStatus = true; + } + + return newState; + }; + + componentDidMount() { + this.onDomUpdate(); + } + + componentDidUpdate() { + this.onDomUpdate(); + } + + onDomUpdate = () => { + const { status, newStatus } = this.state; + const { + onAppearStart, onEnterStart, onLeaveStart, + onAppearActive, onEnterActive, onLeaveActive, + } = this.props; + + // Init status + if (newStatus && status === STATUS_APPEAR) { + this.updateStatus(onAppearStart); + } else if (newStatus && status === STATUS_ENTER) { + this.updateStatus(onEnterStart); + } else if (newStatus && status === STATUS_LEAVE) { + this.updateStatus(onLeaveStart); + } + + // To be active + if (!newStatus && status === STATUS_APPEAR) { + this.asyncUpdateStatus(onAppearActive, STATUS_APPEAR, STATUS_APPEAR_ACTIVE); + } else if (!newStatus && status === STATUS_ENTER) { + this.asyncUpdateStatus(onEnterActive, STATUS_ENTER, STATUS_ENTER_ACTIVE); + } else if (!newStatus && status === STATUS_LEAVE) { + this.asyncUpdateStatus(onLeaveActive, STATUS_LEAVE, STATUS_LEAVE_ACTIVE); + } + }; + + updateStatus = (styleFunc, additionalState) => { + this.setState({ + statusStyle: styleFunc ? styleFunc(ReactDOM.findDOMNode(this)) : null, + newStatus: false, + ...additionalState, + }); + }; + + asyncUpdateStatus = (styleFunc, fromStatus, toStatus) => { + raf(() => { + const { status } = this.state; + if (status !== fromStatus) return; + + this.updateStatus(styleFunc, { status: toStatus }); + }); + }; + + render() { + const { status, statusStyle } = this.state; + const { children, transitionName } = this.props; + + if (!children) return null; + + return children({ + className: classNames({ + [getTransitionName(transitionName, status)]: status !== STATUS_NONE, + [transitionName]: typeof transitionName === 'string', + }), + style: statusStyle, + }); + } +} + +polyfill(CSSTransition); + +export default CSSTransition; From eb82a1d4a360c617c5e14b098349c076490ca2ac Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 26 Jul 2018 17:51:10 +0800 Subject: [PATCH 02/10] add transition event --- examples/CSSTransition.js | 14 +++--- examples/CSSTransition.less | 11 +++-- src/CSSTransition.js | 95 ++++++++++++++++++++++++++++++------- 3 files changed, 93 insertions(+), 27 deletions(-) diff --git a/examples/CSSTransition.js b/examples/CSSTransition.js index 448c910..f09af13 100644 --- a/examples/CSSTransition.js +++ b/examples/CSSTransition.js @@ -18,9 +18,7 @@ class Demo extends React.Component { }); }; - onAppearStart = () => ({ height: 0 }); - - onLeaveActive = () => ({ height: 0 }); + onCollapse = () => ({ height: 0 }); render() { const { show } = this.state; @@ -35,12 +33,16 @@ class Demo extends React.Component { {({ style, className }) => { return ( -
+
666
); diff --git a/examples/CSSTransition.less b/examples/CSSTransition.less index 0679986..459584e 100644 --- a/examples/CSSTransition.less +++ b/examples/CSSTransition.less @@ -3,24 +3,27 @@ height: 300px; width: 300px; background: red; + overflow: hidden; } .transition { transition: all 1s; - &.transition-appear { + &.transition-appear, + &.transition-enter { opacity: 0; } - &.transition-appear-active { + &.transition-appear-active, + &.transition-enter-active { opacity: 1; } - &.transition-leave-active { + &.transition-leave { opacity: 1; } - &.transition-leave { + &.transition-leave-active { opacity: 0; } } diff --git a/src/CSSTransition.js b/src/CSSTransition.js index 8f83eee..2810325 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -4,7 +4,10 @@ import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classNames from 'classnames'; import raf from 'raf'; -import { getTransitionName } from './util'; +import { + getTransitionName, + animationEndName, transitionEndName, +} from './util'; const STATUS_NONE = 'none'; const STATUS_APPEAR = 'appear'; @@ -24,10 +27,13 @@ class CSSTransition extends React.Component { transitionLeave: PropTypes.bool, onAppearStart: PropTypes.func, onAppearActive: PropTypes.func, + onAppearEnd: PropTypes.func, onEnterStart: PropTypes.func, onEnterActive: PropTypes.func, + onEnterEnd: PropTypes.func, onLeaveStart: PropTypes.func, onLeaveActive: PropTypes.func, + onLeaveEnd: PropTypes.func, }; static defaultProps = { @@ -36,32 +42,37 @@ class CSSTransition extends React.Component { transitionLeave: true, }; - state = { - status: STATUS_NONE, - newStatus: false, - statusStyle: null, - }; + constructor() { + super(); + + this.state = { + status: STATUS_NONE, + newStatus: false, + statusStyle: null, + }; + this.$ele = null; + } static getDerivedStateFromProps(props, { prevProps }) { - const { visible } = props; + const { visible, transitionAppear, transitionEnter, transitionLeave } = props; const newState = { prevProps: props, }; // Appear - if (!prevProps && visible) { + if (!prevProps && visible && transitionAppear) { newState.status = STATUS_APPEAR; newState.newStatus = true; } // Enter - if (prevProps && !prevProps.visible && visible) { + if (prevProps && !prevProps.visible && visible && transitionEnter) { newState.status = STATUS_ENTER; newState.newStatus = true; } // Leave - if (prevProps && prevProps.visible && !visible) { + if (prevProps && prevProps.visible && !visible && transitionLeave) { newState.status = STATUS_LEAVE; newState.newStatus = true; } @@ -77,32 +88,76 @@ class CSSTransition extends React.Component { this.onDomUpdate(); } + componentWillUnmount() { + this.removeEventListener(this.$ele); + } + onDomUpdate = () => { const { status, newStatus } = this.state; const { onAppearStart, onEnterStart, onLeaveStart, onAppearActive, onEnterActive, onLeaveActive, + transitionAppear, transitionEnter, transitionLeave, } = this.props; + // Event injection + const $ele = ReactDOM.findDOMNode(this); + if (this.$ele !== $ele) { + this.removeEventListener(this.$ele); + this.addEventListener($ele); + this.$ele = $ele; + } + // Init status - if (newStatus && status === STATUS_APPEAR) { + if (newStatus && status === STATUS_APPEAR && transitionAppear) { this.updateStatus(onAppearStart); - } else if (newStatus && status === STATUS_ENTER) { + } else if (newStatus && status === STATUS_ENTER && transitionEnter) { this.updateStatus(onEnterStart); - } else if (newStatus && status === STATUS_LEAVE) { + } else if (newStatus && status === STATUS_LEAVE && transitionLeave) { this.updateStatus(onLeaveStart); } // To be active - if (!newStatus && status === STATUS_APPEAR) { + if (!newStatus && status === STATUS_APPEAR && transitionAppear) { this.asyncUpdateStatus(onAppearActive, STATUS_APPEAR, STATUS_APPEAR_ACTIVE); - } else if (!newStatus && status === STATUS_ENTER) { + } else if (!newStatus && status === STATUS_ENTER && transitionEnter) { this.asyncUpdateStatus(onEnterActive, STATUS_ENTER, STATUS_ENTER_ACTIVE); - } else if (!newStatus && status === STATUS_LEAVE) { + } else if (!newStatus && status === STATUS_LEAVE && transitionLeave) { this.asyncUpdateStatus(onLeaveActive, STATUS_LEAVE, STATUS_LEAVE_ACTIVE); } }; + onAnimationEnd = (event) => { + console.log('Animate End >', event); + }; + onTransitionEnd = (event) => { + const { status } = this.state; + const { onAppearEnd, onEnterEnd, onLeaveEnd } = this.props; + if (status === STATUS_APPEAR_ACTIVE) { + this.updateStatus(onAppearEnd, { status: STATUS_NONE }); + } else if (status === STATUS_ENTER_ACTIVE) { + this.updateStatus(onEnterEnd, { status: STATUS_NONE }); + } else if (status === STATUS_LEAVE_ACTIVE) { + this.updateStatus(onLeaveEnd, { status: STATUS_NONE }); + } + console.log('Trans End >', event); + }; + + addEventListener = ($ele) => { + if (!$ele) return; + + console.log('ADD EVENT'); + $ele.addEventListener(transitionEndName, this.onTransitionEnd); + $ele.addEventListener(animationEndName, this.onAnimationEnd); + }; + removeEventListener = ($ele) => { + if (!$ele) return; + + console.log('REMOVE EVENT'); + $ele.removeEventListener(transitionEndName, this.onTransitionEnd); + $ele.removeEventListener(animationEndName, this.onAnimationEnd); + }; + updateStatus = (styleFunc, additionalState) => { this.setState({ statusStyle: styleFunc ? styleFunc(ReactDOM.findDOMNode(this)) : null, @@ -122,10 +177,16 @@ class CSSTransition extends React.Component { render() { const { status, statusStyle } = this.state; - const { children, transitionName } = this.props; + const { children, transitionName, visible } = this.props; + + console.log('>', status, statusStyle); if (!children) return null; + if (status === STATUS_NONE) { + return visible ? children({}) : null; + } + return children({ className: classNames({ [getTransitionName(transitionName, status)]: status !== STATUS_NONE, From 4f7d991449dc33e6b4c624892438163dba195fc4 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 26 Jul 2018 18:02:33 +0800 Subject: [PATCH 03/10] support false break next step --- src/CSSTransition.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/CSSTransition.js b/src/CSSTransition.js index 2810325..3509457 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -128,39 +128,45 @@ class CSSTransition extends React.Component { }; onAnimationEnd = (event) => { - console.log('Animate End >', event); + this.onMotionEnd(event); }; + onTransitionEnd = (event) => { + this.onMotionEnd(event); + }; + + onMotionEnd = (event) => { const { status } = this.state; const { onAppearEnd, onEnterEnd, onLeaveEnd } = this.props; if (status === STATUS_APPEAR_ACTIVE) { - this.updateStatus(onAppearEnd, { status: STATUS_NONE }); + this.updateStatus(onAppearEnd, { status: STATUS_NONE }, event); } else if (status === STATUS_ENTER_ACTIVE) { - this.updateStatus(onEnterEnd, { status: STATUS_NONE }); + this.updateStatus(onEnterEnd, { status: STATUS_NONE }, event); } else if (status === STATUS_LEAVE_ACTIVE) { - this.updateStatus(onLeaveEnd, { status: STATUS_NONE }); + this.updateStatus(onLeaveEnd, { status: STATUS_NONE }, event); } - console.log('Trans End >', event); }; addEventListener = ($ele) => { if (!$ele) return; - console.log('ADD EVENT'); $ele.addEventListener(transitionEndName, this.onTransitionEnd); $ele.addEventListener(animationEndName, this.onAnimationEnd); }; removeEventListener = ($ele) => { if (!$ele) return; - console.log('REMOVE EVENT'); $ele.removeEventListener(transitionEndName, this.onTransitionEnd); $ele.removeEventListener(animationEndName, this.onAnimationEnd); }; - updateStatus = (styleFunc, additionalState) => { + updateStatus = (styleFunc, additionalState, event) => { + const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; + + if (statusStyle === false) return; + this.setState({ - statusStyle: styleFunc ? styleFunc(ReactDOM.findDOMNode(this)) : null, + statusStyle, newStatus: false, ...additionalState, }); @@ -179,8 +185,6 @@ class CSSTransition extends React.Component { const { status, statusStyle } = this.state; const { children, transitionName, visible } = this.props; - console.log('>', status, statusStyle); - if (!children) return null; if (status === STATUS_NONE) { From 40aac3400ade9857ffb731f737f7b13fcf51f9fb Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 26 Jul 2018 18:11:50 +0800 Subject: [PATCH 04/10] wait 2 frames --- src/CSSTransition.js | 8 +++++--- src/util.js | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/CSSTransition.js b/src/CSSTransition.js index 3509457..663a896 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -3,10 +3,10 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classNames from 'classnames'; -import raf from 'raf'; import { getTransitionName, animationEndName, transitionEndName, + nextFrame, } from './util'; const STATUS_NONE = 'none'; @@ -173,12 +173,14 @@ class CSSTransition extends React.Component { }; asyncUpdateStatus = (styleFunc, fromStatus, toStatus) => { - raf(() => { + // React use `postMessage` to do the UI render which may cause status update in one frame. + // Let's delay 2 frame to avoid this. + nextFrame(() => { const { status } = this.state; if (status !== fromStatus) return; this.updateStatus(styleFunc, { status: toStatus }); - }); + }, 2); }; render() { diff --git a/src/util.js b/src/util.js index 508cab1..0c68af7 100644 --- a/src/util.js +++ b/src/util.js @@ -1,5 +1,6 @@ import toArray from 'rc-util/lib/Children/toArray'; import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; +import raf from 'raf'; // =================== Style ==================== const stylePrefixes = ['-webkit-', '-moz-', '-o-', 'ms-', '']; @@ -161,3 +162,12 @@ export function getTransitionName(transitionName, transitionType) { return `${transitionName}-${transitionType}`; } +export function nextFrame(callback, frames = 1) { + raf(() => { + if (frames <=0) { + callback(); + } else { + nextFrame(callback, frames - 1); + } + }); +} From 9a9d36de3ff113251dbdb012e45eeb0f0614d18d Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 26 Jul 2018 21:08:09 +0800 Subject: [PATCH 05/10] check for old browser (IE 9) --- examples/CSSTransition.js | 67 +++++++++++++++++++++-------- examples/CSSTransition.less | 66 ++++++++++++++++++++++++++--- src/CSSTransition.js | 84 ++++++++++++++++++++----------------- src/util.js | 11 ----- 4 files changed, 156 insertions(+), 72 deletions(-) diff --git a/examples/CSSTransition.js b/examples/CSSTransition.js index f09af13..e1cc4ac 100644 --- a/examples/CSSTransition.js +++ b/examples/CSSTransition.js @@ -20,6 +20,19 @@ class Demo extends React.Component { onCollapse = () => ({ height: 0 }); + skipColorTransition = (_, event) => { + // CSSTransition support multiple transition. + // You can return false to prevent motion end when fast transition finished. + if (event.propertyName === 'background-color') { + return false; + } + return true; + }; + + styleGreen = () => ({ + background: 'green', + }); + render() { const { show } = this.state; @@ -30,27 +43,45 @@ class Demo extends React.Component { {' '} Show Component - - {({ style, className }) => { - return ( -
- 666 -
- ); - }} -
+
+
+

With Transition Class

+ + {({ style, className }) => ( +
+ )} + +
+ +
+

With Animation Class

+ + {({ style, className }) => ( +
+ )} + +
+
); } } ReactDOM.render(, document.getElementById('__react-content')); + +// Remove for IE9 test +const aaa = document.getElementsByClassName('navbar')[0]; +aaa.parentNode.removeChild(aaa); diff --git a/examples/CSSTransition.less b/examples/CSSTransition.less index 459584e..3acec0d 100644 --- a/examples/CSSTransition.less +++ b/examples/CSSTransition.less @@ -1,3 +1,11 @@ +.grid { + display: flex; + + > div { + flex: auto; + } +} + .demo-block { display: block; height: 300px; @@ -7,23 +15,71 @@ } .transition { - transition: all 1s; + transition: background .3s, height 1.3s, opacity 1.3s; &.transition-appear, &.transition-enter { opacity: 0; } - &.transition-appear-active, - &.transition-enter-active { + &.transition-appear.transition-appear-active, + &.transition-enter.transition-enter-active { opacity: 1; } - &.transition-leave { + &.transition-leave-active { + opacity: 0; + background: green; + } +} + +.animation { + animation-duration: 1.3s; + animation-fill-mode: both; + + &.animation-appear, + &.animation-enter { + animation-name: enter; + animation-fill-mode: both; + animation-play-state: paused; + } + + &.animation-appear.animation-appear-active, + &.animation-enter.animation-enter-active { + animation-name: enter; + animation-play-state: running; + } + + &.animation-leave { + animation-name: leave; + animation-fill-mode: both; + animation-play-state: paused; + + &.animation-leave-active { + animation-name: leave; + animation-play-state: running; + } + } +} + +@keyframes enter { + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); opacity: 1; } +} - &.transition-leave-active { +@keyframes leave { + from { + transform: scale(1); + opacity: 1; + } + to { + transform: scale(0); opacity: 0; } } diff --git a/src/CSSTransition.js b/src/CSSTransition.js index 663a896..88de559 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -3,19 +3,17 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classNames from 'classnames'; +import raf from 'raf'; import { getTransitionName, animationEndName, transitionEndName, - nextFrame, + supportTransition, } from './util'; const STATUS_NONE = 'none'; const STATUS_APPEAR = 'appear'; -const STATUS_APPEAR_ACTIVE = 'appear-active'; const STATUS_ENTER = 'enter'; -const STATUS_ENTER_ACTIVE = 'enter-active'; const STATUS_LEAVE = 'leave'; -const STATUS_LEAVE_ACTIVE = 'leave-active'; class CSSTransition extends React.Component { static propTypes = { @@ -47,6 +45,7 @@ class CSSTransition extends React.Component { this.state = { status: STATUS_NONE, + statusActive: false, newStatus: false, statusStyle: null, }; @@ -54,6 +53,8 @@ class CSSTransition extends React.Component { } static getDerivedStateFromProps(props, { prevProps }) { + if (!supportTransition) return {}; + const { visible, transitionAppear, transitionEnter, transitionLeave } = props; const newState = { prevProps: props, @@ -62,18 +63,21 @@ class CSSTransition extends React.Component { // Appear if (!prevProps && visible && transitionAppear) { newState.status = STATUS_APPEAR; + newState.statusActive = false; newState.newStatus = true; } // Enter if (prevProps && !prevProps.visible && visible && transitionEnter) { newState.status = STATUS_ENTER; + newState.statusActive = false; newState.newStatus = true; } // Leave if (prevProps && prevProps.visible && !visible && transitionLeave) { newState.status = STATUS_LEAVE; + newState.statusActive = false; newState.newStatus = true; } @@ -100,6 +104,10 @@ class CSSTransition extends React.Component { transitionAppear, transitionEnter, transitionLeave, } = this.props; + if (!supportTransition) { + return; + } + // Event injection const $ele = ReactDOM.findDOMNode(this); if (this.$ele !== $ele) { @@ -110,20 +118,17 @@ class CSSTransition extends React.Component { // Init status if (newStatus && status === STATUS_APPEAR && transitionAppear) { - this.updateStatus(onAppearStart); + this.updateStatus(onAppearStart).then(() => { + this.updateActiveStatus(onAppearActive, STATUS_APPEAR); + }); } else if (newStatus && status === STATUS_ENTER && transitionEnter) { - this.updateStatus(onEnterStart); + this.updateStatus(onEnterStart).then(() => { + this.updateActiveStatus(onEnterActive, STATUS_ENTER); + }); } else if (newStatus && status === STATUS_LEAVE && transitionLeave) { - this.updateStatus(onLeaveStart); - } - - // To be active - if (!newStatus && status === STATUS_APPEAR && transitionAppear) { - this.asyncUpdateStatus(onAppearActive, STATUS_APPEAR, STATUS_APPEAR_ACTIVE); - } else if (!newStatus && status === STATUS_ENTER && transitionEnter) { - this.asyncUpdateStatus(onEnterActive, STATUS_ENTER, STATUS_ENTER_ACTIVE); - } else if (!newStatus && status === STATUS_LEAVE && transitionLeave) { - this.asyncUpdateStatus(onLeaveActive, STATUS_LEAVE, STATUS_LEAVE_ACTIVE); + this.updateStatus(onLeaveStart).then(() => { + this.updateActiveStatus(onLeaveActive, STATUS_LEAVE); + }); } }; @@ -136,13 +141,13 @@ class CSSTransition extends React.Component { }; onMotionEnd = (event) => { - const { status } = this.state; + const { status, statusActive } = this.state; const { onAppearEnd, onEnterEnd, onLeaveEnd } = this.props; - if (status === STATUS_APPEAR_ACTIVE) { + if (status === STATUS_APPEAR && statusActive) { this.updateStatus(onAppearEnd, { status: STATUS_NONE }, event); - } else if (status === STATUS_ENTER_ACTIVE) { + } else if (status === STATUS_ENTER && statusActive) { this.updateStatus(onEnterEnd, { status: STATUS_NONE }, event); - } else if (status === STATUS_LEAVE_ACTIVE) { + } else if (status === STATUS_LEAVE && statusActive) { this.updateStatus(onLeaveEnd, { status: STATUS_NONE }, event); } }; @@ -160,42 +165,45 @@ class CSSTransition extends React.Component { $ele.removeEventListener(animationEndName, this.onAnimationEnd); }; - updateStatus = (styleFunc, additionalState, event) => { - const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; + updateStatus = (styleFunc, additionalState, event) => ( + new Promise((resolve) => { + const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; - if (statusStyle === false) return; + if (statusStyle === false) return; - this.setState({ - statusStyle, - newStatus: false, - ...additionalState, - }); - }; + this.setState({ + statusStyle, + newStatus: false, + ...additionalState, + }, resolve); // Trigger before next frame & after `componentDidMount` + }) + ); - asyncUpdateStatus = (styleFunc, fromStatus, toStatus) => { - // React use `postMessage` to do the UI render which may cause status update in one frame. - // Let's delay 2 frame to avoid this. - nextFrame(() => { + updateActiveStatus = (styleFunc, currentStatus) => { + // `setState` use `postMessage` to trigger at the end of frame. + // Let's use requestAnimationFrame to update new state in next frame. + raf(() => { const { status } = this.state; - if (status !== fromStatus) return; + if (status !== currentStatus) return; - this.updateStatus(styleFunc, { status: toStatus }); - }, 2); + this.updateStatus(styleFunc, { statusActive: true }); + }); }; render() { - const { status, statusStyle } = this.state; + const { status, statusActive, statusStyle } = this.state; const { children, transitionName, visible } = this.props; if (!children) return null; - if (status === STATUS_NONE) { + if (status === STATUS_NONE || !supportTransition) { return visible ? children({}) : null; } return children({ className: classNames({ [getTransitionName(transitionName, status)]: status !== STATUS_NONE, + [getTransitionName(transitionName, `${status}-active`)]: status !== STATUS_NONE && statusActive, [transitionName]: typeof transitionName === 'string', }), style: statusStyle, diff --git a/src/util.js b/src/util.js index 0c68af7..1f018a1 100644 --- a/src/util.js +++ b/src/util.js @@ -1,6 +1,5 @@ import toArray from 'rc-util/lib/Children/toArray'; import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; -import raf from 'raf'; // =================== Style ==================== const stylePrefixes = ['-webkit-', '-moz-', '-o-', 'ms-', '']; @@ -161,13 +160,3 @@ export function getTransitionName(transitionName, transitionType) { return `${transitionName}-${transitionType}`; } - -export function nextFrame(callback, frames = 1) { - raf(() => { - if (frames <=0) { - callback(); - } else { - nextFrame(callback, frames - 1); - } - }); -} From 1b1fee4dd2b7940cc98c6976cbb3e552c4d8709d Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 27 Jul 2018 10:20:22 +0800 Subject: [PATCH 06/10] check statusStyle typeof --- examples/CSSTransition.js | 4 ++-- src/CSSTransition.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/CSSTransition.js b/examples/CSSTransition.js index e1cc4ac..a990904 100644 --- a/examples/CSSTransition.js +++ b/examples/CSSTransition.js @@ -83,5 +83,5 @@ class Demo extends React.Component { ReactDOM.render(, document.getElementById('__react-content')); // Remove for IE9 test -const aaa = document.getElementsByClassName('navbar')[0]; -aaa.parentNode.removeChild(aaa); +// const aaa = document.getElementsByClassName('navbar')[0]; +// aaa.parentNode.removeChild(aaa); diff --git a/src/CSSTransition.js b/src/CSSTransition.js index 88de559..43dfa20 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -172,7 +172,7 @@ class CSSTransition extends React.Component { if (statusStyle === false) return; this.setState({ - statusStyle, + statusStyle: typeof statusStyle === 'object' ? statusStyle : null, newStatus: false, ...additionalState, }, resolve); // Trigger before next frame & after `componentDidMount` From 18e39996c21412f5ef16d26ac92e8d6bee8dbb6a Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 27 Jul 2018 10:56:14 +0800 Subject: [PATCH 07/10] update doc --- README.md | 52 ++++++++++++++++++- .../{CSSTransition.html => CSSMotion.html} | 0 examples/{CSSTransition.js => CSSMotion.js} | 18 +++---- .../{CSSTransition.less => CSSMotion.less} | 5 +- index.js | 2 +- src/{CSSTransition.js => CSSMotion.jsx} | 44 ++++++++-------- 6 files changed, 86 insertions(+), 35 deletions(-) rename examples/{CSSTransition.html => CSSMotion.html} (100%) rename examples/{CSSTransition.js => CSSMotion.js} (86%) rename examples/{CSSTransition.less => CSSMotion.less} (95%) rename src/{CSSTransition.js => CSSMotion.jsx} (79%) diff --git a/README.md b/README.md index ceb066c..a384daa 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,57 @@ ReactDOM.render( , mountNode); ``` -## API + +## CSSMotion + +### props + +| Property | Type | Default | Description| +| -------- | ---- | ------- | ---------- | +| visible | boolean | | Display child content or not | +| children | function | | Render props of children content. Example [see below](#sample usage) | +| motionName | string \| [motionNameObjProps](#motionNameObjProps) | | Set the className when motion start | +| motionAppear | boolean | true | Support motion on appear | +| motionEnter | boolean | true | Support motion on enter | +| motionLeave | boolean | true | Support motion on leave | +| onAppearStart | function | | Trigger when appear start | +| onAppearActive | function | | Trigger when appear active | +| onAppearEnd | function | | Trigger when appear end | +| onEnterStart | function | | Trigger when enter start | +| onEnterActive | function | | Trigger when enter active | +| onEnterEnd | function | | Trigger when enter end | +| onLeaveStart | function | | Trigger when leave start | +| onLeaveActive | function | | Trigger when leave active | +| onLeaveEnd | function | | Trigger when leave end | + +#### motionNameObjProps +| Property | Type | +| -------- | ---- | +| appear | string | +| appearActive | string | +| enter | string | +| enterActive | string | +| leave | string | +| leaveActive | string | + +### sample usage + +```jsx +// Return customize style +const onAppearStart = (ele) => ({ height: 0 }); + + + {({ style, className }) => ( +
+ )} + +``` + +## Animate (Deprecated) ### props diff --git a/examples/CSSTransition.html b/examples/CSSMotion.html similarity index 100% rename from examples/CSSTransition.html rename to examples/CSSMotion.html diff --git a/examples/CSSTransition.js b/examples/CSSMotion.js similarity index 86% rename from examples/CSSTransition.js rename to examples/CSSMotion.js index a990904..d085786 100644 --- a/examples/CSSTransition.js +++ b/examples/CSSMotion.js @@ -3,9 +3,9 @@ import React from 'react'; // import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; -import { CSSTransition } from 'rc-animate'; +import { CSSMotion } from 'rc-animate'; import classNames from 'classnames'; -import './CSSTransition.less'; +import './CSSMotion.less'; class Demo extends React.Component { state = { @@ -21,7 +21,7 @@ class Demo extends React.Component { onCollapse = () => ({ height: 0 }); skipColorTransition = (_, event) => { - // CSSTransition support multiple transition. + // CSSMotion support multiple transition. // You can return false to prevent motion end when fast transition finished. if (event.propertyName === 'background-color') { return false; @@ -46,9 +46,9 @@ class Demo extends React.Component {

With Transition Class

- (
)} - +

With Animation Class

- {({ style, className }) => (
)} - +
diff --git a/examples/CSSTransition.less b/examples/CSSMotion.less similarity index 95% rename from examples/CSSTransition.less rename to examples/CSSMotion.less index 3acec0d..0e9ed42 100644 --- a/examples/CSSTransition.less +++ b/examples/CSSMotion.less @@ -1,8 +1,9 @@ .grid { - display: flex; + display: table; > div { - flex: auto; + display: table-cell; + min-width: 350px; } } diff --git a/index.js b/index.js index 1c7f276..02ed4e1 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ // do not modify this file import Animate from './src/Animate'; -export CSSTransition from './src/CSSTransition'; +export CSSMotion from './src/CSSMotion'; export default Animate; diff --git a/src/CSSTransition.js b/src/CSSMotion.jsx similarity index 79% rename from src/CSSTransition.js rename to src/CSSMotion.jsx index 43dfa20..9029983 100644 --- a/src/CSSTransition.js +++ b/src/CSSMotion.jsx @@ -15,14 +15,14 @@ const STATUS_APPEAR = 'appear'; const STATUS_ENTER = 'enter'; const STATUS_LEAVE = 'leave'; -class CSSTransition extends React.Component { +class CSSMotion extends React.Component { static propTypes = { visible: PropTypes.bool, children: PropTypes.func, - transitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - transitionAppear: PropTypes.bool, - transitionEnter: PropTypes.bool, - transitionLeave: PropTypes.bool, + motionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + motionAppear: PropTypes.bool, + motionEnter: PropTypes.bool, + motionLeave: PropTypes.bool, onAppearStart: PropTypes.func, onAppearActive: PropTypes.func, onAppearEnd: PropTypes.func, @@ -35,9 +35,9 @@ class CSSTransition extends React.Component { }; static defaultProps = { - transitionEnter: true, - transitionAppear: true, - transitionLeave: true, + motionEnter: true, + motionAppear: true, + motionLeave: true, }; constructor() { @@ -55,27 +55,27 @@ class CSSTransition extends React.Component { static getDerivedStateFromProps(props, { prevProps }) { if (!supportTransition) return {}; - const { visible, transitionAppear, transitionEnter, transitionLeave } = props; + const { visible, motionAppear, motionEnter, motionLeave } = props; const newState = { prevProps: props, }; // Appear - if (!prevProps && visible && transitionAppear) { + if (!prevProps && visible && motionAppear) { newState.status = STATUS_APPEAR; newState.statusActive = false; newState.newStatus = true; } // Enter - if (prevProps && !prevProps.visible && visible && transitionEnter) { + if (prevProps && !prevProps.visible && visible && motionEnter) { newState.status = STATUS_ENTER; newState.statusActive = false; newState.newStatus = true; } // Leave - if (prevProps && prevProps.visible && !visible && transitionLeave) { + if (prevProps && prevProps.visible && !visible && motionLeave) { newState.status = STATUS_LEAVE; newState.statusActive = false; newState.newStatus = true; @@ -101,7 +101,7 @@ class CSSTransition extends React.Component { const { onAppearStart, onEnterStart, onLeaveStart, onAppearActive, onEnterActive, onLeaveActive, - transitionAppear, transitionEnter, transitionLeave, + motionAppear, motionEnter, motionLeave, } = this.props; if (!supportTransition) { @@ -117,15 +117,15 @@ class CSSTransition extends React.Component { } // Init status - if (newStatus && status === STATUS_APPEAR && transitionAppear) { + if (newStatus && status === STATUS_APPEAR && motionAppear) { this.updateStatus(onAppearStart).then(() => { this.updateActiveStatus(onAppearActive, STATUS_APPEAR); }); - } else if (newStatus && status === STATUS_ENTER && transitionEnter) { + } else if (newStatus && status === STATUS_ENTER && motionEnter) { this.updateStatus(onEnterStart).then(() => { this.updateActiveStatus(onEnterActive, STATUS_ENTER); }); - } else if (newStatus && status === STATUS_LEAVE && transitionLeave) { + } else if (newStatus && status === STATUS_LEAVE && motionLeave) { this.updateStatus(onLeaveStart).then(() => { this.updateActiveStatus(onLeaveActive, STATUS_LEAVE); }); @@ -192,7 +192,7 @@ class CSSTransition extends React.Component { render() { const { status, statusActive, statusStyle } = this.state; - const { children, transitionName, visible } = this.props; + const { children, motionName, visible } = this.props; if (!children) return null; @@ -202,15 +202,15 @@ class CSSTransition extends React.Component { return children({ className: classNames({ - [getTransitionName(transitionName, status)]: status !== STATUS_NONE, - [getTransitionName(transitionName, `${status}-active`)]: status !== STATUS_NONE && statusActive, - [transitionName]: typeof transitionName === 'string', + [getTransitionName(motionName, status)]: status !== STATUS_NONE, + [getTransitionName(motionName, `${status}-active`)]: status !== STATUS_NONE && statusActive, + [motionName]: typeof motionName === 'string', }), style: statusStyle, }); } } -polyfill(CSSTransition); +polyfill(CSSMotion); -export default CSSTransition; +export default CSSMotion; From 0eb671f9427267a2a09392f3194e0d29e7346f1f Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 27 Jul 2018 16:49:26 +0800 Subject: [PATCH 08/10] add testcase --- README.md | 2 +- src/CSSMotion.jsx | 343 ++++++++++++++++++++------------------- tests/CSSMotion.spec.css | 71 ++++++++ tests/CSSMotion.spec.js | 244 ++++++++++++++++++++++++++++ tests/index.js | 2 + 5 files changed, 490 insertions(+), 172 deletions(-) create mode 100644 tests/CSSMotion.spec.css create mode 100644 tests/CSSMotion.spec.js diff --git a/README.md b/README.md index a384daa..a1354a0 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ReactDOM.render( | Property | Type | Default | Description| | -------- | ---- | ------- | ---------- | -| visible | boolean | | Display child content or not | +| visible | boolean | true | Display child content or not | | children | function | | Render props of children content. Example [see below](#sample usage) | | motionName | string \| [motionNameObjProps](#motionNameObjProps) | | Set the className when motion start | | motionAppear | boolean | true | Support motion on appear | diff --git a/src/CSSMotion.jsx b/src/CSSMotion.jsx index 9029983..6f58fe8 100644 --- a/src/CSSMotion.jsx +++ b/src/CSSMotion.jsx @@ -15,202 +15,203 @@ const STATUS_APPEAR = 'appear'; const STATUS_ENTER = 'enter'; const STATUS_LEAVE = 'leave'; -class CSSMotion extends React.Component { - static propTypes = { - visible: PropTypes.bool, - children: PropTypes.func, - motionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - motionAppear: PropTypes.bool, - motionEnter: PropTypes.bool, - motionLeave: PropTypes.bool, - onAppearStart: PropTypes.func, - onAppearActive: PropTypes.func, - onAppearEnd: PropTypes.func, - onEnterStart: PropTypes.func, - onEnterActive: PropTypes.func, - onEnterEnd: PropTypes.func, - onLeaveStart: PropTypes.func, - onLeaveActive: PropTypes.func, - onLeaveEnd: PropTypes.func, - }; - - static defaultProps = { - motionEnter: true, - motionAppear: true, - motionLeave: true, - }; - - constructor() { - super(); - - this.state = { - status: STATUS_NONE, - statusActive: false, - newStatus: false, - statusStyle: null, +/** + * `transitionSupport` is used for none transition test case. + * Default we use browser transition event support check. + */ +export function genCSSMotion(transitionSupport) { + class CSSMotion extends React.Component { + static propTypes = { + visible: PropTypes.bool, + children: PropTypes.func, + motionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + motionAppear: PropTypes.bool, + motionEnter: PropTypes.bool, + motionLeave: PropTypes.bool, + onAppearStart: PropTypes.func, + onAppearActive: PropTypes.func, + onAppearEnd: PropTypes.func, + onEnterStart: PropTypes.func, + onEnterActive: PropTypes.func, + onEnterEnd: PropTypes.func, + onLeaveStart: PropTypes.func, + onLeaveActive: PropTypes.func, + onLeaveEnd: PropTypes.func, }; - this.$ele = null; - } - static getDerivedStateFromProps(props, { prevProps }) { - if (!supportTransition) return {}; + static defaultProps = { + visible: true, + motionEnter: true, + motionAppear: true, + motionLeave: true, + }; + + constructor() { + super(); + + this.state = { + status: STATUS_NONE, + statusActive: false, + newStatus: false, + statusStyle: null, + }; + this.$ele = null; + } - const { visible, motionAppear, motionEnter, motionLeave } = props; - const newState = { - prevProps: props, + static getDerivedStateFromProps(props, { prevProps }) { + if (!transitionSupport) return {}; + + const { visible, motionAppear, motionEnter, motionLeave } = props; + const newState = { + prevProps: props, + }; + + // Appear + if (!prevProps && visible && motionAppear) { + newState.status = STATUS_APPEAR; + newState.statusActive = false; + newState.newStatus = true; + } + + // Enter + if (prevProps && !prevProps.visible && visible && motionEnter) { + newState.status = STATUS_ENTER; + newState.statusActive = false; + newState.newStatus = true; + } + + // Leave + if (prevProps && prevProps.visible && !visible && motionLeave) { + newState.status = STATUS_LEAVE; + newState.statusActive = false; + newState.newStatus = true; + } + + return newState; }; - // Appear - if (!prevProps && visible && motionAppear) { - newState.status = STATUS_APPEAR; - newState.statusActive = false; - newState.newStatus = true; + componentDidMount() { + this.onDomUpdate(); } - // Enter - if (prevProps && !prevProps.visible && visible && motionEnter) { - newState.status = STATUS_ENTER; - newState.statusActive = false; - newState.newStatus = true; + componentDidUpdate() { + this.onDomUpdate(); } - // Leave - if (prevProps && prevProps.visible && !visible && motionLeave) { - newState.status = STATUS_LEAVE; - newState.statusActive = false; - newState.newStatus = true; + componentWillUnmount() { + this.removeEventListener(this.$ele); } - return newState; - }; + onDomUpdate = () => { + const { status, newStatus } = this.state; + const { + onAppearStart, onEnterStart, onLeaveStart, + onAppearActive, onEnterActive, onLeaveActive, + motionAppear, motionEnter, motionLeave, + } = this.props; + + if (!transitionSupport) { + return; + } + + // Event injection + const $ele = ReactDOM.findDOMNode(this); + if (this.$ele !== $ele) { + this.removeEventListener(this.$ele); + this.addEventListener($ele); + this.$ele = $ele; + } + + // Init status + if (newStatus && status === STATUS_APPEAR && motionAppear) { + this.updateStatus(onAppearStart).then(() => { + this.updateActiveStatus(onAppearActive, STATUS_APPEAR); + }); + } else if (newStatus && status === STATUS_ENTER && motionEnter) { + this.updateStatus(onEnterStart).then(() => { + this.updateActiveStatus(onEnterActive, STATUS_ENTER); + }); + } else if (newStatus && status === STATUS_LEAVE && motionLeave) { + this.updateStatus(onLeaveStart).then(() => { + this.updateActiveStatus(onLeaveActive, STATUS_LEAVE); + }); + } + }; - componentDidMount() { - this.onDomUpdate(); - } + onMotionEnd = (event) => { + const { status, statusActive } = this.state; + const { onAppearEnd, onEnterEnd, onLeaveEnd } = this.props; + if (status === STATUS_APPEAR && statusActive) { + this.updateStatus(onAppearEnd, { status: STATUS_NONE }, event); + } else if (status === STATUS_ENTER && statusActive) { + this.updateStatus(onEnterEnd, { status: STATUS_NONE }, event); + } else if (status === STATUS_LEAVE && statusActive) { + this.updateStatus(onLeaveEnd, { status: STATUS_NONE }, event); + } + }; - componentDidUpdate() { - this.onDomUpdate(); - } + addEventListener = ($ele) => { + if (!$ele) return; - componentWillUnmount() { - this.removeEventListener(this.$ele); - } + $ele.addEventListener(transitionEndName, this.onMotionEnd); + $ele.addEventListener(animationEndName, this.onMotionEnd); + }; + removeEventListener = ($ele) => { + if (!$ele) return; - onDomUpdate = () => { - const { status, newStatus } = this.state; - const { - onAppearStart, onEnterStart, onLeaveStart, - onAppearActive, onEnterActive, onLeaveActive, - motionAppear, motionEnter, motionLeave, - } = this.props; + $ele.removeEventListener(transitionEndName, this.onMotionEnd); + $ele.removeEventListener(animationEndName, this.onMotionEnd); + }; - if (!supportTransition) { - return; - } + updateStatus = (styleFunc, additionalState, event) => ( + new Promise((resolve) => { + const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; - // Event injection - const $ele = ReactDOM.findDOMNode(this); - if (this.$ele !== $ele) { - this.removeEventListener(this.$ele); - this.addEventListener($ele); - this.$ele = $ele; - } + if (statusStyle === false) return; - // Init status - if (newStatus && status === STATUS_APPEAR && motionAppear) { - this.updateStatus(onAppearStart).then(() => { - this.updateActiveStatus(onAppearActive, STATUS_APPEAR); - }); - } else if (newStatus && status === STATUS_ENTER && motionEnter) { - this.updateStatus(onEnterStart).then(() => { - this.updateActiveStatus(onEnterActive, STATUS_ENTER); - }); - } else if (newStatus && status === STATUS_LEAVE && motionLeave) { - this.updateStatus(onLeaveStart).then(() => { - this.updateActiveStatus(onLeaveActive, STATUS_LEAVE); - }); - } - }; - - onAnimationEnd = (event) => { - this.onMotionEnd(event); - }; - - onTransitionEnd = (event) => { - this.onMotionEnd(event); - }; - - onMotionEnd = (event) => { - const { status, statusActive } = this.state; - const { onAppearEnd, onEnterEnd, onLeaveEnd } = this.props; - if (status === STATUS_APPEAR && statusActive) { - this.updateStatus(onAppearEnd, { status: STATUS_NONE }, event); - } else if (status === STATUS_ENTER && statusActive) { - this.updateStatus(onEnterEnd, { status: STATUS_NONE }, event); - } else if (status === STATUS_LEAVE && statusActive) { - this.updateStatus(onLeaveEnd, { status: STATUS_NONE }, event); - } - }; + this.setState({ + statusStyle: typeof statusStyle === 'object' ? statusStyle : null, + newStatus: false, + ...additionalState, + }, resolve); // Trigger before next frame & after `componentDidMount` + }) + ); - addEventListener = ($ele) => { - if (!$ele) return; + updateActiveStatus = (styleFunc, currentStatus) => { + // `setState` use `postMessage` to trigger at the end of frame. + // Let's use requestAnimationFrame to update new state in next frame. + raf(() => { + const { status } = this.state; + if (status !== currentStatus) return; - $ele.addEventListener(transitionEndName, this.onTransitionEnd); - $ele.addEventListener(animationEndName, this.onAnimationEnd); - }; - removeEventListener = ($ele) => { - if (!$ele) return; + this.updateStatus(styleFunc, { statusActive: true }); + }); + }; - $ele.removeEventListener(transitionEndName, this.onTransitionEnd); - $ele.removeEventListener(animationEndName, this.onAnimationEnd); - }; + render() { + const { status, statusActive, statusStyle } = this.state; + const { children, motionName, visible } = this.props; - updateStatus = (styleFunc, additionalState, event) => ( - new Promise((resolve) => { - const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; + if (!children) return null; - if (statusStyle === false) return; + if (status === STATUS_NONE || !transitionSupport) { + return visible ? children({}) : null; + } - this.setState({ - statusStyle: typeof statusStyle === 'object' ? statusStyle : null, - newStatus: false, - ...additionalState, - }, resolve); // Trigger before next frame & after `componentDidMount` - }) - ); - - updateActiveStatus = (styleFunc, currentStatus) => { - // `setState` use `postMessage` to trigger at the end of frame. - // Let's use requestAnimationFrame to update new state in next frame. - raf(() => { - const { status } = this.state; - if (status !== currentStatus) return; - - this.updateStatus(styleFunc, { statusActive: true }); - }); - }; - - render() { - const { status, statusActive, statusStyle } = this.state; - const { children, motionName, visible } = this.props; - - if (!children) return null; - - if (status === STATUS_NONE || !supportTransition) { - return visible ? children({}) : null; + return children({ + className: classNames({ + [getTransitionName(motionName, status)]: status !== STATUS_NONE, + [getTransitionName(motionName, `${status}-active`)]: status !== STATUS_NONE && statusActive, + [motionName]: typeof motionName === 'string', + }), + style: statusStyle, + }); } - - return children({ - className: classNames({ - [getTransitionName(motionName, status)]: status !== STATUS_NONE, - [getTransitionName(motionName, `${status}-active`)]: status !== STATUS_NONE && statusActive, - [motionName]: typeof motionName === 'string', - }), - style: statusStyle, - }); } -} -polyfill(CSSMotion); + polyfill(CSSMotion); + + return CSSMotion; +} -export default CSSMotion; +export default genCSSMotion(supportTransition); diff --git a/tests/CSSMotion.spec.css b/tests/CSSMotion.spec.css new file mode 100644 index 0000000..ddbedda --- /dev/null +++ b/tests/CSSMotion.spec.css @@ -0,0 +1,71 @@ +.motion-box { + width: 100px; + height: 100px; +} + +/* Transition */ +.motion-box.transition { + transition: all .3s; +} + +.motion-box.transition-appear, +.motion-box.transition-enter { + opacity: 0; +} + +.motion-box.transition-appear-active, +.motion-box.transition-enter-active { + opacity: 1; +} + +/* Animation */ +.motion-box.animation { + animation-duration: .3s; + animation-fill-mode: both; +} + +.motion-box.animation-appear, +.motion-box.animation-enter { + animation-name: enter; + animation-fill-mode: both; + animation-play-state: paused; +} + +.motion-box.animation-appear-active, +.motion-box.animation-enter-active { + animation-name: enter; + animation-play-state: running; +} + +.motion-box.animation-leave { + animation-name: leave; + animation-fill-mode: both; + animation-play-state: paused; +} + +.motion-box.animation-leave-active { + animation-name: leave; + animation-play-state: running; +} + +@keyframes enter { + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes leave { + from { + transform: scale(1); + opacity: 1; + } + to { + transform: scale(0); + opacity: 0; + } +} diff --git a/tests/CSSMotion.spec.js b/tests/CSSMotion.spec.js new file mode 100644 index 0000000..0ecee6f --- /dev/null +++ b/tests/CSSMotion.spec.js @@ -0,0 +1,244 @@ +/* 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 classNames from 'classnames'; +import TestUtils from 'react-dom/test-utils'; +import expect from 'expect.js'; +import $ from 'jquery'; +import raf from 'raf'; +import CSSMotion, { genCSSMotion } from '../src/CSSMotion'; + +import './CSSMotion.spec.css'; + +describe('motion', () => { + let div; + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + }); + + afterEach(() => { + try { + ReactDOM.unmountComponentAtNode(div); + document.body.removeChild(div); + } catch (e) { + // Do nothing + } + }); + + describe('transition', () => { + const onCollapse = () => ({ height: 0 }); + const actionList = [ + { + name: 'appear', + props: { motionAppear: true, onAppearStart: onCollapse }, + visible: [true], + oriHeight: 0, + tgtHeight: 100, + oriOpacity: 0, + tgtOpacity: 1, + }, + { + name: 'enter', + props: { motionEnter: true, onEnterStart: onCollapse }, + visible: [false, true], + oriHeight: 0, + tgtHeight: 100, + oriOpacity: 0, + tgtOpacity: 1, + }, + { + name: 'leave', + props: { motionLeave: true, onLeaveActive: onCollapse }, + visible: [true, false], + oriHeight: 100, + tgtHeight: 0, + oriOpacity: 1, + tgtOpacity: 0, + }, + ]; + + actionList.forEach(({ name, props, visible, oriHeight, tgtHeight, oriOpacity, tgtOpacity }) => { + class Demo extends React.Component { + state = { + visible: visible[0], + }; + + render() { + return ( + + {({ style, className }) => ( +
+ )} + + ); + } + } + + it(name, (done) => { + ReactDOM.render(, div, function init() { + const nextVisible = visible[1]; + const instance = this; + + const doStartTest = () => { + const $ele = $(div).find('.motion-box'); + + const basicClassName = TestUtils.findRenderedDOMComponentWithClass(instance, 'motion-box').className; + expect(basicClassName).to.contain('transition'); + expect(basicClassName).to.contain(`transition-${name}`); + expect(basicClassName).to.not.contain(`transition-${name}-active`); + + raf(() => { + // After first dom render, merge the style into element + expect($ele.height()).to.be(oriHeight); + expect(Number($ele.css('opacity'))).to.be(oriOpacity); + + setTimeout(() => { + const activeClassName = TestUtils.findRenderedDOMComponentWithClass(instance, 'motion-box').className; + expect(activeClassName).to.contain('transition'); + expect(activeClassName).to.contain(`transition-${name}`); + expect(activeClassName).to.contain(`transition-${name}-active`); + + setTimeout(() => { + if (nextVisible === false) { + expect( + TestUtils.scryRenderedDOMComponentsWithClass(instance, 'motion-box').length + ).to.be(0); + } else { + const endClassName = TestUtils.findRenderedDOMComponentWithClass(instance, 'motion-box').className; + expect(endClassName).to.not.contain('transition'); + expect(endClassName).to.not.contain(`transition-${name}`); + expect(endClassName).to.not.contain(`transition-${name}-active`); + + expect($ele.height()).to.be(tgtHeight); + expect(Number($ele.css('opacity'))).to.be(tgtOpacity); + } + done(); + }, 300); + }, 100); + }); + } + + // Delay for the visible finished + if (nextVisible !== undefined) { + setTimeout(() => { + instance.setState({ visible: nextVisible }); + doStartTest(); + }, 100); + } else { + doStartTest(); + } + }); + // End of test case + }); + }); + }); + + describe('animation', () => { + const actionList = [ + { + name: 'appear', + props: { motionAppear: true }, + visible: [true], + }, + { + name: 'enter', + props: { motionEnter: true }, + visible: [false, true], + }, + { + name: 'leave', + props: { motionLeave: true }, + visible: [true, false], + }, + ]; + + actionList.forEach(({ name, visible, props }) => { + class Demo extends React.Component { + state = { + visible: visible[0], + }; + + render() { + return ( + + {({ style, className }) => ( +
+ )} + + ); + } + } + + it(name, (done) => { + ReactDOM.render(, div, function init() { + const nextVisible = visible[1]; + const instance = this; + + const doStartTest = () => { + const basicClassName = TestUtils.findRenderedDOMComponentWithClass(instance, 'motion-box').className; + expect(basicClassName).to.contain('animation'); + expect(basicClassName).to.contain(`animation-${name}`); + expect(basicClassName).to.not.contain(`animation-${name}-active`); + + setTimeout(() => { + const activeClassName = TestUtils.findRenderedDOMComponentWithClass(instance, 'motion-box').className; + expect(activeClassName).to.contain('animation'); + expect(activeClassName).to.contain(`animation-${name}`); + expect(activeClassName).to.contain(`animation-${name}-active`); + + // Simulation browser env not support animation. Not check end event + done(); + }); + }; + + // Delay for the visible finished + if (nextVisible !== undefined) { + setTimeout(() => { + instance.setState({ visible: nextVisible }); + doStartTest(); + }, 100); + } else { + doStartTest(); + } + }); + }); + // End of it + }); + }); + + it('no transition', (done) => { + const NoCSSTransition = genCSSMotion(false); + + ReactDOM.render( + + {({ style, className }) => ( +
+ )} + + , div, function init() { + const basicClassName = TestUtils.findRenderedDOMComponentWithClass(this, 'motion-box').className; + expect(basicClassName).to.not.contain('transition'); + expect(basicClassName).to.not.contain('transition-appear'); + expect(basicClassName).to.not.contain('transition-appear-active'); + + done(); + }); + }); +}); \ No newline at end of file diff --git a/tests/index.js b/tests/index.js index 6ec1dac..c7e5641 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,7 +1,9 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; +import 'core-js/es6/promise'; import './basic.spec'; import './single.spec'; import './single-animation.spec'; import './multiple.spec'; import './no.transition.spec'; +import './CSSMotion.spec'; \ No newline at end of file From c23eabd24ccce34f510d613bcc1637b203aa25e5 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 30 Jul 2018 16:15:25 +0800 Subject: [PATCH 09/10] rm promise --- package.json | 2 +- src/CSSMotion.jsx | 33 +++++++++++++++++++-------------- tests/CSSMotion.spec.js | 14 +++++++------- tests/index.js | 3 +-- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 1d066e3..8ec60ca 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "prepublish": "rc-tools run guard" }, "devDependencies": { - "babel-runtime": "6.x", "core-js": "^2.5.1", "expect.js": "0.3.x", "jquery": "^3.3.1", @@ -56,6 +55,7 @@ "lint" ], "dependencies": { + "babel-runtime": "6.x", "classnames": "^2.2.5", "component-classes": "^1.2.6", "fbjs": "^0.8.16", diff --git a/src/CSSMotion.jsx b/src/CSSMotion.jsx index 6f58fe8..5b88774 100644 --- a/src/CSSMotion.jsx +++ b/src/CSSMotion.jsx @@ -124,15 +124,15 @@ export function genCSSMotion(transitionSupport) { // Init status if (newStatus && status === STATUS_APPEAR && motionAppear) { - this.updateStatus(onAppearStart).then(() => { + this.updateStatus(onAppearStart, null, null, () => { this.updateActiveStatus(onAppearActive, STATUS_APPEAR); }); } else if (newStatus && status === STATUS_ENTER && motionEnter) { - this.updateStatus(onEnterStart).then(() => { + this.updateStatus(onEnterStart, null, null, () => { this.updateActiveStatus(onEnterActive, STATUS_ENTER); }); } else if (newStatus && status === STATUS_LEAVE && motionLeave) { - this.updateStatus(onLeaveStart).then(() => { + this.updateStatus(onLeaveStart, null, null, () => { this.updateActiveStatus(onLeaveActive, STATUS_LEAVE); }); } @@ -163,19 +163,24 @@ export function genCSSMotion(transitionSupport) { $ele.removeEventListener(animationEndName, this.onMotionEnd); }; - updateStatus = (styleFunc, additionalState, event) => ( - new Promise((resolve) => { - const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; + updateStatus = (styleFunc, additionalState, event, callback) => { + const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; - if (statusStyle === false) return; + if (statusStyle === false) return; - this.setState({ - statusStyle: typeof statusStyle === 'object' ? statusStyle : null, - newStatus: false, - ...additionalState, - }, resolve); // Trigger before next frame & after `componentDidMount` - }) - ); + let nextStep; + if (callback) { + nextStep = () => { + raf(callback); + }; + } + + this.setState({ + statusStyle: typeof statusStyle === 'object' ? statusStyle : null, + newStatus: false, + ...additionalState, + }, nextStep); // Trigger before next frame & after `componentDidMount` + }; updateActiveStatus = (styleFunc, currentStatus) => { // `setState` use `postMessage` to trigger at the end of frame. diff --git a/tests/CSSMotion.spec.js b/tests/CSSMotion.spec.js index 0ecee6f..fe08f1d 100644 --- a/tests/CSSMotion.spec.js +++ b/tests/CSSMotion.spec.js @@ -89,23 +89,23 @@ describe('motion', () => { const doStartTest = () => { const $ele = $(div).find('.motion-box'); - + const basicClassName = TestUtils.findRenderedDOMComponentWithClass(instance, 'motion-box').className; expect(basicClassName).to.contain('transition'); expect(basicClassName).to.contain(`transition-${name}`); expect(basicClassName).to.not.contain(`transition-${name}-active`); - + raf(() => { // After first dom render, merge the style into element expect($ele.height()).to.be(oriHeight); expect(Number($ele.css('opacity'))).to.be(oriOpacity); - + setTimeout(() => { const activeClassName = TestUtils.findRenderedDOMComponentWithClass(instance, 'motion-box').className; expect(activeClassName).to.contain('transition'); expect(activeClassName).to.contain(`transition-${name}`); expect(activeClassName).to.contain(`transition-${name}-active`); - + setTimeout(() => { if (nextVisible === false) { expect( @@ -124,7 +124,7 @@ describe('motion', () => { }, 300); }, 100); }); - } + }; // Delay for the visible finished if (nextVisible !== undefined) { @@ -200,7 +200,7 @@ describe('motion', () => { expect(activeClassName).to.contain('animation'); expect(activeClassName).to.contain(`animation-${name}`); expect(activeClassName).to.contain(`animation-${name}-active`); - + // Simulation browser env not support animation. Not check end event done(); }); @@ -241,4 +241,4 @@ describe('motion', () => { done(); }); }); -}); \ No newline at end of file +}); diff --git a/tests/index.js b/tests/index.js index c7e5641..98a2bf0 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,9 +1,8 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; -import 'core-js/es6/promise'; import './basic.spec'; import './single.spec'; import './single-animation.spec'; import './multiple.spec'; import './no.transition.spec'; -import './CSSMotion.spec'; \ No newline at end of file +import './CSSMotion.spec'; From 8db8a4da0701df74cc03f9a1f4fe59737a9eb91b Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 30 Jul 2018 16:33:07 +0800 Subject: [PATCH 10/10] add destroy check --- src/CSSMotion.jsx | 3 ++- tests/CSSMotion.spec.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CSSMotion.jsx b/src/CSSMotion.jsx index 5b88774..966fc27 100644 --- a/src/CSSMotion.jsx +++ b/src/CSSMotion.jsx @@ -100,6 +100,7 @@ export function genCSSMotion(transitionSupport) { componentWillUnmount() { this.removeEventListener(this.$ele); + this._destroyed = true; } onDomUpdate = () => { @@ -166,7 +167,7 @@ export function genCSSMotion(transitionSupport) { updateStatus = (styleFunc, additionalState, event, callback) => { const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null; - if (statusStyle === false) return; + if (statusStyle === false || this._destroyed) return; let nextStep; if (callback) { diff --git a/tests/CSSMotion.spec.js b/tests/CSSMotion.spec.js index fe08f1d..7a39709 100644 --- a/tests/CSSMotion.spec.js +++ b/tests/CSSMotion.spec.js @@ -203,7 +203,7 @@ describe('motion', () => { // Simulation browser env not support animation. Not check end event done(); - }); + }, 100); }; // Delay for the visible finished