From 41e0322ab664a4572b6698935b3b736b5d7406de Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Thu, 29 Jul 2021 17:48:08 +0200 Subject: [PATCH] chore(TransitionGroup): use React.forwardRef() (#4266) --- src/modules/Transition/Transition.js | 14 +- src/modules/Transition/TransitionGroup.js | 131 ++++++++++-------- .../Transition/TransitionGroup-test.js | 38 ++--- 3 files changed, 96 insertions(+), 87 deletions(-) diff --git a/src/modules/Transition/Transition.js b/src/modules/Transition/Transition.js index 0fdea6a222..3ad3c88018 100644 --- a/src/modules/Transition/Transition.js +++ b/src/modules/Transition/Transition.js @@ -1,7 +1,7 @@ import cx from 'clsx' import _ from 'lodash' import PropTypes from 'prop-types' -import { cloneElement, Component } from 'react' +import * as React from 'react' import { makeDebugger, normalizeTransitionDuration, SUI, useKeyOnly } from '../../lib' import TransitionGroup from './TransitionGroup' @@ -29,15 +29,7 @@ const TRANSITION_STYLE_TYPE = { /** * A transition is an animation usually used to move content in or out of view. */ -export default class Transition extends Component { - /** @deprecated Static properties will be removed in v2. */ - static INITIAL = TRANSITION_STATUS_INITIAL - static ENTERED = TRANSITION_STATUS_ENTERED - static ENTERING = TRANSITION_STATUS_ENTERING - static EXITED = TRANSITION_STATUS_EXITED - static EXITING = TRANSITION_STATUS_EXITING - static UNMOUNTED = TRANSITION_STATUS_UNMOUNTED - +export default class Transition extends React.Component { static Group = TransitionGroup state = { @@ -171,7 +163,7 @@ export default class Transition extends Component { return null } - return cloneElement(children, { + return React.cloneElement(children, { className: this.computeClasses(), style: this.computeStyle(), ...(process.env.NODE_ENV !== 'production' && { diff --git a/src/modules/Transition/TransitionGroup.js b/src/modules/Transition/TransitionGroup.js index 4716c903ce..94aa4f206c 100644 --- a/src/modules/Transition/TransitionGroup.js +++ b/src/modules/Transition/TransitionGroup.js @@ -2,64 +2,66 @@ import _ from 'lodash' import PropTypes from 'prop-types' import React from 'react' -import { getElementType, getUnhandledProps, makeDebugger, SUI } from '../../lib' +import { getElementType, getUnhandledProps, makeDebugger, SUI, useEventCallback } from '../../lib' import { getChildMapping, mergeChildMappings } from './utils/childMapping' import wrapChild from './utils/wrapChild' const debug = makeDebugger('transition_group') /** - * A Transition.Group animates children as they mount and unmount. + * Wraps all children elements with proper callbacks and props. + * + * @param {React.ReactNode} children + * @param {Stream} animation + * @param {Number|String|Object} duration + * @param {Boolean} directional + * + * @return {Object} */ -export default class TransitionGroup extends React.Component { - state = { - // Keeping a callback under the state is a hack to make it accessible under getDerivedStateFromProps() - handleOnHide: (nothing, childProps) => { - debug('handleOnHide', childProps) - const { reactKey } = childProps - - this.setState((state) => { - const children = { ...state.children } - delete children[reactKey] - - return { children } - }) - }, - } +function useWrappedChildren(children, animation, duration, directional) { + debug('wrapChildren()') - static getDerivedStateFromProps(props, state) { - debug('getDerivedStateFromProps()') - - const { animation, duration, directional } = props - const { children: prevMapping } = state - - // A short circuit for an initial render as there will be no `prevMapping` - if (typeof prevMapping === 'undefined') { - return { - children: _.mapValues(getChildMapping(props.children), (child) => - wrapChild(child, state.handleOnHide, { - animation, - duration, - directional, - }), - ), - } - } + const [, forceUpdate] = React.useReducer((x) => x + 1, 0) + + const previousChildren = React.useRef() + let wrappedChildren - const nextMapping = getChildMapping(props.children) - const children = mergeChildMappings(prevMapping, nextMapping) + React.useEffect(() => { + previousChildren.current = wrappedChildren + }) - _.forEach(children, (child, key) => { - const hasPrev = _.has(prevMapping, key) - const hasNext = _.has(nextMapping, key) + const handleChildHide = useEventCallback((nothing, childProps) => { + debug('handleOnHide', childProps) + const { reactKey } = childProps + + delete previousChildren.current[reactKey] + forceUpdate() + }) + + // A short circuit for an initial render as there will be no `prevMapping` + if (typeof previousChildren.current === 'undefined') { + wrappedChildren = _.mapValues(getChildMapping(children), (child) => + wrapChild(child, handleChildHide, { + animation, + duration, + directional, + }), + ) + } else { + const nextMapping = getChildMapping(children) + wrappedChildren = mergeChildMappings(previousChildren.current, nextMapping) - const { [key]: prevChild } = prevMapping + _.forEach(wrappedChildren, (child, key) => { + const hasPrev = previousChildren.current[key] + const hasNext = nextMapping[key] + + const prevChild = previousChildren.current[key] const isLeaving = !_.get(prevChild, 'props.visible') // Heads up! // An item is new (entering), it will be picked from `nextChildren`, so it should be wrapped if (hasNext && (!hasPrev || isLeaving)) { - children[key] = wrapChild(child, state.handleOnHide, { + wrappedChildren[key] = wrapChild(child, handleChildHide, { animation, duration, directional, @@ -72,7 +74,7 @@ export default class TransitionGroup extends React.Component { // An item is old (exiting), it will be picked from `prevChildren`, so it has been already // wrapped, so should be only updated if (!hasNext && hasPrev && !isLeaving) { - children[key] = React.cloneElement(prevChild, { visible: false }) + wrappedChildren[key] = React.cloneElement(prevChild, { visible: false }) return } @@ -83,7 +85,7 @@ export default class TransitionGroup extends React.Component { props: { visible, transitionOnMount }, } = prevChild - children[key] = wrapChild(child, state.handleOnHide, { + wrappedChildren[key] = wrapChild(child, handleChildHide, { animation, duration, directional, @@ -91,23 +93,36 @@ export default class TransitionGroup extends React.Component { visible, }) }) - - return { children } } - render() { - debug('render') - debug('props', this.props) - debug('state', this.state) - - const { children } = this.state - const ElementType = getElementType(TransitionGroup, this.props) - const rest = getUnhandledProps(TransitionGroup, this.props) - - return {_.values(children)} - } + return wrappedChildren } +/** + * A Transition.Group animates children as they mount and unmount. + */ +const TransitionGroup = React.forwardRef(function (props, ref) { + debug('render') + debug('props', props) + + const children = useWrappedChildren( + props.children, + props.animation, + props.duration, + props.directional, + ) + + const ElementType = getElementType(TransitionGroup, props) + const rest = getUnhandledProps(TransitionGroup, props) + + return ( + + {_.values(children)} + + ) +}) + +TransitionGroup.displayName = 'TransitionGroup' TransitionGroup.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, @@ -137,3 +152,5 @@ TransitionGroup.defaultProps = { animation: 'fade', duration: 500, } + +export default TransitionGroup diff --git a/test/specs/modules/Transition/TransitionGroup-test.js b/test/specs/modules/Transition/TransitionGroup-test.js index 448a1bbcb8..471ca76978 100644 --- a/test/specs/modules/Transition/TransitionGroup-test.js +++ b/test/specs/modules/Transition/TransitionGroup-test.js @@ -7,13 +7,13 @@ import * as common from 'test/specs/commonTests' let wrapper const wrapperMount = (...args) => (wrapper = mount(...args)) -const wrapperShallow = (...args) => (wrapper = shallow(...args)) describe('TransitionGroup', () => { common.isConformant(TransitionGroup, { rendersFragmentByDefault: true, rendersChildren: false, }) + common.forwardsRef(TransitionGroup, { requiredProps: { as: 'div' } }) beforeEach(() => { wrapper = undefined @@ -25,50 +25,50 @@ describe('TransitionGroup', () => { describe('children', () => { it('wraps all children to Transition', () => { - shallow( + wrapperMount(
, ) - .children() - .everyWhere((item) => item.type().should.equal(Transition)) + + wrapper.children().everyWhere((item) => item.type().should.equal(Transition)) }) it('passes props to children', () => { - shallow( + wrapperMount(
, ) - .children() - .everyWhere((item) => { - item.should.have.prop('animation', 'scale') - item.should.have.prop('directional', true) - item.should.have.prop('duration', 1500) - item.type().should.equal(Transition) - }) + + wrapper.children().everyWhere((item) => { + item.should.have.prop('animation', 'scale') + item.should.have.prop('directional', true) + item.should.have.prop('duration', 1500) + item.type().should.equal(Transition) + }) }) it('wraps new child to Transition and sets transitionOnMount to true', () => { - wrapperShallow( + wrapperMount(
, ) wrapper.setProps({ children: [
,
] }) - const child = wrapper.childAt(1) - child.key().should.equal('.$second') - child.type().should.equal(Transition) - child.should.have.prop('transitionOnMount', true) + const secondChild = wrapper.childAt(1) + secondChild.key().should.equal('.$second') + secondChild.type().should.equal(Transition) + secondChild.should.have.prop('transitionOnMount', true) }) it('skips invalid children', () => { - wrapperShallow( + wrapperMount(
, @@ -81,7 +81,7 @@ describe('TransitionGroup', () => { }) it('sets visible to false when child was removed', () => { - wrapperShallow( + wrapperMount(