Skip to content

Commit 679f703

Browse files
authored
chore(Step): use React.forwardRef() (#4240)
1 parent 8eb2411 commit 679f703

File tree

17 files changed

+265
-155
lines changed

17 files changed

+265
-155
lines changed

src/elements/Step/Step.js

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cx from 'clsx'
22
import _ from 'lodash'
33
import PropTypes from 'prop-types'
4-
import React, { Component } from 'react'
4+
import React from 'react'
55

66
import {
77
childrenUtils,
@@ -10,6 +10,7 @@ import {
1010
getElementType,
1111
getUnhandledProps,
1212
useKeyOnly,
13+
useEventCallback,
1314
} from '../../lib'
1415
import Icon from '../Icon'
1516
import StepContent from './StepContent'
@@ -20,70 +21,69 @@ import StepTitle from './StepTitle'
2021
/**
2122
* A step shows the completion status of an activity in a series of activities.
2223
*/
23-
class Step extends Component {
24-
computeElementType = () => {
25-
const { onClick } = this.props
26-
27-
if (onClick) return 'a'
28-
}
29-
30-
handleClick = (e) => {
31-
const { disabled } = this.props
32-
33-
if (!disabled) _.invoke(this.props, 'onClick', e, this.props)
34-
}
35-
36-
render() {
37-
const {
38-
active,
39-
children,
40-
className,
41-
completed,
42-
content,
43-
description,
44-
disabled,
45-
href,
46-
icon,
47-
link,
48-
title,
49-
} = this.props
50-
51-
const classes = cx(
52-
useKeyOnly(active, 'active'),
53-
useKeyOnly(completed, 'completed'),
54-
useKeyOnly(disabled, 'disabled'),
55-
useKeyOnly(link, 'link'),
56-
'step',
57-
className,
58-
)
59-
const rest = getUnhandledProps(Step, this.props)
60-
const ElementType = getElementType(Step, this.props, this.computeElementType)
61-
62-
if (!childrenUtils.isNil(children)) {
63-
return (
64-
<ElementType {...rest} className={classes} href={href} onClick={this.handleClick}>
65-
{children}
66-
</ElementType>
67-
)
24+
const Step = React.forwardRef(function StepInner(props, ref) {
25+
const {
26+
active,
27+
children,
28+
className,
29+
completed,
30+
content,
31+
description,
32+
disabled,
33+
href,
34+
onClick,
35+
icon,
36+
link,
37+
title,
38+
} = props
39+
40+
const handleClick = useEventCallback((e) => {
41+
if (!disabled) {
42+
_.invoke(props, 'onClick', e, props)
6843
}
69-
70-
if (!childrenUtils.isNil(content)) {
71-
return (
72-
<ElementType {...rest} className={classes} href={href} onClick={this.handleClick}>
73-
{content}
74-
</ElementType>
75-
)
44+
})
45+
46+
const classes = cx(
47+
useKeyOnly(active, 'active'),
48+
useKeyOnly(completed, 'completed'),
49+
useKeyOnly(disabled, 'disabled'),
50+
useKeyOnly(link, 'link'),
51+
'step',
52+
className,
53+
)
54+
55+
const rest = getUnhandledProps(Step, props)
56+
const ElementType = getElementType(Step, props, () => {
57+
if (onClick) {
58+
return 'a'
7659
}
60+
})
7761

62+
if (!childrenUtils.isNil(children)) {
7863
return (
79-
<ElementType {...rest} className={classes} href={href} onClick={this.handleClick}>
80-
{Icon.create(icon, { autoGenerateKey: false })}
81-
{StepContent.create({ description, title }, { autoGenerateKey: false })}
64+
<ElementType {...rest} className={classes} href={href} onClick={handleClick} ref={ref}>
65+
{children}
8266
</ElementType>
8367
)
8468
}
85-
}
8669

70+
if (!childrenUtils.isNil(content)) {
71+
return (
72+
<ElementType {...rest} className={classes} href={href} onClick={handleClick} ref={ref}>
73+
{content}
74+
</ElementType>
75+
)
76+
}
77+
78+
return (
79+
<ElementType {...rest} className={classes} href={href} onClick={handleClick} ref={ref}>
80+
{Icon.create(icon, { autoGenerateKey: false })}
81+
{StepContent.create({ description, title }, { autoGenerateKey: false })}
82+
</ElementType>
83+
)
84+
})
85+
86+
Step.displayName = 'Step'
8787
Step.propTypes = {
8888
/** An element type to render as (string or function). */
8989
as: PropTypes.elementType,

src/elements/Step/StepContent.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,37 @@ import StepTitle from './StepTitle'
1515
/**
1616
* A step can contain a content.
1717
*/
18-
function StepContent(props) {
18+
const StepContent = React.forwardRef(function StepContentInner(props, ref) {
1919
const { children, className, content, description, title } = props
2020
const classes = cx('content', className)
2121
const rest = getUnhandledProps(StepContent, props)
2222
const ElementType = getElementType(StepContent, props)
2323

2424
if (!childrenUtils.isNil(children)) {
2525
return (
26-
<ElementType {...rest} className={classes}>
26+
<ElementType {...rest} className={classes} ref={ref}>
2727
{children}
2828
</ElementType>
2929
)
3030
}
31+
3132
if (!childrenUtils.isNil(content)) {
3233
return (
33-
<ElementType {...rest} className={classes}>
34+
<ElementType {...rest} className={classes} ref={ref}>
3435
{content}
3536
</ElementType>
3637
)
3738
}
3839

3940
return (
40-
<ElementType {...rest} className={classes}>
41+
<ElementType {...rest} className={classes} ref={ref}>
4142
{StepTitle.create(title, { autoGenerateKey: false })}
4243
{StepDescription.create(description, { autoGenerateKey: false })}
4344
</ElementType>
4445
)
45-
}
46+
})
4647

48+
StepContent.displayName = 'StepContent'
4749
StepContent.propTypes = {
4850
/** An element type to render as (string or function). */
4951
as: PropTypes.elementType,

src/elements/Step/StepDescription.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@ import {
1010
getUnhandledProps,
1111
} from '../../lib'
1212

13-
function StepDescription(props) {
13+
const StepDescription = React.forwardRef(function StepDescriptionInner(props, ref) {
1414
const { children, className, content } = props
1515
const classes = cx('description', className)
1616
const rest = getUnhandledProps(StepDescription, props)
1717
const ElementType = getElementType(StepDescription, props)
1818

1919
return (
20-
<ElementType {...rest} className={classes}>
20+
<ElementType {...rest} className={classes} ref={ref}>
2121
{childrenUtils.isNil(children) ? content : children}
2222
</ElementType>
2323
)
24-
}
24+
})
2525

26+
StepDescription.displayName = 'StepDescription'
2627
StepDescription.propTypes = {
2728
/** An element type to render as (string or function). */
2829
as: PropTypes.elementType,

src/elements/Step/StepGroup.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const numberMap = _.pickBy(numberToWordMap, (val, key) => key <= 8)
2222
/**
2323
* A set of steps.
2424
*/
25-
function StepGroup(props) {
25+
const StepGroup = React.forwardRef(function StepGroupInner(props, ref) {
2626
const {
2727
attached,
2828
children,
@@ -55,26 +55,27 @@ function StepGroup(props) {
5555

5656
if (!childrenUtils.isNil(children)) {
5757
return (
58-
<ElementType {...rest} className={classes}>
58+
<ElementType {...rest} className={classes} ref={ref}>
5959
{children}
6060
</ElementType>
6161
)
6262
}
6363
if (!childrenUtils.isNil(content)) {
6464
return (
65-
<ElementType {...rest} className={classes}>
65+
<ElementType {...rest} className={classes} ref={ref}>
6666
{content}
6767
</ElementType>
6868
)
6969
}
7070

7171
return (
72-
<ElementType {...rest} className={classes}>
72+
<ElementType {...rest} className={classes} ref={ref}>
7373
{_.map(items, (item) => Step.create(item))}
7474
</ElementType>
7575
)
76-
}
76+
})
7777

78+
StepGroup.displayName = 'StepGroup'
7879
StepGroup.propTypes = {
7980
/** An element type to render as (string or function). */
8081
as: PropTypes.elementType,

src/elements/Step/StepTitle.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ import {
1313
/**
1414
* A step can contain a title.
1515
*/
16-
function StepTitle(props) {
16+
const StepTitle = React.forwardRef(function StepTitleInner(props, ref) {
1717
const { children, className, content } = props
1818
const classes = cx('title', className)
1919
const rest = getUnhandledProps(StepTitle, props)
2020
const ElementType = getElementType(StepTitle, props)
2121

2222
return (
23-
<ElementType {...rest} className={classes}>
23+
<ElementType {...rest} className={classes} ref={ref}>
2424
{childrenUtils.isNil(children) ? content : children}
2525
</ElementType>
2626
)
27-
}
27+
})
2828

29+
StepTitle.displayName = 'StepTitle'
2930
StepTitle.propTypes = {
3031
/** An element type to render as (string or function). */
3132
as: PropTypes.elementType,

src/lib/hooks/useEventCallback.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react'
2+
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'
3+
4+
/**
5+
* https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
6+
*
7+
* Modified `useCallback` that can be used when dependencies change too frequently. Can occur when:
8+
* e.g. user props are depedencies which could change on every render
9+
* e.g. volatile values (i.e. useState/useDispatch) are dependencies which could change frequently
10+
*
11+
* This should not be used often, but can be a useful re-render optimization since the callback is
12+
* a ref and will not be invalidated between rerenders.
13+
*
14+
* @param {Function} fn The callback function that will be used
15+
*/
16+
export default function useEventCallback(fn) {
17+
const callbackRef = React.useRef(() => {
18+
if (process.env.NODE_ENV !== 'production') {
19+
throw new Error('Cannot call an event handler while rendering...')
20+
}
21+
})
22+
23+
useIsomorphicLayoutEffect(() => {
24+
callbackRef.current = fn
25+
}, [fn])
26+
27+
return React.useCallback(
28+
(...args) => {
29+
const callback = callbackRef.current
30+
31+
return callback(...args)
32+
},
33+
[callbackRef],
34+
)
35+
}

src/lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ export { makeDebugger }
4747
//
4848

4949
export useClassNamesOnNode from './hooks/useClassNamesOnNode'
50+
export useEventCallback from './hooks/useEventCallback'

test/specs/addons/Confirm/Confirm-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('Confirm', () => {
2626
if (wrapper && wrapper.unmount) wrapper.unmount()
2727
})
2828

29-
common.isConformant(Confirm)
29+
common.isConformant(Confirm, { rendersPortal: true })
3030

3131
common.implementsShorthandProp(Confirm, {
3232
autoGenerateKey: false,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ const requiredProps = {
1010
}
1111

1212
describe('TransitionablePortal', () => {
13-
common.isConformant(TransitionablePortal, { requiredProps })
13+
common.isConformant(TransitionablePortal, {
14+
rendersPortal: true,
15+
requiredProps,
16+
})
1417

1518
describe('children', () => {
1619
it('renders a Transition', () => {

test/specs/collections/Form/FormInput-test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,48 @@ import * as common from 'test/specs/commonTests'
66

77
describe('FormInput', () => {
88
common.isConformant(FormInput, {
9+
eventTargets: {
10+
// keyboard
11+
onKeyDown: 'input',
12+
onKeyPress: 'input',
13+
onKeyUp: 'input',
14+
15+
// focus
16+
onFocus: 'input',
17+
onBlur: 'input',
18+
19+
// form
20+
onChange: 'input',
21+
onInput: 'input',
22+
23+
// mouse
24+
onClick: 'input',
25+
onContextMenu: 'input',
26+
onDrag: 'input',
27+
onDragEnd: 'input',
28+
onDragEnter: 'input',
29+
onDragExit: 'input',
30+
onDragLeave: 'input',
31+
onDragOver: 'input',
32+
onDragStart: 'input',
33+
onDrop: 'input',
34+
onMouseDown: 'input',
35+
onMouseEnter: 'input',
36+
onMouseLeave: 'input',
37+
onMouseMove: 'input',
38+
onMouseOut: 'input',
39+
onMouseOver: 'input',
40+
onMouseUp: 'input',
41+
42+
// selection
43+
onSelect: 'input',
44+
45+
// touch
46+
onTouchCancel: 'input',
47+
onTouchEnd: 'input',
48+
onTouchMove: 'input',
49+
onTouchStart: 'input',
50+
},
951
ignoredTypingsProps: ['label', 'error'],
1052
})
1153
common.labelImplementsHtmlForProp(FormInput)

0 commit comments

Comments
 (0)