From 940b0692840b4202322c980d783e6cc983a48499 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Mon, 28 May 2018 16:56:25 +0200 Subject: [PATCH 1/4] Add a higher order component to constrain Tab keyboard navigation. --- .../higher-order/with-focus-contain/README.md | 7 +++ .../higher-order/with-focus-contain/index.js | 57 +++++++++++++++++++ components/popover/index.js | 3 +- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 components/higher-order/with-focus-contain/README.md create mode 100644 components/higher-order/with-focus-contain/index.js diff --git a/components/higher-order/with-focus-contain/README.md b/components/higher-order/with-focus-contain/README.md new file mode 100644 index 00000000000000..9a0b0ed44c330b --- /dev/null +++ b/components/higher-order/with-focus-contain/README.md @@ -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`. diff --git a/components/higher-order/with-focus-contain/index.js b/components/higher-order/with-focus-contain/index.js new file mode 100644 index 00000000000000..4b19c02e05d9a1 --- /dev/null +++ b/components/higher-order/with-focus-contain/index.js @@ -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 ( +
+ +
+ ); + /* eslint-enable jsx-a11y/no-static-element-interactions */ + } + }; +}; + +export default withFocusContain; diff --git a/components/popover/index.js b/components/popover/index.js index 55167d806623e1..0777ff334375e4 100644 --- a/components/popover/index.js +++ b/components/popover/index.js @@ -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; From 1003e943ccfa0cc32c5e399d36b7802222fde8af Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 28 Jun 2018 10:38:51 -0400 Subject: [PATCH 2/4] Components: Rename withFocusContain to withConstrainedTabbing --- components/higher-order/with-constrained-tabbing/README.md | 7 +++++++ .../index.js | 4 ++-- components/higher-order/with-focus-contain/README.md | 7 ------- components/popover/index.js | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 components/higher-order/with-constrained-tabbing/README.md rename components/higher-order/{with-focus-contain => with-constrained-tabbing}/index.js (93%) delete mode 100644 components/higher-order/with-focus-contain/README.md 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 00000000000000..3e29122b8faf25 --- /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-focus-contain/index.js b/components/higher-order/with-constrained-tabbing/index.js similarity index 93% rename from components/higher-order/with-focus-contain/index.js rename to components/higher-order/with-constrained-tabbing/index.js index 4b19c02e05d9a1..c2a682e742dcf9 100644 --- a/components/higher-order/with-focus-contain/index.js +++ b/components/higher-order/with-constrained-tabbing/index.js @@ -7,7 +7,7 @@ import { focus } from '@wordpress/dom'; const { TAB } = keycodes; -const withFocusContain = ( WrappedComponent ) => { +const withConstrainedTabbing = ( WrappedComponent ) => { return class extends Component { constructor() { super( ...arguments ); @@ -54,4 +54,4 @@ const withFocusContain = ( WrappedComponent ) => { }; }; -export default withFocusContain; +export default withConstrainedTabbing; diff --git a/components/higher-order/with-focus-contain/README.md b/components/higher-order/with-focus-contain/README.md deleted file mode 100644 index 9a0b0ed44c330b..00000000000000 --- a/components/higher-order/with-focus-contain/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 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`. diff --git a/components/popover/index.js b/components/popover/index.js index 0777ff334375e4..2acc9e9914fdff 100644 --- a/components/popover/index.js +++ b/components/popover/index.js @@ -17,13 +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 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 = withFocusContain( withFocusReturn( ( { children } ) => children ) ); +const FocusManaged = withConstrainedTabbing( withFocusReturn( ( { children } ) => children ) ); const { ESCAPE } = keycodes; From c7f1c762bfcd8dfd6f99bfa653df6dd8dd94e4a0 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 28 Jun 2018 10:39:31 -0400 Subject: [PATCH 3/4] Components: Export withConstrainedTabbing from components module --- components/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/components/index.js b/components/index.js index 0d20eb59de1869..1351d6fb7a4522 100644 --- a/components/index.js +++ b/components/index.js @@ -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'; From 9e96aa2764760cb15228732353371df41b028fdb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 28 Jun 2018 10:43:18 -0400 Subject: [PATCH 4/4] Components: Use createHigherOrderComponent for withConstrainedTabbing Consistency on creation of higher-order components --- .../with-constrained-tabbing/index.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/components/higher-order/with-constrained-tabbing/index.js b/components/higher-order/with-constrained-tabbing/index.js index c2a682e742dcf9..9ce58b976e0330 100644 --- a/components/higher-order/with-constrained-tabbing/index.js +++ b/components/higher-order/with-constrained-tabbing/index.js @@ -1,14 +1,18 @@ /** * WordPress dependencies */ -import { Component, createRef } from '@wordpress/element'; +import { + Component, + createRef, + createHigherOrderComponent, +} from '@wordpress/element'; import { keycodes } from '@wordpress/utils'; import { focus } from '@wordpress/dom'; const { TAB } = keycodes; -const withConstrainedTabbing = ( WrappedComponent ) => { - return class extends Component { +const withConstrainedTabbing = createHigherOrderComponent( + ( WrappedComponent ) => class extends Component { constructor() { super( ...arguments ); @@ -51,7 +55,8 @@ const withConstrainedTabbing = ( WrappedComponent ) => { ); /* eslint-enable jsx-a11y/no-static-element-interactions */ } - }; -}; + }, + 'withConstrainedTabbing' +); export default withConstrainedTabbing;