[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)