From ac81442ace584b6deb2aaa8298a76036dbee77fc Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 4 Aug 2021 14:23:25 +0200 Subject: [PATCH] chore(Dropdown*): use React.forwardRef() (#4273) * chore(Dropdown*): use React.forwardRef() * fix examples-test --- src/modules/Dropdown/DropdownDivider.js | 8 +- src/modules/Dropdown/DropdownHeader.js | 9 +- src/modules/Dropdown/DropdownItem.js | 139 +++++++++--------- src/modules/Dropdown/DropdownMenu.js | 8 +- src/modules/Dropdown/DropdownSearchInput.js | 51 ++++--- src/modules/Dropdown/DropdownText.js | 14 +- test/specs/docs/examples-test.js | 13 +- .../modules/Dropdown/DropdownDivider-test.js | 1 + .../modules/Dropdown/DropdownHeader-test.js | 1 + .../modules/Dropdown/DropdownItem-test.js | 1 + .../modules/Dropdown/DropdownMenu-test.js | 1 + .../Dropdown/DropdownSearchInput-test.js | 3 +- .../modules/Dropdown/DropdownText-test.js | 1 + 13 files changed, 138 insertions(+), 112 deletions(-) diff --git a/src/modules/Dropdown/DropdownDivider.js b/src/modules/Dropdown/DropdownDivider.js index f784bce196..e4f283bbe5 100644 --- a/src/modules/Dropdown/DropdownDivider.js +++ b/src/modules/Dropdown/DropdownDivider.js @@ -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 -} + return +}) +DropdownDivider.displayName = 'DropdownDivider' DropdownDivider.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, diff --git a/src/modules/Dropdown/DropdownHeader.js b/src/modules/Dropdown/DropdownHeader.js index 7d848d317f..9cb15436a0 100644 --- a/src/modules/Dropdown/DropdownHeader.js +++ b/src/modules/Dropdown/DropdownHeader.js @@ -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) @@ -23,20 +23,21 @@ function DropdownHeader(props) { if (!childrenUtils.isNil(children)) { return ( - + {children} ) } return ( - + {Icon.create(icon, { autoGenerateKey: false })} {content} ) -} +}) +DropdownHeader.displayName = 'DropdownHeader' DropdownHeader.propTypes = { /** An element type to render as (string or function) */ as: PropTypes.elementType, diff --git a/src/modules/Dropdown/DropdownItem.js b/src/modules/Dropdown/DropdownItem.js index 5d7c3fd270..5f4fb65419 100644 --- a/src/modules/Dropdown/DropdownItem.js +++ b/src/modules/Dropdown/DropdownItem.js @@ -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, @@ -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 ( - - {children} - - ) - } - - 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 ( - - {imageElement} - {iconElement} - {flagElement} - {labelElement} - {descriptionElement} - {textElement} + + {children} ) } -} + 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 ( + + {imageElement} + {iconElement} + {flagElement} + {labelElement} + {descriptionElement} + {textElement} + + ) +}) + +DropdownItem.displayName = 'DropdownItem' DropdownItem.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, diff --git a/src/modules/Dropdown/DropdownMenu.js b/src/modules/Dropdown/DropdownMenu.js index b16b6fdadc..cdc53ef6b1 100644 --- a/src/modules/Dropdown/DropdownMenu.js +++ b/src/modules/Dropdown/DropdownMenu.js @@ -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'), @@ -26,12 +27,13 @@ function DropdownMenu(props) { const ElementType = getElementType(DropdownMenu, props) return ( - + {childrenUtils.isNil(children) ? content : children} ) -} +}) +DropdownMenu.displayName = 'DropdownMenu' DropdownMenu.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, diff --git a/src/modules/Dropdown/DropdownSearchInput.js b/src/modules/Dropdown/DropdownSearchInput.js index 3f36ebae4d..8f46f28f1c 100644 --- a/src/modules/Dropdown/DropdownSearchInput.js +++ b/src/modules/Dropdown/DropdownSearchInput.js @@ -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 ( - - ) - } -} + return ( + + ) +}) +DropdownSearchInput.displayName = 'DropdownSearchInput' DropdownSearchInput.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, @@ -56,6 +58,7 @@ DropdownSearchInput.propTypes = { } DropdownSearchInput.defaultProps = { + as: 'input', autoComplete: 'off', type: 'text', } diff --git a/src/modules/Dropdown/DropdownText.js b/src/modules/Dropdown/DropdownText.js index 7cf9a0246b..fd7a106947 100644 --- a/src/modules/Dropdown/DropdownText.js +++ b/src/modules/Dropdown/DropdownText.js @@ -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 ( - + {childrenUtils.isNil(children) ? content : children} ) -} +}) +DropdownText.displayName = 'DropdownText' DropdownText.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, diff --git a/test/specs/docs/examples-test.js b/test/specs/docs/examples-test.js index 19900b82e6..ed0886f96c 100644 --- a/test/specs/docs/examples-test.js +++ b/test/specs/docs/examples-test.js @@ -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 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() }) }) }) diff --git a/test/specs/modules/Dropdown/DropdownDivider-test.js b/test/specs/modules/Dropdown/DropdownDivider-test.js index 1a26b43107..cb6edf3dbc 100644 --- a/test/specs/modules/Dropdown/DropdownDivider-test.js +++ b/test/specs/modules/Dropdown/DropdownDivider-test.js @@ -3,4 +3,5 @@ import * as common from 'test/specs/commonTests' describe('DropdownDivider', () => { common.isConformant(DropdownDivider) + common.forwardsRef(DropdownDivider) }) diff --git a/test/specs/modules/Dropdown/DropdownHeader-test.js b/test/specs/modules/Dropdown/DropdownHeader-test.js index faa0d29373..74da305e98 100644 --- a/test/specs/modules/Dropdown/DropdownHeader-test.js +++ b/test/specs/modules/Dropdown/DropdownHeader-test.js @@ -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 }) diff --git a/test/specs/modules/Dropdown/DropdownItem-test.js b/test/specs/modules/Dropdown/DropdownItem-test.js index aeef0ae73e..594ef62730 100644 --- a/test/specs/modules/Dropdown/DropdownItem-test.js +++ b/test/specs/modules/Dropdown/DropdownItem-test.js @@ -8,6 +8,7 @@ import Flag from 'src/elements/Flag' describe('DropdownItem', () => { common.isConformant(DropdownItem) + common.forwardsRef(DropdownItem) common.rendersChildren(DropdownItem, { rendersContent: false, }) diff --git a/test/specs/modules/Dropdown/DropdownMenu-test.js b/test/specs/modules/Dropdown/DropdownMenu-test.js index 412b1ac267..4c2ddd20d5 100644 --- a/test/specs/modules/Dropdown/DropdownMenu-test.js +++ b/test/specs/modules/Dropdown/DropdownMenu-test.js @@ -3,6 +3,7 @@ import * as common from 'test/specs/commonTests' describe('DropdownMenu', () => { common.isConformant(DropdownMenu) + common.forwardsRef(DropdownMenu) common.rendersChildren(DropdownMenu) common.propValueOnlyToClassName(DropdownMenu, 'direction', ['left', 'right']) diff --git a/test/specs/modules/Dropdown/DropdownSearchInput-test.js b/test/specs/modules/Dropdown/DropdownSearchInput-test.js index c9420fe86a..c457453183 100644 --- a/test/specs/modules/Dropdown/DropdownSearchInput-test.js +++ b/test/specs/modules/Dropdown/DropdownSearchInput-test.js @@ -6,7 +6,8 @@ import { sandbox } from 'test/utils' import DropdownSearchInput from 'src/modules/Dropdown/DropdownSearchInput' describe('DropdownSearchInput', () => { - common.hasValidTypings(DropdownSearchInput) + common.isConformant(DropdownSearchInput) + common.forwardsRef(DropdownSearchInput, { tagName: 'input' }) describe('aria', () => { it('should have aria-autocomplete', () => { diff --git a/test/specs/modules/Dropdown/DropdownText-test.js b/test/specs/modules/Dropdown/DropdownText-test.js index cdf1297a74..ef7f31537d 100644 --- a/test/specs/modules/Dropdown/DropdownText-test.js +++ b/test/specs/modules/Dropdown/DropdownText-test.js @@ -5,6 +5,7 @@ import * as common from 'test/specs/commonTests' describe('DropdownText', () => { common.isConformant(DropdownText) + common.forwardsRef(DropdownText) common.rendersChildren(DropdownText) it('aria attributes', () => {