From ed373e6c0b87109e0e12ac1a018443ea085239bf Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Tue, 6 Dec 2016 23:10:36 +0000 Subject: [PATCH 01/22] Full rewrite. --- .eslintrc | 2 +- src/ReactCSSTransitionReplace.jsx | 264 ++++++++++++------------------ 2 files changed, 109 insertions(+), 157 deletions(-) diff --git a/.eslintrc b/.eslintrc index cd43ac6..8f23b45 100644 --- a/.eslintrc +++ b/.eslintrc @@ -124,7 +124,7 @@ */ "indent": [1, 2, {"SwitchCase": 1}], // http://eslint.org/docs/rules/indent "brace-style": [2, // http://eslint.org/docs/rules/brace-style - "stroustrup", { + "1tbs", { "allowSingleLine": true }], "quotes": [ diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index ea9ea59..666ea1a 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -30,8 +30,7 @@ function createTransitionTimeoutPropValidator(transitionType) { + 'https://fb.me/react-animation-transition-group-timeout for more ' + 'information.') // If the duration isn't a number - } - else if (typeof props[timeoutPropName] != 'number') { + } else if (typeof props[timeoutPropName] != 'number') { return new Error(timeoutPropName + ' must be a number (in milliseconds)') } } @@ -65,7 +64,6 @@ export default class ReactCSSTransitionReplace extends React.Component { transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'), transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave'), overflowHidden: PropTypes.bool, - changeWidth: PropTypes.bool, } static defaultProps = { @@ -74,23 +72,25 @@ export default class ReactCSSTransitionReplace extends React.Component { transitionLeave: true, overflowHidden: true, component: 'span', - changeWidth: false, + childComponent: 'span', } state = { + currentKey: '1', currentChild: this.props.children ? React.Children.only(this.props.children) : undefined, - currentChildKey: this.props.children ? '1' : '', - nextChild: undefined, - activeHeightTransition: false, - nextChildKey: '', + prevChildren: {}, height: null, - width: null, - isLeaving: false, + } + + componentWillMount() { + this.shouldEnterCurrent = false + this.keysToLeave = [] + this.transitioningKeys = {} } componentDidMount() { if (this.props.transitionAppear && this.state.currentChild) { - this.appearCurrent() + this.performAppear(this.state.currentKey) } } @@ -99,152 +99,115 @@ export default class ReactCSSTransitionReplace extends React.Component { } componentWillReceiveProps(nextProps) { - // Setting false indicates that the child has changed, but it is a removal so there is no next child. - const nextChild = nextProps.children ? React.Children.only(nextProps.children) : false - const currentChild = this.state.currentChild - - // Avoid silencing the transition when this.state.nextChild exists because it means that there’s - // already a transition ongoing that has to be replaced. - if (currentChild && nextChild && nextChild.key === currentChild.key && !this.state.nextChild) { - // Nothing changed, but we are re-rendering so update the currentChild. - return this.setState({ - currentChild: nextChild, - }) - } + const nextChild = nextProps.children ? React.Children.only(nextProps.children) : null + const {currentChild} = this.state - if (!currentChild && !nextChild && this.state.nextChild) { - // The container was empty before and the entering element is being removed again while - // transitioning in. Since a CSS transition can't be reversed cleanly midway the height - // is just forced back to zero immediately and the child removed. - return this.cancelTransition() + if ((!currentChild && !nextChild) || (currentChild && nextChild && currentChild.key === nextChild.key)) { + return } const {state} = this + const {currentKey} = state - // When transitionLeave is set to false, refs.curr does not exist when refs.next is being - // transitioned into existence. When another child is set for this component at the point - // where only refs.next exists, we want to use the width/height of refs.next instead of - // refs.curr. - const ref = this.refs.curr || this.refs.next - - // Set the next child to start the transition, and set the current height. - this.setState({ - nextChild, - activeHeightTransition: false, - nextChildKey: state.currentChildKey ? String(Number(state.currentChildKey) + 1) : '1', - height: state.currentChild ? ReactDOM.findDOMNode(ref).offsetHeight : 0, - width: state.currentChild && this.props.changeWidth ? ReactDOM.findDOMNode(ref).offsetWidth : null, - }) - - // Enqueue setting the next height to trigger the height transition. - this.enqueueHeightTransition(nextChild) - } + const nextState = { + currentKey: String(Number(currentKey) + 1), + currentChild: nextChild, + height: 0, + } - componentDidUpdate() { - if (!this.isTransitioning && !this.state.isLeaving) { - const {currentChild, nextChild} = this.state + if (nextChild) { + this.shouldEnterCurrent = true + } - if (currentChild && (nextChild || nextChild === false || nextChild === null) && this.props.transitionLeave) { - this.leaveCurrent() + if (currentChild) { + nextState.height = ReactDOM.findDOMNode(this.refs[currentKey]).offsetHeight + nextState.prevChildren = { + ...state.prevChildren, + [currentKey]: currentChild, } - if (nextChild) { - this.enterNext() + if (!this.transitioningKeys[currentKey]) { + this.keysToLeave.push(currentKey) } } + + this.setState(nextState) } - enqueueHeightTransition(nextChild, tickCount = 0) { - this.timeout = setTimeout(() => { - if (!nextChild) { - return this.setState({ - activeHeightTransition: true, - height: 0, - width: this.props.changeWidth ? 0 : null, - }) - } + componentDidUpdate() { + if (this.shouldEnterCurrent) { + this.shouldEnterCurrent = false + this.performEnter(this.state.currentKey) + } - const nextNode = ReactDOM.findDOMNode(this.refs.next) - if (nextNode) { - this.setState({ - activeHeightTransition: true, - height: nextNode.offsetHeight, - width: this.props.changeWidth ? nextNode.offsetWidth : null, - }) - } - else { - // The DOM hasn't rendered the entering element yet, so wait another tick. - // Getting stuck in a loop shouldn't happen, but it's better to be safe. - if (tickCount < 10) { - this.enqueueHeightTransition(nextChild, tickCount + 1) - } - } - }, TICK) + const keysToLeave = this.keysToLeave + this.keysToLeave = [] + keysToLeave.forEach(this.performLeave) } - appearCurrent() { - this.refs.curr.componentWillAppear(this._handleDoneAppearing) - this.isTransitioning = true + performAppear(key) { + this.transitioningKeys[key] = true + this.refs[key].componentWillAppear(this.handleDoneAppearing.bind(this, key)) } - _handleDoneAppearing = () => { - this.isTransitioning = false + handleDoneAppearing = (key) => { + delete this.transitioningKeys[key] + if (key !== this.state.currentKey) { + // This child was removed before it had fully appeared. Remove it. + this.performLeave(key) + } } - enterNext() { - this.refs.next.componentWillEnter(this._handleDoneEntering) - this.isTransitioning = true + performEnter(key) { + this.transitioningKeys[key] = true + this.refs[key].componentWillEnter(this.handleDoneEntering.bind(this, key)) + this.enqueueHeightTransition() } - _handleDoneEntering = () => { - const {state} = this - - this.isTransitioning = false - this.setState({ - currentChild: state.nextChild, - currentChildKey: state.nextChildKey, - activeHeightTransition: false, - nextChild: undefined, - nextChildKey: '', - height: null, - width: null, - }) + handleDoneEntering(key) { + delete this.transitioningKeys[key] + if (key === this.state.currentKey) { + // The current child has finished entering so the height transition is also cleared. + this.setState({height: null}) + } else { + // This child was removed before it had fully appeared. Remove it. + this.performLeave(key) + } } - leaveCurrent() { - this.refs.curr.componentWillLeave(this._handleDoneLeaving) - this.isTransitioning = true - this.setState({isLeaving: true}) + performLeave = (key) => { + this.transitioningKeys[key] = true + this.refs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key)) + if (!this.state.currentChild) { + // The enter transition dominates, but if there is no + // entering component the height is set to zero. + this.enqueueHeightTransition() + } } - // When the leave transition time-out expires the animation classes are removed, so the - // element must be removed from the DOM if the enter transition is still in progress. - _handleDoneLeaving = () => { - if (this.isTransitioning) { - const state = {currentChild: undefined, isLeaving: false} + handleDoneLeaving(key) { + delete this.transitioningKeys[key] - if (!this.state.nextChild) { - this.isTransitioning = false - state.height = null - state.width = null - } + const nextState = {prevChildren: {...this.state.prevChildren}} + delete nextState.prevChildren[key] - this.setState(state) + if (!this.state.currentChild) { + nextState.height = null } + + this.setState(nextState) } - cancelTransition() { - this.isTransitioning = false - clearTimeout(this.timeout) - return this.setState({ - nextChild: undefined, - activeHeightTransition: false, - nextChildKey: '', - height: null, - width: null, - }) + enqueueHeightTransition() { + const {state} = this + this.timeout = setTimeout(() => { + if (!state.currentChild) { + return this.setState({height: 0}) + } + this.setState({height: ReactDOM.findDOMNode(this.refs[state.currentKey]).offsetHeight}) + }, TICK) } - _wrapChild(child, moreProps) { + wrapChild(child, moreProps) { let transitionName = this.props.transitionName if (typeof transitionName == 'object' && transitionName !== null) { @@ -268,42 +231,22 @@ export default class ReactCSSTransitionReplace extends React.Component { } render() { - const {currentChild, currentChildKey, nextChild, nextChildKey, height, width, isLeaving, activeHeightTransition} = this.state + const {currentKey, currentChild, prevChildren, height} = this.state const childrenToRender = [] const { - overflowHidden, transitionName, changeWidth, component, + overflowHidden, transitionName, component, childComponent, transitionAppear, transitionEnter, transitionLeave, transitionAppearTimeout, transitionEnterTimeout, transitionLeaveTimeout, ...containerProps } = this.props - if (currentChild && !nextChild && !transitionLeave || currentChild && transitionLeave) { - childrenToRender.push( - React.createElement( - 'span', - {key: currentChildKey}, - this._wrapChild( - typeof currentChild.type == 'string' ? currentChild : React.cloneElement(currentChild, {isLeaving}), - {ref: 'curr'}) - ) - ) - } - - if (height !== null) { const heightClassName = (typeof transitionName == 'object' && transitionName !== null) ? transitionName.height || '' : `${transitionName}-height` - // Similarly to ReactCSSTransitionGroup, adding `-height-active` suffix to the - // container when we are transitioning height. - const activeHeightClassName = (nextChild && activeHeightTransition && heightClassName) - ? `${heightClassName}-active` - : '' - - containerProps.className = `${containerProps.className || ''} ${heightClassName} ${activeHeightClassName}` - + containerProps.className = `${containerProps.className || ''} ${heightClassName}` containerProps.style = { ...containerProps.style, position: 'relative', @@ -314,16 +257,13 @@ export default class ReactCSSTransitionReplace extends React.Component { if (overflowHidden) { containerProps.style.overflow = 'hidden' } - - if (changeWidth) { - containerProps.style.width = width - } } - if (nextChild) { + Object.keys(prevChildren).forEach(key => { childrenToRender.push( - React.createElement('span', + React.createElement(childComponent, { + key, style: { position: 'absolute', top: 0, @@ -331,9 +271,21 @@ export default class ReactCSSTransitionReplace extends React.Component { right: 0, bottom: 0, }, - key: nextChildKey, }, - this._wrapChild(nextChild, {ref: 'next'}) + this.wrapChild( + typeof prevChildren[key].type == 'string' + ? prevChildren[key] + : React.cloneElement(prevChildren[key], {isLeaving: true}), + {ref: key}) + ) + ) + }) + + if (currentChild) { + childrenToRender.push( + React.createElement(childComponent, + {key: currentKey}, + this.wrapChild(currentChild, {ref: currentKey}) ) ) } From 0249594d8fddb4b01df3b1179ba05c8d7da8fbc3 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Mon, 27 Feb 2017 23:22:44 +0000 Subject: [PATCH 02/22] Add third image to the carousel animation. --- demo/assets/transitions.css | 6 +++--- demo/components/ContentSwapper.jsx | 7 ++++--- demo/components/Demo.jsx | 7 ++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/demo/assets/transitions.css b/demo/assets/transitions.css index 2381a11..0fbdda9 100644 --- a/demo/assets/transitions.css +++ b/demo/assets/transitions.css @@ -49,7 +49,7 @@ /* Carousel-like transition */ .carousel-swap-leave { - transition: transform .3s ease-in-out; + transition: transform 2s ease-in-out; transform: translate(0, 0); } .carousel-swap-leave-active { @@ -57,7 +57,7 @@ } .carousel-swap-enter { - transition: transform .3s ease-in-out; + transition: transform 2s ease-in-out; transform: translate(100%, 0); } .carousel-swap-enter-active { @@ -65,7 +65,7 @@ } .carousel-swap-height { - transition: height .3s ease-in-out; + transition: height 2s ease-in-out; } diff --git a/demo/components/ContentSwapper.jsx b/demo/components/ContentSwapper.jsx index a71a2be..0324eb4 100644 --- a/demo/components/ContentSwapper.jsx +++ b/demo/components/ContentSwapper.jsx @@ -4,10 +4,11 @@ import ReactCSSTransitionReplace from '../../src/ReactCSSTransitionReplace.jsx' class ContentSwapper extends React.Component { - state = {swapped: false} + state = {index: 0} handleClick = () => { - this.setState({swapped: !this.state.swapped}) + const index = this.state.index + 1 + this.setState({index: index >= React.Children.count(this.props.children) ? 0 : index}) } render() { @@ -18,7 +19,7 @@ class ContentSwapper extends React.Component { return ( - {this.state.swapped ? content[1] : content[0]} + {content[this.state.index]} ) } diff --git a/demo/components/Demo.jsx b/demo/components/Demo.jsx index 050ab15..d0a179c 100644 --- a/demo/components/Demo.jsx +++ b/demo/components/Demo.jsx @@ -71,10 +71,11 @@ class Demo extends React.Component { {''}.

- - - + + +

Add/Remove Content

From 4ee94c7c7a0bfc37fb0df97e75c2e1fbc3afbb9d Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Mon, 27 Feb 2017 23:23:06 +0000 Subject: [PATCH 03/22] Not opening the demo in a browser automatically. --- gulpfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/gulpfile.js b/gulpfile.js index 35acdfc..e92a351 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -54,6 +54,7 @@ gulp.task('demo:bundleAndWatch', function() { gulp.task('demo', ['demo:bundleAndWatch'], function() { browserSync.init({ browser: ['google chrome'], + open: false, notify: false, server: { baseDir: "demo/assets" From bb60952f4275b82168548c92f0c68683a4549ba1 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Wed, 8 Mar 2017 21:49:19 +0000 Subject: [PATCH 04/22] Update docs to include childComponent. --- README.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c890d37..26b3b31 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Using `react-css-transition-replace` provides two distinct benefits: Animations are fully configurable with CSS, including having the entering component wait to enter until the leaving component's animation completes. Following suit with the -[React.js API](https://facebook.github.io/react/docs/animation.html#getting-started) the one caveat is +[React.js API](https://facebook.github.io/react/docs/animation.html) the one caveat is that the transition duration must be specified in JavaScript as well as CSS. [Live Examples](http://marnusw.github.io/react-css-transition-replace) | @@ -56,6 +56,10 @@ It is also possible to remove the child component (i.e. leave `ReactCSSTransitio which will animate the `height` going to zero along with the `leave` transition. Similarly, a single child can be added to an empty `ReactCSSTransitionReplace`, triggering the inverse animation. +By default a `span` is rendered as a wrapper of the child components. Each child is also wrapped in a `span` +used in the positioning of the actual rendered child. These can be overridden with the `component` and +`childComponent` props respectively. + ### Cross-fading two components The `ReactCSSTransitionReplace` component is used exactly like its `ReactCSSTransitionGroup` counterpart: diff --git a/package.json b/package.json index 003401f..4c3b11f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-css-transition-replace", - "version": "2.2.1", + "version": "3.0.0-beta.1", "description": "A React component to animate replacing one element with another.", "main": "lib/ReactCSSTransitionReplace.js", "repository": { From ba38fe063214dc1b979f51689cacbb3a658d4fd9 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sat, 29 Apr 2017 20:18:16 +0100 Subject: [PATCH 05/22] Define prop types using the util in react-transition-group. --- src/ReactCSSTransitionReplace.jsx | 45 +++++-------------------------- 1 file changed, 6 insertions(+), 39 deletions(-) diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index 666ea1a..06da7cf 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -9,60 +9,27 @@ import ReactDOM from 'react-dom' import PropTypes from 'prop-types' import ReactCSSTransitionGroupChild from 'react-transition-group/CSSTransitionGroupChild' +import { nameShape, transitionTimeout } from 'react-transition-group/utils/PropTypes' + const reactCSSTransitionGroupChild = React.createFactory(ReactCSSTransitionGroupChild) const TICK = 17 -function createTransitionTimeoutPropValidator(transitionType) { - const timeoutPropName = 'transition' + transitionType + 'Timeout' - const enabledPropName = 'transition' + transitionType - - return function(props) { - // If the transition is enabled - if (props[enabledPropName]) { - // If no timeout duration is provided - if (!props[timeoutPropName]) { - return new Error(timeoutPropName + ' wasn\'t supplied to ReactCSSTransitionReplace: ' - + 'this can cause unreliable animations and won\'t be supported in ' - + 'a future version of React. See ' - + 'https://fb.me/react-animation-transition-group-timeout for more ' + 'information.') - - // If the duration isn't a number - } else if (typeof props[timeoutPropName] != 'number') { - return new Error(timeoutPropName + ' must be a number (in milliseconds)') - } - } - } -} - export default class ReactCSSTransitionReplace extends React.Component { static displayName = 'ReactCSSTransitionReplace' static propTypes = { - transitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({ - enter: PropTypes.string, - leave: PropTypes.string, - active: PropTypes.string, - height: PropTypes.string, - }), PropTypes.shape({ - enter: PropTypes.string, - enterActive: PropTypes.string, - leave: PropTypes.string, - leaveActive: PropTypes.string, - appear: PropTypes.string, - appearActive: PropTypes.string, - height: PropTypes.string, - })]).isRequired, + transitionName: nameShape.isRequired, transitionAppear: PropTypes.bool, transitionEnter: PropTypes.bool, transitionLeave: PropTypes.bool, - transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'), - transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'), - transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave'), + transitionAppearTimeout: transitionTimeout('Appear'), + transitionEnterTimeout: transitionTimeout('Enter'), + transitionLeaveTimeout: transitionTimeout('Leave'), overflowHidden: PropTypes.bool, } From 64ae270b79489737317d3c5d2e3c93457ed8b4a3 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 00:00:22 +0100 Subject: [PATCH 06/22] Maintain component callback refs by not overwriting with string refs. --- package.json | 4 +- src/ReactCSSTransitionReplace.jsx | 73 ++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 4c3b11f..59f1873 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "prepublish": "npm run build" }, "dependencies": { + "chain-function": "^1.0.0", "prop-types": "^15.5.6", - "react-transition-group": "^1.1.1" + "react-transition-group": "^1.1.1", + "warning": "^3.0.0" }, "peerDependencies": { "react": "^15.0.0", diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index 06da7cf..a59ed44 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -7,6 +7,8 @@ import React from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' +import chain from 'chain-function' +import warning from 'warning' import ReactCSSTransitionGroupChild from 'react-transition-group/CSSTransitionGroupChild' import { nameShape, transitionTimeout } from 'react-transition-group/utils/PropTypes' @@ -42,11 +44,17 @@ export default class ReactCSSTransitionReplace extends React.Component { childComponent: 'span', } - state = { - currentKey: '1', - currentChild: this.props.children ? React.Children.only(this.props.children) : undefined, - prevChildren: {}, - height: null, + constructor(props, context) { + super(props, context) + + this.childRefs = Object.create(null) + + this.state = { + currentKey: '1', + currentChild: this.props.children ? React.Children.only(this.props.children) : undefined, + prevChildren: {}, + height: null, + } } componentWillMount() { @@ -73,8 +81,7 @@ export default class ReactCSSTransitionReplace extends React.Component { return } - const {state} = this - const {currentKey} = state + const {currentKey, prevChildren} = this.state const nextState = { currentKey: String(Number(currentKey) + 1), @@ -87,9 +94,9 @@ export default class ReactCSSTransitionReplace extends React.Component { } if (currentChild) { - nextState.height = ReactDOM.findDOMNode(this.refs[currentKey]).offsetHeight + nextState.height = ReactDOM.findDOMNode(this.childRefs[currentKey]).offsetHeight nextState.prevChildren = { - ...state.prevChildren, + ...prevChildren, [currentKey]: currentChild, } if (!this.transitioningKeys[currentKey]) { @@ -113,7 +120,7 @@ export default class ReactCSSTransitionReplace extends React.Component { performAppear(key) { this.transitioningKeys[key] = true - this.refs[key].componentWillAppear(this.handleDoneAppearing.bind(this, key)) + this.childRefs[key].componentWillAppear(this.handleDoneAppearing.bind(this, key)) } handleDoneAppearing = (key) => { @@ -126,7 +133,7 @@ export default class ReactCSSTransitionReplace extends React.Component { performEnter(key) { this.transitioningKeys[key] = true - this.refs[key].componentWillEnter(this.handleDoneEntering.bind(this, key)) + this.childRefs[key].componentWillEnter(this.handleDoneEntering.bind(this, key)) this.enqueueHeightTransition() } @@ -143,7 +150,7 @@ export default class ReactCSSTransitionReplace extends React.Component { performLeave = (key) => { this.transitioningKeys[key] = true - this.refs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key)) + this.childRefs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key)) if (!this.state.currentChild) { // The enter transition dominates, but if there is no // entering component the height is set to zero. @@ -156,6 +163,7 @@ export default class ReactCSSTransitionReplace extends React.Component { const nextState = {prevChildren: {...this.state.prevChildren}} delete nextState.prevChildren[key] + delete this.childRefs[key] if (!this.state.currentChild) { nextState.height = null @@ -170,14 +178,14 @@ export default class ReactCSSTransitionReplace extends React.Component { if (!state.currentChild) { return this.setState({height: 0}) } - this.setState({height: ReactDOM.findDOMNode(this.refs[state.currentKey]).offsetHeight}) + this.setState({height: ReactDOM.findDOMNode(this.childRefs[state.currentKey]).offsetHeight}) }, TICK) } wrapChild(child, moreProps) { let transitionName = this.props.transitionName - if (typeof transitionName == 'object' && transitionName !== null) { + if (typeof transitionName === 'object' && transitionName !== null) { transitionName = {...transitionName} delete transitionName.height } @@ -209,7 +217,7 @@ export default class ReactCSSTransitionReplace extends React.Component { } = this.props if (height !== null) { - const heightClassName = (typeof transitionName == 'object' && transitionName !== null) + const heightClassName = (typeof transitionName === 'object' && transitionName !== null) ? transitionName.height || '' : `${transitionName}-height` @@ -227,6 +235,12 @@ export default class ReactCSSTransitionReplace extends React.Component { } Object.keys(prevChildren).forEach(key => { + const child = prevChildren[key] + const isCallbackRef = typeof child.ref !== 'string' + warning(isCallbackRef, + 'string refs are not supported on children of ReactCSSTransitionReplace and will be ignored. ' + + 'Please use a callback ref instead: https://facebook.github.io/react/docs/refs-and-the-dom.html#the-ref-callback-attribute') + childrenToRender.push( React.createElement(childComponent, { @@ -240,19 +254,38 @@ export default class ReactCSSTransitionReplace extends React.Component { }, }, this.wrapChild( - typeof prevChildren[key].type == 'string' - ? prevChildren[key] - : React.cloneElement(prevChildren[key], {isLeaving: true}), - {ref: key}) + typeof child.type !== 'string' + ? React.cloneElement(child, {isLeaving: true}) + : child, + { + ref: chain( + isCallbackRef ? child.ref : null, + (r) => {this.childRefs[key] = r} + ), + } + ) ) ) }) if (currentChild) { + const isCallbackRef = typeof currentChild.ref !== 'string' + warning(isCallbackRef, + 'string refs are not supported on children of ReactCSSTransitionReplace and will be ignored. ' + + 'Please use a callback ref instead: https://facebook.github.io/react/docs/refs-and-the-dom.html#the-ref-callback-attribute') + childrenToRender.push( React.createElement(childComponent, {key: currentKey}, - this.wrapChild(currentChild, {ref: currentKey}) + this.wrapChild( + currentChild, + { + ref: chain( + isCallbackRef ? currentChild.ref : null, + (r) => {this.childRefs[currentKey] = r} + ), + } + ) ) ) } From f8b0a294d4359795dce0f4e01116bee56e21cfd1 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 00:01:40 +0100 Subject: [PATCH 07/22] Only add the isLeaving prop to children if notifyLeaving is true. --- src/ReactCSSTransitionReplace.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index a59ed44..ecc52dc 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -33,6 +33,7 @@ export default class ReactCSSTransitionReplace extends React.Component { transitionEnterTimeout: transitionTimeout('Enter'), transitionLeaveTimeout: transitionTimeout('Leave'), overflowHidden: PropTypes.bool, + notifyLeaving: PropTypes.bool, } static defaultProps = { @@ -40,6 +41,7 @@ export default class ReactCSSTransitionReplace extends React.Component { transitionEnter: true, transitionLeave: true, overflowHidden: true, + notifyLeaving: false, component: 'span', childComponent: 'span', } @@ -210,7 +212,7 @@ export default class ReactCSSTransitionReplace extends React.Component { const childrenToRender = [] const { - overflowHidden, transitionName, component, childComponent, + overflowHidden, transitionName, component, childComponent, notifyLeaving, transitionAppear, transitionEnter, transitionLeave, transitionAppearTimeout, transitionEnterTimeout, transitionLeaveTimeout, ...containerProps @@ -254,7 +256,7 @@ export default class ReactCSSTransitionReplace extends React.Component { }, }, this.wrapChild( - typeof child.type !== 'string' + notifyLeaving && typeof child.type !== 'string' ? React.cloneElement(child, {isLeaving: true}) : child, { From 6b3938eca3aff2b64cfdc721895a05afeef7429e Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 00:04:52 +0100 Subject: [PATCH 08/22] Support multiple children provided all but one child renders null. --- src/ReactCSSTransitionReplace.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index ecc52dc..cd98bbc 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -19,6 +19,16 @@ const reactCSSTransitionGroupChild = React.createFactory(ReactCSSTransitionGroup const TICK = 17 +// Filter out nulls before looking for an only child +function getChildMapping(children) { + if (!Array.isArray(children)) { + return children + } + const childArray = React.Children.toArray(children).filter(c => c) + return childArray.length === 1 ? childArray[0] : React.Children.only(childArray) +} + + export default class ReactCSSTransitionReplace extends React.Component { static displayName = 'ReactCSSTransitionReplace' @@ -53,7 +63,7 @@ export default class ReactCSSTransitionReplace extends React.Component { this.state = { currentKey: '1', - currentChild: this.props.children ? React.Children.only(this.props.children) : undefined, + currentChild: getChildMapping(this.props.children), prevChildren: {}, height: null, } @@ -76,7 +86,7 @@ export default class ReactCSSTransitionReplace extends React.Component { } componentWillReceiveProps(nextProps) { - const nextChild = nextProps.children ? React.Children.only(nextProps.children) : null + const nextChild = getChildMapping(nextProps.children) const {currentChild} = this.state if ((!currentChild && !nextChild) || (currentChild && nextChild && currentChild.key === nextChild.key)) { From 0000db149ab7c4ae46c559ad8dfb8ec65a4300a2 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 09:28:28 +0100 Subject: [PATCH 09/22] Use requestAnimationFrame to queue the height transition rather than a timeout. --- package.json | 1 + src/ReactCSSTransitionReplace.jsx | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 59f1873..fd88363 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "chain-function": "^1.0.0", + "dom-helpers": "^3.2.0", "prop-types": "^15.5.6", "react-transition-group": "^1.1.1", "warning": "^3.0.0" diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index cd98bbc..bea211e 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -7,17 +7,15 @@ import React from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' +import raf from 'dom-helpers/util/requestAnimationFrame' import chain from 'chain-function' import warning from 'warning' import ReactCSSTransitionGroupChild from 'react-transition-group/CSSTransitionGroupChild' import { nameShape, transitionTimeout } from 'react-transition-group/utils/PropTypes' - const reactCSSTransitionGroupChild = React.createFactory(ReactCSSTransitionGroupChild) -const TICK = 17 - // Filter out nulls before looking for an only child function getChildMapping(children) { @@ -82,7 +80,7 @@ export default class ReactCSSTransitionReplace extends React.Component { } componentWillUnmount() { - clearTimeout(this.timeout) + this.unmounted = true } componentWillReceiveProps(nextProps) { @@ -185,13 +183,21 @@ export default class ReactCSSTransitionReplace extends React.Component { } enqueueHeightTransition() { - const {state} = this - this.timeout = setTimeout(() => { - if (!state.currentChild) { - return this.setState({height: 0}) - } - this.setState({height: ReactDOM.findDOMNode(this.childRefs[state.currentKey]).offsetHeight}) - }, TICK) + if (!this.rafHandle) { + this.rafHandle = raf(this.performHeightTransition) + } + } + + performHeightTransition = () => { + if (!this.unmounted) { + const {state} = this + this.setState({ + height: state.currentChild + ? ReactDOM.findDOMNode(this.childRefs[state.currentKey]).offsetHeight + : 0, + }) + } + this.rafHandle = null } wrapChild(child, moreProps) { From 058551a9675c094599910aa208a9e3fed75021cb Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 11:54:36 +0100 Subject: [PATCH 10/22] The transition nameShape prop type includes the height and heightActive props. --- src/ReactCSSTransitionReplace.jsx | 3 ++- src/utils/PropTypes.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/utils/PropTypes.js diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index bea211e..9ce2eed 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -12,7 +12,8 @@ import chain from 'chain-function' import warning from 'warning' import ReactCSSTransitionGroupChild from 'react-transition-group/CSSTransitionGroupChild' -import { nameShape, transitionTimeout } from 'react-transition-group/utils/PropTypes' +import { transitionTimeout } from 'react-transition-group/utils/PropTypes' +import { nameShape } from './utils/PropTypes' const reactCSSTransitionGroupChild = React.createFactory(ReactCSSTransitionGroupChild) diff --git a/src/utils/PropTypes.js b/src/utils/PropTypes.js new file mode 100644 index 0000000..5d368fd --- /dev/null +++ b/src/utils/PropTypes.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types' + +export const nameShape = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + enter: PropTypes.string, + leave: PropTypes.string, + active: PropTypes.string, + height: PropTypes.string, + }), + PropTypes.shape({ + enter: PropTypes.string, + enterActive: PropTypes.string, + leave: PropTypes.string, + leaveActive: PropTypes.string, + appear: PropTypes.string, + appearActive: PropTypes.string, + height: PropTypes.string, + heightActive: PropTypes.string, + }), +]) From a69c6fe18380bcf832933ae45b10561e1c5166a6 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 11:55:27 +0100 Subject: [PATCH 11/22] Clear the selection after transitions to avoid the child being selected due to multiple clicks. --- src/ReactCSSTransitionReplace.jsx | 9 ++++++++- src/utils/dom-helpers.js | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/utils/dom-helpers.js diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index 9ce2eed..e45a32a 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -7,10 +7,12 @@ import React from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' -import raf from 'dom-helpers/util/requestAnimationFrame' import chain from 'chain-function' import warning from 'warning' +import raf from 'dom-helpers/util/requestAnimationFrame' +import { clearSelection } from './utils/dom-helpers' + import ReactCSSTransitionGroupChild from 'react-transition-group/CSSTransitionGroupChild' import { transitionTimeout } from 'react-transition-group/utils/PropTypes' import { nameShape } from './utils/PropTypes' @@ -127,6 +129,11 @@ export default class ReactCSSTransitionReplace extends React.Component { const keysToLeave = this.keysToLeave this.keysToLeave = [] keysToLeave.forEach(this.performLeave) + + // When the enter completes and the component switches to relative positioning the + // child often gets selected after multiple clicks (at least in Chrome). To compensate + // the current selection is cleared whenever the component updates. + clearSelection() } performAppear(key) { diff --git a/src/utils/dom-helpers.js b/src/utils/dom-helpers.js new file mode 100644 index 0000000..fa8cb8c --- /dev/null +++ b/src/utils/dom-helpers.js @@ -0,0 +1,9 @@ +/* global document, window */ + +export function clearSelection() { + if (document.selection) { + document.selection.empty() + } else if (window.getSelection) { + window.getSelection().removeAllRanges() + } +} From 6c3be56a10fed03550d4cd734073732c13e8e360 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 12:09:45 +0100 Subject: [PATCH 12/22] Entering child renders with absolute positioning since switching from relative to abs on leave cancels active animations. --- demo/assets/styles.css | 2 +- src/ReactCSSTransitionReplace.jsx | 88 +++++++++++++++---------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/demo/assets/styles.css b/demo/assets/styles.css index eb85449..e8d9938 100644 --- a/demo/assets/styles.css +++ b/demo/assets/styles.css @@ -15,6 +15,6 @@ h3 { margin-top: 50px; } -.examples p { +.examples > p { margin-bottom: 20px; } diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index e45a32a..c0669f6 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -231,6 +231,18 @@ export default class ReactCSSTransitionReplace extends React.Component { }, child) } + storeChildRef(child, key) { + const isCallbackRef = typeof child.ref !== 'string' + warning(isCallbackRef, + 'string refs are not supported on children of ReactCSSTransitionReplace and will be ignored. ' + + 'Please use a callback ref instead: https://facebook.github.io/react/docs/refs-and-the-dom.html#the-ref-callback-attribute') + + return chain( + isCallbackRef ? child.ref : null, + (r) => {this.childRefs[key] = r} + ) + } + render() { const {currentKey, currentChild, prevChildren, height} = this.state const childrenToRender = [] @@ -242,75 +254,63 @@ export default class ReactCSSTransitionReplace extends React.Component { ...containerProps } = this.props - if (height !== null) { - const heightClassName = (typeof transitionName === 'object' && transitionName !== null) - ? transitionName.height || '' - : `${transitionName}-height` + containerProps.style = { + ...containerProps.style, + } - containerProps.className = `${containerProps.className || ''} ${heightClassName}` - containerProps.style = { - ...containerProps.style, - position: 'relative', - display: 'block', - height, - } + if (Object.keys(this.transitioningKeys).length) { + containerProps.style.position = 'relative' + containerProps.style.display = 'block' if (overflowHidden) { containerProps.style.overflow = 'hidden' } } + if (height !== null) { + const heightClassName = typeof transitionName === 'string' + ? `${transitionName}-height` + : (transitionName && transitionName.height) || '' + + containerProps.className = `${containerProps.className || ''} ${heightClassName}` + containerProps.style.height = height + } + + const positionAbsolute = { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + } + Object.keys(prevChildren).forEach(key => { const child = prevChildren[key] - const isCallbackRef = typeof child.ref !== 'string' - warning(isCallbackRef, - 'string refs are not supported on children of ReactCSSTransitionReplace and will be ignored. ' + - 'Please use a callback ref instead: https://facebook.github.io/react/docs/refs-and-the-dom.html#the-ref-callback-attribute') - childrenToRender.push( React.createElement(childComponent, - { - key, - style: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - }, - }, + {key, style: positionAbsolute}, this.wrapChild( notifyLeaving && typeof child.type !== 'string' ? React.cloneElement(child, {isLeaving: true}) : child, - { - ref: chain( - isCallbackRef ? child.ref : null, - (r) => {this.childRefs[key] = r} - ), - } + {ref: this.storeChildRef(child, key)} ) ) ) }) if (currentChild) { - const isCallbackRef = typeof currentChild.ref !== 'string' - warning(isCallbackRef, - 'string refs are not supported on children of ReactCSSTransitionReplace and will be ignored. ' + - 'Please use a callback ref instead: https://facebook.github.io/react/docs/refs-and-the-dom.html#the-ref-callback-attribute') - childrenToRender.push( React.createElement(childComponent, - {key: currentKey}, + { + key: currentKey, + // Positioning must always be specified to keep the + // current child on top of the leaving children + style: this.transitioningKeys[currentKey] ? positionAbsolute : {position: 'relative'}, + }, this.wrapChild( currentChild, - { - ref: chain( - isCallbackRef ? currentChild.ref : null, - (r) => {this.childRefs[currentKey] = r} - ), - } + {ref: this.storeChildRef(currentChild, currentKey)} ) ) ) From 9ce33e97141a058b299eeb8ae1386dacc949276d Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 22:13:07 +0100 Subject: [PATCH 13/22] Fix enter animation of absolutely positioned elements in Chrome not working by skipping one animation frame. --- src/ReactCSSTransitionReplace.jsx | 6 ++--- src/ReactCSSTransitionReplaceChild.js | 35 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/ReactCSSTransitionReplaceChild.js diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index c0669f6..6cb758d 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -13,11 +13,11 @@ import warning from 'warning' import raf from 'dom-helpers/util/requestAnimationFrame' import { clearSelection } from './utils/dom-helpers' -import ReactCSSTransitionGroupChild from 'react-transition-group/CSSTransitionGroupChild' +import ReactCSSTransitionReplaceChild from './ReactCSSTransitionReplaceChild' import { transitionTimeout } from 'react-transition-group/utils/PropTypes' import { nameShape } from './utils/PropTypes' -const reactCSSTransitionGroupChild = React.createFactory(ReactCSSTransitionGroupChild) +const reactCSSTransitionReplaceChild = React.createFactory(ReactCSSTransitionReplaceChild) // Filter out nulls before looking for an only child @@ -219,7 +219,7 @@ export default class ReactCSSTransitionReplace extends React.Component { // We need to provide this childFactory so that // ReactCSSTransitionReplaceChild can receive updates to name, // enter, and leave while it is leaving. - return reactCSSTransitionGroupChild({ + return reactCSSTransitionReplaceChild({ name: transitionName, appear: this.props.transitionAppear, enter: this.props.transitionEnter, diff --git a/src/ReactCSSTransitionReplaceChild.js b/src/ReactCSSTransitionReplaceChild.js new file mode 100644 index 0000000..d69a355 --- /dev/null +++ b/src/ReactCSSTransitionReplaceChild.js @@ -0,0 +1,35 @@ +/** + * Uses react-transition-group/CSSTransitionGroupChild with the exception that + * the first animation frame is skipped when starting new transitions since + * entering absolutely positioned elements in Chrome does not animate otherwise. + */ +import ReactCSSTransitionReplaceChild from 'react-transition-group/CSSTransitionGroupChild' +import raf from 'dom-helpers/util/requestAnimationFrame' + + +ReactCSSTransitionReplaceChild.prototype.queueClassAndNode = function queueClassAndNode(className, node) { + const _this2 = this + + this.classNameAndNodeQueue.push({ + className: className, + node: node, + }) + + if (!this.rafHandle) { + this.rafHandle = raf(function() { + return _this2.flushClassNameAndNodeQueueOnNextFrame() + }) + } +} + +// In Chrome the absolutely positioned children would not animate on enter +// if the immediate animation frame is used so this skips to the next one. +ReactCSSTransitionReplaceChild.prototype.flushClassNameAndNodeQueueOnNextFrame = function flushClassNameAndNodeQueueOnNextFrame() { + const _this2 = this + + this.rafHandle = raf(function() { + return _this2.flushClassNameAndNodeQueue() + }) +} + +export default ReactCSSTransitionReplaceChild From 7b7a2b068d73e80f0fa7bc0f52a66655df683e20 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 22:14:02 +0100 Subject: [PATCH 14/22] Fix Edge glitch when render starts by always applying container position, display and overflow styles. --- src/ReactCSSTransitionReplace.jsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index 6cb758d..0a08fdd 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -53,7 +53,7 @@ export default class ReactCSSTransitionReplace extends React.Component { transitionLeave: true, overflowHidden: true, notifyLeaving: false, - component: 'span', + component: 'div', childComponent: 'span', } @@ -254,17 +254,15 @@ export default class ReactCSSTransitionReplace extends React.Component { ...containerProps } = this.props + // In edge there is a glitch as the container switches from not positioned + // to a positioned element at the start of a transition which is solved + // by applying the position and overflow style rules at all times. containerProps.style = { ...containerProps.style, + position: 'relative', } - - if (Object.keys(this.transitioningKeys).length) { - containerProps.style.position = 'relative' - containerProps.style.display = 'block' - - if (overflowHidden) { - containerProps.style.overflow = 'hidden' - } + if (overflowHidden) { + containerProps.style.overflow = 'hidden' } if (height !== null) { From 5e924c7c946c10dfd1d0321769c8e397ca2560af Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 22:14:24 +0100 Subject: [PATCH 15/22] Remove demo animated paragraph margins. --- demo/assets/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/demo/assets/styles.css b/demo/assets/styles.css index e8d9938..b694999 100644 --- a/demo/assets/styles.css +++ b/demo/assets/styles.css @@ -15,6 +15,10 @@ h3 { margin-top: 50px; } +p { + margin-bottom: 0; +} + .examples > p { margin-bottom: 20px; } From cd8246065ef25115f9a47fb3dc928752c40b40e9 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 22:17:00 +0100 Subject: [PATCH 16/22] 3.0.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd88363..a1361f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-css-transition-replace", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "description": "A React component to animate replacing one element with another.", "main": "lib/ReactCSSTransitionReplace.js", "repository": { From 05069bda1d261aa1f3524656ce565873f2b4f37b Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 22:31:40 +0100 Subject: [PATCH 17/22] Revert the getChildMapping change since detecting nulls can't work. --- src/ReactCSSTransitionReplace.jsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index 0a08fdd..a1a9c47 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -20,16 +20,6 @@ import { nameShape } from './utils/PropTypes' const reactCSSTransitionReplaceChild = React.createFactory(ReactCSSTransitionReplaceChild) -// Filter out nulls before looking for an only child -function getChildMapping(children) { - if (!Array.isArray(children)) { - return children - } - const childArray = React.Children.toArray(children).filter(c => c) - return childArray.length === 1 ? childArray[0] : React.Children.only(childArray) -} - - export default class ReactCSSTransitionReplace extends React.Component { static displayName = 'ReactCSSTransitionReplace' @@ -64,7 +54,7 @@ export default class ReactCSSTransitionReplace extends React.Component { this.state = { currentKey: '1', - currentChild: getChildMapping(this.props.children), + currentChild: this.props.children ? React.Children.only(this.props.children) : undefined, prevChildren: {}, height: null, } @@ -87,7 +77,7 @@ export default class ReactCSSTransitionReplace extends React.Component { } componentWillReceiveProps(nextProps) { - const nextChild = getChildMapping(nextProps.children) + const nextChild = nextProps.children ? React.Children.only(nextProps.children) : undefined const {currentChild} = this.state if ((!currentChild && !nextChild) || (currentChild && nextChild && currentChild.key === nextChild.key)) { From 1b73b4f4de23f463f102ecc73536695a028b394b Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 30 Apr 2017 22:39:01 +0100 Subject: [PATCH 18/22] Update the changelog for v3. --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1098028..381a984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +### v3.0.0 (x May 2017) + +* [ENHANCEMENT] Maintain component callback refs by not overwriting with string refs similar to `react-transition-group`. +* [FEATURE] Only add the `isLeaving` prop to children if opted in with `notifyLeaving={true}` since it's + a departure from `react-transition-group` features. +* [ENHANCEMENT] Use `requestAnimationFrame` to queue the height transition rather than a timeout. +* [ENHANCEMENT] Handle the enter and leave animation of changes due to successive child updates before the current transition ends. + * Clear the selection after transitions to avoid the child being selected after multiple clicks. + * Entering child renders with absolute positioning since switching from relative to abs on a premature leave cancels the active enter animation. + * Fix enter animation of absolutely positioned elements in Chrome not working by skipping one animation frame in the Child component. + * Fix Edge glitch when render starts by always applying container `position`, `display` (use a `div`) and `overflow` styles. + ### v2.2.1 (29 April 2017) * [UPGRADE] Add a `yarn` lock file. From 6513728639ae5e745e772294501c5ba72a498910 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 27 Aug 2017 15:01:09 +0100 Subject: [PATCH 19/22] Add back support for changeWidth. --- demo/assets/transitions.css | 29 +++++++++++++++++++++++++--- demo/components/ContentAddRemove.jsx | 2 +- demo/components/Demo.jsx | 11 ++++++++++- gulpfile.js | 1 + src/ReactCSSTransitionReplace.jsx | 24 ++++++++++++++++------- 5 files changed, 55 insertions(+), 12 deletions(-) diff --git a/demo/assets/transitions.css b/demo/assets/transitions.css index 0fbdda9..a065cae 100644 --- a/demo/assets/transitions.css +++ b/demo/assets/transitions.css @@ -49,7 +49,7 @@ /* Carousel-like transition */ .carousel-swap-leave { - transition: transform 2s ease-in-out; + transition: transform 1s ease-in-out; transform: translate(0, 0); } .carousel-swap-leave-active { @@ -57,7 +57,7 @@ } .carousel-swap-enter { - transition: transform 2s ease-in-out; + transition: transform 1s ease-in-out; transform: translate(100%, 0); } .carousel-swap-enter-active { @@ -65,7 +65,7 @@ } .carousel-swap-height { - transition: height 2s ease-in-out; + transition: height 1s ease-in-out; } @@ -94,3 +94,26 @@ .roll-up-height { transition: height .8s ease-in-out; } + + +/* Fade with width transition */ + +.fade-width-leave { + opacity: 1; +} +.fade-width-leave.fade-width-leave-active { + opacity: 0; + transition: opacity .5s ease-in-out; +} + +.fade-width-enter { + opacity: 0; +} +.fade-width-enter.fade-width-enter-active { + opacity: 1; + transition: opacity .5s ease-in-out; +} + +.fade-width-height { + transition: height .5s ease-in-out, width .5s ease-in-out; +} diff --git a/demo/components/ContentAddRemove.jsx b/demo/components/ContentAddRemove.jsx index 5416554..05c0c24 100644 --- a/demo/components/ContentAddRemove.jsx +++ b/demo/components/ContentAddRemove.jsx @@ -19,7 +19,7 @@ class ContentAddRemove extends React.Component {
Click to {this.state.added ? 'remove' : 'add'} content

- + {this.state.added ? this.props.children : null}
diff --git a/demo/components/Demo.jsx b/demo/components/Demo.jsx index d0a179c..830128a 100644 --- a/demo/components/Demo.jsx +++ b/demo/components/Demo.jsx @@ -1,5 +1,6 @@ import React from 'react' import { Navbar, Nav, NavItem, Grid } from 'react-bootstrap' + const NavbarBrand = Navbar.Brand import PageHead from './PageHead.jsx' @@ -36,7 +37,7 @@ class Demo extends React.Component { - +

Examples

@@ -89,6 +90,14 @@ class Demo extends React.Component { +

Height and Width animation

+

+ + + +
diff --git a/gulpfile.js b/gulpfile.js index e92a351..6e1942f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -53,6 +53,7 @@ gulp.task('demo:bundleAndWatch', function() { gulp.task('demo', ['demo:bundleAndWatch'], function() { browserSync.init({ + port: 3010, browser: ['google chrome'], open: false, notify: false, diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index a1a9c47..155e157 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -5,7 +5,7 @@ */ import React from 'react' -import ReactDOM from 'react-dom' +import { findDOMNode } from 'react-dom' import PropTypes from 'prop-types' import chain from 'chain-function' import warning from 'warning' @@ -34,6 +34,7 @@ export default class ReactCSSTransitionReplace extends React.Component { transitionEnterTimeout: transitionTimeout('Enter'), transitionLeaveTimeout: transitionTimeout('Leave'), overflowHidden: PropTypes.bool, + changeWidth: PropTypes.bool, notifyLeaving: PropTypes.bool, } @@ -42,6 +43,7 @@ export default class ReactCSSTransitionReplace extends React.Component { transitionEnter: true, transitionLeave: true, overflowHidden: true, + changeWidth: false, notifyLeaving: false, component: 'div', childComponent: 'span', @@ -57,6 +59,7 @@ export default class ReactCSSTransitionReplace extends React.Component { currentChild: this.props.children ? React.Children.only(this.props.children) : undefined, prevChildren: {}, height: null, + width: null, } } @@ -90,6 +93,7 @@ export default class ReactCSSTransitionReplace extends React.Component { currentKey: String(Number(currentKey) + 1), currentChild: nextChild, height: 0, + width: this.props.changeWidth ? 0 : null, } if (nextChild) { @@ -97,7 +101,8 @@ export default class ReactCSSTransitionReplace extends React.Component { } if (currentChild) { - nextState.height = ReactDOM.findDOMNode(this.childRefs[currentKey]).offsetHeight + nextState.height = findDOMNode(this.childRefs[currentKey]).offsetHeight + nextState.width = this.props.changeWidth ? findDOMNode(this.childRefs[currentKey]).offsetWidth : null nextState.prevChildren = { ...prevChildren, [currentKey]: currentChild, @@ -190,9 +195,10 @@ export default class ReactCSSTransitionReplace extends React.Component { if (!this.unmounted) { const {state} = this this.setState({ - height: state.currentChild - ? ReactDOM.findDOMNode(this.childRefs[state.currentKey]).offsetHeight - : 0, + height: state.currentChild ? findDOMNode(this.childRefs[state.currentKey]).offsetHeight : 0, + width: this.props.changeWidth + ? (state.currentChild ? findDOMNode(this.childRefs[state.currentKey]).offsetWidth : 0) + : null, }) } this.rafHandle = null @@ -234,12 +240,12 @@ export default class ReactCSSTransitionReplace extends React.Component { } render() { - const {currentKey, currentChild, prevChildren, height} = this.state + const {currentKey, currentChild, prevChildren, height, width} = this.state const childrenToRender = [] const { overflowHidden, transitionName, component, childComponent, notifyLeaving, - transitionAppear, transitionEnter, transitionLeave, + transitionAppear, transitionEnter, transitionLeave, changeWidth, transitionAppearTimeout, transitionEnterTimeout, transitionLeaveTimeout, ...containerProps } = this.props @@ -264,6 +270,10 @@ export default class ReactCSSTransitionReplace extends React.Component { containerProps.style.height = height } + if (width !== null) { + containerProps.style.width = width + } + const positionAbsolute = { position: 'absolute', top: 0, From 64b4693f1ed9126783fd9fe2a17f2143bcd7097a Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 27 Aug 2017 20:35:20 +0100 Subject: [PATCH 20/22] React-Router 4 demo and support children rendering null. --- demo/assets/styles.css | 4 ++ demo/assets/transitions.css | 10 ++--- demo/components/AnimatedRouter.jsx | 58 ++++++++++++++++++++++++ demo/components/Demo.jsx | 8 +++- package.json | 1 + src/ReactCSSTransitionReplace.jsx | 17 ++++--- yarn.lock | 71 +++++++++++++++++++++++------- 7 files changed, 143 insertions(+), 26 deletions(-) create mode 100644 demo/components/AnimatedRouter.jsx diff --git a/demo/assets/styles.css b/demo/assets/styles.css index b694999..20d939a 100644 --- a/demo/assets/styles.css +++ b/demo/assets/styles.css @@ -22,3 +22,7 @@ p { .examples > p { margin-bottom: 20px; } + +.router-example h2 { + margin-top: 0; +} diff --git a/demo/assets/transitions.css b/demo/assets/transitions.css index a065cae..3c5494f 100644 --- a/demo/assets/transitions.css +++ b/demo/assets/transitions.css @@ -98,22 +98,22 @@ /* Fade with width transition */ -.fade-width-leave { +.fade-fast-leave { opacity: 1; } -.fade-width-leave.fade-width-leave-active { +.fade-fast-leave.fade-fast-leave-active { opacity: 0; transition: opacity .5s ease-in-out; } -.fade-width-enter { +.fade-fast-enter { opacity: 0; } -.fade-width-enter.fade-width-enter-active { +.fade-fast-enter.fade-fast-enter-active { opacity: 1; transition: opacity .5s ease-in-out; } -.fade-width-height { +.fade-fast-height { transition: height .5s ease-in-out, width .5s ease-in-out; } diff --git a/demo/components/AnimatedRouter.jsx b/demo/components/AnimatedRouter.jsx new file mode 100644 index 0000000..e65e578 --- /dev/null +++ b/demo/components/AnimatedRouter.jsx @@ -0,0 +1,58 @@ +/* eslint-disable react/no-multi-comp */ +import React from 'react' +import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' +import ReactCSSTransitionReplace from '../../src/ReactCSSTransitionReplace.jsx' + +const Home = () => ( +
+

Home

+ +
+) + +const One = () => ( +
+

One

+ +
+) + +const Two = () => ( +
+

Two

+ +
+) + +const AnimatedRouter = () => ( + +
+
    +
  • Home
  • +
  • One
  • +
  • Two
  • +
  • Three (no match)
  • +
+ +
+ + ( + + + + + + + + )}/> + +
+
+
+) + +export default AnimatedRouter diff --git a/demo/components/Demo.jsx b/demo/components/Demo.jsx index 830128a..f97561f 100644 --- a/demo/components/Demo.jsx +++ b/demo/components/Demo.jsx @@ -6,6 +6,7 @@ const NavbarBrand = Navbar.Brand import PageHead from './PageHead.jsx' import ContentSwapper from './ContentSwapper.jsx' import ContentAddRemove from './ContentAddRemove.jsx' +import AnimatedRouter from './AnimatedRouter.jsx' import ContentLong from './ContentLong.jsx' import ContentShort from './ContentShort.jsx' @@ -94,10 +95,15 @@ class Demo extends React.Component {

+ +

React Router v4

+

+ + diff --git a/package.json b/package.json index a1361f7..972cf20 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "react": "^15.3.2", "react-bootstrap": "^0.31.0", "react-dom": "^15.3.2", + "react-router-dom": "^4.1.1", "rimraf": "^2.5.4", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index 155e157..be99e67 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -101,8 +101,11 @@ export default class ReactCSSTransitionReplace extends React.Component { } if (currentChild) { - nextState.height = findDOMNode(this.childRefs[currentKey]).offsetHeight - nextState.width = this.props.changeWidth ? findDOMNode(this.childRefs[currentKey]).offsetWidth : null + const currentChildNode = findDOMNode(this.childRefs[currentKey]) + nextState.height = currentChildNode ? currentChildNode.offsetHeight : 0 + nextState.width = this.props.changeWidth + ? (currentChildNode ? currentChildNode.offsetWidth : 0) + : null nextState.prevChildren = { ...prevChildren, [currentKey]: currentChild, @@ -118,7 +121,10 @@ export default class ReactCSSTransitionReplace extends React.Component { componentDidUpdate() { if (this.shouldEnterCurrent) { this.shouldEnterCurrent = false - this.performEnter(this.state.currentKey) + // If the current child renders null there is nothing to enter + if (findDOMNode(this.childRefs[this.state.currentKey])) { + this.performEnter(this.state.currentKey) + } } const keysToLeave = this.keysToLeave @@ -194,10 +200,11 @@ export default class ReactCSSTransitionReplace extends React.Component { performHeightTransition = () => { if (!this.unmounted) { const {state} = this + const currentChildNode = state.currentChild ? findDOMNode(this.childRefs[state.currentKey]) : null this.setState({ - height: state.currentChild ? findDOMNode(this.childRefs[state.currentKey]).offsetHeight : 0, + height: currentChildNode ? currentChildNode.offsetHeight : 0, width: this.props.changeWidth - ? (state.currentChild ? findDOMNode(this.childRefs[state.currentKey]).offsetWidth : 0) + ? (currentChildNode ? currentChildNode.offsetWidth : 0) : null, }) } diff --git a/yarn.lock b/yarn.lock index 2fd9ec8..adef11e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1254,15 +1254,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.5.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -concat-stream@~1.5.0, concat-stream@~1.5.1: +concat-stream@^1.5.2, concat-stream@~1.5.0, concat-stream@~1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" dependencies: @@ -2378,6 +2370,16 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +history@^4.5.1, history@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.6.1.tgz#911cf8eb65728555a94f2b12780a0c531a14d2fd" + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.0.0" + value-equal "^0.2.0" + warning "^3.0.0" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -2390,6 +2392,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoist-non-react-statics@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -2477,7 +2483,7 @@ inherits@1: version "1.0.2" resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -2530,7 +2536,7 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1: +invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3068,7 +3074,7 @@ lodash@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -3573,6 +3579,12 @@ path-root@^0.1.1: dependencies: path-root-regex "^0.1.0" +path-to-regexp@^1.5.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -3650,7 +3662,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7: +prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7: version "15.5.8" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" dependencies: @@ -3762,6 +3774,27 @@ react-prop-types@^0.4.0: dependencies: warning "^3.0.0" +react-router-dom@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.1.1.tgz#3021ade1f2c160af97cf94e25594c5f294583025" + dependencies: + history "^4.5.1" + loose-envify "^1.3.1" + prop-types "^15.5.4" + react-router "^4.1.1" + +react-router@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.1.1.tgz#d448f3b7c1b429a6fbb03395099949c606b1fe95" + dependencies: + history "^4.6.0" + hoist-non-react-statics "^1.2.0" + invariant "^2.2.2" + loose-envify "^1.3.1" + path-to-regexp "^1.5.3" + prop-types "^15.5.4" + warning "^3.0.0" + react-transition-group@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.1.2.tgz#374cd778070f74b0a658045fc532edfd471ad836" @@ -3810,7 +3843,7 @@ read-pkg@^1.0.0: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6: +readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.6: version "2.2.9" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8" dependencies: @@ -4008,6 +4041,10 @@ resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" +resolve-pathname@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.1.0.tgz#e8358801b86b83b17560d4e3c382d7aef2100944" + resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" @@ -4527,7 +4564,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -typedarray@^0.0.6, typedarray@~0.0.5: +typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -4617,6 +4654,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" +value-equal@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.2.1.tgz#c220a304361fce6994dbbedaa3c7e1a1b895871d" + verror@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" From 143ee628159cc9282e53118ac67cc4ce19f2e5dc Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 27 Aug 2017 21:34:14 +0100 Subject: [PATCH 21/22] Leave transition dominates for no current child or one that renders null. --- src/ReactCSSTransitionReplace.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ReactCSSTransitionReplace.jsx b/src/ReactCSSTransitionReplace.jsx index be99e67..b170966 100644 --- a/src/ReactCSSTransitionReplace.jsx +++ b/src/ReactCSSTransitionReplace.jsx @@ -170,9 +170,9 @@ export default class ReactCSSTransitionReplace extends React.Component { performLeave = (key) => { this.transitioningKeys[key] = true this.childRefs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key)) - if (!this.state.currentChild) { - // The enter transition dominates, but if there is no - // entering component the height is set to zero. + if (!this.state.currentChild || !findDOMNode(this.childRefs[this.state.currentKey])) { + // The enter transition dominates, but if there is no entering + // component or it renders null the height is set to zero. this.enqueueHeightTransition() } } @@ -184,7 +184,7 @@ export default class ReactCSSTransitionReplace extends React.Component { delete nextState.prevChildren[key] delete this.childRefs[key] - if (!this.state.currentChild) { + if (!this.state.currentChild || !findDOMNode(this.childRefs[this.state.currentKey])) { nextState.height = null } From 3f9a4caeb3454bcaea79b8d1adffdcf37df6ab79 Mon Sep 17 00:00:00 2001 From: Marnus Weststrate Date: Sun, 27 Aug 2017 22:14:04 +0100 Subject: [PATCH 22/22] Update demo and docs. --- .eslintrc | 2 +- CHANGELOG.md | 11 +++---- README.md | 46 ++++++++++++++++++++++++++++-- UPGRADE_GUIDE.md | 11 ++++--- demo/assets/styles.css | 4 +++ demo/components/AnimatedRouter.jsx | 12 ++++---- demo/components/Demo.jsx | 16 +++++++---- package.json | 2 +- 8 files changed, 81 insertions(+), 23 deletions(-) diff --git a/.eslintrc b/.eslintrc index 8f23b45..3d753d7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -155,7 +155,7 @@ "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines "max": 2 }], - "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary + "no-nested-ternary": 0, // http://eslint.org/docs/rules/no-nested-ternary "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces diff --git a/CHANGELOG.md b/CHANGELOG.md index 381a984..bef416d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,15 @@ -### v3.0.0 (x May 2017) +### v3.0.0 (27 August 2017) +* [ENHANCEMENT] Treat a child rendering `null` as if there is no child; specifically helps avoid errors with RR4. * [ENHANCEMENT] Maintain component callback refs by not overwriting with string refs similar to `react-transition-group`. * [FEATURE] Only add the `isLeaving` prop to children if opted in with `notifyLeaving={true}` since it's a departure from `react-transition-group` features. * [ENHANCEMENT] Use `requestAnimationFrame` to queue the height transition rather than a timeout. * [ENHANCEMENT] Handle the enter and leave animation of changes due to successive child updates before the current transition ends. - * Clear the selection after transitions to avoid the child being selected after multiple clicks. - * Entering child renders with absolute positioning since switching from relative to abs on a premature leave cancels the active enter animation. - * Fix enter animation of absolutely positioned elements in Chrome not working by skipping one animation frame in the Child component. - * Fix Edge glitch when render starts by always applying container `position`, `display` (use a `div`) and `overflow` styles. +* [ENHANCEMENT] Clear the selection after transitions to avoid the child being selected after multiple clicks. +* [ENHANCEMENT] Entering child renders with absolute positioning since switching from relative to abs on a premature leave cancels the active enter animation. +* [ENHANCEMENT] Fix enter animation of absolutely positioned elements in Chrome not working by skipping one animation frame in the Child component. +* [ENHANCEMENT] Fix Edge glitch when render starts by always applying container `position`, `display` (use a `div`) and `overflow` styles. ### v2.2.1 (29 April 2017) diff --git a/README.md b/README.md index 26b3b31..8a87e07 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,9 @@ follows the exact same API as `ReactCSSTransitionGroup`, with support for `trans and `transitionAppear`. When the `key` of the child component changes, the previous component is animated out and the new component animated in. During this process: - - The leaving component continues to be rendered as usual with `static` positioning. - - The entering component is positioned on top of the leaving component with `absolute` positioning. + - All leaving components continue to be rendered; if the animation is slow there may be multiple components + in the process of leaving. + - The entering component is positioned on top of the leaving component(s) with `absolute` positioning. - The height of the container is set to that of the leaving component, and then immediately to that of the entering component. If the `transitionName` is a `String` the `{animation-name}-height` class name is applied to it, and if `transitionName` is an `Object` the `transitionName.height` class will be used if present. @@ -145,6 +146,47 @@ the duration of the transition. In this case: See the live example [here](http://marnusw.github.io/react-css-transition-replace#fade-wait). +### React-Router v4 + +Animated transitions of react-router v4 routes is supported with two caveats shown in the example below: + +1. The current `location` must be applied to the `Switch` to force it to maintain the previous matched route on + the leaving component. +2. If the `Switch` might render `null`, i.e. there is no catch-all `"*"` route, the `Switch` must be wrapped in a + `div` or similar for the leave transition to work; if not the previous component will disappear instantaneously + when there is no match. + +```javascript + +
+
    +
  • Home
  • +
  • One
  • +
  • Two
  • +
  • Three (no match)
  • +
+ ( + +
+ + + + + +
+
+ )}/> +
+
+``` + +See the live example [here](http://marnusw.github.io/react-css-transition-replace#react-router-v4). + + ### Hardware acceleration for smoother transitions For smoother transitions hardware acceleration, which is achieved by using translate3d instead of the 2D diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md index 83d7b80..f184bcf 100644 --- a/UPGRADE_GUIDE.md +++ b/UPGRADE_GUIDE.md @@ -1,5 +1,11 @@ # Upgrade Guide +## 2.2.1 -> 3.0.0 + +Applying the `isLeaving` prop on leaving children is now an opt-in behaviour controlled +by the `notifyLeaving` prop. + + ## 2.1.0 -> 2.2.0 The library has always had a bug causing subsequent changes while an animation is in @@ -7,9 +13,6 @@ progress to be ignored. This has been fixed in v2.2.0. While the functioning of library is now technically more correct, this but may have been a feature used to smooth over multiple transitions by some which will no longer be the case. -If this causes problems it might be worthwhile to introduce a flag that could direct -the component to ignore changes during transitions to restore the previous behaviour. - ## 1.3.0 -> 2.0.0 @@ -20,7 +23,7 @@ You can pass `transitionName={{ height: 'my-height-className' }}` now, if you need to use a custom className (useful for `css-modules`). The leaving component will receive `isLeaving={true}` prop during it's leaving transition. -You can use it in your child components to prevent their rerendering during that period, for example. +You can use it in your child components to prevent their re-rendering during that period, for example. ## 1.0.x -> 1.1.0 diff --git a/demo/assets/styles.css b/demo/assets/styles.css index 20d939a..24c55d3 100644 --- a/demo/assets/styles.css +++ b/demo/assets/styles.css @@ -19,6 +19,10 @@ p { margin-bottom: 0; } +hr { + border-color: #bbb; +} + .examples > p { margin-bottom: 20px; } diff --git a/demo/components/AnimatedRouter.jsx b/demo/components/AnimatedRouter.jsx index e65e578..e929b7d 100644 --- a/demo/components/AnimatedRouter.jsx +++ b/demo/components/AnimatedRouter.jsx @@ -42,11 +42,13 @@ const AnimatedRouter = () => ( transitionEnterTimeout={500} transitionLeaveTimeout={500} > - - - - - +
+ + + + + +
)}/> diff --git a/demo/components/Demo.jsx b/demo/components/Demo.jsx index f97561f..a4ced32 100644 --- a/demo/components/Demo.jsx +++ b/demo/components/Demo.jsx @@ -91,8 +91,12 @@ class Demo extends React.Component { -

Height and Width animation

-

+

Height and Width animation

+

By setting the changeWidth prop the container width is managed along with the height. + This example realizes the same effect as above using this property and just a fade CSS animation. + In this case the height class should configure the transition of both the height + and width properties: transition: height .5s ease-in-out, width .5s ease-in-out; +

-

React Router v4

-

+

React Router v4

+

Animating React-Router v4 transitions is supported. See the example for details.

- + diff --git a/package.json b/package.json index 972cf20..0388b08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-css-transition-replace", - "version": "3.0.0-beta.2", + "version": "3.0.0", "description": "A React component to animate replacing one element with another.", "main": "lib/ReactCSSTransitionReplace.js", "repository": {