Skip to content

Commit e7037ac

Browse files
committed
chore(Flag|Icon|ImageGroup): use React.forwardRef() (#4264)
1 parent a8fb613 commit e7037ac

File tree

12 files changed

+134
-123
lines changed

12 files changed

+134
-123
lines changed

src/elements/Flag/Flag.js

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import cx from 'clsx'
22
import PropTypes from 'prop-types'
3-
import React, { PureComponent } from 'react'
3+
import React from 'react'
44

55
import {
66
createShorthandFactory,
@@ -509,17 +509,16 @@ export const names = [
509509
/**
510510
* A flag is is used to represent a political state.
511511
*/
512-
class Flag extends PureComponent {
513-
render() {
514-
const { className, name } = this.props
515-
const classes = cx(name, 'flag', className)
516-
const rest = getUnhandledProps(Flag, this.props)
517-
const ElementType = getElementType(Flag, this.props)
512+
const Flag = React.forwardRef(function (props, ref) {
513+
const { className, name } = props
514+
const classes = cx(name, 'flag', className)
515+
const rest = getUnhandledProps(Flag, props)
516+
const ElementType = getElementType(Flag, props)
518517

519-
return <ElementType {...rest} className={classes} />
520-
}
521-
}
518+
return <ElementType {...rest} className={classes} ref={ref} />
519+
})
522520

521+
Flag.displayName = 'Flag'
523522
Flag.propTypes = {
524523
/** An element type to render as (string or function). */
525524
as: PropTypes.elementType,
@@ -535,6 +534,10 @@ Flag.defaultProps = {
535534
as: 'i',
536535
}
537536

538-
Flag.create = createShorthandFactory(Flag, (value) => ({ name: value }))
537+
// Heads up!
538+
// .create() factories should be defined on exported component to be visible as static properties
539+
const MemoFlag = React.memo(Flag)
540+
541+
MemoFlag.create = createShorthandFactory(MemoFlag, (value) => ({ name: value }))
539542

540-
export default Flag
543+
export default MemoFlag

src/elements/Icon/Icon.js

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

66
import {
77
createShorthandFactory,
88
customPropTypes,
99
getElementType,
1010
getUnhandledProps,
1111
SUI,
12+
useEventCallback,
1213
useKeyOnly,
1314
useKeyOrValueAndKey,
1415
useValueAndKey,
1516
} from '../../lib'
1617
import IconGroup from './IconGroup'
1718

18-
/**
19-
* An icon is a glyph used to represent something else.
20-
* @see Image
21-
*/
22-
class Icon extends PureComponent {
23-
getIconAriaOptions() {
24-
const ariaOptions = {}
25-
const { 'aria-label': ariaLabel, 'aria-hidden': ariaHidden } = this.props
26-
27-
if (_.isNil(ariaLabel)) {
28-
ariaOptions['aria-hidden'] = 'true'
29-
} else {
30-
ariaOptions['aria-label'] = ariaLabel
31-
}
19+
function getAriaProps(props) {
20+
const ariaOptions = {}
21+
const { 'aria-label': ariaLabel, 'aria-hidden': ariaHidden } = props
3222

33-
if (!_.isNil(ariaHidden)) {
34-
ariaOptions['aria-hidden'] = ariaHidden
35-
}
23+
if (_.isNil(ariaLabel)) {
24+
ariaOptions['aria-hidden'] = 'true'
25+
} else {
26+
ariaOptions['aria-label'] = ariaLabel
27+
}
3628

37-
return ariaOptions
29+
if (!_.isNil(ariaHidden)) {
30+
ariaOptions['aria-hidden'] = ariaHidden
3831
}
3932

40-
handleClick = (e) => {
41-
const { disabled } = this.props
33+
return ariaOptions
34+
}
4235

36+
/**
37+
* An icon is a glyph used to represent something else.
38+
* @see Image
39+
*/
40+
const Icon = React.forwardRef(function (props, ref) {
41+
const {
42+
bordered,
43+
circular,
44+
className,
45+
color,
46+
corner,
47+
disabled,
48+
fitted,
49+
flipped,
50+
inverted,
51+
link,
52+
loading,
53+
name,
54+
rotated,
55+
size,
56+
} = props
57+
58+
const classes = cx(
59+
color,
60+
name,
61+
size,
62+
useKeyOnly(bordered, 'bordered'),
63+
useKeyOnly(circular, 'circular'),
64+
useKeyOnly(disabled, 'disabled'),
65+
useKeyOnly(fitted, 'fitted'),
66+
useKeyOnly(inverted, 'inverted'),
67+
useKeyOnly(link, 'link'),
68+
useKeyOnly(loading, 'loading'),
69+
useKeyOrValueAndKey(corner, 'corner'),
70+
useValueAndKey(flipped, 'flipped'),
71+
useValueAndKey(rotated, 'rotated'),
72+
'icon',
73+
className,
74+
)
75+
76+
const rest = getUnhandledProps(Icon, props)
77+
const ElementType = getElementType(Icon, props)
78+
const ariaProps = getAriaProps(props)
79+
80+
const handleClick = useEventCallback((e) => {
4381
if (disabled) {
4482
e.preventDefault()
4583
return
4684
}
4785

48-
_.invoke(this.props, 'onClick', e, this.props)
49-
}
86+
_.invoke(props, 'onClick', e, props)
87+
})
5088

51-
render() {
52-
const {
53-
bordered,
54-
circular,
55-
className,
56-
color,
57-
corner,
58-
disabled,
59-
fitted,
60-
flipped,
61-
inverted,
62-
link,
63-
loading,
64-
name,
65-
rotated,
66-
size,
67-
} = this.props
68-
69-
const classes = cx(
70-
color,
71-
name,
72-
size,
73-
useKeyOnly(bordered, 'bordered'),
74-
useKeyOnly(circular, 'circular'),
75-
useKeyOnly(disabled, 'disabled'),
76-
useKeyOnly(fitted, 'fitted'),
77-
useKeyOnly(inverted, 'inverted'),
78-
useKeyOnly(link, 'link'),
79-
useKeyOnly(loading, 'loading'),
80-
useKeyOrValueAndKey(corner, 'corner'),
81-
useValueAndKey(flipped, 'flipped'),
82-
useValueAndKey(rotated, 'rotated'),
83-
'icon',
84-
className,
85-
)
86-
const rest = getUnhandledProps(Icon, this.props)
87-
const ElementType = getElementType(Icon, this.props)
88-
const ariaOptions = this.getIconAriaOptions()
89-
90-
return <ElementType {...rest} {...ariaOptions} className={classes} onClick={this.handleClick} />
91-
}
92-
}
89+
return (
90+
<ElementType {...rest} {...ariaProps} className={classes} onClick={handleClick} ref={ref} />
91+
)
92+
})
9393

94+
Icon.displayName = 'Icon'
9495
Icon.propTypes = {
9596
/** An element type to render as (string or function). */
9697
as: PropTypes.elementType,
@@ -151,8 +152,11 @@ Icon.defaultProps = {
151152
as: 'i',
152153
}
153154

154-
Icon.Group = IconGroup
155+
// Heads up!
156+
// .create() factories should be defined on exported component to be visible as static properties
157+
const MemoIcon = React.memo(Icon)
155158

156-
Icon.create = createShorthandFactory(Icon, (value) => ({ name: value }))
159+
MemoIcon.Group = IconGroup
160+
MemoIcon.create = createShorthandFactory(MemoIcon, (value) => ({ name: value }))
157161

158-
export default Icon
162+
export default MemoIcon

src/elements/Icon/IconGroup.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ import { childrenUtils, customPropTypes, getElementType, getUnhandledProps, SUI
88
/**
99
* Several icons can be used together as a group.
1010
*/
11-
function IconGroup(props) {
11+
const IconGroup = React.forwardRef(function (props, ref) {
1212
const { children, className, content, size } = props
13+
1314
const classes = cx(size, 'icons', className)
1415
const rest = getUnhandledProps(IconGroup, props)
1516
const ElementType = getElementType(IconGroup, props)
1617

1718
return (
18-
<ElementType {...rest} className={classes}>
19+
<ElementType {...rest} className={classes} ref={ref}>
1920
{childrenUtils.isNil(children) ? content : children}
2021
</ElementType>
2122
)
22-
}
23+
})
2324

25+
IconGroup.displayName = 'IconGroup'
2426
IconGroup.propTypes = {
2527
/** An element type to render as (string or function). */
2628
as: PropTypes.elementType,

src/elements/Image/ImageGroup.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ import { childrenUtils, customPropTypes, getElementType, getUnhandledProps, SUI
77
/**
88
* A group of images.
99
*/
10-
function ImageGroup(props) {
10+
const ImageGroup = React.forwardRef(function (props, ref) {
1111
const { children, className, content, size } = props
12+
1213
const classes = cx('ui', size, className, 'images')
1314
const rest = getUnhandledProps(ImageGroup, props)
1415
const ElementType = getElementType(ImageGroup, props)
1516

1617
return (
17-
<ElementType {...rest} className={classes}>
18+
<ElementType {...rest} className={classes} ref={ref}>
1819
{childrenUtils.isNil(children) ? content : children}
1920
</ElementType>
2021
)
21-
}
22+
})
2223

24+
ImageGroup.displayName = 'ImageGroup'
2325
ImageGroup.propTypes = {
2426
/** An element type to render as (string or function). */
2527
as: PropTypes.elementType,

test/specs/commonTests/forwardsRef.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ import { consoleUtil, sandbox } from 'test/utils'
66
/**
77
* Assert a Component correctly implements a shorthand create method.
88
* @param {React.ElementType} Component The component to test
9-
* @param {{ requiredProps?: Object, tagName?: string }} options Options for a test
9+
* @param {{ isMemoized?: Boolean, requiredProps?: Object, tagName?: string }} options
1010
*/
1111
export default function forwardsRef(Component, options = {}) {
1212
describe('forwardsRef', () => {
13-
const { requiredProps = {}, tagName = 'div' } = options
13+
const { isMemoized = false, requiredProps = {}, tagName = 'div' } = options
14+
const RootComponent = isMemoized ? Component.type : Component
1415

1516
it('is produced by React.forwardRef() call', () => {
16-
expect(ReactIs.isForwardRef(<Component {...requiredProps} />)).to.equal(true)
17+
expect(ReactIs.isForwardRef(<RootComponent {...requiredProps} />)).to.equal(true)
1718
})
1819

1920
it('a render function is anonymous', () => {
20-
const innerFunctionName = Component.render.name
21+
const innerFunctionName = RootComponent.render.name
2122
expect(innerFunctionName).to.equal('')
2223
})
2324

test/specs/commonTests/implementsShorthandProp.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ import _ from 'lodash'
22
import React, { createElement } from 'react'
33

44
import { createShorthand } from 'src/lib'
5-
import { consoleUtil } from 'test/utils'
5+
import { consoleUtil, getComponentName } from 'test/utils'
66
import { noDefaultClassNameFromProp } from './classNameHelpers'
77
import helpers from './commonHelpers'
88

99
const shorthandComponentName = (ShorthandComponent) => {
10-
if (typeof ShorthandComponent === 'string') return ShorthandComponent
10+
if (typeof ShorthandComponent === 'string') {
11+
return ShorthandComponent
12+
}
1113

12-
return (
13-
_.get(ShorthandComponent, 'prototype.constructor.name') ||
14-
ShorthandComponent.displayName ||
15-
ShorthandComponent.name
16-
)
14+
return getComponentName(ShorthandComponent)
1715
}
1816

1917
/**
@@ -78,15 +76,15 @@ export default (Component, options = {}) => {
7876

7977
if (alwaysPresent || (Component.defaultProps && Component.defaultProps[propKey])) {
8078
it(`has default ${name} when not defined`, () => {
81-
shallow(<Component {...requiredProps} />).should.have.descendants(name)
79+
shallow(<Component {...requiredProps} />).should.have.descendants(ShorthandComponent)
8280
})
8381
} else {
8482
if (!parentIsFragment) {
8583
noDefaultClassNameFromProp(Component, propKey, [], options)
8684
}
8785

8886
it(`has no ${name} when not defined`, () => {
89-
shallow(<Component {...requiredProps} />).should.not.have.descendants(name)
87+
shallow(<Component {...requiredProps} />).should.not.have.descendants(ShorthandComponent)
9088
})
9189
}
9290

test/specs/elements/Flag/Flag-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const requiredProps = { name: 'us' }
77

88
describe('Flag', () => {
99
common.isConformant(Flag, { requiredProps })
10+
common.forwardsRef(Flag, { isMemoized: true, requiredProps, tagName: 'i' })
1011

1112
common.implementsCreateMethod(Flag)
1213

test/specs/elements/Icon/Icon-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { sandbox } from 'test/utils'
99

1010
describe('Icon', () => {
1111
common.isConformant(Icon)
12+
common.forwardsRef(Icon, { isMemoized: true, tagName: 'i' })
1213
common.hasSubcomponents(Icon, [IconGroup])
1314

1415
common.implementsCreateMethod(Icon)

test/specs/elements/Image/ImageGroup-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as common from 'test/specs/commonTests'
44

55
describe('ImageGroup', () => {
66
common.isConformant(ImageGroup)
7+
common.forwardsRef(ImageGroup)
78
common.hasUIClassName(ImageGroup)
89
common.rendersChildren(ImageGroup)
910

0 commit comments

Comments
 (0)