+ );
},
'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';