Skip to content

Commit 3717c25

Browse files
authored
[react-interactions] More Tab Focus control handling (#16751)
1 parent 0a2215c commit 3717c25

File tree

6 files changed

+285
-125
lines changed

6 files changed

+285
-125
lines changed

packages/react-dom/src/client/focus/TabFocusContainer.js

Lines changed: 0 additions & 81 deletions
This file was deleted.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import React from 'react';
11+
import {TabbableScope} from './TabbableScope';
12+
import {useKeyboard} from 'react-events/keyboard';
13+
14+
type TabFocusControllerProps = {
15+
children: React.Node,
16+
contain?: boolean,
17+
};
18+
19+
type KeyboardEventType = 'keydown' | 'keyup';
20+
21+
type KeyboardEvent = {|
22+
altKey: boolean,
23+
ctrlKey: boolean,
24+
isComposing: boolean,
25+
key: string,
26+
metaKey: boolean,
27+
shiftKey: boolean,
28+
target: Element | Document,
29+
type: KeyboardEventType,
30+
timeStamp: number,
31+
defaultPrevented: boolean,
32+
|};
33+
34+
type ControllerHandle = {|
35+
focusFirst: () => void,
36+
focusNext: () => boolean,
37+
focusPrevious: () => boolean,
38+
getNextController: () => null | ControllerHandle,
39+
getPreviousController: () => null | ControllerHandle,
40+
|};
41+
42+
const {useImperativeHandle, useRef} = React;
43+
44+
function getTabbableNodes(scopeRef) {
45+
const tabbableScope = scopeRef.current;
46+
const tabbableNodes = tabbableScope.getScopedNodes();
47+
const firstTabbableElem = tabbableNodes[0];
48+
const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1];
49+
const currentIndex = tabbableNodes.indexOf(document.activeElement);
50+
let focusedElement = null;
51+
if (currentIndex !== -1) {
52+
focusedElement = tabbableNodes[currentIndex];
53+
}
54+
return [
55+
tabbableNodes,
56+
firstTabbableElem,
57+
lastTabbableElem,
58+
currentIndex,
59+
focusedElement,
60+
];
61+
}
62+
63+
export const TabFocusController = React.forwardRef(
64+
({children, contain}: TabFocusControllerProps, ref): React.Node => {
65+
const scopeRef = useRef(null);
66+
const keyboard = useKeyboard({
67+
onKeyDown(event: KeyboardEvent): boolean {
68+
if (event.key !== 'Tab') {
69+
return true;
70+
}
71+
if (event.shiftKey) {
72+
return focusPrevious();
73+
} else {
74+
return focusNext();
75+
}
76+
},
77+
preventKeys: ['Tab', ['Tab', {shiftKey: true}]],
78+
});
79+
80+
function focusFirst(): void {
81+
const [, firstTabbableElem] = getTabbableNodes(scopeRef);
82+
firstTabbableElem.focus();
83+
}
84+
85+
function focusNext(): boolean {
86+
const [
87+
tabbableNodes,
88+
firstTabbableElem,
89+
lastTabbableElem,
90+
currentIndex,
91+
focusedElement,
92+
] = getTabbableNodes(scopeRef);
93+
94+
if (focusedElement === null) {
95+
firstTabbableElem.focus();
96+
} else if (focusedElement === lastTabbableElem) {
97+
if (contain === true) {
98+
firstTabbableElem.focus();
99+
} else {
100+
return true;
101+
}
102+
} else {
103+
tabbableNodes[currentIndex + 1].focus();
104+
}
105+
return false;
106+
}
107+
108+
function focusPrevious(): boolean {
109+
const [
110+
tabbableNodes,
111+
firstTabbableElem,
112+
lastTabbableElem,
113+
currentIndex,
114+
focusedElement,
115+
] = getTabbableNodes(scopeRef);
116+
117+
if (focusedElement === null) {
118+
firstTabbableElem.focus();
119+
} else if (focusedElement === firstTabbableElem) {
120+
if (contain === true) {
121+
lastTabbableElem.focus();
122+
} else {
123+
return true;
124+
}
125+
} else {
126+
tabbableNodes[currentIndex - 1].focus();
127+
}
128+
return false;
129+
}
130+
131+
function getPreviousController(): null | ControllerHandle {
132+
const tabbableScope = scopeRef.current;
133+
const allScopes = tabbableScope.getChildrenFromRoot();
134+
if (allScopes === null) {
135+
return null;
136+
}
137+
const currentScopeIndex = allScopes.indexOf(tabbableScope);
138+
if (currentScopeIndex <= 0) {
139+
return null;
140+
}
141+
return allScopes[currentScopeIndex - 1].getHandle();
142+
}
143+
144+
function getNextController(): null | ControllerHandle {
145+
const tabbableScope = scopeRef.current;
146+
const allScopes = tabbableScope.getChildrenFromRoot();
147+
if (allScopes === null) {
148+
return null;
149+
}
150+
const currentScopeIndex = allScopes.indexOf(tabbableScope);
151+
if (
152+
currentScopeIndex === -1 ||
153+
currentScopeIndex === allScopes.length - 1
154+
) {
155+
return null;
156+
}
157+
return allScopes[currentScopeIndex + 1].getHandle();
158+
}
159+
160+
const controllerHandle: ControllerHandle = {
161+
focusFirst,
162+
focusNext,
163+
focusPrevious,
164+
getNextController,
165+
getPreviousController,
166+
};
167+
168+
useImperativeHandle(ref, () => controllerHandle);
169+
170+
return (
171+
<TabbableScope
172+
ref={scopeRef}
173+
handle={controllerHandle}
174+
listeners={keyboard}>
175+
{children}
176+
</TabbableScope>
177+
);
178+
},
179+
);

0 commit comments

Comments
 (0)