Skip to content

Commit

Permalink
chore(Flag|Icon|ImageGroup): use React.forwardRef() (#4264)
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Dec 13, 2022
1 parent 898dc3f commit ac7134a
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 123 deletions.
27 changes: 15 additions & 12 deletions src/elements/Flag/Flag.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cx from 'clsx'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import React from 'react'

import {
createShorthandFactory,
Expand Down Expand Up @@ -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 <ElementType {...rest} className={classes} />
}
}
return <ElementType {...rest} className={classes} ref={ref} />
})

Flag.displayName = 'Flag'
Flag.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand All @@ -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
140 changes: 72 additions & 68 deletions src/elements/Icon/Icon.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,97 @@
import cx from 'clsx'
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import React from 'react'

import {
createShorthandFactory,
customPropTypes,
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 <ElementType {...rest} {...ariaOptions} className={classes} onClick={this.handleClick} />
}
}
return (
<ElementType {...rest} {...ariaProps} className={classes} onClick={handleClick} ref={ref} />
)
})

Icon.displayName = 'Icon'
Icon.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down Expand Up @@ -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
8 changes: 5 additions & 3 deletions src/elements/Icon/IconGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{childrenUtils.isNil(children) ? content : children}
</ElementType>
)
}
})

IconGroup.displayName = 'IconGroup'
IconGroup.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
8 changes: 5 additions & 3 deletions src/elements/Image/ImageGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{childrenUtils.isNil(children) ? content : children}
</ElementType>
)
}
})

ImageGroup.displayName = 'ImageGroup'
ImageGroup.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
9 changes: 5 additions & 4 deletions test/specs/commonTests/forwardsRef.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Component {...requiredProps} />)).to.equal(true)
expect(ReactIs.isForwardRef(<RootComponent {...requiredProps} />)).to.equal(true)
})

it('a render function is anonymous', () => {
const innerFunctionName = Component.render.name
const innerFunctionName = RootComponent.render.name
expect(innerFunctionName).to.equal('')
})

Expand Down
16 changes: 7 additions & 9 deletions test/specs/commonTests/implementsShorthandProp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -78,15 +76,15 @@ export default (Component, options = {}) => {

if (alwaysPresent || (Component.defaultProps && Component.defaultProps[propKey])) {
it(`has default ${name} when not defined`, () => {
shallow(<Component {...requiredProps} />).should.have.descendants(name)
shallow(<Component {...requiredProps} />).should.have.descendants(ShorthandComponent)
})
} else {
if (!parentIsFragment) {
noDefaultClassNameFromProp(Component, propKey, [], options)
}

it(`has no ${name} when not defined`, () => {
shallow(<Component {...requiredProps} />).should.not.have.descendants(name)
shallow(<Component {...requiredProps} />).should.not.have.descendants(ShorthandComponent)
})
}

Expand Down
1 change: 1 addition & 0 deletions test/specs/elements/Flag/Flag-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions test/specs/elements/Icon/Icon-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions test/specs/elements/Image/ImageGroup-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading

0 comments on commit ac7134a

Please sign in to comment.