Skip to content

Commit

Permalink
chore(Image): use React.forwardRef() (#4234)
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Jun 21, 2022
1 parent b6ede3f commit 0df28d3
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 52 deletions.
1 change: 0 additions & 1 deletion docs/src/utils/docTypes/componentInfoShape.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const componentInfoShape = PropTypes.shape({
name: PropTypes.string,
}),
),
constructorName: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
isParent: PropTypes.bool.isRequired,
isChild: PropTypes.bool.isRequired,
Expand Down
8 changes: 5 additions & 3 deletions gulp/plugins/util/getComponentInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ const getComponentInfo = (filepath) => {
// remove keys we don't use
delete info.methods

// add exported Component info
const Component = require(absPath).default
info.constructorName = _.get(Component, 'prototype.constructor.name', null)
if (!info.displayName) {
throw new Error(
`Please check that static property "displayName" is defined on a component in "${filepath}".`,
)
}

// add component type
info.type = componentType
Expand Down
17 changes: 11 additions & 6 deletions src/elements/Image/Image.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import ImageGroup from './ImageGroup'
* An image is a graphic representation of something.
* @see Icon
*/
function Image(props) {
const Image = React.forwardRef(function ImageInner(props, ref) {
const {
avatar,
bordered,
Expand Down Expand Up @@ -68,8 +68,10 @@ function Image(props) {
'image',
className,
)

const rest = getUnhandledProps(Image, props)
const [imgTagProps, rootProps] = partitionHTMLProps(rest, { htmlProps: htmlImageProps })

const ElementType = getElementType(Image, props, () => {
if (
!_.isNil(dimmer) ||
Expand All @@ -83,33 +85,36 @@ function Image(props) {

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

if (ElementType === 'img') {
return <ElementType {...rootProps} {...imgTagProps} className={classes} />
return <ElementType {...rootProps} {...imgTagProps} className={classes} ref={ref} />
}

return (
<ElementType {...rootProps} className={classes} href={href}>
{Dimmer.create(dimmer, { autoGenerateKey: false })}
{Label.create(label, { autoGenerateKey: false })}
<img {...imgTagProps} />

<img {...imgTagProps} ref={ref} />
</ElementType>
)
}
})

Image.Group = ImageGroup

Image.displayName = 'Image'
Image.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
9 changes: 5 additions & 4 deletions src/lib/factories.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import cx from 'clsx'
import _ from 'lodash'
import * as React from 'react'
import * as ReactIs from 'react-is'

const DEPRECATED_CALLS = {}

Expand All @@ -21,8 +22,8 @@ const DEPRECATED_CALLS = {}
* @returns {object|null}
*/
export function createShorthand(Component, mapValueToProps, val, options = {}) {
if (typeof Component !== 'function' && typeof Component !== 'string') {
throw new Error('createShorthand() Component must be a string or function.')
if (!ReactIs.isValidElementType(Component)) {
throw new Error('createShorthand(): Component should be a valid element type.')
}

// short circuit noop values
Expand Down Expand Up @@ -157,8 +158,8 @@ export function createShorthand(Component, mapValueToProps, val, options = {}) {
* @returns {function} A shorthand factory function waiting for `val` and `defaultProps`.
*/
export function createShorthandFactory(Component, mapValueToProps) {
if (typeof Component !== 'function' && typeof Component !== 'string') {
throw new Error('createShorthandFactory() Component must be a string or function.')
if (!ReactIs.isValidElementType(Component)) {
throw new Error('createShorthandFactory(): Component should be a valid element type.')
}

return (val, options) => createShorthand(Component, mapValueToProps, val, options)
Expand Down
21 changes: 21 additions & 0 deletions test/specs/commonTests/forwardsRef.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'
import { sandbox } from 'test/utils'

/**
* Assert a Component correctly implements a shorthand create method.
* @param {React.Component} Component The component to test
*/
export default function implementsCreateMethod(Component, options = {}) {
describe('forwardsRef', () => {
const { requiredProps = {}, tagName = 'div' } = options

it(`forwards ref to "${tagName}"`, () => {
const ref = sandbox.spy()

mount(<Component {...requiredProps} ref={ref} />)

ref.should.have.been.calledOnce()
ref.should.have.been.calledWithMatch({ tagName: tagName.toUpperCase() })
})
})
}
7 changes: 4 additions & 3 deletions test/specs/commonTests/hasValidTypings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from 'lodash'

import { customPropTypes } from 'src/lib'
import { componentInfoContext } from 'docs/src/utils'
import { getComponentName, getComponentProps } from 'test/utils'
import { getNodes, getInterfaces, hasAnySignature, requireTs } from './tsHelpers'

const isShorthand = (propType) =>
Expand All @@ -27,8 +28,8 @@ const shorthandMap = {
* @param {array} [options.ignoredTypingsProps=[]] Props that will be ignored in tests.
* @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings.
*/
export default (Component, componentInfo, options = {}) => {
const { displayName, repoPath } = componentInfoContext.fromComponent(Component)
export default function hasValidTypings(Component, componentInfo, options = {}) {
const { displayName, repoPath } = componentInfoContext.byDisplayName[getComponentName(Component)]
const { ignoredTypingsProps = [], requiredProps } = options

const tsFile = repoPath.replace('src/', '').replace('.js', '.d.ts')
Expand Down Expand Up @@ -79,7 +80,7 @@ export default (Component, componentInfo, options = {}) => {
})

it('match the typings interface', () => {
const componentPropTypes = _.get(Component, 'propTypes')
const componentPropTypes = getComponentProps(Component).propTypes
const componentProps = _.keys(componentPropTypes)
const interfaceProps = _.without(_.map(props, 'name'), ...ignoredTypingsProps)

Expand Down
9 changes: 4 additions & 5 deletions test/specs/commonTests/implementsCreateMethod.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import _ from 'lodash'
import React, { isValidElement } from 'react'
import { consoleUtil } from 'test/utils'
import { consoleUtil, getComponentName } from 'test/utils'

/**
* Assert a Component correctly implements a shorthand create method.
* @param {React.Component|Function} Component The component to test.
*/
export default (Component) => {
const { name } = _.get(Component, 'prototype.constructor.name')

export default function implementsCreateMethod(Component) {
describe('create shorthand method (common)', () => {
const name = getComponentName(Component)

beforeEach(() => {
// we generate prop values which may throw warnings
// prevent failures due to console activity
Expand Down
2 changes: 2 additions & 0 deletions test/specs/commonTests/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export forwardsRef from './forwardsRef'

export hasSubcomponents from './hasSubcomponents'
export hasValidTypings from './hasValidTypings'
export hasUIClassName from './hasUIClassName'
Expand Down
63 changes: 37 additions & 26 deletions test/specs/commonTests/isConformant.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import faker from 'faker'
import _ from 'lodash'
import React from 'react'
import ReactIs from 'react-is'
import ReactDOMServer from 'react-dom/server'
import * as semanticUIReact from 'semantic-ui-react'

import { componentInfoContext } from 'docs/src/utils'
import { assertBodyContains, consoleUtil, sandbox, syntheticEvent } from 'test/utils'
import helpers from './commonHelpers'
import {
assertBodyContains,
consoleUtil,
getComponentName,
getComponentProps,
sandbox,
syntheticEvent,
} from 'test/utils'
import hasValidTypings from './hasValidTypings'

/**
Expand All @@ -20,7 +27,7 @@ import hasValidTypings from './hasValidTypings'
* @param {boolean} [options.rendersPortal=false] Does this component render a Portal powered component?
* @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings.
*/
export default (Component, options = {}) => {
export default function isConformant(Component, options = {}) {
const {
eventTargets = {},
nestingLevel = 0,
Expand All @@ -29,25 +36,26 @@ export default (Component, options = {}) => {
rendersFragmentByDefault = false,
rendersPortal = false,
} = options
const { throwError } = helpers('isConformant', Component)
const constructorName = getComponentName(Component)

const componentType = typeof Component

// make sure components are properly exported
if (componentType !== 'function') {
throwError(`Components should export a class or function, got: ${componentType}.`)
}

// tests depend on Component constructor names, enforce them
const constructorName = Component.prototype.constructor.name
if (!constructorName) {
throwError(
[
'Component is not a named function. This should help identify it:\n\n',
`${ReactDOMServer.renderToStaticMarkup(<Component />)}`,
].join(''),
it('a valid component should be exported', () => {
expect(ReactIs.isValidElementType(Component)).to.equal(
true,
`Components should export a class or function, got: ${typeof Component}.`,
)
}
})

it('a component should be a function/class or "displayName" should be defined', () => {
if (!constructorName) {
throw new Error(
[
'Component is not a named function and does not have a "displayName".',
'This should help identify it:\n\n',
`${ReactDOMServer.renderToStaticMarkup(<Component {...requiredProps} />)}`,
].join(''),
)
}
})

const info = componentInfoContext.byDisplayName[constructorName]

Expand Down Expand Up @@ -192,20 +200,22 @@ export default (Component, options = {}) => {
}

describe('handles props', () => {
const componentProps = getComponentProps(Component)

it('defines handled props in Component.handledProps', () => {
Component.should.have.any.keys('handledProps')
Component.handledProps.should.be.an('array')
componentProps.should.have.any.keys('handledProps')
componentProps.handledProps.should.be.an('array')
})

it('Component.handledProps includes all handled props', () => {
const computedProps = _.union(
Component.autoControlledProps,
_.keys(Component.defaultProps),
_.keys(Component.propTypes),
componentProps.autoControlledProps,
_.keys(componentProps.defaultProps),
_.keys(componentProps.propTypes),
)
const expectedProps = _.uniq(computedProps).sort()

Component.handledProps.should.to.deep.equal(
componentProps.handledProps.should.to.deep.equal(
expectedProps,
'It seems that not all props were defined in Component.handledProps, you need to check that they are equal ' +
'to the union of Component.autoControlledProps and keys of Component.defaultProps and Component.propTypes',
Expand Down Expand Up @@ -376,6 +386,7 @@ export default (Component, options = {}) => {
})
})
}

// ----------------------------------------
// Test typings
// ----------------------------------------
Expand Down
22 changes: 18 additions & 4 deletions test/specs/elements/Image/Image-test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'lodash'
import faker from 'faker'
import React from 'react'

import Image from 'src/elements/Image/Image'
Expand All @@ -9,6 +10,21 @@ import * as common from 'test/specs/commonTests'

describe('Image', () => {
common.isConformant(Image)

common.forwardsRef(Image, { tagName: 'img' })
common.forwardsRef(Image, {
requiredProps: { as: 'div', children: <span /> },
tagName: 'div',
})
common.forwardsRef(Image, {
requiredProps: { as: 'div', content: <span /> },
tagName: 'div',
})
common.forwardsRef(Image, {
requiredProps: { label: faker.lorem.word() },
tagName: 'img',
})

common.hasSubcomponents(Image, [ImageGroup])
common.hasUIClassName(Image)
common.rendersChildren(Image)
Expand Down Expand Up @@ -40,10 +56,8 @@ describe('Image', () => {
common.propValueOnlyToClassName(Image, 'size', SUI.SIZES)

describe('as', () => {
it('renders an img tag', () => {
shallow(<Image />)
.type()
.should.equal('img')
it('renders "i" by default', () => {
mount(<Image />).should.have.tagName('img')
})
})

Expand Down
19 changes: 19 additions & 0 deletions test/utils/getComponentName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as ReactIs from 'react-is'

/**
* Gets a proper `displayName` for a component.
*
* @param {React.ElementType} Component
* @return {String}
*/
export default function getComponentName(Component) {
if (Component.$$typeof === ReactIs.Memo) {
return getComponentName(Component.type)
}

if (Component.$$typeof === ReactIs.ForwardRef) {
return Component.displayName
}

return Component.prototype?.constructor?.name
}
20 changes: 20 additions & 0 deletions test/utils/getComponentProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as ReactIs from 'react-is'

/**
* Gets proper props for a component.
*
* @param {React.ElementType} Component
* @return {Object}
*/
export default function getComponentProps(Component) {
if (Component.$$typeof === ReactIs.Memo) {
return getComponentProps(Component.type)
}

return {
autoControlledProps: Component.autoControlledProps,
defaultProps: Component.defaultProps,
handledProps: Component.handledProps,
propTypes: Component.propTypes,
}
}
2 changes: 2 additions & 0 deletions test/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export * from './assertNodeContains'
export assertWithTimeout from './assertWithTimeout'
export consoleUtil from './consoleUtil'
export domEvent from './domEvent'
export getComponentName from './getComponentName'
export getComponentProps from './getComponentProps'
export nestedShallow from './nestedShallow'
export sandbox from './sandbox'
export syntheticEvent from './syntheticEvent'

0 comments on commit 0df28d3

Please sign in to comment.