Skip to content

Commit ac81442

Browse files
authored
chore(Dropdown*): use React.forwardRef() (#4273)
* chore(Dropdown*): use React.forwardRef() * fix examples-test
1 parent 67dd227 commit ac81442

13 files changed

+138
-112
lines changed

src/modules/Dropdown/DropdownDivider.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ import { getElementType, getUnhandledProps } from '../../lib'
77
/**
88
* A dropdown menu can contain dividers to separate related content.
99
*/
10-
function DropdownDivider(props) {
10+
const DropdownDivider = React.forwardRef(function (props, ref) {
1111
const { className } = props
12+
1213
const classes = cx('divider', className)
1314
const rest = getUnhandledProps(DropdownDivider, props)
1415
const ElementType = getElementType(DropdownDivider, props)
1516

16-
return <ElementType {...rest} className={classes} />
17-
}
17+
return <ElementType {...rest} className={classes} ref={ref} />
18+
})
1819

20+
DropdownDivider.displayName = 'DropdownDivider'
1921
DropdownDivider.propTypes = {
2022
/** An element type to render as (string or function). */
2123
as: PropTypes.elementType,

src/modules/Dropdown/DropdownHeader.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Icon from '../../elements/Icon'
1414
/**
1515
* A dropdown menu can contain a header.
1616
*/
17-
function DropdownHeader(props) {
17+
const DropdownHeader = React.forwardRef(function (props, ref) {
1818
const { children, className, content, icon } = props
1919

2020
const classes = cx('header', className)
@@ -23,20 +23,21 @@ function DropdownHeader(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
}
3131

3232
return (
33-
<ElementType {...rest} className={classes}>
33+
<ElementType {...rest} className={classes} ref={ref}>
3434
{Icon.create(icon, { autoGenerateKey: false })}
3535
{content}
3636
</ElementType>
3737
)
38-
}
38+
})
3939

40+
DropdownHeader.displayName = 'DropdownHeader'
4041
DropdownHeader.propTypes = {
4142
/** An element type to render as (string or function) */
4243
as: PropTypes.elementType,

src/modules/Dropdown/DropdownItem.js

Lines changed: 69 additions & 70 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,
@@ -20,83 +20,82 @@ import Label from '../../elements/Label'
2020
/**
2121
* An item sub-component for Dropdown component.
2222
*/
23-
class DropdownItem extends Component {
24-
handleClick = (e) => {
25-
_.invoke(this.props, 'onClick', e, this.props)
23+
const DropdownItem = React.forwardRef(function (props, ref) {
24+
const {
25+
active,
26+
children,
27+
className,
28+
content,
29+
disabled,
30+
description,
31+
flag,
32+
icon,
33+
image,
34+
label,
35+
selected,
36+
text,
37+
} = props
38+
39+
const handleClick = (e) => {
40+
_.invoke(props, 'onClick', e, props)
2641
}
2742

28-
render() {
29-
const {
30-
active,
31-
children,
32-
className,
33-
content,
34-
disabled,
35-
description,
36-
flag,
37-
icon,
38-
image,
39-
label,
40-
selected,
41-
text,
42-
} = this.props
43-
44-
const classes = cx(
45-
useKeyOnly(active, 'active'),
46-
useKeyOnly(disabled, 'disabled'),
47-
useKeyOnly(selected, 'selected'),
48-
'item',
49-
className,
50-
)
51-
// add default dropdown icon if item contains another menu
52-
const iconName = _.isNil(icon)
53-
? childrenUtils.someByType(children, 'DropdownMenu') && 'dropdown'
54-
: icon
55-
const rest = getUnhandledProps(DropdownItem, this.props)
56-
const ElementType = getElementType(DropdownItem, this.props)
57-
const ariaOptions = {
58-
role: 'option',
59-
'aria-disabled': disabled,
60-
'aria-checked': active,
61-
'aria-selected': selected,
62-
}
63-
64-
if (!childrenUtils.isNil(children)) {
65-
return (
66-
<ElementType {...rest} {...ariaOptions} className={classes} onClick={this.handleClick}>
67-
{children}
68-
</ElementType>
69-
)
70-
}
71-
72-
const flagElement = Flag.create(flag, { autoGenerateKey: false })
73-
const iconElement = Icon.create(iconName, { autoGenerateKey: false })
74-
const imageElement = Image.create(image, { autoGenerateKey: false })
75-
const labelElement = Label.create(label, { autoGenerateKey: false })
76-
const descriptionElement = createShorthand('span', (val) => ({ children: val }), description, {
77-
defaultProps: { className: 'description' },
78-
autoGenerateKey: false,
79-
})
80-
const textElement = createShorthand(
81-
'span',
82-
(val) => ({ children: val }),
83-
childrenUtils.isNil(content) ? text : content,
84-
{ defaultProps: { className: 'text' }, autoGenerateKey: false },
85-
)
43+
const classes = cx(
44+
useKeyOnly(active, 'active'),
45+
useKeyOnly(disabled, 'disabled'),
46+
useKeyOnly(selected, 'selected'),
47+
'item',
48+
className,
49+
)
50+
// add default dropdown icon if item contains another menu
51+
const iconName = _.isNil(icon)
52+
? childrenUtils.someByType(children, 'DropdownMenu') && 'dropdown'
53+
: icon
54+
const rest = getUnhandledProps(DropdownItem, props)
55+
const ElementType = getElementType(DropdownItem, props)
56+
const ariaOptions = {
57+
role: 'option',
58+
'aria-disabled': disabled,
59+
'aria-checked': active,
60+
'aria-selected': selected,
61+
}
8662

63+
if (!childrenUtils.isNil(children)) {
8764
return (
88-
<ElementType {...rest} {...ariaOptions} className={classes} onClick={this.handleClick}>
89-
{imageElement}
90-
{iconElement}
91-
{flagElement}
92-
{labelElement}
93-
{descriptionElement}
94-
{textElement}
65+
<ElementType {...rest} {...ariaOptions} className={classes} onClick={handleClick} ref={ref}>
66+
{children}
9567
</ElementType>
9668
)
9769
}
98-
}
9970

71+
const flagElement = Flag.create(flag, { autoGenerateKey: false })
72+
const iconElement = Icon.create(iconName, { autoGenerateKey: false })
73+
const imageElement = Image.create(image, { autoGenerateKey: false })
74+
const labelElement = Label.create(label, { autoGenerateKey: false })
75+
const descriptionElement = createShorthand('span', (val) => ({ children: val }), description, {
76+
defaultProps: { className: 'description' },
77+
autoGenerateKey: false,
78+
})
79+
const textElement = createShorthand(
80+
'span',
81+
(val) => ({ children: val }),
82+
childrenUtils.isNil(content) ? text : content,
83+
{ defaultProps: { className: 'text' }, autoGenerateKey: false },
84+
)
85+
86+
return (
87+
<ElementType {...rest} {...ariaOptions} className={classes} onClick={handleClick} ref={ref}>
88+
{imageElement}
89+
{iconElement}
90+
{flagElement}
91+
{labelElement}
92+
{descriptionElement}
93+
{textElement}
94+
</ElementType>
95+
)
96+
})
97+
98+
DropdownItem.displayName = 'DropdownItem'
10099
DropdownItem.propTypes = {
101100
/** An element type to render as (string or function). */
102101
as: PropTypes.elementType,

src/modules/Dropdown/DropdownMenu.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import {
1313
/**
1414
* A dropdown menu can contain a menu.
1515
*/
16-
function DropdownMenu(props) {
16+
const DropdownMenu = React.forwardRef(function (props, ref) {
1717
const { children, className, content, direction, open, scrolling } = props
18+
1819
const classes = cx(
1920
direction,
2021
useKeyOnly(open, 'visible'),
@@ -26,12 +27,13 @@ function DropdownMenu(props) {
2627
const ElementType = getElementType(DropdownMenu, props)
2728

2829
return (
29-
<ElementType {...rest} className={classes}>
30+
<ElementType {...rest} className={classes} ref={ref}>
3031
{childrenUtils.isNil(children) ? content : children}
3132
</ElementType>
3233
)
33-
}
34+
})
3435

36+
DropdownMenu.displayName = 'DropdownMenu'
3537
DropdownMenu.propTypes = {
3638
/** An element type to render as (string or function). */
3739
as: PropTypes.elementType,

src/modules/Dropdown/DropdownSearchInput.js

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,42 @@
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

6-
import { createShorthandFactory, getUnhandledProps } from '../../lib'
6+
import { createShorthandFactory, getElementType, getUnhandledProps } from '../../lib'
77

88
/**
99
* A search item sub-component for Dropdown component.
1010
*/
11-
class DropdownSearchInput extends Component {
12-
handleChange = (e) => {
13-
const value = _.get(e, 'target.value')
11+
const DropdownSearchInput = React.forwardRef(function (props, ref) {
12+
const { autoComplete, className, tabIndex, type, value } = props
1413

15-
_.invoke(this.props, 'onChange', e, { ...this.props, value })
14+
const handleChange = (e) => {
15+
const newValue = _.get(e, 'target.value')
16+
17+
_.invoke(props, 'onChange', e, { ...props, value: newValue })
1618
}
1719

18-
render() {
19-
const { autoComplete, className, tabIndex, type, value } = this.props
20-
const classes = cx('search', className)
21-
const rest = getUnhandledProps(DropdownSearchInput, this.props)
20+
const classes = cx('search', className)
21+
const ElementType = getElementType(DropdownSearchInput, props)
22+
const rest = getUnhandledProps(DropdownSearchInput, props)
2223

23-
return (
24-
<input
25-
{...rest}
26-
aria-autocomplete='list'
27-
autoComplete={autoComplete}
28-
className={classes}
29-
onChange={this.handleChange}
30-
tabIndex={tabIndex}
31-
type={type}
32-
value={value}
33-
/>
34-
)
35-
}
36-
}
24+
return (
25+
<ElementType
26+
aria-autocomplete='list'
27+
{...rest}
28+
autoComplete={autoComplete}
29+
className={classes}
30+
onChange={handleChange}
31+
ref={ref}
32+
tabIndex={tabIndex}
33+
type={type}
34+
value={value}
35+
/>
36+
)
37+
})
3738

39+
DropdownSearchInput.displayName = 'DropdownSearchInput'
3840
DropdownSearchInput.propTypes = {
3941
/** An element type to render as (string or function). */
4042
as: PropTypes.elementType,
@@ -56,6 +58,7 @@ DropdownSearchInput.propTypes = {
5658
}
5759

5860
DropdownSearchInput.defaultProps = {
61+
as: 'input',
5962
autoComplete: 'off',
6063
type: 'text',
6164
}

src/modules/Dropdown/DropdownText.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,27 @@ import {
1313
/**
1414
* A dropdown contains a selected value.
1515
*/
16-
function DropdownText(props) {
16+
const DropdownText = React.forwardRef(function (props, ref) {
1717
const { children, className, content } = props
1818
const classes = cx('divider', className)
1919
const rest = getUnhandledProps(DropdownText, props)
2020
const ElementType = getElementType(DropdownText, props)
2121

2222
return (
23-
<ElementType aria-atomic aria-live='polite' role='alert' {...rest} className={classes}>
23+
<ElementType
24+
aria-atomic
25+
aria-live='polite'
26+
role='alert'
27+
{...rest}
28+
className={classes}
29+
ref={ref}
30+
>
2431
{childrenUtils.isNil(children) ? content : children}
2532
</ElementType>
2633
)
27-
}
34+
})
2835

36+
DropdownText.displayName = 'DropdownText'
2937
DropdownText.propTypes = {
3038
/** An element type to render as (string or function). */
3139
as: PropTypes.elementType,

test/specs/docs/examples-test.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
import { createElement } from 'react'
1+
import * as React from 'react'
22

33
const exampleContext = require.context('docs/src/examples', true, /\w+Example\w*\.js$/)
4+
let wrapper
45

56
describe('examples', () => {
7+
afterEach(() => {
8+
wrapper.unmount()
9+
})
10+
611
exampleContext.keys().forEach((path) => {
712
const filename = path.replace(/^.*\/(\w+\.js)$/, '$1')
813

914
it(`${filename} renders without console activity`, () => {
10-
// TODO also render the example's path in a <ComponentExample /> just as the docs do
11-
const wrapper = mount(createElement(exampleContext(path).default))
15+
const Component = exampleContext(path).default
1216

13-
wrapper.unmount()
17+
wrapper = mount(React.createElement(Component))
18+
wrapper.should.not.be.blank()
1419
})
1520
})
1621
})

test/specs/modules/Dropdown/DropdownDivider-test.js

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

44
describe('DropdownDivider', () => {
55
common.isConformant(DropdownDivider)
6+
common.forwardsRef(DropdownDivider)
67
})

test/specs/modules/Dropdown/DropdownHeader-test.js

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

44
describe('DropdownHeader', () => {
55
common.isConformant(DropdownHeader)
6+
common.forwardsRef(DropdownHeader)
67
common.rendersChildren(DropdownHeader)
78

89
common.implementsIconProp(DropdownHeader, { autoGenerateKey: false })

test/specs/modules/Dropdown/DropdownItem-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Flag from 'src/elements/Flag'
88

99
describe('DropdownItem', () => {
1010
common.isConformant(DropdownItem)
11+
common.forwardsRef(DropdownItem)
1112
common.rendersChildren(DropdownItem, {
1213
rendersContent: false,
1314
})

0 commit comments

Comments
 (0)