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()
- .find('Icon')
+ .find(Icon)
.should.have.prop('name', 'delete')
})
it('uses passed removeIcon string', () => {
shallow()
- .find('Icon')
+ .find(Icon)
.should.have.prop('name', 'foo')
})
it('uses passed removeIcon props', () => {
shallow()
- .find('Icon')
+ .find(Icon)
.should.have.prop('data-foo', true)
})
@@ -86,7 +87,7 @@ describe('Label', () => {
const labelProps = { onRemove: labelSpy, removeIcon: iconProps }
mount()
- .find('Icon')
+ .find(Icon)
.simulate('click', event)
iconSpy.should.have.been.calledOnce()
diff --git a/test/specs/modules/Dropdown/Dropdown-test.js b/test/specs/modules/Dropdown/Dropdown-test.js
index 76d80811a4..94f321f8b4 100644
--- a/test/specs/modules/Dropdown/Dropdown-test.js
+++ b/test/specs/modules/Dropdown/Dropdown-test.js
@@ -4,6 +4,7 @@ import React from 'react'
import * as common from 'test/specs/commonTests'
import { consoleUtil, domEvent, sandbox } from 'test/utils'
+import Icon from 'src/elements/Icon/Icon'
import Label from 'src/elements/Label/Label'
import Dropdown from 'src/modules/Dropdown/Dropdown'
import DropdownDivider from 'src/modules/Dropdown/DropdownDivider'
@@ -377,7 +378,7 @@ describe('Dropdown', () => {
const onChange = sandbox.spy()
wrapperShallow()
- wrapper.find('Icon').simulate('click', { stopPropagation: _.noop })
+ wrapper.find(Icon).simulate('click', { stopPropagation: _.noop })
onChange.should.have.not.been.called()
})
@@ -385,7 +386,7 @@ describe('Dropdown', () => {
const onChange = sandbox.spy()
wrapperShallow()
- wrapper.find('Icon').simulate('click', { stopPropagation: _.noop })
+ wrapper.find(Icon).simulate('click', { stopPropagation: _.noop })
onChange.should.have.not.been.called()
})
@@ -1413,7 +1414,7 @@ describe('Dropdown', () => {
// /~https://github.com/Semantic-Org/Semantic-UI-React/issues/2600
const onOpen = sandbox.spy()
wrapperShallow()
- .find('Icon')
+ .find(Icon)
.simulate('click', { stopPropagation: _.noop })
onOpen.should.have.been.calledOnce()