From bf2640749c8b7b9544053343c6160d8a016daa8d Mon Sep 17 00:00:00 2001 From: Nikita Nafranets Date: Tue, 12 Mar 2019 22:54:29 +0300 Subject: [PATCH 1/5] add switch transition component --- src/SwitchTransition.js | 137 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/SwitchTransition.js diff --git a/src/SwitchTransition.js b/src/SwitchTransition.js new file mode 100644 index 00000000..74297187 --- /dev/null +++ b/src/SwitchTransition.js @@ -0,0 +1,137 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function areChildrenDifferent(oldChildren, newChildren) { + if (oldChildren === newChildren) return false; + if ( + React.isValidElement(oldChildren) && + React.isValidElement(newChildren) && + oldChildren.key != null && + oldChildren.key === newChildren.key + ) { + return false; + } + return true; +} + +const ENTERED = 'ENTERED'; +const ENTERING = 'ENTERING'; +const EXITING = 'EXITING'; + +export const modes = { + out: 'out-in', + in: 'in-out' +}; + +const leaveRenders = { + [modes.out]: ({ current, changeState }) => + React.cloneElement(current, { + in: false, + onExited: () => { + changeState(ENTERING, null); + } + }), + [modes.in]: ({ current, changeState, children }) => [ + current, + React.cloneElement(children, { + in: true, + onEntered: () => { + changeState(ENTERING); + } + }) + ] +}; + +const enterRenders = { + [modes.out]: ({ children, changeState }) => + React.cloneElement(children, { + in: true, + onEntered: () => { + changeState(ENTERED, React.cloneElement(children, { in: true })); + } + }), + [modes.in]: ({ current, children, changeState }) => [ + React.cloneElement(current, { + in: false, + onExited: () => { + changeState(ENTERED, React.cloneElement(children, { in: true })); + } + }), + React.cloneElement(children, { + in: true + }) + ] +}; + +export class SwitchTransition extends React.Component { + static childContextTypes = { + transitionGroup: PropTypes.object.isRequired + }; + + getChildContext() { + return { + transitionGroup: { isMounting: !this.appeared } + }; + } + + state = { + status: ENTERED, + current: null + }; + + appeared = false; + + componentDidMount() { + this.appeared = true; + } + + static getDerivedStateFromProps(props, state) { + if (state.status === ENTERING && props.mode === modes.in) { + return { + status: state.status + }; + } + + if (state.current && areChildrenDifferent(state.current, props.children)) { + return { + status: EXITING + }; + } + + return { + current: React.cloneElement(props.children, { + in: true + }) + }; + } + + changeState = (status, current = this.state.current) => { + this.setState({ + status, + current + }); + }; + + render() { + const { + props: { children, mode }, + state: { status, current } + } = this; + + const data = { children, current, changeState: this.changeState, status }; + + switch (status) { + case ENTERING: + return enterRenders[mode](data); + case EXITING: + return leaveRenders[mode](data); + case ENTERED: + return current; + } + } +} + + +SwitchTransition.propTypes = { + mode: PropTypes.arrayOf([modes.in, modes.out]), +} From a013811750df0e668ec5957353449863e46fa15d Mon Sep 17 00:00:00 2001 From: Nikita Nafranets Date: Wed, 3 Apr 2019 23:09:03 +0300 Subject: [PATCH 2/5] change prop type --- src/SwitchTransition.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SwitchTransition.js b/src/SwitchTransition.js index 74297187..721e8333 100644 --- a/src/SwitchTransition.js +++ b/src/SwitchTransition.js @@ -133,5 +133,5 @@ export class SwitchTransition extends React.Component { SwitchTransition.propTypes = { - mode: PropTypes.arrayOf([modes.in, modes.out]), + mode: PropTypes.oneOf([modes.in, modes.out]), } From 56b67cd15581ab315c6b68fbb398a403cb928a95 Mon Sep 17 00:00:00 2001 From: Nikita Nafranets Date: Thu, 4 Apr 2019 01:30:29 +0300 Subject: [PATCH 3/5] add some documentations and tests --- src/SwitchTransition.js | 84 ++++++++++++++++++---- test/SwitchTransition-test.js | 129 ++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 15 deletions(-) create mode 100644 test/SwitchTransition-test.js diff --git a/src/SwitchTransition.js b/src/SwitchTransition.js index 721e8333..b58eda7a 100644 --- a/src/SwitchTransition.js +++ b/src/SwitchTransition.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { ENTERED, ENTERING, EXITING } from './Transition' function areChildrenDifferent(oldChildren, newChildren) { if (oldChildren === newChildren) return false; @@ -14,30 +15,35 @@ function areChildrenDifferent(oldChildren, newChildren) { return true; } -const ENTERED = 'ENTERED'; -const ENTERING = 'ENTERING'; -const EXITING = 'EXITING'; - +/** + * Enum of modes for SwitchTransition component + * @enum { string } + */ export const modes = { out: 'out-in', in: 'in-out' }; +const callHook = (element, name, cb) => (...args) => { + element.props[name] && element.props[name](...args) + cb() +} + const leaveRenders = { [modes.out]: ({ current, changeState }) => React.cloneElement(current, { in: false, - onExited: () => { + onExited: callHook(current, 'onExited', () => { changeState(ENTERING, null); - } + }) }), [modes.in]: ({ current, changeState, children }) => [ current, React.cloneElement(children, { in: true, - onEntered: () => { + onEntered: callHook(children, 'onEntered', () => { changeState(ENTERING); - } + }) }) ] }; @@ -46,16 +52,16 @@ const enterRenders = { [modes.out]: ({ children, changeState }) => React.cloneElement(children, { in: true, - onEntered: () => { + onEntered: callHook(children, 'onEntered', () => { changeState(ENTERED, React.cloneElement(children, { in: true })); - } + }) }), [modes.in]: ({ current, children, changeState }) => [ React.cloneElement(current, { in: false, - onExited: () => { + onExited: callHook(current, 'onExited', () => { changeState(ENTERED, React.cloneElement(children, { in: true })); - } + }) }), React.cloneElement(children, { in: true @@ -63,6 +69,32 @@ const enterRenders = { ] }; +/** + * A transition component inspired by the [vue transition modes](https://vuejs.org/v2/guide/transitions.html#Transition-Modes). + * You can use it when you want to control the render between state transitions. + * Based on the selected mode and the child's key which is the `Transition` or `CSSTransition` component, the `SwitchTransition` makes a consistent transition between them. + * + * If the `out-in` mode is selected, the `SwitchTransition` waits until the old child leaves and then inserts a new child. + * If the `in-out` mode is selected, the `SwitchTransition` inserts a new child first, waits for the new child to enter and then removes the old child + * + * ```jsx + * + * function App() { + * const [state, setState] = useState(false); + * return ( + * + * node.addEventListener("transitionend", done, false)} + * classNames='fade' > + * + * + * + * ) + * } + * ``` + */ export class SwitchTransition extends React.Component { static childContextTypes = { transitionGroup: PropTypes.object.isRequired @@ -86,9 +118,15 @@ export class SwitchTransition extends React.Component { } static getDerivedStateFromProps(props, state) { + if (props.children == null) { + return { + current: null + } + } + if (state.status === ENTERING && props.mode === modes.in) { return { - status: state.status + status: ENTERING }; } @@ -119,12 +157,11 @@ export class SwitchTransition extends React.Component { } = this; const data = { children, current, changeState: this.changeState, status }; - switch (status) { case ENTERING: return enterRenders[mode](data); case EXITING: - return leaveRenders[mode](data); + return leaveRenders[mode](data) case ENTERED: return current; } @@ -133,5 +170,22 @@ export class SwitchTransition extends React.Component { SwitchTransition.propTypes = { + /** + * Transition modes. + * `out-in`: Current element transitions out first, then when complete, the new element transitions in. + * `in-out: New element transitions in first, then when complete, the current element transitions out.` + * + * @type {'out-in'|'in-out'} + */ mode: PropTypes.oneOf([modes.in, modes.out]), + /** + * Any `Transition` or `CSSTransition` component + */ + children: PropTypes.oneOfType([ + PropTypes.element.isRequired, + ]), +} + +SwitchTransition.defaultProps = { + mode: modes.out } diff --git a/test/SwitchTransition-test.js b/test/SwitchTransition-test.js new file mode 100644 index 00000000..833da0d7 --- /dev/null +++ b/test/SwitchTransition-test.js @@ -0,0 +1,129 @@ +import React from 'react'; + +import { mount } from 'enzyme'; + +import Transition, { ENTERED } from '../src/Transition'; +import { SwitchTransition, modes } from '../src/SwitchTransition'; + +describe('SwitchTransition', () => { + let log, Parent; + beforeEach(() => { + log = []; + let events = { + onEnter: (_, m) => log.push(m ? 'appear' : 'enter'), + onEntering: (_, m) => log.push(m ? 'appearing' : 'entering'), + onEntered: (_, m) => log.push(m ? 'appeared' : 'entered'), + onExit: () => log.push('exit'), + onExiting: () => log.push('exiting'), + onExited: () => log.push('exited') + }; + + Parent = function Parent({ on, rendered = true }) { + return ( + + {rendered ? ( + + + + ) : null} + + ); + }; + }); + + it('should have default status ENTERED', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.state('status')).toBe(ENTERED); + }); + + it('should have default mode: out-in', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.prop('mode')).toBe(modes.out); + }); + + it('should work without childs', () => { + expect(() => { + mount( + + + + + + ); + }).not.toThrow(); + }); + + it('should switch between components on change state', () => { + const wrapper = mount(); + + jest.useFakeTimers(); + expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe( + 'first' + ); + wrapper.setProps({ on: false }); + expect(log).toEqual(['exit', 'exiting']); + jest.runAllTimers(); + expect(log).toEqual([ + 'exit', + 'exiting', + 'exited', + 'enter', + 'entering', + 'entered' + ]); + expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe( + 'second' + ); + }); + + it('should switch between null and component', () => { + const wrapper = mount(); + + expect( + wrapper.find(SwitchTransition).getElement().props.children + ).toBeFalsy(); + + jest.useFakeTimers(); + + wrapper.setProps({ rendered: true }); + jest.runAllTimers(); + expect(log).toEqual(['enter', 'entering', 'entered']); + expect( + wrapper.find(SwitchTransition).getElement().props.children + ).toBeTruthy(); + expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe( + 'first' + ); + + wrapper.setProps({ on: false, rendered: true }); + jest.runAllTimers(); + expect(log).toEqual([ + 'enter', + 'entering', + 'entered', + 'exit', + 'exiting', + 'exited', + 'enter', + 'entering', + 'entered' + ]); + expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe( + 'second' + ); + }); +}); From 92b7a0fa29b4d754d53f0304a53600d1ccdfe4c9 Mon Sep 17 00:00:00 2001 From: Nikita Nafranets Date: Thu, 4 Apr 2019 01:32:53 +0300 Subject: [PATCH 4/5] add export in entry point --- src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index e1ce1316..197e926a 100644 --- a/src/index.js +++ b/src/index.js @@ -2,11 +2,13 @@ import CSSTransition from './CSSTransition'; import ReplaceTransition from './ReplaceTransition'; import TransitionGroup from './TransitionGroup'; import Transition from './Transition'; - +import { SwitchTransition, modes } from './SwitchTransition' module.exports = { Transition, TransitionGroup, ReplaceTransition, CSSTransition, + SwitchTransition, + switchTransitionModes: modes }; From 43d31d4be4c0022bb950672b8394ad749707d90d Mon Sep 17 00:00:00 2001 From: Nikita Nafranets Date: Thu, 18 Apr 2019 12:55:55 +0300 Subject: [PATCH 5/5] update switchtransition for new api --- src/SwitchTransition.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/SwitchTransition.js b/src/SwitchTransition.js index b58eda7a..e7714588 100644 --- a/src/SwitchTransition.js +++ b/src/SwitchTransition.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ENTERED, ENTERING, EXITING } from './Transition' +import TransitionGroupContext from './TransitionGroupContext'; function areChildrenDifferent(oldChildren, newChildren) { if (oldChildren === newChildren) return false; @@ -96,16 +97,6 @@ const enterRenders = { * ``` */ export class SwitchTransition extends React.Component { - static childContextTypes = { - transitionGroup: PropTypes.object.isRequired - }; - - getChildContext() { - return { - transitionGroup: { isMounting: !this.appeared } - }; - } - state = { status: ENTERED, current: null @@ -157,14 +148,23 @@ export class SwitchTransition extends React.Component { } = this; const data = { children, current, changeState: this.changeState, status }; + let component switch (status) { case ENTERING: - return enterRenders[mode](data); + component = enterRenders[mode](data); + break; case EXITING: - return leaveRenders[mode](data) + component = leaveRenders[mode](data) + break; case ENTERED: - return current; + component = current; } + + return ( + + {component} + + ) } }