diff --git a/components/higher-order/with-constrained-tabbing/README.md b/components/higher-order/with-constrained-tabbing/README.md new file mode 100644 index 0000000000000..3e29122b8faf2 --- /dev/null +++ b/components/higher-order/with-constrained-tabbing/README.md @@ -0,0 +1,7 @@ +# withConstrainedTabbing + +`withConstrainedTabbing` is a React [higher-order component](https://facebook.github.io/react/docs/higher-order-components.html) adding the ability to constrain keyboard navigation with the Tab key within a component. For accessibility reasons, some UI components need to constrain Tab navigation, for example modal dialogs or similar UI. Use of this component is recommended only in cases where a way to navigate away from the wrapped component is implemented by other means, usually by pressing the Escape key or using a specific UI control, e.g. a "Close" button. + +## Usage + +Wrap your original component with `withConstrainedTabbing`. diff --git a/components/higher-order/with-constrained-tabbing/index.js b/components/higher-order/with-constrained-tabbing/index.js new file mode 100644 index 0000000000000..9ce58b976e033 --- /dev/null +++ b/components/higher-order/with-constrained-tabbing/index.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +import { + Component, + createRef, + createHigherOrderComponent, +} from '@wordpress/element'; +import { keycodes } from '@wordpress/utils'; +import { focus } from '@wordpress/dom'; + +const { TAB } = keycodes; + +const withConstrainedTabbing = createHigherOrderComponent( + ( WrappedComponent ) => class extends Component { + constructor() { + super( ...arguments ); + + this.focusContainRef = createRef(); + this.handleTabBehaviour = this.handleTabBehaviour.bind( this ); + } + + handleTabBehaviour( event ) { + if ( event.keyCode !== TAB ) { + return; + } + + const tabbables = focus.tabbable.find( this.focusContainRef.current ); + if ( ! tabbables.length ) { + return; + } + const firstTabbable = tabbables[ 0 ]; + const lastTabbable = tabbables[ tabbables.length - 1 ]; + + if ( event.shiftKey && event.target === firstTabbable ) { + event.preventDefault(); + lastTabbable.focus(); + } else if ( ! event.shiftKey && event.target === lastTabbable ) { + event.preventDefault(); + firstTabbable.focus(); + } + } + + render() { + // Disable reason: this component is non-interactive, but must capture + // events from the wrapped component to determine when the Tab key is used. + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return ( +