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 1 commit
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-focus-contain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# withFocusContain

`withFocusContain` 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 `withFocusContain`.
57 changes: 57 additions & 0 deletions components/higher-order/with-focus-contain/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* WordPress dependencies
*/
import { Component, createRef } from '@wordpress/element';
import { keycodes } from '@wordpress/utils';
import { focus } from '@wordpress/dom';

const { TAB } = keycodes;

const withFocusContain = ( WrappedComponent ) => {
return 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 */
}
};
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure a similar mechanism is also implemented elsewhere. Maybe NavigableContainer? Should we consolidate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth also considering the name should clarify this is something different from what other components do, for example NavigableContainer.

🙂 Yes and no. The mechanism in NavigableContainer is used also for elements with tabindex="-1" like menu items.


export default withFocusContain;
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 withFocusContain from '../higher-order/with-focus-contain';
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 = withFocusContain( withFocusReturn( ( { children } ) => children ) );

const { ESCAPE } = keycodes;

Expand Down