diff --git a/package-lock.json b/package-lock.json index d0ab6f113ef0c..12c7f0945f41e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17480,8 +17480,10 @@ "requires": { "@babel/runtime": "^7.11.2", "@wordpress/deprecated": "file:packages/deprecated", + "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", + "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/priority-queue": "file:packages/priority-queue", "clipboard": "^2.0.1", "lodash": "^4.17.19", diff --git a/packages/components/src/higher-order/with-constrained-tabbing/index.js b/packages/components/src/higher-order/with-constrained-tabbing/index.js index d3e372d8655de..db1dbe27f6908 100644 --- a/packages/components/src/higher-order/with-constrained-tabbing/index.js +++ b/packages/components/src/higher-order/with-constrained-tabbing/index.js @@ -1,69 +1,20 @@ /** * WordPress dependencies */ -import { Component, createRef } from '@wordpress/element'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { TAB } from '@wordpress/keycodes'; -import { focus } from '@wordpress/dom'; +import { + createHigherOrderComponent, + useConstrainedTabbing, +} from '@wordpress/compose'; 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(); - /* - * When pressing Tab and none of the tabbables has focus, the keydown - * event happens on the wrapper div: move focus on the first tabbable. - */ - } else if ( ! tabbables.includes( event.target ) ) { - 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 */ - } + function ComponentWithConstrainedTabbing( props ) { + const ref = useConstrainedTabbing(); + return ( +
+ +
+ ); }, 'withConstrainedTabbing' ); diff --git a/packages/compose/README.md b/packages/compose/README.md index 7054d34721144..e1019caade225 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -132,6 +132,31 @@ _Returns_ - `Array`: Async array. +# **useConstrainedTabbing** + +In Dialogs/modals, the tabbing must be constrained to the content of +the wrapper element. This hook adds the behavior to the returned ref. + +_Usage_ + +```js +import { useConstrainedTabbing } from '@wordpress/compose'; + +const ConstrainedTabbingExample = () => { + const constrainedTabbingRef = useConstrainedTabbing() + return ( +
+
+ ); +} +``` + +_Returns_ + +- `Function`: Element Ref. + # **useCopyOnClick** Copies the text to the clipboard when the element is clicked. diff --git a/packages/compose/package.json b/packages/compose/package.json index 76913e4d2d1a4..731a566b6b621 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -27,8 +27,10 @@ "dependencies": { "@babel/runtime": "^7.11.2", "@wordpress/deprecated": "file:../deprecated", + "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/keycodes": "file:../keycodes", "@wordpress/priority-queue": "file:../priority-queue", "clipboard": "^2.0.1", "lodash": "^4.17.19", diff --git a/packages/compose/src/hooks/use-constrained-tabbing/README.md b/packages/compose/src/hooks/use-constrained-tabbing/README.md new file mode 100644 index 0000000000000..bbeae7fe27eda --- /dev/null +++ b/packages/compose/src/hooks/use-constrained-tabbing/README.md @@ -0,0 +1,33 @@ +`useConstrainedTabbing` +====================== + +In Dialogs/modals, the tabbing must be constrained to the content of the wrapper element. To achieve this behavior you can use the `useConstrainedTabbing` hook. + +## Return Object Properties + +### `ref` + +- Type: `Function` + +A function reference that must be passed to the DOM element where constrained tabbing should be enabled. + +## Usage +The following example allows us to drag & drop a red square around the entire viewport. + +```jsx +/** + * WordPress dependencies + */ +import { useConstrainedTabbing } from '@wordpress/compose'; + + +const ConstrainedTabbingExample = () => { + const ref = useConstrainedTabbing() + return ( +
+
+ ); +}; +``` diff --git a/packages/compose/src/hooks/use-constrained-tabbing/index.js b/packages/compose/src/hooks/use-constrained-tabbing/index.js new file mode 100644 index 0000000000000..ff107e025ff6d --- /dev/null +++ b/packages/compose/src/hooks/use-constrained-tabbing/index.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; +import { TAB } from '@wordpress/keycodes'; +import { focus } from '@wordpress/dom'; + +/** + * In Dialogs/modals, the tabbing must be constrained to the content of + * the wrapper element. This hook adds the behavior to the returned ref. + * + * @return {Function} Element Ref. + * + * @example + * ```js + * import { useConstrainedTabbing } from '@wordpress/compose'; + * + * const ConstrainedTabbingExample = () => { + * const constrainedTabbingRef = useConstrainedTabbing() + * return ( + *
+ *
+ * ); + * } + * ``` + */ +function useConstrainedTabbing() { + const ref = useCallback( ( node ) => { + if ( ! node ) { + return; + } + node.addEventListener( 'keydown', ( event ) => { + if ( event.keyCode !== TAB ) { + return; + } + + const tabbables = focus.tabbable.find( node ); + 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(); + /* + * When pressing Tab and none of the tabbables has focus, the keydown + * event happens on the wrapper div: move focus on the first tabbable. + */ + } else if ( ! tabbables.includes( event.target ) ) { + event.preventDefault(); + firstTabbable.focus(); + } + } ); + }, [] ); + + return ref; +} + +export default useConstrainedTabbing; diff --git a/packages/compose/src/hooks/use-constrained-tabbing/index.native.js b/packages/compose/src/hooks/use-constrained-tabbing/index.native.js new file mode 100644 index 0000000000000..e67ada8e203b9 --- /dev/null +++ b/packages/compose/src/hooks/use-constrained-tabbing/index.native.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; + +function useConstrainedTabbing() { + const ref = useRef(); + + // Do nothing on mobile as tabbing is not a mobile behavior. + + return ref; +} + +export default useConstrainedTabbing; diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 4e855a47abdcd..a523dcfca94ee 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -13,6 +13,7 @@ export { default as withSafeTimeout } from './higher-order/with-safe-timeout'; export { default as withState } from './higher-order/with-state'; // Hooks +export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing'; export { default as useCopyOnClick } from './hooks/use-copy-on-click'; export { default as __experimentalUseDragging } from './hooks/use-dragging'; export { default as useInstanceId } from './hooks/use-instance-id'; diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js index 53abcc48aca48..8181c26221453 100644 --- a/packages/compose/src/index.native.js +++ b/packages/compose/src/index.native.js @@ -13,6 +13,7 @@ export { default as withSafeTimeout } from './higher-order/with-safe-timeout'; export { default as withState } from './higher-order/with-state'; // Hooks +export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing'; export { default as __experimentalUseDragging } from './hooks/use-dragging'; export { default as useInstanceId } from './hooks/use-instance-id'; export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut';