diff --git a/src/elements/Flag/Flag.js b/src/elements/Flag/Flag.js index cae20263c9..0122003554 100644 --- a/src/elements/Flag/Flag.js +++ b/src/elements/Flag/Flag.js @@ -1,6 +1,6 @@ import cx from 'clsx' import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' +import React from 'react' import { createShorthandFactory, @@ -509,17 +509,16 @@ export const names = [ /** * A flag is is used to represent a political state. */ -class Flag extends PureComponent { - render() { - const { className, name } = this.props - const classes = cx(name, 'flag', className) - const rest = getUnhandledProps(Flag, this.props) - const ElementType = getElementType(Flag, this.props) +const Flag = React.forwardRef(function (props, ref) { + const { className, name } = props + const classes = cx(name, 'flag', className) + const rest = getUnhandledProps(Flag, props) + const ElementType = getElementType(Flag, props) - return - } -} + return +}) +Flag.displayName = 'Flag' Flag.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, @@ -535,6 +534,10 @@ Flag.defaultProps = { as: 'i', } -Flag.create = createShorthandFactory(Flag, (value) => ({ name: value })) +// Heads up! +// .create() factories should be defined on exported component to be visible as static properties +const MemoFlag = React.memo(Flag) + +MemoFlag.create = createShorthandFactory(MemoFlag, (value) => ({ name: value })) -export default Flag +export default MemoFlag diff --git a/src/elements/Icon/Icon.js b/src/elements/Icon/Icon.js index 71c2651ab1..f7d607fcd0 100644 --- a/src/elements/Icon/Icon.js +++ b/src/elements/Icon/Icon.js @@ -1,7 +1,7 @@ import cx from 'clsx' import _ from 'lodash' import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' +import React from 'react' import { createShorthandFactory, @@ -9,88 +9,89 @@ import { getElementType, getUnhandledProps, SUI, + useEventCallback, useKeyOnly, useKeyOrValueAndKey, useValueAndKey, } from '../../lib' import IconGroup from './IconGroup' -/** - * An icon is a glyph used to represent something else. - * @see Image - */ -class Icon extends PureComponent { - getIconAriaOptions() { - const ariaOptions = {} - const { 'aria-label': ariaLabel, 'aria-hidden': ariaHidden } = this.props - - if (_.isNil(ariaLabel)) { - ariaOptions['aria-hidden'] = 'true' - } else { - ariaOptions['aria-label'] = ariaLabel - } +function getAriaProps(props) { + const ariaOptions = {} + const { 'aria-label': ariaLabel, 'aria-hidden': ariaHidden } = props - if (!_.isNil(ariaHidden)) { - ariaOptions['aria-hidden'] = ariaHidden - } + if (_.isNil(ariaLabel)) { + ariaOptions['aria-hidden'] = 'true' + } else { + ariaOptions['aria-label'] = ariaLabel + } - return ariaOptions + if (!_.isNil(ariaHidden)) { + ariaOptions['aria-hidden'] = ariaHidden } - handleClick = (e) => { - const { disabled } = this.props + return ariaOptions +} +/** + * An icon is a glyph used to represent something else. + * @see Image + */ +const Icon = React.forwardRef(function (props, ref) { + const { + bordered, + circular, + className, + color, + corner, + disabled, + fitted, + flipped, + inverted, + link, + loading, + name, + rotated, + size, + } = props + + const classes = cx( + color, + name, + size, + useKeyOnly(bordered, 'bordered'), + useKeyOnly(circular, 'circular'), + useKeyOnly(disabled, 'disabled'), + useKeyOnly(fitted, 'fitted'), + useKeyOnly(inverted, 'inverted'), + useKeyOnly(link, 'link'), + useKeyOnly(loading, 'loading'), + useKeyOrValueAndKey(corner, 'corner'), + useValueAndKey(flipped, 'flipped'), + useValueAndKey(rotated, 'rotated'), + 'icon', + className, + ) + + const rest = getUnhandledProps(Icon, props) + const ElementType = getElementType(Icon, props) + const ariaProps = getAriaProps(props) + + const handleClick = useEventCallback((e) => { if (disabled) { e.preventDefault() return } - _.invoke(this.props, 'onClick', e, this.props) - } + _.invoke(props, 'onClick', e, props) + }) - render() { - const { - bordered, - circular, - className, - color, - corner, - disabled, - fitted, - flipped, - inverted, - link, - loading, - name, - rotated, - size, - } = this.props - - const classes = cx( - color, - name, - size, - useKeyOnly(bordered, 'bordered'), - useKeyOnly(circular, 'circular'), - useKeyOnly(disabled, 'disabled'), - useKeyOnly(fitted, 'fitted'), - useKeyOnly(inverted, 'inverted'), - useKeyOnly(link, 'link'), - useKeyOnly(loading, 'loading'), - useKeyOrValueAndKey(corner, 'corner'), - useValueAndKey(flipped, 'flipped'), - useValueAndKey(rotated, 'rotated'), - 'icon', - className, - ) - const rest = getUnhandledProps(Icon, this.props) - const ElementType = getElementType(Icon, this.props) - const ariaOptions = this.getIconAriaOptions() - - return - } -} + return ( + + ) +}) +Icon.displayName = 'Icon' Icon.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, @@ -151,8 +152,11 @@ Icon.defaultProps = { as: 'i', } -Icon.Group = IconGroup +// Heads up! +// .create() factories should be defined on exported component to be visible as static properties +const MemoIcon = React.memo(Icon) -Icon.create = createShorthandFactory(Icon, (value) => ({ name: value })) +MemoIcon.Group = IconGroup +MemoIcon.create = createShorthandFactory(MemoIcon, (value) => ({ name: value })) -export default Icon +export default MemoIcon diff --git a/src/elements/Icon/IconGroup.js b/src/elements/Icon/IconGroup.js index 934381c432..2c17cfbf36 100644 --- a/src/elements/Icon/IconGroup.js +++ b/src/elements/Icon/IconGroup.js @@ -8,19 +8,21 @@ import { childrenUtils, customPropTypes, getElementType, getUnhandledProps, SUI /** * Several icons can be used together as a group. */ -function IconGroup(props) { +const IconGroup = React.forwardRef(function (props, ref) { const { children, className, content, size } = props + const classes = cx(size, 'icons', className) const rest = getUnhandledProps(IconGroup, props) const ElementType = getElementType(IconGroup, props) return ( - + {childrenUtils.isNil(children) ? content : children} ) -} +}) +IconGroup.displayName = 'IconGroup' IconGroup.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, diff --git a/src/elements/Image/ImageGroup.js b/src/elements/Image/ImageGroup.js index 8f81a54c2c..8ac80b9221 100644 --- a/src/elements/Image/ImageGroup.js +++ b/src/elements/Image/ImageGroup.js @@ -7,19 +7,21 @@ import { childrenUtils, customPropTypes, getElementType, getUnhandledProps, SUI /** * A group of images. */ -function ImageGroup(props) { +const ImageGroup = React.forwardRef(function (props, ref) { const { children, className, content, size } = props + const classes = cx('ui', size, className, 'images') const rest = getUnhandledProps(ImageGroup, props) const ElementType = getElementType(ImageGroup, props) return ( - + {childrenUtils.isNil(children) ? content : children} ) -} +}) +ImageGroup.displayName = 'ImageGroup' ImageGroup.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, diff --git a/test/specs/commonTests/forwardsRef.js b/test/specs/commonTests/forwardsRef.js index 64f0147670..89dd769eef 100644 --- a/test/specs/commonTests/forwardsRef.js +++ b/test/specs/commonTests/forwardsRef.js @@ -6,18 +6,19 @@ import { consoleUtil, sandbox } from 'test/utils' /** * Assert a Component correctly implements a shorthand create method. * @param {React.ElementType} Component The component to test - * @param {{ requiredProps?: Object, tagName?: string }} options Options for a test + * @param {{ isMemoized?: Boolean, requiredProps?: Object, tagName?: string }} options */ export default function forwardsRef(Component, options = {}) { describe('forwardsRef', () => { - const { requiredProps = {}, tagName = 'div' } = options + const { isMemoized = false, requiredProps = {}, tagName = 'div' } = options + const RootComponent = isMemoized ? Component.type : Component it('is produced by React.forwardRef() call', () => { - expect(ReactIs.isForwardRef()).to.equal(true) + expect(ReactIs.isForwardRef()).to.equal(true) }) it('a render function is anonymous', () => { - const innerFunctionName = Component.render.name + const innerFunctionName = RootComponent.render.name expect(innerFunctionName).to.equal('') }) diff --git a/test/specs/commonTests/implementsShorthandProp.js b/test/specs/commonTests/implementsShorthandProp.js index 8dda77f11b..3516a6d251 100644 --- a/test/specs/commonTests/implementsShorthandProp.js +++ b/test/specs/commonTests/implementsShorthandProp.js @@ -2,18 +2,16 @@ import _ from 'lodash' import React, { createElement } from 'react' import { createShorthand } from 'src/lib' -import { consoleUtil } from 'test/utils' +import { consoleUtil, getComponentName } from 'test/utils' import { noDefaultClassNameFromProp } from './classNameHelpers' import helpers from './commonHelpers' const shorthandComponentName = (ShorthandComponent) => { - if (typeof ShorthandComponent === 'string') return ShorthandComponent + if (typeof ShorthandComponent === 'string') { + return ShorthandComponent + } - return ( - _.get(ShorthandComponent, 'prototype.constructor.name') || - ShorthandComponent.displayName || - ShorthandComponent.name - ) + return getComponentName(ShorthandComponent) } /** @@ -78,7 +76,7 @@ export default (Component, options = {}) => { if (alwaysPresent || (Component.defaultProps && Component.defaultProps[propKey])) { it(`has default ${name} when not defined`, () => { - shallow().should.have.descendants(name) + shallow().should.have.descendants(ShorthandComponent) }) } else { if (!parentIsFragment) { @@ -86,7 +84,7 @@ export default (Component, options = {}) => { } it(`has no ${name} when not defined`, () => { - shallow().should.not.have.descendants(name) + shallow().should.not.have.descendants(ShorthandComponent) }) } diff --git a/test/specs/elements/Flag/Flag-test.js b/test/specs/elements/Flag/Flag-test.js index 2d97b300ce..01d04ad9d1 100644 --- a/test/specs/elements/Flag/Flag-test.js +++ b/test/specs/elements/Flag/Flag-test.js @@ -7,6 +7,7 @@ const requiredProps = { name: 'us' } describe('Flag', () => { common.isConformant(Flag, { requiredProps }) + common.forwardsRef(Flag, { isMemoized: true, requiredProps, tagName: 'i' }) common.implementsCreateMethod(Flag) diff --git a/test/specs/elements/Icon/Icon-test.js b/test/specs/elements/Icon/Icon-test.js index b7fd3ca126..3f49e3e46b 100644 --- a/test/specs/elements/Icon/Icon-test.js +++ b/test/specs/elements/Icon/Icon-test.js @@ -9,6 +9,7 @@ import { sandbox } from 'test/utils' describe('Icon', () => { common.isConformant(Icon) + common.forwardsRef(Icon, { isMemoized: true, tagName: 'i' }) common.hasSubcomponents(Icon, [IconGroup]) common.implementsCreateMethod(Icon) diff --git a/test/specs/elements/Image/ImageGroup-test.js b/test/specs/elements/Image/ImageGroup-test.js index 869ea9051a..aac75bc77c 100644 --- a/test/specs/elements/Image/ImageGroup-test.js +++ b/test/specs/elements/Image/ImageGroup-test.js @@ -4,6 +4,7 @@ import * as common from 'test/specs/commonTests' describe('ImageGroup', () => { common.isConformant(ImageGroup) + common.forwardsRef(ImageGroup) common.hasUIClassName(ImageGroup) common.rendersChildren(ImageGroup) diff --git a/test/specs/elements/Input/Input-test.js b/test/specs/elements/Input/Input-test.js index 429c68192d..c005931db3 100644 --- a/test/specs/elements/Input/Input-test.js +++ b/test/specs/elements/Input/Input-test.js @@ -1,5 +1,6 @@ import React from 'react' +import Icon from 'src/elements/Icon/Icon' import Input from 'src/elements/Input/Input' import { htmlInputProps } from 'src/lib' import * as common from 'test/specs/commonTests' @@ -184,13 +185,13 @@ describe('Input', () => { describe('loading', () => { it("don't add icon if it's defined", () => { shallow() - .find('Icon') + .find(Icon) .should.have.prop('name', 'user') }) it("adds icon if it's not defined", () => { shallow() - .find('Icon') + .find(Icon) .should.have.prop('name', 'spinner') }) }) @@ -291,41 +292,36 @@ describe('Input', () => { describe('icon', () => { it('is second child', () => { shallow() - .children() - .at(1) - .is('Icon') + .childAt(1) + .is(Icon) .should.be.true() }) it('is third child with action positioned left', () => { shallow() - .children() - .at(2) - .is('Icon') + .childAt(2) + .is(Icon) .should.be.true() }) it('is third child with label', () => { shallow() - .children() - .at(2) - .is('Icon') + .childAt(2) + .is(Icon) .should.be.true() }) it('is second child with action', () => { shallow() - .children() - .at(1) - .is('Icon') + .childAt(1) + .is(Icon) .should.be.true() }) it('is second child with label positioned right', () => { shallow() - .children() - .at(1) - .is('Icon') + .childAt(1) + .is(Icon) .should.be.true() }) }) diff --git a/test/specs/elements/Label/Label-test.js b/test/specs/elements/Label/Label-test.js index ee6492778c..b7b53bc1c5 100644 --- a/test/specs/elements/Label/Label-test.js +++ b/test/specs/elements/Label/Label-test.js @@ -1,6 +1,7 @@ import _ from 'lodash' import React from 'react' +import Icon from 'src/elements/Icon/Icon' import Label from 'src/elements/Label/Label' import LabelDetail from 'src/elements/Label/LabelDetail' import LabelGroup from 'src/elements/Label/LabelGroup' @@ -61,19 +62,19 @@ describe('Label', () => { it('has delete icon by default', () => { shallow(