@samitouri / QOS-React-2 / commits / 4b0b556dcf

[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 |};