Skip to content

Commit 3f60f95

Browse files
committed
chore(TransitionablePortal): convert to be functional component (#4269)
1 parent e753041 commit 3f60f95

File tree

6 files changed

+103
-72
lines changed

6 files changed

+103
-72
lines changed
Lines changed: 77 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,127 @@
11
import _ from 'lodash'
22
import PropTypes from 'prop-types'
3-
import React, { Component } from 'react'
3+
import React from 'react'
44

55
import Portal from '../Portal'
66
import Transition from '../../modules/Transition'
77
import { TRANSITION_STATUS_ENTERING } from '../../modules/Transition/utils/computeStatuses'
8-
import { getUnhandledProps, makeDebugger } from '../../lib'
8+
import { getUnhandledProps, makeDebugger, useForceUpdate } from '../../lib'
99

1010
const debug = makeDebugger('transitionable_portal')
1111

12-
/**
13-
* A sugar for `Portal` and `Transition`.
14-
* @see Portal
15-
* @see Transition
16-
*/
17-
export default class TransitionablePortal extends Component {
18-
state = {}
12+
function usePortalState(props) {
13+
const portalOpen = React.useRef(false)
14+
const forceUpdate = useForceUpdate()
1915

20-
// ----------------------------------------
21-
// Lifecycle
22-
// ----------------------------------------
16+
const setPortalOpen = React.useCallback((value) => {
17+
portalOpen.current = value
18+
forceUpdate()
19+
}, [])
2320

24-
static getDerivedStateFromProps(props, state) {
21+
React.useEffect(() => {
22+
if (!_.isUndefined(props.open)) {
23+
portalOpen.current = props.open
24+
}
25+
}, [props.open])
26+
27+
if (_.isUndefined(props.open)) {
2528
// This is definitely a hack :(
2629
//
2730
// It's coupled with handlePortalClose() for force set the state of `portalOpen` omitting
2831
// props.open. It's related to implementation of the component itself as `onClose()` will be
2932
// called after a transition will end.
3033
// https://github.com/Semantic-Org/Semantic-UI-React/issues/2382
31-
if (state.portalOpen === -1) {
32-
return { portalOpen: false }
33-
}
34-
35-
if (_.isUndefined(props.open)) {
36-
return null
34+
if (portalOpen.current === -1) {
35+
return [false, setPortalOpen]
3736
}
3837

39-
return { portalOpen: props.open }
38+
return [portalOpen.current, setPortalOpen]
4039
}
4140

41+
return [props.open, setPortalOpen]
42+
}
43+
44+
/**
45+
* A sugar for `Portal` and `Transition`.
46+
* @see Portal
47+
* @see Transition
48+
*/
49+
function TransitionablePortal(props) {
50+
const { children, transition } = props
51+
52+
const [portalOpen, setPortalOpen] = usePortalState(props)
53+
const [transitionVisible, setTransitionVisible] = React.useState(false)
54+
55+
const open = portalOpen || transitionVisible
56+
4257
// ----------------------------------------
4358
// Callback handling
4459
// ----------------------------------------
4560

46-
handlePortalClose = () => {
61+
const handlePortalClose = () => {
4762
debug('handlePortalClose()')
48-
49-
this.setState({ portalOpen: -1 })
63+
setPortalOpen(-1)
5064
}
5165

52-
handlePortalOpen = () => {
66+
const handlePortalOpen = () => {
5367
debug('handlePortalOpen()')
54-
55-
this.setState({ portalOpen: true })
68+
setPortalOpen(true)
5669
}
5770

58-
handleTransitionHide = (nothing, data) => {
71+
const handleTransitionHide = (nothing, data) => {
5972
debug('handleTransitionHide()')
60-
const { portalOpen } = this.state
6173

62-
this.setState({ transitionVisible: false })
63-
_.invoke(this.props, 'onClose', null, { ...data, portalOpen: false, transitionVisible: false })
64-
_.invoke(this.props, 'onHide', null, { ...data, portalOpen, transitionVisible: false })
74+
setTransitionVisible(false)
75+
_.invoke(props, 'onClose', null, { ...data, portalOpen: false, transitionVisible: false })
76+
_.invoke(props, 'onHide', null, { ...data, portalOpen, transitionVisible: false })
6577
}
6678

67-
handleTransitionStart = (nothing, data) => {
79+
const handleTransitionStart = (nothing, data) => {
6880
debug('handleTransitionStart()')
69-
const { portalOpen } = this.state
7081
const { status } = data
71-
const transitionVisible = status === TRANSITION_STATUS_ENTERING
82+
const nextTransitionVisible = status === TRANSITION_STATUS_ENTERING
7283

73-
_.invoke(this.props, 'onStart', null, { ...data, portalOpen, transitionVisible })
84+
_.invoke(props, 'onStart', null, {
85+
...data,
86+
portalOpen,
87+
transitionVisible: nextTransitionVisible,
88+
})
7489

7590
// Heads up! TransitionablePortal fires onOpen callback on the start of transition animation
76-
if (!transitionVisible) return
91+
if (!nextTransitionVisible) {
92+
return
93+
}
7794

78-
this.setState({ transitionVisible })
79-
_.invoke(this.props, 'onOpen', null, { ...data, transitionVisible, portalOpen: true })
95+
setTransitionVisible(nextTransitionVisible)
96+
_.invoke(props, 'onOpen', null, {
97+
...data,
98+
transitionVisible: nextTransitionVisible,
99+
portalOpen: true,
100+
})
80101
}
81102

82103
// ----------------------------------------
83104
// Render
84105
// ----------------------------------------
85106

86-
render() {
87-
debug('render()', this.state)
88-
89-
const { children, transition } = this.props
90-
const { portalOpen, transitionVisible } = this.state
91-
92-
const open = portalOpen || transitionVisible
93-
const rest = getUnhandledProps(TransitionablePortal, this.props)
94-
95-
return (
96-
<Portal {...rest} open={open} onOpen={this.handlePortalOpen} onClose={this.handlePortalClose}>
97-
<Transition
98-
{...transition}
99-
transitionOnMount
100-
onStart={this.handleTransitionStart}
101-
onHide={this.handleTransitionHide}
102-
visible={portalOpen}
103-
>
104-
{children}
105-
</Transition>
106-
</Portal>
107-
)
108-
}
107+
const rest = getUnhandledProps(TransitionablePortal, props)
108+
109+
return (
110+
<Portal {...rest} open={open} onOpen={handlePortalOpen} onClose={handlePortalClose}>
111+
<Transition
112+
{...transition}
113+
transitionOnMount
114+
onStart={handleTransitionStart}
115+
onHide={handleTransitionHide}
116+
visible={portalOpen}
117+
>
118+
{children}
119+
</Transition>
120+
</Portal>
121+
)
109122
}
110123

124+
TransitionablePortal.displayName = 'TransitionablePortal'
111125
TransitionablePortal.propTypes = {
112126
/** Primary content. */
113127
children: PropTypes.node.isRequired,
@@ -157,3 +171,5 @@ TransitionablePortal.defaultProps = {
157171
duration: 400,
158172
},
159173
}
174+
175+
export default TransitionablePortal

src/lib/hooks/useForceUpdate.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as React from 'react'
2+
3+
/**
4+
* Returns a callback that causes force render of a component.
5+
*/
6+
export default function useForceUpdate() {
7+
return React.useReducer((x) => x + 1, 0)[1]
8+
}

src/lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export { makeDebugger }
4949
export useAutoControlledValue from './hooks/useAutoControlledValue'
5050
export useClassNamesOnNode from './hooks/useClassNamesOnNode'
5151
export useEventCallback from './hooks/useEventCallback'
52+
export useForceUpdate from './hooks/useForceUpdate'
5253
export useIsomorphicLayoutEffect from './hooks/useIsomorphicLayoutEffect'
5354
export useMergedRefs, { setRef } from './hooks/useMergedRefs'
5455
export usePrevious from './hooks/usePrevious'

src/modules/Sidebar/Sidebar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
useKeyOnly,
1515
useIsomorphicLayoutEffect,
1616
useEventCallback,
17+
useForceUpdate,
1718
useMergedRefs,
1819
usePrevious,
1920
} from '../../lib'
@@ -30,7 +31,7 @@ function useAnimationTick(visible) {
3031
const tickIncrement = !!visible === !!previousVisible ? 0 : 1
3132

3233
const animationTick = React.useRef(0)
33-
const [, forceUpdate] = React.useReducer((x) => x + 1, 0)
34+
const forceUpdate = useForceUpdate()
3435

3536
const currentTick = animationTick.current + tickIncrement
3637
const resetAnimationTick = React.useCallback(() => {

src/modules/Transition/TransitionGroup.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import _ from 'lodash'
22
import PropTypes from 'prop-types'
33
import React from 'react'
44

5-
import { getElementType, getUnhandledProps, makeDebugger, SUI, useEventCallback } from '../../lib'
5+
import {
6+
getElementType,
7+
getUnhandledProps,
8+
makeDebugger,
9+
SUI,
10+
useEventCallback,
11+
useForceUpdate,
12+
} from '../../lib'
613
import { getChildMapping, mergeChildMappings } from './utils/childMapping'
714
import wrapChild from './utils/wrapChild'
815

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

24-
const [, forceUpdate] = React.useReducer((x) => x + 1, 0)
25-
31+
const forceUpdate = useForceUpdate()
2632
const previousChildren = React.useRef()
27-
let wrappedChildren
2833

34+
let wrappedChildren
2935
React.useEffect(() => {
3036
previousChildren.current = wrappedChildren
3137
})

test/specs/addons/TransitionablePortal/TransitionablePortal-test.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,22 @@ describe('TransitionablePortal', () => {
106106
})
107107

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

113113
domEvent.click(document.body)
114-
wrapper.update()
115-
wrapper.should.have.descendants('.out#children')
114+
wrapper.find('#children').should.have.className('in')
116115
})
117116

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

121120
wrapper.setProps({ open: true })
122-
wrapper.should.have.descendants('.in#children')
121+
wrapper.find('#children').should.have.className('in')
123122

124123
wrapper.setProps({ open: false })
125-
wrapper.should.have.descendants('.out#children')
124+
wrapper.find('#children').should.have.className('out')
126125
})
127126

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

0 commit comments

Comments
 (0)