@samitouri / QOS-React-2 / commits / 1a6294d3e2

[react-interaction] Refactor a11y components more (#16866)

Dominic Gannaway committed Sep 23, 2019 at 21:30 UTC 1a6294d3e2254f03267da3412becbe51188e830f
9 files changed +220 -180
packages/react-interactions/accessibility/focus-control.js new
+12
@@ -0,0 +1,12 @@
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 +'use strict';
11 +
12 +module.exports = require('./src/FocusControl');
packages/react-interactions/accessibility/src/FocusControl.js new
+139
@@ -0,0 +1,139 @@
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 +import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12 +
13 +function getTabbableNodes(scope: ReactScopeMethods) {
14 + const tabbableNodes = scope.getScopedNodes();
15 + if (tabbableNodes === null || tabbableNodes.length === 0) {
16 + return [null, null, null, 0, null];
17 + }
18 + const firstTabbableElem = tabbableNodes[0];
19 + const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1];
20 + const currentIndex = tabbableNodes.indexOf(document.activeElement);
21 + let focusedElement = null;
22 + if (currentIndex !== -1) {
23 + focusedElement = tabbableNodes[currentIndex];
24 + }
25 + return [
26 + tabbableNodes,
27 + firstTabbableElem,
28 + lastTabbableElem,
29 + currentIndex,
30 + focusedElement,
31 + ];
32 +}
33 +
34 +export function focusFirst(scope: ReactScopeMethods): void {
35 + const [, firstTabbableElem] = getTabbableNodes(scope);
36 + focusElem(firstTabbableElem);
37 +}
38 +
39 +function focusElem(elem: null | HTMLElement): void {
40 + if (elem !== null) {
41 + elem.focus();
42 + }
43 +}
44 +
45 +export function focusNext(
46 + scope: ReactScopeMethods,
47 + event?: KeyboardEvent,
48 + contain?: boolean,
49 +): void {
50 + const [
51 + tabbableNodes,
52 + firstTabbableElem,
53 + lastTabbableElem,
54 + currentIndex,
55 + focusedElement,
56 + ] = getTabbableNodes(scope);
57 +
58 + if (focusedElement === null) {
59 + if (event) {
60 + event.continuePropagation();
61 + }
62 + } else if (focusedElement === lastTabbableElem) {
63 + if (contain) {
64 + focusElem(firstTabbableElem);
65 + if (event) {
66 + event.preventDefault();
67 + }
68 + } else if (event) {
69 + event.continuePropagation();
70 + }
71 + } else {
72 + focusElem((tabbableNodes: any)[currentIndex + 1]);
73 + if (event) {
74 + event.preventDefault();
75 + }
76 + }
77 +}
78 +
79 +export function focusPrevious(
80 + scope: ReactScopeMethods,
81 + event?: KeyboardEvent,
82 + contain?: boolean,
83 +): void {
84 + const [
85 + tabbableNodes,
86 + firstTabbableElem,
87 + lastTabbableElem,
88 + currentIndex,
89 + focusedElement,
90 + ] = getTabbableNodes(scope);
91 +
92 + if (focusedElement === null) {
93 + if (event) {
94 + event.continuePropagation();
95 + }
96 + } else if (focusedElement === firstTabbableElem) {
97 + if (contain) {
98 + focusElem(lastTabbableElem);
99 + if (event) {
100 + event.preventDefault();
101 + }
102 + } else if (event) {
103 + event.continuePropagation();
104 + }
105 + } else {
106 + focusElem((tabbableNodes: any)[currentIndex - 1]);
107 + if (event) {
108 + event.preventDefault();
109 + }
110 + }
111 +}
112 +
113 +export function getNextController(
114 + scope: ReactScopeMethods,
115 +): null | ReactScopeMethods {
116 + const allScopes = scope.getChildrenFromRoot();
117 + if (allScopes === null) {
118 + return null;
119 + }
120 + const currentScopeIndex = allScopes.indexOf(scope);
121 + if (currentScopeIndex === -1 || currentScopeIndex === allScopes.length - 1) {
122 + return null;
123 + }
124 + return allScopes[currentScopeIndex + 1];
125 +}
126 +
127 +export function getPreviousController(
128 + scope: ReactScopeMethods,
129 +): null | ReactScopeMethods {
130 + const allScopes = scope.getChildrenFromRoot();
131 + if (allScopes === null) {
132 + return null;
133 + }
134 + const currentScopeIndex = allScopes.indexOf(scope);
135 + if (currentScopeIndex <= 0) {
136 + return null;
137 + }
138 + return allScopes[currentScopeIndex - 1];
139 +}
packages/react-interactions/accessibility/src/FocusTable.js
+4 -3
@@ -11,7 +11,6 @@ import type {ReactScopeMethods} from 'shared/ReactTypes';
11 import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12
13 import React from 'react';
14 -import {tabFocusableImpl} from 'react-interactions/accessibility/tabbable-scope';
14 import {useKeyboard} from 'react-interactions/events/keyboard';
15
16 type FocusCellProps = {
@@ -128,8 +127,10 @@ function triggerNavigateOut(
127 }
128 }
129
131 -export function createFocusTable(): Array<React.Component> {
132 - const TableScope = React.unstable_createScope(tabFocusableImpl);
130 +export function createFocusTable(
131 + scopeImpl: (type: string, props: Object) => boolean,
132 +): Array<React.Component> {
133 + const TableScope = React.unstable_createScope(scopeImpl);
134
135 function Table({children, onKeyboardOut, id}): FocusTableProps {
136 return (
packages/react-interactions/accessibility/src/TabFocus.js
+15 -145
@@ -7,158 +7,26 @@
7 * @flow
8 */
9
10 -import type {ReactScopeMethods} from 'shared/ReactTypes';
10 +import type {ReactScope} from 'shared/ReactTypes';
11 import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12
13 import React from 'react';
14 -import {TabbableScope} from 'react-interactions/accessibility/tabbable-scope';
14 import {useKeyboard} from 'react-interactions/events/keyboard';
15 +import {
16 + focusPrevious,
17 + focusNext,
18 +} from 'react-interactions/accessibility/focus-control';
19
17 -type TabFocusControllerProps = {
20 +type TabFocusProps = {
21 children: React.Node,
22 contain?: boolean,
23 + scope: ReactScope,
24 };
25
26 const {useRef} = React;
27
24 -function getTabbableNodes(scope: ReactScopeMethods) {
25 - const tabbableNodes = scope.getScopedNodes();
26 - if (tabbableNodes === null || tabbableNodes.length === 0) {
27 - return [null, null, null, 0, null];
28 - }
29 - const firstTabbableElem = tabbableNodes[0];
30 - const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1];
31 - const currentIndex = tabbableNodes.indexOf(document.activeElement);
32 - let focusedElement = null;
33 - if (currentIndex !== -1) {
34 - focusedElement = tabbableNodes[currentIndex];
35 - }
36 - return [
37 - tabbableNodes,
38 - firstTabbableElem,
39 - lastTabbableElem,
40 - currentIndex,
41 - focusedElement,
42 - ];
43 -}
44 -
45 -export function focusFirst(scope: ReactScopeMethods): void {
46 - const [, firstTabbableElem] = getTabbableNodes(scope);
47 - focusElem(firstTabbableElem);
48 -}
49 -
50 -function focusElem(elem: null | HTMLElement): void {
51 - if (elem !== null) {
52 - elem.focus();
53 - }
54 -}
55 -
56 -function internalFocusNext(
57 - scope: ReactScopeMethods,
58 - event?: KeyboardEvent,
59 - contain?: boolean,
60 -): void {
61 - const [
62 - tabbableNodes,
63 - firstTabbableElem,
64 - lastTabbableElem,
65 - currentIndex,
66 - focusedElement,
67 - ] = getTabbableNodes(scope);
68 -
69 - if (focusedElement === null) {
70 - if (event) {
71 - event.continuePropagation();
72 - }
73 - } else if (focusedElement === lastTabbableElem) {
74 - if (contain) {
75 - focusElem(firstTabbableElem);
76 - if (event) {
77 - event.preventDefault();
78 - }
79 - } else if (event) {
80 - event.continuePropagation();
81 - }
82 - } else {
83 - focusElem((tabbableNodes: any)[currentIndex + 1]);
84 - if (event) {
85 - event.preventDefault();
86 - }
87 - }
88 -}
89 -
90 -function internalFocusPrevious(
91 - scope: ReactScopeMethods,
92 - event?: KeyboardEvent,
93 - contain?: boolean,
94 -): void {
95 - const [
96 - tabbableNodes,
97 - firstTabbableElem,
98 - lastTabbableElem,
99 - currentIndex,
100 - focusedElement,
101 - ] = getTabbableNodes(scope);
102 -
103 - if (focusedElement === null) {
104 - if (event) {
105 - event.continuePropagation();
106 - }
107 - } else if (focusedElement === firstTabbableElem) {
108 - if (contain) {
109 - focusElem(lastTabbableElem);
110 - if (event) {
111 - event.preventDefault();
112 - }
113 - } else if (event) {
114 - event.continuePropagation();
115 - }
116 - } else {
117 - focusElem((tabbableNodes: any)[currentIndex - 1]);
118 - if (event) {
119 - event.preventDefault();
120 - }
121 - }
122 -}
123 -
124 -export function focusPrevious(scope: ReactScopeMethods): void {
125 - internalFocusPrevious(scope);
126 -}
127 -
128 -export function focusNext(scope: ReactScopeMethods): void {
129 - internalFocusNext(scope);
130 -}
131 -
132 -export function getNextController(
133 - scope: ReactScopeMethods,
134 -): null | ReactScopeMethods {
135 - const allScopes = scope.getChildrenFromRoot();
136 - if (allScopes === null) {
137 - return null;
138 - }
139 - const currentScopeIndex = allScopes.indexOf(scope);
140 - if (currentScopeIndex === -1 || currentScopeIndex === allScopes.length - 1) {
141 - return null;
142 - }
143 - return allScopes[currentScopeIndex + 1];
144 -}
145 -
146 -export function getPreviousController(
147 - scope: ReactScopeMethods,
148 -): null | ReactScopeMethods {
149 - const allScopes = scope.getChildrenFromRoot();
150 - if (allScopes === null) {
151 - return null;
152 - }
153 - const currentScopeIndex = allScopes.indexOf(scope);
154 - if (currentScopeIndex <= 0) {
155 - return null;
156 - }
157 - return allScopes[currentScopeIndex - 1];
158 -}
159 -
160 -export const TabFocusController = React.forwardRef(
161 - ({children, contain}: TabFocusControllerProps, ref): React.Node => {
28 +const TabFocus = React.forwardRef(
29 + ({children, contain, scope: Scope}: TabFocusProps, ref): React.Node => {
30 const scopeRef = useRef(null);
31 const keyboard = useKeyboard({
32 onKeyDown(event: KeyboardEvent): void {
@@ -169,16 +37,16 @@ export const TabFocusController = React.forwardRef(
37 const scope = scopeRef.current;
38 if (scope !== null) {
39 if (event.shiftKey) {
172 - internalFocusPrevious(scope, event, contain);
40 + focusPrevious(scope, event, contain);
41 } else {
174 - internalFocusNext(scope, event, contain);
42 + focusNext(scope, event, contain);
43 }
44 }
45 },
46 });
47
48 return (
181 - <TabbableScope
49 + <Scope
50 ref={node => {
51 if (ref) {
52 if (typeof ref === 'function') {
@@ -191,7 +59,9 @@ export const TabFocusController = React.forwardRef(
59 }}
60 listeners={keyboard}>
61 {children}
194 - </TabbableScope>
62 + </Scope>
63 );
64 },
65 );
66 +
67 +export default TabFocus;
packages/react-interactions/accessibility/src/TabbableScope.js
+3 -1
@@ -32,4 +32,6 @@ export const tabFocusableImpl = (type: string, props: Object): boolean => {
32 );
33 };
34
35 -export const TabbableScope = React.unstable_createScope(tabFocusableImpl);
35 +const TabbableScope = React.unstable_createScope(tabFocusableImpl);
36 +
37 +export default TabbableScope;
packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js
+5 -1
@@ -12,6 +12,7 @@ import {createEventTarget} from 'react-interactions/events/src/dom/testing-libra
12 let React;
13 let ReactFeatureFlags;
14 let createFocusTable;
15 +let tabFocusableImpl;
16
17 describe('ReactFocusTable', () => {
18 beforeEach(() => {
@@ -20,6 +21,7 @@ describe('ReactFocusTable', () => {
21 ReactFeatureFlags.enableScopeAPI = true;
22 ReactFeatureFlags.enableFlareAPI = true;
23 createFocusTable = require('../FocusTable').createFocusTable;
24 + tabFocusableImpl = require('../TabbableScope').tabFocusableImpl;
25 React = require('react');
26 });
27
@@ -39,7 +41,9 @@ describe('ReactFocusTable', () => {
41 });
42
43 function createFocusTableComponent() {
42 - const [FocusTable, FocusTableRow, FocusTableCell] = createFocusTable();
44 + const [FocusTable, FocusTableRow, FocusTableCell] = createFocusTable(
45 + tabFocusableImpl,
46 + );
47
48 return ({onKeyboardOut, id}) => (
49 <FocusTable onKeyboardOut={onKeyboardOut} id={id}>
packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js
+31 -29
@@ -11,8 +11,9 @@ import {createEventTarget} from 'react-interactions/events/src/dom/testing-libra
11
12 let React;
13 let ReactFeatureFlags;
14 -let TabFocusController;
15 -let ReactTabFocus;
14 +let TabFocus;
15 +let TabbableScope;
16 +let FocusControl;
17
18 describe('TabFocusController', () => {
19 beforeEach(() => {
@@ -20,8 +21,9 @@ describe('TabFocusController', () => {
21 ReactFeatureFlags = require('shared/ReactFeatureFlags');
22 ReactFeatureFlags.enableScopeAPI = true;
23 ReactFeatureFlags.enableFlareAPI = true;
23 - ReactTabFocus = require('../TabFocus');
24 - TabFocusController = ReactTabFocus.TabFocusController;
24 + TabFocus = require('../TabFocus').default;
25 + TabbableScope = require('../TabbableScope').default;
26 + FocusControl = require('../FocusControl');
27 React = require('react');
28 });
29
@@ -48,13 +50,13 @@ describe('TabFocusController', () => {
50 const divRef = React.createRef();
51
52 const Test = () => (
51 - <TabFocusController>
53 + <TabFocus scope={TabbableScope}>
54 <input ref={inputRef} />
55 <button ref={buttonRef} />
56 <div ref={divRef} tabIndex={0} />
57 <input ref={input2Ref} tabIndex={-1} />
58 <button ref={butto2nRef} />
57 - </TabFocusController>
59 + </TabFocus>
60 );
61
62 ReactDOM.render(<Test />, container);
@@ -76,12 +78,12 @@ describe('TabFocusController', () => {
78 const button2Ref = React.createRef();
79
80 const Test = () => (
79 - <TabFocusController contain={true}>
81 + <TabFocus scope={TabbableScope} contain={true}>
82 <input ref={inputRef} tabIndex={-1} />
83 <button ref={buttonRef} id={1} />
84 <button ref={button2Ref} id={2} />
85 <input ref={input2Ref} tabIndex={-1} />
84 - </TabFocusController>
86 + </TabFocus>
87 );
88
89 ReactDOM.render(<Test />, container);
@@ -107,16 +109,16 @@ describe('TabFocusController', () => {
109 const button4Ref = React.createRef();
110
111 const Test = () => (
110 - <TabFocusController>
112 + <TabFocus scope={TabbableScope}>
113 <input ref={inputRef} tabIndex={-1} />
114 <button ref={buttonRef} id={1} />
113 - <TabFocusController>
115 + <TabFocus scope={TabbableScope}>
116 <button ref={button2Ref} id={2} />
117 <button ref={button3Ref} id={3} />
116 - </TabFocusController>
118 + </TabFocus>
119 <input ref={input2Ref} tabIndex={-1} />
120 <button ref={button4Ref} id={4} />
119 - </TabFocusController>
121 + </TabFocus>
122 );
123
124 ReactDOM.render(<Test />, container);
@@ -143,16 +145,16 @@ describe('TabFocusController', () => {
145 const button4Ref = React.createRef();
146
147 const Test = () => (
146 - <TabFocusController>
148 + <TabFocus scope={TabbableScope}>
149 <input ref={inputRef} tabIndex={-1} />
150 <button ref={buttonRef} id={1} />
149 - <TabFocusController contain={true}>
151 + <TabFocus contain={true} scope={TabbableScope}>
152 <button ref={button2Ref} id={2} />
153 <button ref={button3Ref} id={3} />
152 - </TabFocusController>
154 + </TabFocus>
155 <input ref={input2Ref} tabIndex={-1} />
156 <button ref={button4Ref} id={4} />
155 - </TabFocusController>
157 + </TabFocus>
158 );
159
160 ReactDOM.render(<Test />, container);
@@ -193,14 +195,14 @@ describe('TabFocusController', () => {
195 }
196
197 const Test = () => (
196 - <TabFocusController>
198 + <TabFocus scope={TabbableScope}>
199 <button ref={buttonRef} id={1} />
200 <button ref={button2Ref} id={2} />
201 <React.Suspense fallback={<button ref={button3Ref} id={3} />}>
202 <Component />
203 </React.Suspense>
204 <button ref={button4Ref} id={4} />
203 - </TabFocusController>
205 + </TabFocus>
206 );
207
208 ReactDOM.render(<Test />, container);
@@ -227,16 +229,16 @@ describe('TabFocusController', () => {
229
230 const Test = () => (
231 <div>
230 - <TabFocusController ref={firstFocusControllerRef}>
232 + <TabFocus ref={firstFocusControllerRef} scope={TabbableScope}>
233 <input tabIndex={-1} />
234 <button ref={buttonRef} />
235 <button ref={button2Ref} />
236 <input tabIndex={-1} />
235 - </TabFocusController>
236 - <TabFocusController ref={secondFocusControllerRef}>
237 + </TabFocus>
238 + <TabFocus ref={secondFocusControllerRef} scope={TabbableScope}>
239 <input tabIndex={-1} />
240 <div ref={divRef} tabIndex={0} />
239 - </TabFocusController>
241 + </TabFocus>
242 </div>
243 );
244
@@ -244,25 +246,25 @@ describe('TabFocusController', () => {
246 const firstFocusController = firstFocusControllerRef.current;
247 const secondFocusController = secondFocusControllerRef.current;
248
247 - ReactTabFocus.focusFirst(firstFocusController);
249 + FocusControl.focusFirst(firstFocusController);
250 expect(document.activeElement).toBe(buttonRef.current);
249 - ReactTabFocus.focusNext(firstFocusController);
251 + FocusControl.focusNext(firstFocusController);
252 expect(document.activeElement).toBe(button2Ref.current);
251 - ReactTabFocus.focusPrevious(firstFocusController);
253 + FocusControl.focusPrevious(firstFocusController);
254 expect(document.activeElement).toBe(buttonRef.current);
255
254 - const nextController = ReactTabFocus.getNextController(
256 + const nextController = FocusControl.getNextController(
257 firstFocusController,
258 );
259 expect(nextController).toBe(secondFocusController);
258 - ReactTabFocus.focusFirst(nextController);
260 + FocusControl.focusFirst(nextController);
261 expect(document.activeElement).toBe(divRef.current);
262
261 - const previousController = ReactTabFocus.getPreviousController(
263 + const previousController = FocusControl.getPreviousController(
264 nextController,
265 );
266 expect(previousController).toBe(firstFocusController);
265 - ReactTabFocus.focusFirst(previousController);
267 + FocusControl.focusFirst(previousController);
268 expect(document.activeElement).toBe(buttonRef.current);
269 });
270 });
packages/react-interactions/accessibility/src/__tests__/TabbableScope-test.internal.js
+1 -1
@@ -16,7 +16,7 @@ describe('TabbableScope', () => {
16 jest.resetModules();
17 ReactFeatureFlags = require('shared/ReactFeatureFlags');
18 ReactFeatureFlags.enableScopeAPI = true;
19 - TabbableScope = require('../TabbableScope').TabbableScope;
19 + TabbableScope = require('../TabbableScope').default;
20 React = require('react');
21 });
22
scripts/rollup/bundles.js
+10
@@ -687,9 +687,18 @@ const bundles = [
687 'react',
688 'react-interactions/events/keyboard',
689 'react-interactions/accessibility/tabbable-scope',
690 + 'react-interactions/accessibility/focus-control',
691 ],
692 },
693
694 + {
695 + bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
696 + moduleType: NON_FIBER_RENDERER,
697 + entry: 'react-interactions/accessibility/focus-control',
698 + global: 'ReactFocusControl',
699 + externals: ['react'],
700 + },
701 +
702 {
703 bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
704 moduleType: NON_FIBER_RENDERER,
@@ -703,6 +712,7 @@ const fbBundleExternalsMap = {
712 'react-interactions/events/keyboard': 'ReactEventsKeyboard',
713 'react-interactions/events/tap': 'ReactEventsTap',
714 'react-interactions/accessibility/tabbable-scope': 'ReactTabbableScope',
715 + 'react-interactions/accessibility/focus-control': 'ReactFocusControl',
716 };
717
718 // Based on deep-freeze by substack (public domain)