[react-interactions] Refactor TabFocusController (#16768)
Dominic Gannaway committed
Sep 12, 2019 at 22:28 UTC
4b0b556dcf6f84c3e4a4e2a9dd0426c3430d1c86
5 files changed
+202
-192
packages/react-dom/src/client/focus/ReactTabFocus.js
new
+188
@@ -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
deleted
-179
@@ -1,179 +0,0 @@
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
-);
packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js
+14
-8
@@ -12,6 +12,7 @@ import {createEventTarget} from 'react-events/src/dom/testing-library';
12
let React;
13
let ReactFeatureFlags;
14
let TabFocusController;
15
+let ReactTabFocus;
16
17
describe('TabFocusController', () => {
18
beforeEach(() => {
@@ -19,7 +20,8 @@ describe('TabFocusController', () => {
20
ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
ReactFeatureFlags.enableScopeAPI = true;
22
ReactFeatureFlags.enableFlareAPI = true;
22
- TabFocusController = require('../TabFocusController').TabFocusController;
23
+ ReactTabFocus = require('../ReactTabFocus');
24
+ TabFocusController = ReactTabFocus.TabFocusController;
25
React = require('react');
26
});
27
@@ -242,21 +244,25 @@ describe('TabFocusController', () => {
244
const firstFocusController = firstFocusControllerRef.current;
245
const secondFocusController = secondFocusControllerRef.current;
246
245
- firstFocusController.focusFirst();
247
+ ReactTabFocus.focusFirst(firstFocusController);
248
expect(document.activeElement).toBe(buttonRef.current);
247
- firstFocusController.focusNext();
249
+ ReactTabFocus.focusNext(firstFocusController);
250
expect(document.activeElement).toBe(button2Ref.current);
249
- firstFocusController.focusPrevious();
251
+ ReactTabFocus.focusPrevious(firstFocusController);
252
expect(document.activeElement).toBe(buttonRef.current);
253
252
- const nextController = firstFocusController.getNextController();
254
+ const nextController = ReactTabFocus.getNextController(
255
+ firstFocusController,
256
+ );
257
expect(nextController).toBe(secondFocusController);
254
- nextController.focusNext();
258
+ ReactTabFocus.focusNext(nextController);
259
expect(document.activeElement).toBe(divRef.current);
260
257
- const previousController = nextController.getPreviousController();
261
+ const previousController = ReactTabFocus.getPreviousController(
262
+ nextController,
263
+ );
264
expect(previousController).toBe(firstFocusController);
259
- previousController.focusNext();
265
+ ReactTabFocus.focusNext(previousController);
266
expect(document.activeElement).toBe(buttonRef.current);
267
});
268
});
packages/react-reconciler/src/ReactFiberScope.js
-4
@@ -133,10 +133,6 @@ export function createScopeMethods(
133
collectNearestChildScopeMethods(node.child, scope, childrenScopes);
134
return childrenScopes.length === 0 ? null : childrenScopes;
135
},
136
- getHandle(): null | mixed {
137
- const currentFiber = ((instance.fiber: any): Fiber);
138
- return currentFiber.memoizedProps.handle || null;
139
- },
136
getParent(): null | ReactScopeMethods {
137
let node = ((instance.fiber: any): Fiber).return;
138
while (node !== null) {
packages/shared/ReactTypes.js
-1
@@ -167,7 +167,6 @@ export type ReactScope = {|
167
export type ReactScopeMethods = {|
168
getChildren(): null | Array<ReactScopeMethods>,
169
getChildrenFromRoot(): null | Array<ReactScopeMethods>,
170
- getHandle(): null | mixed,
170
getParent(): null | ReactScopeMethods,
171
getScopedNodes(): null | Array<Object>,
172
|};