Skip to content

Commit

Permalink
Add a useConstrainedTabbing hook (#27544)
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad authored Dec 7, 2020
1 parent 26db39a commit cc3e478
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 60 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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 (
<div
onKeyDown={ this.handleTabBehaviour }
ref={ this.focusContainRef }
tabIndex="-1"
>
<WrappedComponent { ...this.props } />
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
function ComponentWithConstrainedTabbing( props ) {
const ref = useConstrainedTabbing();
return (
<div ref={ ref } tabIndex="-1">
<WrappedComponent { ...props } />
</div>
);
},
'withConstrainedTabbing'
);
Expand Down
25 changes: 25 additions & 0 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,31 @@ _Returns_

- `Array`: Async array.

<a name="useConstrainedTabbing" href="#useConstrainedTabbing">#</a> **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 (
<div ref={ constrainedTabbingRef }>
<Button />
<Button />
</div>
);
}
```

_Returns_

- `Function`: Element Ref.

<a name="useCopyOnClick" href="#useCopyOnClick">#</a> **useCopyOnClick**

Copies the text to the clipboard when the element is clicked.
Expand Down
2 changes: 2 additions & 0 deletions packages/compose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions packages/compose/src/hooks/use-constrained-tabbing/README.md
Original file line number Diff line number Diff line change
@@ -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 (
<div ref={ ref }>
<Button />
<Button />
</div>
);
};
```
66 changes: 66 additions & 0 deletions packages/compose/src/hooks/use-constrained-tabbing/index.js
Original file line number Diff line number Diff line change
@@ -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 (
* <div ref={ constrainedTabbingRef }>
* <Button />
* <Button />
* </div>
* );
* }
* ```
*/
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;
14 changes: 14 additions & 0 deletions packages/compose/src/hooks/use-constrained-tabbing/index.native.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/compose/src/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit cc3e478

Please sign in to comment.