Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a higher order component to constrain Tab keyboard navigation. #6987

Merged
merged 4 commits into from
Jun 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -17,12 +17,13 @@ import { keycodes } from '@wordpress/utils';
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 ) );

const { ESCAPE } = keycodes;

Expand Down