-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Expand file tree
/
Copy pathuse-tab-nav.js
More file actions
152 lines (130 loc) · 4.35 KB
/
use-tab-nav.js
File metadata and controls
152 lines (130 loc) · 4.35 KB
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
* WordPress dependencies
*/
import { focus } from '@wordpress/dom';
import { TAB, ESCAPE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { useRefEffect, useMergeRefs } from '@wordpress/compose';
import { useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
/**
* Useful for positioning an element within the viewport so focussing the
* element does not scroll the page.
*/
const PREVENT_SCROLL_ON_FOCUS = { position: 'fixed' };
function isFormElement( element ) {
const { tagName } = element;
return (
tagName === 'INPUT' ||
tagName === 'BUTTON' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
);
}
export default function useTabNav() {
const container = useRef();
const focusCaptureBeforeRef = useRef();
const focusCaptureAfterRef = useRef();
const lastFocus = useRef();
const { hasMultiSelection, getSelectedBlockClientId } = useSelect(
blockEditorStore
);
const { setNavigationMode } = useDispatch( blockEditorStore );
const isNavigationMode = useSelect(
( select ) => select( blockEditorStore ).isNavigationMode(),
[]
);
// Don't allow tabbing to this element in Navigation mode.
const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined;
// Reference that holds the a flag for enabling or disabling
// capturing on the focus capture elements.
const noCapture = useRef();
function onFocusCapture( event ) {
// Do not capture incoming focus if set by us in WritingFlow.
if ( noCapture.current ) {
noCapture.current = null;
} else if ( hasMultiSelection() ) {
container.current.focus();
} else if ( getSelectedBlockClientId() ) {
lastFocus.current.focus();
} else {
setNavigationMode( true );
const isBefore =
// eslint-disable-next-line no-bitwise
event.target.compareDocumentPosition( container.current ) &
event.target.DOCUMENT_POSITION_FOLLOWING;
const action = isBefore ? 'findNext' : 'findPrevious';
focus.tabbable[ action ]( event.target ).focus();
}
}
const before = (
<div
ref={ focusCaptureBeforeRef }
tabIndex={ focusCaptureTabIndex }
onFocus={ onFocusCapture }
style={ PREVENT_SCROLL_ON_FOCUS }
/>
);
const after = (
<div
ref={ focusCaptureAfterRef }
tabIndex={ focusCaptureTabIndex }
onFocus={ onFocusCapture }
style={ PREVENT_SCROLL_ON_FOCUS }
/>
);
const ref = useRefEffect( ( node ) => {
function onKeyDown( event ) {
if ( event.keyCode === ESCAPE && ! hasMultiSelection() ) {
event.stopPropagation();
setNavigationMode( true );
return;
}
// In Edit mode, Tab should focus the first tabbable element after
// the content, which is normally the sidebar (with block controls)
// and Shift+Tab should focus the first tabbable element before the
// content, which is normally the block toolbar.
// Arrow keys can be used, and Tab and arrow keys can be used in
// Navigation mode (press Esc), to navigate through blocks.
if ( event.keyCode !== TAB ) {
return;
}
const isShift = event.shiftKey;
const direction = isShift ? 'findPrevious' : 'findNext';
if ( ! hasMultiSelection() && ! getSelectedBlockClientId() ) {
return;
}
// Allow tabbing between form elements rendered in a block,
// such as inside a placeholder. Form elements are generally
// meant to be UI rather than part of the content. Ideally
// these are not rendered in the content and perhaps in the
// future they can be rendered in an iframe or shadow DOM.
if (
isFormElement( event.target ) &&
isFormElement( focus.tabbable[ direction ]( event.target ) )
) {
return;
}
const next = isShift ? focusCaptureBeforeRef : focusCaptureAfterRef;
// Disable focus capturing on the focus capture element, so it
// doesn't refocus this block and so it allows default behaviour
// (moving focus to the next tabbable element).
noCapture.current = true;
next.current.focus();
}
function onFocusOut( event ) {
lastFocus.current = event.target;
}
node.addEventListener( 'keydown', onKeyDown );
node.addEventListener( 'focusout', onFocusOut );
return () => {
node.removeEventListener( 'keydown', onKeyDown );
node.removeEventListener( 'focusout', onFocusOut );
};
}, [] );
const mergedRefs = useMergeRefs( [ container, ref ] );
return [ before, mergedRefs, after ];
}