forked from WordPress/gutenberg
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
92 lines (80 loc) · 2.61 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* WordPress dependencies
*/
import { focus } from '@wordpress/dom';
/**
* Internal dependencies
*/
import useRefEffect from '../use-ref-effect';
/**
* 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 {import('react').RefCallback<Element>} Element Ref.
*
* @example
* ```js
* import { useConstrainedTabbing } from '@wordpress/compose';
*
* const ConstrainedTabbingExample = () => {
* const constrainedTabbingRef = useConstrainedTabbing()
* return (
* <div ref={ constrainedTabbingRef }>
* <Button />
* <Button />
* </div>
* );
* }
* ```
*/
function useConstrainedTabbing() {
return useRefEffect( ( /** @type {HTMLElement} */ node ) => {
function onKeyDown( /** @type {KeyboardEvent} */ event ) {
const { key, shiftKey, target } = event;
if ( key !== 'Tab' ) {
return;
}
const action = shiftKey ? 'findPrevious' : 'findNext';
const nextElement =
focus.tabbable[ action ](
/** @type {HTMLElement} */ ( target )
) || null;
// When the target element contains the element that is about to
// receive focus, for example when the target is a tabbable
// container, browsers may disagree on where to move focus next.
// In this case we can't rely on native browsers behavior. We need
// to manage focus instead.
// See https://github.com/WordPress/gutenberg/issues/46041.
if (
/** @type {HTMLElement} */ ( target ).contains( nextElement )
) {
event.preventDefault();
nextElement?.focus();
return;
}
// If the element that is about to receive focus is inside the
// area, rely on native browsers behavior and let tabbing follow
// the native tab sequence.
if ( node.contains( nextElement ) ) {
return;
}
// If the element that is about to receive focus is outside the
// area, move focus to a div and insert it at the start or end of
// the area, depending on the direction. Without preventing default
// behaviour, the browser will then move focus to the next element.
const domAction = shiftKey ? 'append' : 'prepend';
const { ownerDocument } = node;
const trap = ownerDocument.createElement( 'div' );
trap.tabIndex = -1;
node[ domAction ]( trap );
// Remove itself when the trap loses focus.
trap.addEventListener( 'blur', () => node.removeChild( trap ) );
trap.focus();
}
node.addEventListener( 'keydown', onKeyDown );
return () => {
node.removeEventListener( 'keydown', onKeyDown );
};
}, [] );
}
export default useConstrainedTabbing;