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 );
+ }
+ };
+} );