Skip to content

Commit

Permalink
chore(Dropdown*): use React.forwardRef() (#4273)
Browse files Browse the repository at this point in the history
* chore(Dropdown*): use React.forwardRef()

* fix examples-test
  • Loading branch information
layershifter committed Apr 22, 2022
1 parent 61c5c67 commit fec735d
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 112 deletions.
8 changes: 5 additions & 3 deletions src/modules/Dropdown/DropdownDivider.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import { getElementType, getUnhandledProps } from '../../lib'
/**
* A dropdown menu can contain dividers to separate related content.
*/
function DropdownDivider(props) {
const DropdownDivider = React.forwardRef(function (props, ref) {
const { className } = props

const classes = cx('divider', className)
const rest = getUnhandledProps(DropdownDivider, props)
const ElementType = getElementType(DropdownDivider, props)

return <ElementType {...rest} className={classes} />
}
return <ElementType {...rest} className={classes} ref={ref} />
})

DropdownDivider.displayName = 'DropdownDivider'
DropdownDivider.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
9 changes: 5 additions & 4 deletions src/modules/Dropdown/DropdownHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Icon from '../../elements/Icon'
/**
* A dropdown menu can contain a header.
*/
function DropdownHeader(props) {
const DropdownHeader = React.forwardRef(function (props, ref) {
const { children, className, content, icon } = props

const classes = cx('header', className)
Expand All @@ -23,20 +23,21 @@ function DropdownHeader(props) {

if (!childrenUtils.isNil(children)) {
return (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{children}
</ElementType>
)
}

return (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{Icon.create(icon, { autoGenerateKey: false })}
{content}
</ElementType>
)
}
})

DropdownHeader.displayName = 'DropdownHeader'
DropdownHeader.propTypes = {
/** An element type to render as (string or function) */
as: PropTypes.elementType,
Expand Down
139 changes: 69 additions & 70 deletions src/modules/Dropdown/DropdownItem.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cx from 'clsx'
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import React from 'react'

import {
childrenUtils,
Expand All @@ -20,83 +20,82 @@ import Label from '../../elements/Label'
/**
* An item sub-component for Dropdown component.
*/
class DropdownItem extends Component {
handleClick = (e) => {
_.invoke(this.props, 'onClick', e, this.props)
const DropdownItem = React.forwardRef(function (props, ref) {
const {
active,
children,
className,
content,
disabled,
description,
flag,
icon,
image,
label,
selected,
text,
} = props

const handleClick = (e) => {
_.invoke(props, 'onClick', e, props)
}

render() {
const {
active,
children,
className,
content,
disabled,
description,
flag,
icon,
image,
label,
selected,
text,
} = this.props

const classes = cx(
useKeyOnly(active, 'active'),
useKeyOnly(disabled, 'disabled'),
useKeyOnly(selected, 'selected'),
'item',
className,
)
// add default dropdown icon if item contains another menu
const iconName = _.isNil(icon)
? childrenUtils.someByType(children, 'DropdownMenu') && 'dropdown'
: icon
const rest = getUnhandledProps(DropdownItem, this.props)
const ElementType = getElementType(DropdownItem, this.props)
const ariaOptions = {
role: 'option',
'aria-disabled': disabled,
'aria-checked': active,
'aria-selected': selected,
}

if (!childrenUtils.isNil(children)) {
return (
<ElementType {...rest} {...ariaOptions} className={classes} onClick={this.handleClick}>
{children}
</ElementType>
)
}

const flagElement = Flag.create(flag, { autoGenerateKey: false })
const iconElement = Icon.create(iconName, { autoGenerateKey: false })
const imageElement = Image.create(image, { autoGenerateKey: false })
const labelElement = Label.create(label, { autoGenerateKey: false })
const descriptionElement = createShorthand('span', (val) => ({ children: val }), description, {
defaultProps: { className: 'description' },
autoGenerateKey: false,
})
const textElement = createShorthand(
'span',
(val) => ({ children: val }),
childrenUtils.isNil(content) ? text : content,
{ defaultProps: { className: 'text' }, autoGenerateKey: false },
)
const classes = cx(
useKeyOnly(active, 'active'),
useKeyOnly(disabled, 'disabled'),
useKeyOnly(selected, 'selected'),
'item',
className,
)
// add default dropdown icon if item contains another menu
const iconName = _.isNil(icon)
? childrenUtils.someByType(children, 'DropdownMenu') && 'dropdown'
: icon
const rest = getUnhandledProps(DropdownItem, props)
const ElementType = getElementType(DropdownItem, props)
const ariaOptions = {
role: 'option',
'aria-disabled': disabled,
'aria-checked': active,
'aria-selected': selected,
}

if (!childrenUtils.isNil(children)) {
return (
<ElementType {...rest} {...ariaOptions} className={classes} onClick={this.handleClick}>
{imageElement}
{iconElement}
{flagElement}
{labelElement}
{descriptionElement}
{textElement}
<ElementType {...rest} {...ariaOptions} className={classes} onClick={handleClick} ref={ref}>
{children}
</ElementType>
)
}
}

const flagElement = Flag.create(flag, { autoGenerateKey: false })
const iconElement = Icon.create(iconName, { autoGenerateKey: false })
const imageElement = Image.create(image, { autoGenerateKey: false })
const labelElement = Label.create(label, { autoGenerateKey: false })
const descriptionElement = createShorthand('span', (val) => ({ children: val }), description, {
defaultProps: { className: 'description' },
autoGenerateKey: false,
})
const textElement = createShorthand(
'span',
(val) => ({ children: val }),
childrenUtils.isNil(content) ? text : content,
{ defaultProps: { className: 'text' }, autoGenerateKey: false },
)

return (
<ElementType {...rest} {...ariaOptions} className={classes} onClick={handleClick} ref={ref}>
{imageElement}
{iconElement}
{flagElement}
{labelElement}
{descriptionElement}
{textElement}
</ElementType>
)
})

DropdownItem.displayName = 'DropdownItem'
DropdownItem.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
8 changes: 5 additions & 3 deletions src/modules/Dropdown/DropdownMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
/**
* A dropdown menu can contain a menu.
*/
function DropdownMenu(props) {
const DropdownMenu = React.forwardRef(function (props, ref) {
const { children, className, content, direction, open, scrolling } = props

const classes = cx(
direction,
useKeyOnly(open, 'visible'),
Expand All @@ -26,12 +27,13 @@ function DropdownMenu(props) {
const ElementType = getElementType(DropdownMenu, props)

return (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{childrenUtils.isNil(children) ? content : children}
</ElementType>
)
}
})

DropdownMenu.displayName = 'DropdownMenu'
DropdownMenu.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
51 changes: 27 additions & 24 deletions src/modules/Dropdown/DropdownSearchInput.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
import cx from 'clsx'
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import React from 'react'

import { createShorthandFactory, getUnhandledProps } from '../../lib'
import { createShorthandFactory, getElementType, getUnhandledProps } from '../../lib'

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

_.invoke(this.props, 'onChange', e, { ...this.props, value })
const handleChange = (e) => {
const newValue = _.get(e, 'target.value')

_.invoke(props, 'onChange', e, { ...props, value: newValue })
}

render() {
const { autoComplete, className, tabIndex, type, value } = this.props
const classes = cx('search', className)
const rest = getUnhandledProps(DropdownSearchInput, this.props)
const classes = cx('search', className)
const ElementType = getElementType(DropdownSearchInput, props)
const rest = getUnhandledProps(DropdownSearchInput, props)

return (
<input
{...rest}
aria-autocomplete='list'
autoComplete={autoComplete}
className={classes}
onChange={this.handleChange}
tabIndex={tabIndex}
type={type}
value={value}
/>
)
}
}
return (
<ElementType
aria-autocomplete='list'
{...rest}
autoComplete={autoComplete}
className={classes}
onChange={handleChange}
ref={ref}
tabIndex={tabIndex}
type={type}
value={value}
/>
)
})

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

DropdownSearchInput.defaultProps = {
as: 'input',
autoComplete: 'off',
type: 'text',
}
Expand Down
14 changes: 11 additions & 3 deletions src/modules/Dropdown/DropdownText.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,27 @@ import {
/**
* A dropdown contains a selected value.
*/
function DropdownText(props) {
const DropdownText = React.forwardRef(function (props, ref) {
const { children, className, content } = props
const classes = cx('divider', className)
const rest = getUnhandledProps(DropdownText, props)
const ElementType = getElementType(DropdownText, props)

return (
<ElementType aria-atomic aria-live='polite' role='alert' {...rest} className={classes}>
<ElementType
aria-atomic
aria-live='polite'
role='alert'
{...rest}
className={classes}
ref={ref}
>
{childrenUtils.isNil(children) ? content : children}
</ElementType>
)
}
})

DropdownText.displayName = 'DropdownText'
DropdownText.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
13 changes: 9 additions & 4 deletions test/specs/docs/examples-test.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { createElement } from 'react'
import * as React from 'react'

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

describe('examples', () => {
afterEach(() => {
wrapper.unmount()
})

exampleContext.keys().forEach((path) => {
const filename = path.replace(/^.*\/(\w+\.js)$/, '$1')

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

wrapper.unmount()
wrapper = mount(React.createElement(Component))
wrapper.should.not.be.blank()
})
})
})
1 change: 1 addition & 0 deletions test/specs/modules/Dropdown/DropdownDivider-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import * as common from 'test/specs/commonTests'

describe('DropdownDivider', () => {
common.isConformant(DropdownDivider)
common.forwardsRef(DropdownDivider)
})
1 change: 1 addition & 0 deletions test/specs/modules/Dropdown/DropdownHeader-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as common from 'test/specs/commonTests'

describe('DropdownHeader', () => {
common.isConformant(DropdownHeader)
common.forwardsRef(DropdownHeader)
common.rendersChildren(DropdownHeader)

common.implementsIconProp(DropdownHeader, { autoGenerateKey: false })
Expand Down
1 change: 1 addition & 0 deletions test/specs/modules/Dropdown/DropdownItem-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Flag from 'src/elements/Flag'

describe('DropdownItem', () => {
common.isConformant(DropdownItem)
common.forwardsRef(DropdownItem)
common.rendersChildren(DropdownItem, {
rendersContent: false,
})
Expand Down

0 comments on commit fec735d

Please sign in to comment.