Skip to content

Commit

Permalink
chore(TransitionGroup): use React.forwardRef() (#4266)
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter authored Jul 29, 2021
1 parent 87097f0 commit fab41fd
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 87 deletions.
14 changes: 3 additions & 11 deletions src/modules/Transition/Transition.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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' && {
Expand Down
131 changes: 74 additions & 57 deletions src/modules/Transition/TransitionGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand All @@ -83,31 +85,44 @@ 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,
transitionOnMount,
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 <ElementType {...rest}>{_.values(children)}</ElementType>
}
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 (
<ElementType {...rest} ref={ref}>
{_.values(children)}
</ElementType>
)
})

TransitionGroup.displayName = 'TransitionGroup'
TransitionGroup.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down Expand Up @@ -137,3 +152,5 @@ TransitionGroup.defaultProps = {
animation: 'fade',
duration: 500,
}

export default TransitionGroup
38 changes: 19 additions & 19 deletions test/specs/modules/Transition/TransitionGroup-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,50 +25,50 @@ describe('TransitionGroup', () => {

describe('children', () => {
it('wraps all children to Transition', () => {
shallow(
wrapperMount(
<TransitionGroup>
<div />
<div />
<div />
</TransitionGroup>,
)
.children()
.everyWhere((item) => item.type().should.equal(Transition))

wrapper.children().everyWhere((item) => item.type().should.equal(Transition))
})

it('passes props to children', () => {
shallow(
wrapperMount(
<TransitionGroup animation='scale' directional duration={1500}>
<div />
<div />
<div />
</TransitionGroup>,
)
.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(
<TransitionGroup>
<div key='first' />
</TransitionGroup>,
)
wrapper.setProps({ children: [<div key='first' />, <div key='second' />] })

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(
<TransitionGroup>
<div key='first' />
</TransitionGroup>,
Expand All @@ -81,7 +81,7 @@ describe('TransitionGroup', () => {
})

it('sets visible to false when child was removed', () => {
wrapperShallow(
wrapperMount(
<TransitionGroup>
<div key='first' />
<div key='second' />
Expand Down

0 comments on commit fab41fd

Please sign in to comment.