diff --git a/README.md b/README.md
index ceb066c..a1354a0 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,57 @@ ReactDOM.render(
, mountNode);
```
-## API
+
+## CSSMotion
+
+### props
+
+| Property | Type | Default | Description|
+| -------- | ---- | ------- | ---------- |
+| 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 |
+| 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/CSSMotion.html b/examples/CSSMotion.html
new file mode 100644
index 0000000..e69de29
diff --git a/examples/CSSMotion.js b/examples/CSSMotion.js
new file mode 100644
index 0000000..d085786
--- /dev/null
+++ b/examples/CSSMotion.js
@@ -0,0 +1,87 @@
+/* eslint no-console:0, react/no-multi-comp:0 */
+
+import React from 'react';
+// import PropTypes from 'prop-types';
+import ReactDOM from 'react-dom';
+import { CSSMotion } from 'rc-animate';
+import classNames from 'classnames';
+import './CSSMotion.less';
+
+class Demo extends React.Component {
+ state = {
+ show: true,
+ };
+
+ onTrigger = () => {
+ this.setState({
+ show: !this.state.show,
+ });
+ };
+
+ onCollapse = () => ({ height: 0 });
+
+ skipColorTransition = (_, event) => {
+ // CSSMotion 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;
+
+ return (
+
+
+
+
+
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/CSSMotion.less b/examples/CSSMotion.less
new file mode 100644
index 0000000..0e9ed42
--- /dev/null
+++ b/examples/CSSMotion.less
@@ -0,0 +1,86 @@
+.grid {
+ display: table;
+
+ > div {
+ display: table-cell;
+ min-width: 350px;
+ }
+}
+
+.demo-block {
+ display: block;
+ height: 300px;
+ width: 300px;
+ background: red;
+ overflow: hidden;
+}
+
+.transition {
+ transition: background .3s, height 1.3s, opacity 1.3s;
+
+ &.transition-appear,
+ &.transition-enter {
+ opacity: 0;
+ }
+
+ &.transition-appear.transition-appear-active,
+ &.transition-enter.transition-enter-active {
+ opacity: 1;
+ }
+
+ &.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;
+ }
+}
+
+@keyframes leave {
+ from {
+ transform: scale(1);
+ opacity: 1;
+ }
+ to {
+ transform: scale(0);
+ opacity: 0;
+ }
+}
diff --git a/index.js b/index.js
index a1d1242..02ed4e1 100644
--- a/index.js
+++ b/index.js
@@ -1,3 +1,4 @@
// do not modify this file
import Animate from './src/Animate';
+export CSSMotion from './src/CSSMotion';
export default Animate;
diff --git a/src/CSSMotion.jsx b/src/CSSMotion.jsx
new file mode 100644
index 0000000..966fc27
--- /dev/null
+++ b/src/CSSMotion.jsx
@@ -0,0 +1,223 @@
+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,
+ animationEndName, transitionEndName,
+ supportTransition,
+} from './util';
+
+const STATUS_NONE = 'none';
+const STATUS_APPEAR = 'appear';
+const STATUS_ENTER = 'enter';
+const STATUS_LEAVE = 'leave';
+
+/**
+ * `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,
+ };
+
+ 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;
+ }
+
+ 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;
+ };
+
+ componentDidMount() {
+ this.onDomUpdate();
+ }
+
+ componentDidUpdate() {
+ this.onDomUpdate();
+ }
+
+ componentWillUnmount() {
+ this.removeEventListener(this.$ele);
+ this._destroyed = true;
+ }
+
+ 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, null, null, () => {
+ this.updateActiveStatus(onAppearActive, STATUS_APPEAR);
+ });
+ } else if (newStatus && status === STATUS_ENTER && motionEnter) {
+ this.updateStatus(onEnterStart, null, null, () => {
+ this.updateActiveStatus(onEnterActive, STATUS_ENTER);
+ });
+ } else if (newStatus && status === STATUS_LEAVE && motionLeave) {
+ this.updateStatus(onLeaveStart, null, null, () => {
+ this.updateActiveStatus(onLeaveActive, STATUS_LEAVE);
+ });
+ }
+ };
+
+ 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);
+ }
+ };
+
+ addEventListener = ($ele) => {
+ if (!$ele) return;
+
+ $ele.addEventListener(transitionEndName, this.onMotionEnd);
+ $ele.addEventListener(animationEndName, this.onMotionEnd);
+ };
+ removeEventListener = ($ele) => {
+ if (!$ele) return;
+
+ $ele.removeEventListener(transitionEndName, this.onMotionEnd);
+ $ele.removeEventListener(animationEndName, this.onMotionEnd);
+ };
+
+ updateStatus = (styleFunc, additionalState, event, callback) => {
+ const statusStyle = styleFunc ? styleFunc(ReactDOM.findDOMNode(this), event) : null;
+
+ if (statusStyle === false || this._destroyed) return;
+
+ 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.
+ // 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 || !transitionSupport) {
+ 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,
+ });
+ }
+ }
+
+ polyfill(CSSMotion);
+
+ return CSSMotion;
+}
+
+export default genCSSMotion(supportTransition);
diff --git a/src/util.js b/src/util.js
index 508cab1..1f018a1 100644
--- a/src/util.js
+++ b/src/util.js
@@ -160,4 +160,3 @@ export function getTransitionName(transitionName, transitionType) {
return `${transitionName}-${transitionType}`;
}
-
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..7a39709
--- /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();
+ }, 100);
+ };
+
+ // 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();
+ });
+ });
+});
diff --git a/tests/index.js b/tests/index.js
index 6ec1dac..98a2bf0 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -5,3 +5,4 @@ import './single.spec';
import './single-animation.spec';
import './multiple.spec';
import './no.transition.spec';
+import './CSSMotion.spec';