diff --git a/components/button/index.js b/components/button/index.js index 26d576b78d47e..d824433d4f502 100644 --- a/components/button/index.js +++ b/components/button/index.js @@ -6,67 +6,47 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Component, createElement } from '@wordpress/element'; +import { createElement, forwardRef } from '@wordpress/element'; /** * Internal dependencies */ import './style.scss'; -class Button extends Component { - constructor( props ) { - super( props ); - this.setRef = this.setRef.bind( this ); - } - - componentDidMount() { - if ( this.props.focus ) { - this.ref.focus(); - } - } - - setRef( ref ) { - this.ref = ref; - } - - focus() { - this.ref.focus(); - } - - render() { - const { - href, - target, - isPrimary, - isLarge, - isSmall, - isToggled, - isBusy, - className, - disabled, - ...additionalProps - } = this.props; - const classes = classnames( 'components-button', className, { - button: ( isPrimary || isLarge || isSmall ), - 'button-primary': isPrimary, - 'button-large': isLarge, - 'button-small': isSmall, - 'is-toggled': isToggled, - 'is-busy': isBusy, - } ); - - const tag = href !== undefined && ! disabled ? 'a' : 'button'; - const tagProps = tag === 'a' ? { href, target } : { type: 'button', disabled }; - - delete additionalProps.focus; - - return createElement( tag, { - ...tagProps, - ...additionalProps, - className: classes, - ref: this.setRef, - } ); - } +export function Button( props, ref ) { + const { + href, + target, + isPrimary, + isLarge, + isSmall, + isToggled, + isBusy, + className, + disabled, + focus, + ...additionalProps + } = props; + + const classes = classnames( 'components-button', className, { + button: ( isPrimary || isLarge || isSmall ), + 'button-primary': isPrimary, + 'button-large': isLarge, + 'button-small': isSmall, + 'is-toggled': isToggled, + 'is-busy': isBusy, + } ); + + const tag = href !== undefined && ! disabled ? 'a' : 'button'; + const tagProps = tag === 'a' ? { href, target } : { type: 'button', disabled }; + + return createElement( tag, { + ...tagProps, + ...additionalProps, + className: classes, + autoFocus: focus, + ref, + } ); } -export default Button; +export default forwardRef( Button ); diff --git a/components/button/test/index.js b/components/button/test/index.js index 089d40721ec98..1d2848f77c5da 100644 --- a/components/button/test/index.js +++ b/components/button/test/index.js @@ -1,12 +1,20 @@ /** * External dependencies */ -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { createRef } from '@wordpress/element'; /** * Internal dependencies */ -import Button from '../'; +import ButtonWithForwardedRef, { Button } from '../'; + +// [TEMPORARY]: Only needed so long as Enzyme does not support React.forwardRef +jest.unmock( '../' ); describe( 'Button', () => { describe( 'basic rendering', () => { @@ -93,4 +101,18 @@ describe( 'Button', () => { expect( button.type() ).toBe( 'button' ); } ); } ); + + // Disable reason: This test is desirable, but unsupported by Enzyme in + // the current version, as it depends on features new to React in 16.3.0. + // + // eslint-disable-next-line jest/no-disabled-tests + describe.skip( 'ref forwarding', () => { + it( 'should enable access to DOM element', () => { + const ref = createRef(); + + mount( ); + + expect( ref.current.nodeName ).toBe( 'button' ); + } ); + } ); } ); diff --git a/element/index.js b/element/index.js index 5ac0991425653..0cfbb49d5e167 100644 --- a/element/index.js +++ b/element/index.js @@ -5,6 +5,7 @@ import { createElement, createContext, createRef, + forwardRef, Component, cloneElement, Children, @@ -51,6 +52,19 @@ export { createElement }; */ export { createRef }; +/** + * Component enhancer used to enable passing a ref to its wrapped component. + * Pass a function argument which receives `props` and `ref` as its arguments, + * returning an element using the forwarded ref. The return value is a new + * component which forwards its ref. + * + * @param {Function} forwarder Function passed `props` and `ref`, expected to + * return an element. + * + * @return {WPComponent} Enhanced component. + */ +export { forwardRef }; + /** * Renders a given element into the target DOM node. * diff --git a/test/unit/jest.config.json b/test/unit/jest.config.json index 3e309518c9f76..76712378953fc 100644 --- a/test/unit/jest.config.json +++ b/test/unit/jest.config.json @@ -10,7 +10,8 @@ "setupFiles": [ "core-js/fn/symbol/async-iterator", "/test/unit/setup-blocks.js", - "/test/unit/setup-wp-aliases.js" + "/test/unit/setup-wp-aliases.js", + "/test/unit/setup-mocks.js" ], "transform": { "\\.pegjs$": "/test/unit/pegjs-transform.js" diff --git a/test/unit/setup-mocks.js b/test/unit/setup-mocks.js new file mode 100644 index 0000000000000..9e6eaec2d1811 --- /dev/null +++ b/test/unit/setup-mocks.js @@ -0,0 +1,16 @@ +// [TEMPORARY]: Button uses React.forwardRef, added in react@16.3.0 but not yet +// supported by Enzyme as of enzyme-adapter-react-16@1.1.1 . This mock unwraps +// the ref forwarding, so any tests relying on this behavior will fail. +// +// See: https://github.com/airbnb/enzyme/issues/1604 +// See: https://github.com/airbnb/enzyme/pull/1592/files +jest.mock( '../../components/button', () => { + const { Button: RawButton } = require.requireActual( '../../components/button' ); + const { Component } = require( 'react' ); + + return class Button extends Component { + render() { + return RawButton( this.props ); + } + }; +} );