Skip to content

Commit

Permalink
chore(TransitionablePortal): convert to be functional component (#4269)
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Feb 1, 2022
1 parent 0471d25 commit 54a9de9
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 72 deletions.
138 changes: 77 additions & 61 deletions src/addons/TransitionablePortal/TransitionablePortal.js
Original file line number Diff line number Diff line change
@@ -1,113 +1,127 @@
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import React from 'react'

import Portal from '../Portal'
import Transition from '../../modules/Transition'
import { TRANSITION_STATUS_ENTERING } from '../../modules/Transition/utils/computeStatuses'
import { getUnhandledProps, makeDebugger } from '../../lib'
import { getUnhandledProps, makeDebugger, useForceUpdate } from '../../lib'

const debug = makeDebugger('transitionable_portal')

/**
* A sugar for `Portal` and `Transition`.
* @see Portal
* @see Transition
*/
export default class TransitionablePortal extends Component {
state = {}
function usePortalState(props) {
const portalOpen = React.useRef(false)
const forceUpdate = useForceUpdate()

// ----------------------------------------
// Lifecycle
// ----------------------------------------
const setPortalOpen = React.useCallback((value) => {
portalOpen.current = value
forceUpdate()
}, [])

static getDerivedStateFromProps(props, state) {
React.useEffect(() => {
if (!_.isUndefined(props.open)) {
portalOpen.current = props.open
}
}, [props.open])

if (_.isUndefined(props.open)) {
// This is definitely a hack :(
//
// It's coupled with handlePortalClose() for force set the state of `portalOpen` omitting
// props.open. It's related to implementation of the component itself as `onClose()` will be
// called after a transition will end.
// https://github.com/Semantic-Org/Semantic-UI-React/issues/2382
if (state.portalOpen === -1) {
return { portalOpen: false }
}

if (_.isUndefined(props.open)) {
return null
if (portalOpen.current === -1) {
return [false, setPortalOpen]
}

return { portalOpen: props.open }
return [portalOpen.current, setPortalOpen]
}

return [props.open, setPortalOpen]
}

/**
* A sugar for `Portal` and `Transition`.
* @see Portal
* @see Transition
*/
function TransitionablePortal(props) {
const { children, transition } = props

const [portalOpen, setPortalOpen] = usePortalState(props)
const [transitionVisible, setTransitionVisible] = React.useState(false)

const open = portalOpen || transitionVisible

// ----------------------------------------
// Callback handling
// ----------------------------------------

handlePortalClose = () => {
const handlePortalClose = () => {
debug('handlePortalClose()')

this.setState({ portalOpen: -1 })
setPortalOpen(-1)
}

handlePortalOpen = () => {
const handlePortalOpen = () => {
debug('handlePortalOpen()')

this.setState({ portalOpen: true })
setPortalOpen(true)
}

handleTransitionHide = (nothing, data) => {
const handleTransitionHide = (nothing, data) => {
debug('handleTransitionHide()')
const { portalOpen } = this.state

this.setState({ transitionVisible: false })
_.invoke(this.props, 'onClose', null, { ...data, portalOpen: false, transitionVisible: false })
_.invoke(this.props, 'onHide', null, { ...data, portalOpen, transitionVisible: false })
setTransitionVisible(false)
_.invoke(props, 'onClose', null, { ...data, portalOpen: false, transitionVisible: false })
_.invoke(props, 'onHide', null, { ...data, portalOpen, transitionVisible: false })
}

handleTransitionStart = (nothing, data) => {
const handleTransitionStart = (nothing, data) => {
debug('handleTransitionStart()')
const { portalOpen } = this.state
const { status } = data
const transitionVisible = status === TRANSITION_STATUS_ENTERING
const nextTransitionVisible = status === TRANSITION_STATUS_ENTERING

_.invoke(this.props, 'onStart', null, { ...data, portalOpen, transitionVisible })
_.invoke(props, 'onStart', null, {
...data,
portalOpen,
transitionVisible: nextTransitionVisible,
})

// Heads up! TransitionablePortal fires onOpen callback on the start of transition animation
if (!transitionVisible) return
if (!nextTransitionVisible) {
return
}

this.setState({ transitionVisible })
_.invoke(this.props, 'onOpen', null, { ...data, transitionVisible, portalOpen: true })
setTransitionVisible(nextTransitionVisible)
_.invoke(props, 'onOpen', null, {
...data,
transitionVisible: nextTransitionVisible,
portalOpen: true,
})
}

// ----------------------------------------
// Render
// ----------------------------------------

render() {
debug('render()', this.state)

const { children, transition } = this.props
const { portalOpen, transitionVisible } = this.state

const open = portalOpen || transitionVisible
const rest = getUnhandledProps(TransitionablePortal, this.props)

return (
<Portal {...rest} open={open} onOpen={this.handlePortalOpen} onClose={this.handlePortalClose}>
<Transition
{...transition}
transitionOnMount
onStart={this.handleTransitionStart}
onHide={this.handleTransitionHide}
visible={portalOpen}
>
{children}
</Transition>
</Portal>
)
}
const rest = getUnhandledProps(TransitionablePortal, props)

return (
<Portal {...rest} open={open} onOpen={handlePortalOpen} onClose={handlePortalClose}>
<Transition
{...transition}
transitionOnMount
onStart={handleTransitionStart}
onHide={handleTransitionHide}
visible={portalOpen}
>
{children}
</Transition>
</Portal>
)
}

TransitionablePortal.displayName = 'TransitionablePortal'
TransitionablePortal.propTypes = {
/** Primary content. */
children: PropTypes.node.isRequired,
Expand Down Expand Up @@ -157,3 +171,5 @@ TransitionablePortal.defaultProps = {
duration: 400,
},
}

export default TransitionablePortal
8 changes: 8 additions & 0 deletions src/lib/hooks/useForceUpdate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as React from 'react'

/**
* Returns a callback that causes force render of a component.
*/
export default function useForceUpdate() {
return React.useReducer((x) => x + 1, 0)[1]
}
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export { makeDebugger }
export useAutoControlledValue from './hooks/useAutoControlledValue'
export useClassNamesOnNode from './hooks/useClassNamesOnNode'
export useEventCallback from './hooks/useEventCallback'
export useForceUpdate from './hooks/useForceUpdate'
export useIsomorphicLayoutEffect from './hooks/useIsomorphicLayoutEffect'
export useMergedRefs, { setRef } from './hooks/useMergedRefs'
export usePrevious from './hooks/usePrevious'
3 changes: 2 additions & 1 deletion src/modules/Sidebar/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
useKeyOnly,
useIsomorphicLayoutEffect,
useEventCallback,
useForceUpdate,
useMergedRefs,
usePrevious,
} from '../../lib'
Expand All @@ -30,7 +31,7 @@ function useAnimationTick(visible) {
const tickIncrement = !!visible === !!previousVisible ? 0 : 1

const animationTick = React.useRef(0)
const [, forceUpdate] = React.useReducer((x) => x + 1, 0)
const forceUpdate = useForceUpdate()

const currentTick = animationTick.current + tickIncrement
const resetAnimationTick = React.useCallback(() => {
Expand Down
14 changes: 10 additions & 4 deletions src/modules/Transition/TransitionGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import _ from 'lodash'
import PropTypes from 'prop-types'
import React from 'react'

import { getElementType, getUnhandledProps, makeDebugger, SUI, useEventCallback } from '../../lib'
import {
getElementType,
getUnhandledProps,
makeDebugger,
SUI,
useEventCallback,
useForceUpdate,
} from '../../lib'
import { getChildMapping, mergeChildMappings } from './utils/childMapping'
import wrapChild from './utils/wrapChild'

Expand All @@ -21,11 +28,10 @@ const debug = makeDebugger('transition_group')
function useWrappedChildren(children, animation, duration, directional) {
debug('wrapChildren()')

const [, forceUpdate] = React.useReducer((x) => x + 1, 0)

const forceUpdate = useForceUpdate()
const previousChildren = React.useRef()
let wrappedChildren

let wrappedChildren
React.useEffect(() => {
previousChildren.current = wrappedChildren
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,23 +106,22 @@ describe('TransitionablePortal', () => {
})

describe('open', () => {
it('does not block update of state on a portal close', () => {
it('blocks update of state on a portal close', () => {
const wrapper = mount(<TransitionablePortal {...requiredProps} open />)
wrapper.should.have.descendants('.in#children')
wrapper.find('#children').should.have.className('in')

domEvent.click(document.body)
wrapper.update()
wrapper.should.have.descendants('.out#children')
wrapper.find('#children').should.have.className('in')
})

it('passes `open` prop to Transition when defined', () => {
const wrapper = mount(<TransitionablePortal {...requiredProps} />)

wrapper.setProps({ open: true })
wrapper.should.have.descendants('.in#children')
wrapper.find('#children').should.have.className('in')

wrapper.setProps({ open: false })
wrapper.should.have.descendants('.out#children')
wrapper.find('#children').should.have.className('out')
})

it('does not pass `open` prop to Transition when not defined', () => {
Expand Down

0 comments on commit 54a9de9

Please sign in to comment.