Skip to content

Commit

Permalink
Add a higher order component to constrain Tab keyboard navigation. (#…
Browse files Browse the repository at this point in the history
…6987)

* Add a higher order component to constrain Tab keyboard navigation.

* Components: Rename withFocusContain to withConstrainedTabbing

* Components: Export withConstrainedTabbing from components module

* Components: Use createHigherOrderComponent for withConstrainedTabbing

Consistency on creation of higher-order components
  • Loading branch information
afercia authored and aduth committed Jun 28, 2018
1 parent ef5cfe0 commit eecb1ea
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 1 deletion.
7 changes: 7 additions & 0 deletions components/higher-order/with-constrained-tabbing/README.md
Original file line number Diff line number Diff line change
@@ -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`.
62 changes: 62 additions & 0 deletions components/higher-order/with-constrained-tabbing/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div
onKeyDown={ this.handleTabBehaviour }
ref={ this.focusContainRef }
>
<WrappedComponent { ...this.props } />
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
},
'withConstrainedTabbing'
);

export default withConstrainedTabbing;
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export { default as ifCondition } from './higher-order/if-condition';
export { default as navigateRegions } from './higher-order/navigate-regions';
export { default as withAPIData } from './higher-order/with-api-data';
export { default as withContext } from './higher-order/with-context';
export { default as withConstrainedTabbing } from './higher-order/with-constrained-tabbing';
export { default as withFallbackStyles } from './higher-order/with-fallback-styles';
export { default as withFilters } from './higher-order/with-filters';
export { default as withFocusOutside } from './higher-order/with-focus-outside';
Expand Down
3 changes: 2 additions & 1 deletion components/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import { ESCAPE } from '@wordpress/keycodes';
import './style.scss';
import { computePopoverPosition } from './utils';
import withFocusReturn from '../higher-order/with-focus-return';
import withConstrainedTabbing from '../higher-order/with-constrained-tabbing';
import PopoverDetectOutside from './detect-outside';
import IconButton from '../icon-button';
import ScrollLock from '../scroll-lock';
import { Slot, Fill } from '../slot-fill';

const FocusManaged = withFocusReturn( ( { children } ) => children );
const FocusManaged = withConstrainedTabbing( withFocusReturn( ( { children } ) => children ) );

/**
* Name of slot in which popover should fill.
Expand Down

0 comments on commit eecb1ea

Please sign in to comment.