Skip to content

Commit 4b0b556

Browse files
authored
[react-interactions] Refactor TabFocusController (#16768)
1 parent fb39f62 commit 4b0b556

File tree

5 files changed

+202
-192
lines changed

5 files changed

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

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

Lines changed: 0 additions & 179 deletions
This file was deleted.

0 commit comments

Comments
 (0)