[react-interactions] Repurpose React a11y modules (#16997)
Dominic Gannaway committed
Oct 3, 2019 at 11:19 UTC
b33633d932d070c1665f2c8700b025e7da6fbd5d
19 files changed
+698
-722
packages/react-interactions/accessibility/docs/FocusContain.md
new
+27
@@ -0,0 +1,27 @@
1
+# FocusContain
2
+
3
+`FocusContain` is a component that contains user-focusability to only that
4
+of the children of the component. This means focus control will not escape
5
+unless the componoent is disabled (using the `disabled` prop) or unmounted.
6
+Additionally, `FocusContain` can contain tab focus when passed a `ReactScope`
7
+using the `tabFocus` prop.
8
+
9
+## Usage
10
+
11
+```jsx
12
+import FocusContain from 'react-interactions/accessibility/focus-contain';
13
+import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
14
+
15
+function MyDialog(props) {
16
+ return (
17
+ <FocusContain tabScope={TabbableScope} disabled={false}>
18
+ <div>
19
+ <h2>{props.title}<h2>
20
+ <p>{props.text}</p>
21
+ <Button onPress={...}>Accept</Button>
22
+ <Button onPress={...}>Close</Button>
23
+ </div>
24
+ </FocusContain>
25
+ )
26
+}
27
+```
packages/react-interactions/accessibility/docs/FocusControl.md
deleted
-60
@@ -1,60 +0,0 @@
1
-# FocusControl
2
-
3
-`FocusControl` is a module that exports a selection of helpful utility functions to be used
4
-in conjunction with the `ref` from a React Scope, such as `TabbableScope`.
5
-A ref from `FocusManager` can also be used instead.
6
-
7
-## Example
8
-
9
-```jsx
10
-const {
11
- focusFirst,
12
- focusNext,
13
- focusPrevious,
14
- getNextScope,
15
- getPreviousScope,
16
-} = FocusControl;
17
-
18
-function KeyboardFocusMover(props) {
19
- const scopeRef = useRef(null);
20
-
21
- useEffect(() => {
22
- const scope = scopeRef.current;
23
-
24
- if (scope) {
25
- // Focus the first tabbable DOM node in my children
26
- focusFirst(scope);
27
- // Then focus the next chilkd
28
- focusNext(scope);
29
- }
30
- });
31
-
32
- return (
33
- <TabbableScope ref={scopeRef}>
34
- {props.children}
35
- </TabbableScope>
36
- );
37
-}
38
-```
39
-
40
-## FocusControl API
41
-
42
-### `focusFirst`
43
-
44
-Focus the first node that matches the given scope.
45
-
46
-### `focusNext`
47
-
48
-Focus the next sequential node that matchs the given scope.
49
-
50
-### `focusPrevious`
51
-
52
-Focus the previous sequential node that matchs the given scope.
53
-
54
-### `getNextScope`
55
-
56
-Focus the first node that matches the next sibling scope from the given scope.
57
-
58
-### `getPreviousScope`
59
-
60
-Focus the first node that matches the previous sibling scope from the given scope.
\ No newline at end of file
packages/react-interactions/accessibility/docs/FocusManager.md
+47
-27
@@ -1,39 +1,59 @@
1
# FocusManager
2
3
-`FocusManager` is a component that is designed to provide basic focus management
4
-control. These are the various props that `FocusManager` accepts:
3
+`FocusManager` is a module that exports a selection of helpful utility functions to be used
4
+in conjunction with the `ref` from a React Scope, such as `TabbableScope`.
5
6
-## Usage
6
+## Example
7
8
```jsx
9
-function MyDialog(props) {
9
+import {
10
+ focusFirst,
11
+ focusNext,
12
+ focusPrevious,
13
+ getNextScope,
14
+ getPreviousScope,
15
+} from 'react-interactions/accessibility/focus-manager';
16
+
17
+function KeyboardFocusMover(props) {
18
+ const scopeRef = useRef(null);
19
+
20
+ useEffect(() => {
21
+ const scope = scopeRef.current;
22
+
23
+ if (scope) {
24
+ // Focus the first tabbable DOM node in my children
25
+ focusFirst(scope);
26
+ // Then focus the next chilkd
27
+ focusNext(scope);
28
+ }
29
+ });
30
+
31
return (
11
- <FocusManager containFocus={true} autoFocus={true}>
12
- <div>
13
- <h2>{props.title}<h2>
14
- <p>{props.text}</p>
15
- <Button onPress={...}>Accept</Button>
16
- <Button onPress={...}>Close</Button>
17
- </div>
18
- </FocusManager>
19
- )
32
+ <TabbableScope ref={scopeRef}>
33
+ {props.children}
34
+ </TabbableScope>
35
+ );
36
}
37
```
38
23
-### `scope`
24
-`FocusManager` accepts a custom `ReactScope`. If a custom one is not supplied, `FocusManager`
25
-will default to using `TabbableScope`.
39
+## FocusManager API
40
+
41
+### `focusFirst`
42
+
43
+Focus the first node that matches the given scope.
44
+
45
+### `focusNext`
46
+
47
+Focus the next sequential node that matches the given scope.
48
+
49
+### `focusPrevious`
50
+
51
+Focus the previous sequential node that matches the given scope.
52
+
53
+### `getNextScope`
54
27
-### `autoFocus`
28
-When enabled, the first host node that matches the `FocusManager` scope will be focused
29
-upon the `FocusManager` mounting.
55
+Focus the first node that matches the next sibling scope from the given scope.
56
31
-### `restoreFocus`
32
-When enabled, the previous host node that was focused as `FocusManager` is mounted,
33
-has its focus restored upon `FocusManager` unmounting.
57
+### `getPreviousScope`
58
35
-### `containFocus`
36
-This contains the user focus to only that of `FocusManager`s sub-tree. Tabbing or
37
-interacting with nodes outside the sub-tree will restore focus back into the `FocusManager`.
38
-This is useful for modals, dialogs, dropdowns and other UI elements that require
39
-a form of user-focus control that is similar to the `inert` property on the web.
\ No newline at end of file
59
+Focus the first node that matches the previous sibling scope from the given scope.
packages/react-interactions/accessibility/docs/TabbableScope.md
+3
-1
@@ -1,11 +1,13 @@
1
# TabbableScope
2
3
`TabbableScope` is a custom scope implementation that can be used with
4
-`FocusManager`, `FocusList`, `FocusTable` and `FocusControl` modules.
4
+`FocusContain`, `FocusGroup`, `FocusTable` and `FocusManager` modules.
5
6
## Usage
7
8
```jsx
9
+import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
10
+
11
function FocusableNodeCollector(props) {
12
const scopeRef = useRef(null);
13
packages/react-interactions/accessibility/focus-contain.js
renamed
+1
-1
@@ -9,4 +9,4 @@
9
10
'use strict';
11
12
-module.exports = require('./src/FocusControl');
12
+module.exports = require('./src/FocusContain');
packages/react-interactions/accessibility/focus-group.js
renamed
+1
-1
@@ -9,4 +9,4 @@
9
10
'use strict';
11
12
-module.exports = require('./src/FocusList');
12
+module.exports = require('./src/FocusGroup');
packages/react-interactions/accessibility/src/FocusContain.js
new
+84
@@ -0,0 +1,84 @@
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 {ReactScope} from 'shared/ReactTypes';
11
+import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12
+
13
+import React from 'react';
14
+import {useFocusWithin} from 'react-interactions/events/focus';
15
+import {useKeyboard} from 'react-interactions/events/keyboard';
16
+import {
17
+ focusPrevious,
18
+ focusNext,
19
+} from 'react-interactions/accessibility/focus-manager';
20
+
21
+type FocusContainProps = {|
22
+ children: React.Node,
23
+ disabled?: boolean,
24
+ tabScope: ReactScope,
25
+|};
26
+
27
+const {useLayoutEffect, useRef} = React;
28
+
29
+export default function FocusContain({
30
+ children,
31
+ disabled,
32
+ tabScope: TabScope,
33
+}: FocusContainProps) {
34
+ const scopeRef = useRef(null);
35
+ // This ensures tabbing works through the React tree (including Portals and Suspense nodes)
36
+ const keyboard = useKeyboard({
37
+ onKeyDown(event: KeyboardEvent): void {
38
+ if (disabled || event.key !== 'Tab') {
39
+ event.continuePropagation();
40
+ return;
41
+ }
42
+ const scope = scopeRef.current;
43
+ if (scope !== null) {
44
+ if (event.shiftKey) {
45
+ focusPrevious(scope, event, true);
46
+ } else {
47
+ focusNext(scope, event, true);
48
+ }
49
+ }
50
+ },
51
+ });
52
+ const focusWithin = useFocusWithin({
53
+ onBlurWithin: function(event) {
54
+ if (disabled) {
55
+ event.continuePropagation();
56
+ return;
57
+ }
58
+ const lastNode = event.target;
59
+ if (lastNode) {
60
+ requestAnimationFrame(() => {
61
+ (lastNode: any).focus();
62
+ });
63
+ }
64
+ },
65
+ });
66
+ useLayoutEffect(
67
+ () => {
68
+ const scope = scopeRef.current;
69
+ if (scope && !disabled) {
70
+ const elems = scope.getScopedNodes();
71
+ if (elems && elems.indexOf(document.activeElement) === -1) {
72
+ elems[0].focus();
73
+ }
74
+ }
75
+ },
76
+ [disabled],
77
+ );
78
+
79
+ return (
80
+ <TabScope ref={scopeRef} listeners={[keyboard, focusWithin]}>
81
+ {children}
82
+ </TabScope>
83
+ );
84
+}
packages/react-interactions/accessibility/src/FocusControl.js
deleted
-178
@@ -1,178 +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 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 getNextScope(
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 getPreviousScope(
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
-}
140
-
141
-const tabIndexDesc = Object.getOwnPropertyDescriptor(
142
- HTMLElement.prototype,
143
- 'tabIndex',
144
-);
145
-const tabIndexSetter = (tabIndexDesc: any).set;
146
-
147
-export function setElementCanTab(elem: HTMLElement, canTab: boolean): void {
148
- let tabIndexState = (elem: any)._tabIndexState;
149
- if (!tabIndexState) {
150
- tabIndexState = {
151
- value: elem.tabIndex,
152
- canTab,
153
- };
154
- (elem: any)._tabIndexState = tabIndexState;
155
- if (!canTab) {
156
- elem.tabIndex = -1;
157
- }
158
- // We track the tabIndex value so we can restore the correct
159
- // tabIndex after we're done with it.
160
- // $FlowFixMe: Flow comoplains that we are missing value?
161
- Object.defineProperty(elem, 'tabIndex', {
162
- enumerable: false,
163
- configurable: true,
164
- get() {
165
- return tabIndexState.canTab ? tabIndexState.value : -1;
166
- },
167
- set(val) {
168
- if (tabIndexState.canTab) {
169
- tabIndexSetter.call(elem, val);
170
- }
171
- tabIndexState.value = val;
172
- },
173
- });
174
- } else if (tabIndexState.canTab !== canTab) {
175
- tabIndexSetter.call(elem, canTab ? tabIndexState.value : -1);
176
- tabIndexState.canTab = canTab;
177
- }
178
-}
packages/react-interactions/accessibility/src/FocusGroup.js
renamed
+43
-43
@@ -12,14 +12,14 @@ import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12
13
import React from 'react';
14
import {useKeyboard} from 'react-interactions/events/keyboard';
15
-import {setElementCanTab} from 'react-interactions/accessibility/focus-control';
15
+import setElementCanTab from './shared/setElementCanTab';
16
17
type FocusItemProps = {
18
children?: React.Node,
19
onKeyDown?: KeyboardEvent => void,
20
};
21
22
-type FocusListProps = {|
22
+type FocusGroupProps = {|
23
children: React.Node,
24
portrait: boolean,
25
wrap?: boolean,
@@ -29,7 +29,7 @@ type FocusListProps = {|
29
30
const {useRef} = React;
31
32
-function focusListItem(cell: ReactScopeMethods, event: KeyboardEvent): void {
32
+function focusGroupItem(cell: ReactScopeMethods, event: KeyboardEvent): void {
33
const tabbableNodes = cell.getScopedNodes();
34
if (tabbableNodes !== null && tabbableNodes.length > 0) {
35
tabbableNodes[0].focus();
@@ -37,14 +37,14 @@ function focusListItem(cell: ReactScopeMethods, event: KeyboardEvent): void {
37
}
38
}
39
40
-function getPreviousListItem(
41
- list: ReactScopeMethods,
40
+function getPreviousGroupItem(
41
+ group: ReactScopeMethods,
42
currentItem: ReactScopeMethods,
43
): null | ReactScopeMethods {
44
- const items = list.getChildren();
44
+ const items = group.getChildren();
45
if (items !== null) {
46
const currentItemIndex = items.indexOf(currentItem);
47
- const wrap = getListProps(currentItem).wrap;
47
+ const wrap = getGroupProps(currentItem).wrap;
48
if (currentItemIndex === 0 && wrap) {
49
return items[items.length - 1] || null;
50
} else if (currentItemIndex > 0) {
@@ -54,14 +54,14 @@ function getPreviousListItem(
54
return null;
55
}
56
57
-function getNextListItem(
58
- list: ReactScopeMethods,
57
+function getNextGroupItem(
58
+ group: ReactScopeMethods,
59
currentItem: ReactScopeMethods,
60
): null | ReactScopeMethods {
61
- const items = list.getChildren();
61
+ const items = group.getChildren();
62
if (items !== null) {
63
const currentItemIndex = items.indexOf(currentItem);
64
- const wrap = getListProps(currentItem).wrap;
64
+ const wrap = getGroupProps(currentItem).wrap;
65
const end = currentItemIndex === items.length - 1;
66
if (end && wrap) {
67
return items[0] || null;
@@ -72,12 +72,12 @@ function getNextListItem(
72
return null;
73
}
74
75
-function getListProps(currentCell: ReactScopeMethods): Object {
76
- const list = currentCell.getParent();
77
- if (list !== null) {
78
- const listProps = list.getProps();
79
- if (listProps && listProps.type === 'list') {
80
- return listProps;
75
+function getGroupProps(currentCell: ReactScopeMethods): Object {
76
+ const group = currentCell.getParent();
77
+ if (group !== null) {
78
+ const groupProps = group.getProps();
79
+ if (groupProps && groupProps.type === 'group') {
80
+ return groupProps;
81
}
82
}
83
return {};
@@ -90,20 +90,20 @@ function hasModifierKey(event: KeyboardEvent): boolean {
90
);
91
}
92
93
-export function createFocusList(scope: ReactScope): Array<React.Component> {
93
+export function createFocusGroup(scope: ReactScope): Array<React.Component> {
94
const TableScope = React.unstable_createScope(scope.fn);
95
96
- function List({
96
+ function Group({
97
children,
98
portrait,
99
wrap,
100
tabScope: TabScope,
101
allowModifiers,
102
- }): FocusListProps {
102
+ }): FocusGroupProps {
103
const tabScopeRef = useRef(null);
104
return (
105
<TableScope
106
- type="list"
106
+ type="group"
107
portrait={portrait}
108
wrap={wrap}
109
tabScopeRef={tabScopeRef}
@@ -123,14 +123,14 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
123
onKeyDown(event: KeyboardEvent): void {
124
const currentItem = scopeRef.current;
125
if (currentItem !== null) {
126
- const list = currentItem.getParent();
127
- const listProps = list && list.getProps();
128
- if (list !== null && listProps.type === 'list') {
129
- const portrait = listProps.portrait;
126
+ const group = currentItem.getParent();
127
+ const groupProps = group && group.getProps();
128
+ if (group !== null && groupProps.type === 'group') {
129
+ const portrait = groupProps.portrait;
130
const key = event.key;
131
132
if (key === 'Tab') {
133
- const tabScope = getListProps(currentItem).tabScopeRef.current;
133
+ const tabScope = getGroupProps(currentItem).tabScopeRef.current;
134
if (tabScope) {
135
const activeNode = document.activeElement;
136
const nodes = tabScope.getScopedNodes();
@@ -148,9 +148,9 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
148
return;
149
}
150
// Using modifier keys with keyboard arrow events should be no-ops
151
- // unless an explicit allowModifiers flag is set on the FocusList.
151
+ // unless an explicit allowModifiers flag is set on the FocusGroup.
152
if (hasModifierKey(event)) {
153
- const allowModifiers = getListProps(currentItem).allowModifiers;
153
+ const allowModifiers = getGroupProps(currentItem).allowModifiers;
154
if (!allowModifiers) {
155
event.continuePropagation();
156
return;
@@ -159,12 +159,12 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
159
switch (key) {
160
case 'ArrowUp': {
161
if (portrait) {
162
- const previousListItem = getPreviousListItem(
163
- list,
162
+ const previousGroupItem = getPreviousGroupItem(
163
+ group,
164
currentItem,
165
);
166
- if (previousListItem) {
167
- focusListItem(previousListItem, event);
166
+ if (previousGroupItem) {
167
+ focusGroupItem(previousGroupItem, event);
168
return;
169
}
170
}
@@ -172,9 +172,9 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
172
}
173
case 'ArrowDown': {
174
if (portrait) {
175
- const nextListItem = getNextListItem(list, currentItem);
176
- if (nextListItem) {
177
- focusListItem(nextListItem, event);
175
+ const nextGroupItem = getNextGroupItem(group, currentItem);
176
+ if (nextGroupItem) {
177
+ focusGroupItem(nextGroupItem, event);
178
return;
179
}
180
}
@@ -182,12 +182,12 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
182
}
183
case 'ArrowLeft': {
184
if (!portrait) {
185
- const previousListItem = getPreviousListItem(
186
- list,
185
+ const previousGroupItem = getPreviousGroupItem(
186
+ group,
187
currentItem,
188
);
189
- if (previousListItem) {
190
- focusListItem(previousListItem, event);
189
+ if (previousGroupItem) {
190
+ focusGroupItem(previousGroupItem, event);
191
return;
192
}
193
}
@@ -195,9 +195,9 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
195
}
196
case 'ArrowRight': {
197
if (!portrait) {
198
- const nextListItem = getNextListItem(list, currentItem);
199
- if (nextListItem) {
200
- focusListItem(nextListItem, event);
198
+ const nextGroupItem = getNextGroupItem(group, currentItem);
199
+ if (nextGroupItem) {
200
+ focusGroupItem(nextGroupItem, event);
201
return;
202
}
203
}
@@ -219,5 +219,5 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
219
);
220
}
221
222
- return [List, Item];
222
+ return [Group, Item];
223
}
packages/react-interactions/accessibility/src/FocusManager.js
+104
-99
@@ -7,109 +7,114 @@
7
* @flow
8
*/
9
10
-import type {ReactScope} from 'shared/ReactTypes';
10
+import type {ReactScopeMethods} from 'shared/ReactTypes';
11
import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12
13
-import React from 'react';
14
-import {useKeyboard} from 'react-interactions/events/keyboard';
15
-import {useFocusWithin} from 'react-interactions/events/focus';
16
-import {
17
- focusFirst,
18
- focusPrevious,
19
- focusNext,
20
-} from 'react-interactions/accessibility/focus-control';
21
-import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
13
+import getTabbableNodes from './shared/getTabbableNodes';
14
23
-type TabFocusProps = {|
24
- autoFocus?: boolean,
25
- children: React.Node,
26
- containFocus?: boolean,
27
- restoreFocus?: boolean,
28
- scope: ReactScope,
29
-|};
15
+export function focusFirst(scope: ReactScopeMethods): void {
16
+ const [, firstTabbableElem] = getTabbableNodes(scope);
17
+ focusElem(firstTabbableElem);
18
+}
19
31
-const {useLayoutEffect, useRef} = React;
20
+function focusElem(elem: null | HTMLElement): void {
21
+ if (elem !== null) {
22
+ elem.focus();
23
+ }
24
+}
25
33
-const FocusManager = React.forwardRef(
34
- (
35
- {
36
- autoFocus,
37
- children,
38
- containFocus,
39
- restoreFocus,
40
- scope: CustomScope,
41
- }: TabFocusProps,
42
- ref,
43
- ): React.Node => {
44
- const ScopeToUse = CustomScope || TabbableScope;
45
- const scopeRef = useRef(null);
46
- // This ensures tabbing works through the React tree (including Portals and Suspense nodes)
47
- const keyboard = useKeyboard({
48
- onKeyDown(event: KeyboardEvent): void {
49
- if (event.key !== 'Tab') {
50
- event.continuePropagation();
51
- return;
52
- }
53
- const scope = scopeRef.current;
54
- if (scope !== null) {
55
- if (event.shiftKey) {
56
- focusPrevious(scope, event, containFocus);
57
- } else {
58
- focusNext(scope, event, containFocus);
59
- }
60
- }
61
- },
62
- });
63
- const focusWithin = useFocusWithin({
64
- onBlurWithin: function(event) {
65
- if (!containFocus) {
66
- event.continuePropagation();
67
- return;
68
- }
69
- const lastNode = event.target;
70
- if (lastNode) {
71
- requestAnimationFrame(() => {
72
- (lastNode: any).focus();
73
- });
74
- }
75
- },
76
- });
77
- useLayoutEffect(
78
- () => {
79
- const scope = scopeRef.current;
80
- let restoreElem;
81
- if (restoreFocus) {
82
- restoreElem = document.activeElement;
83
- }
84
- if (autoFocus && scope !== null) {
85
- focusFirst(scope);
86
- }
87
- if (restoreElem) {
88
- return () => {
89
- (restoreElem: any).focus();
90
- };
91
- }
92
- },
93
- [scopeRef],
94
- );
26
+export function focusNext(
27
+ scope: ReactScopeMethods,
28
+ event?: KeyboardEvent,
29
+ contain?: boolean,
30
+): void {
31
+ const [
32
+ tabbableNodes,
33
+ firstTabbableElem,
34
+ lastTabbableElem,
35
+ currentIndex,
36
+ focusedElement,
37
+ ] = getTabbableNodes(scope);
38
96
- return (
97
- <ScopeToUse
98
- ref={node => {
99
- if (ref) {
100
- if (typeof ref === 'function') {
101
- ref(node);
102
- } else {
103
- ref.current = node;
104
- }
105
- }
106
- scopeRef.current = node;
107
- }}
108
- listeners={[keyboard, focusWithin]}>
109
- {children}
110
- </ScopeToUse>
111
- );
112
- },
113
-);
39
+ if (focusedElement === null) {
40
+ if (event) {
41
+ event.continuePropagation();
42
+ }
43
+ } else if (focusedElement === lastTabbableElem) {
44
+ if (contain) {
45
+ focusElem(firstTabbableElem);
46
+ if (event) {
47
+ event.preventDefault();
48
+ }
49
+ } else if (event) {
50
+ event.continuePropagation();
51
+ }
52
+ } else {
53
+ focusElem((tabbableNodes: any)[currentIndex + 1]);
54
+ if (event) {
55
+ event.preventDefault();
56
+ }
57
+ }
58
+}
59
115
-export default FocusManager;
60
+export function focusPrevious(
61
+ scope: ReactScopeMethods,
62
+ event?: KeyboardEvent,
63
+ contain?: boolean,
64
+): void {
65
+ const [
66
+ tabbableNodes,
67
+ firstTabbableElem,
68
+ lastTabbableElem,
69
+ currentIndex,
70
+ focusedElement,
71
+ ] = getTabbableNodes(scope);
72
+
73
+ if (focusedElement === null) {
74
+ if (event) {
75
+ event.continuePropagation();
76
+ }
77
+ } else if (focusedElement === firstTabbableElem) {
78
+ if (contain) {
79
+ focusElem(lastTabbableElem);
80
+ if (event) {
81
+ event.preventDefault();
82
+ }
83
+ } else if (event) {
84
+ event.continuePropagation();
85
+ }
86
+ } else {
87
+ focusElem((tabbableNodes: any)[currentIndex - 1]);
88
+ if (event) {
89
+ event.preventDefault();
90
+ }
91
+ }
92
+}
93
+
94
+export function getNextScope(
95
+ scope: ReactScopeMethods,
96
+): null | ReactScopeMethods {
97
+ const allScopes = scope.getChildrenFromRoot();
98
+ if (allScopes === null) {
99
+ return null;
100
+ }
101
+ const currentScopeIndex = allScopes.indexOf(scope);
102
+ if (currentScopeIndex === -1 || currentScopeIndex === allScopes.length - 1) {
103
+ return null;
104
+ }
105
+ return allScopes[currentScopeIndex + 1];
106
+}
107
+
108
+export function getPreviousScope(
109
+ scope: ReactScopeMethods,
110
+): null | ReactScopeMethods {
111
+ const allScopes = scope.getChildrenFromRoot();
112
+ if (allScopes === null) {
113
+ return null;
114
+ }
115
+ const currentScopeIndex = allScopes.indexOf(scope);
116
+ if (currentScopeIndex <= 0) {
117
+ return null;
118
+ }
119
+ return allScopes[currentScopeIndex - 1];
120
+}
packages/react-interactions/accessibility/src/FocusTable.js
+1
-1
@@ -12,7 +12,7 @@ import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12
13
import React from 'react';
14
import {useKeyboard} from 'react-interactions/events/keyboard';
15
-import {setElementCanTab} from 'react-interactions/accessibility/focus-control';
15
+import setElementCanTab from './shared/setElementCanTab';
16
17
type FocusCellProps = {
18
children?: React.Node,
packages/react-interactions/accessibility/src/__tests__/FocusContain-test.internal.js
new
+228
@@ -0,0 +1,228 @@
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 {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
11
+
12
+let React;
13
+let ReactFeatureFlags;
14
+let FocusContain;
15
+let TabbableScope;
16
+
17
+describe('FocusContain', () => {
18
+ beforeEach(() => {
19
+ jest.resetModules();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.enableScopeAPI = true;
22
+ ReactFeatureFlags.enableFlareAPI = true;
23
+ FocusContain = require('../FocusContain').default;
24
+ TabbableScope = require('../TabbableScope').default;
25
+ React = require('react');
26
+ });
27
+
28
+ describe('ReactDOM', () => {
29
+ let ReactDOM;
30
+ let container;
31
+
32
+ beforeEach(() => {
33
+ ReactDOM = require('react-dom');
34
+ container = document.createElement('div');
35
+ document.body.appendChild(container);
36
+ });
37
+
38
+ afterEach(() => {
39
+ document.body.removeChild(container);
40
+ container = null;
41
+ });
42
+
43
+ it('handles tab operations by default', () => {
44
+ const inputRef = React.createRef();
45
+ const input2Ref = React.createRef();
46
+ const buttonRef = React.createRef();
47
+ const button2Ref = React.createRef();
48
+ const divRef = React.createRef();
49
+
50
+ const Test = () => (
51
+ <FocusContain tabScope={TabbableScope}>
52
+ <input ref={inputRef} />
53
+ <button ref={buttonRef} />
54
+ <div ref={divRef} tabIndex={0} />
55
+ <input ref={input2Ref} tabIndex={-1} />
56
+ <button ref={button2Ref} />
57
+ </FocusContain>
58
+ );
59
+
60
+ ReactDOM.render(<Test />, container);
61
+ expect(document.activeElement).toBe(inputRef.current);
62
+ createEventTarget(document.activeElement).tabNext();
63
+ expect(document.activeElement).toBe(buttonRef.current);
64
+ createEventTarget(document.activeElement).tabNext();
65
+ expect(document.activeElement).toBe(divRef.current);
66
+ createEventTarget(document.activeElement).tabNext();
67
+ expect(document.activeElement).toBe(button2Ref.current);
68
+ createEventTarget(document.activeElement).tabPrevious();
69
+ expect(document.activeElement).toBe(divRef.current);
70
+ });
71
+
72
+ it('handles focus containment', () => {
73
+ const inputRef = React.createRef();
74
+ const input2Ref = React.createRef();
75
+ const input3Ref = React.createRef();
76
+ const buttonRef = React.createRef();
77
+ const button2Ref = React.createRef();
78
+
79
+ const Test = () => (
80
+ <div>
81
+ <FocusContain tabScope={TabbableScope}>
82
+ <input ref={inputRef} tabIndex={-1} />
83
+ <button ref={buttonRef} id={1} />
84
+ <button ref={button2Ref} id={2} />
85
+ <input ref={input2Ref} tabIndex={-1} />
86
+ </FocusContain>
87
+ <input ref={input3Ref} />
88
+ </div>
89
+ );
90
+
91
+ ReactDOM.render(<Test />, container);
92
+ buttonRef.current.focus();
93
+ createEventTarget(document.activeElement).tabNext();
94
+ expect(document.activeElement).toBe(button2Ref.current);
95
+ createEventTarget(document.activeElement).tabNext();
96
+ expect(document.activeElement).toBe(buttonRef.current);
97
+ createEventTarget(document.activeElement).tabNext();
98
+ expect(document.activeElement).toBe(button2Ref.current);
99
+ createEventTarget(document.activeElement).tabPrevious();
100
+ expect(document.activeElement).toBe(buttonRef.current);
101
+ createEventTarget(document.activeElement).tabPrevious();
102
+ expect(document.activeElement).toBe(button2Ref.current);
103
+ // Focus should be restored to the contained area
104
+ const rAF = window.requestAnimationFrame;
105
+ window.requestAnimationFrame = x => setTimeout(x);
106
+ input3Ref.current.focus();
107
+ jest.advanceTimersByTime(1);
108
+ window.requestAnimationFrame = rAF;
109
+ expect(document.activeElement).toBe(button2Ref.current);
110
+ });
111
+
112
+ it('works with a disabled nested FocusContain', () => {
113
+ const inputRef = React.createRef();
114
+ const input2Ref = React.createRef();
115
+ const buttonRef = React.createRef();
116
+ const button2Ref = React.createRef();
117
+ const button3Ref = React.createRef();
118
+ const button4Ref = React.createRef();
119
+
120
+ const Test = () => (
121
+ <FocusContain tabScope={TabbableScope}>
122
+ <input ref={inputRef} tabIndex={-1} />
123
+ <button ref={buttonRef} id={1} />
124
+ <FocusContain tabScope={TabbableScope} disabled={true}>
125
+ <button ref={button2Ref} id={2} />
126
+ <button ref={button3Ref} id={3} />
127
+ </FocusContain>
128
+ <input ref={input2Ref} tabIndex={-1} />
129
+ <button ref={button4Ref} id={4} />
130
+ </FocusContain>
131
+ );
132
+
133
+ ReactDOM.render(<Test />, container);
134
+ expect(document.activeElement).toBe(buttonRef.current);
135
+ createEventTarget(document.activeElement).tabNext();
136
+ expect(document.activeElement).toBe(button2Ref.current);
137
+ createEventTarget(document.activeElement).tabNext();
138
+ expect(document.activeElement).toBe(button3Ref.current);
139
+ createEventTarget(document.activeElement).tabNext();
140
+ expect(document.activeElement).toBe(button4Ref.current);
141
+ createEventTarget(document.activeElement).tabPrevious();
142
+ expect(document.activeElement).toBe(button3Ref.current);
143
+ createEventTarget(document.activeElement).tabPrevious();
144
+ expect(document.activeElement).toBe(button2Ref.current);
145
+ });
146
+
147
+ it('works with enabled nested FocusContain', () => {
148
+ const inputRef = React.createRef();
149
+ const input2Ref = React.createRef();
150
+ const buttonRef = React.createRef();
151
+ const button2Ref = React.createRef();
152
+ const button3Ref = React.createRef();
153
+ const button4Ref = React.createRef();
154
+
155
+ const Test = () => (
156
+ <FocusContain tabScope={TabbableScope}>
157
+ <input ref={inputRef} tabIndex={-1} />
158
+ <button ref={buttonRef} id={1} />
159
+ <FocusContain tabScope={TabbableScope} disabled={false}>
160
+ <button ref={button2Ref} id={2} />
161
+ <button ref={button3Ref} id={3} />
162
+ </FocusContain>
163
+ <input ref={input2Ref} tabIndex={-1} />
164
+ <button ref={button4Ref} id={4} />
165
+ </FocusContain>
166
+ );
167
+
168
+ ReactDOM.render(<Test />, container);
169
+ expect(document.activeElement).toBe(button2Ref.current);
170
+ createEventTarget(document.activeElement).tabNext();
171
+ expect(document.activeElement).toBe(button3Ref.current);
172
+ createEventTarget(document.activeElement).tabNext();
173
+ expect(document.activeElement).toBe(button2Ref.current);
174
+ createEventTarget(document.activeElement).tabPrevious();
175
+ expect(document.activeElement).toBe(button3Ref.current);
176
+ createEventTarget(document.activeElement).tabPrevious();
177
+ expect(document.activeElement).toBe(button2Ref.current);
178
+ });
179
+
180
+ it('should work as expected with suspense fallbacks', () => {
181
+ const buttonRef = React.createRef();
182
+ const button2Ref = React.createRef();
183
+ const button3Ref = React.createRef();
184
+ const button4Ref = React.createRef();
185
+ const button5Ref = React.createRef();
186
+
187
+ function SuspendedComponent() {
188
+ throw new Promise(() => {
189
+ // Never resolve
190
+ });
191
+ }
192
+
193
+ function Component() {
194
+ return (
195
+ <React.Fragment>
196
+ <button ref={button5Ref} id={5} />
197
+ <SuspendedComponent />
198
+ </React.Fragment>
199
+ );
200
+ }
201
+
202
+ const Test = () => (
203
+ <FocusContain tabScope={TabbableScope}>
204
+ <button ref={buttonRef} id={1} />
205
+ <button ref={button2Ref} id={2} />
206
+ <React.Suspense fallback={<button ref={button3Ref} id={3} />}>
207
+ <Component />
208
+ </React.Suspense>
209
+ <button ref={button4Ref} id={4} />
210
+ </FocusContain>
211
+ );
212
+
213
+ ReactDOM.render(<Test />, container);
214
+ buttonRef.current.focus();
215
+ expect(document.activeElement).toBe(buttonRef.current);
216
+ createEventTarget(document.activeElement).tabNext();
217
+ expect(document.activeElement).toBe(button2Ref.current);
218
+ createEventTarget(document.activeElement).tabNext();
219
+ expect(document.activeElement).toBe(button3Ref.current);
220
+ createEventTarget(document.activeElement).tabNext();
221
+ expect(document.activeElement).toBe(button4Ref.current);
222
+ createEventTarget(document.activeElement).tabPrevious();
223
+ expect(document.activeElement).toBe(button3Ref.current);
224
+ createEventTarget(document.activeElement).tabPrevious();
225
+ expect(document.activeElement).toBe(button2Ref.current);
226
+ });
227
+ });
228
+});
packages/react-interactions/accessibility/src/__tests__/FocusGroup-test.internal.js
renamed
+50
-50
@@ -8,20 +8,20 @@
8
*/
9
10
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
11
-import {emulateBrowserTab} from '../emulateBrowserTab';
11
+import {emulateBrowserTab} from '../shared/emulateBrowserTab';
12
13
let React;
14
let ReactFeatureFlags;
15
-let createFocusList;
15
+let createFocusGroup;
16
let TabbableScope;
17
18
-describe('FocusList', () => {
18
+describe('FocusGroup', () => {
19
beforeEach(() => {
20
jest.resetModules();
21
ReactFeatureFlags = require('shared/ReactFeatureFlags');
22
ReactFeatureFlags.enableScopeAPI = true;
23
ReactFeatureFlags.enableFlareAPI = true;
24
- createFocusList = require('../FocusList').createFocusList;
24
+ createFocusGroup = require('../FocusGroup').createFocusGroup;
25
TabbableScope = require('../TabbableScope').default;
26
React = require('react');
27
});
@@ -41,11 +41,11 @@ describe('FocusList', () => {
41
container = null;
42
});
43
44
- function createFocusListComponent() {
45
- const [FocusList, FocusItem] = createFocusList(TabbableScope);
44
+ function createFocusGroupComponent() {
45
+ const [FocusGroup, FocusItem] = createFocusGroup(TabbableScope);
46
47
return ({portrait, wrap, allowModifiers}) => (
48
- <FocusList
48
+ <FocusGroup
49
portrait={portrait}
50
wrap={wrap}
51
allowModifiers={allowModifiers}>
@@ -60,43 +60,43 @@ describe('FocusList', () => {
60
<li tabIndex={0}>Item 3</li>
61
</FocusItem>
62
</ul>
63
- </FocusList>
63
+ </FocusGroup>
64
);
65
}
66
67
it('handles keyboard arrow operations (portrait)', () => {
68
- const Test = createFocusListComponent();
68
+ const Test = createFocusGroupComponent();
69
70
ReactDOM.render(<Test portrait={true} />, container);
71
- const listItems = document.querySelectorAll('li');
72
- const firstListItem = createEventTarget(listItems[0]);
73
- firstListItem.focus();
74
- firstListItem.keydown({
71
+ const groupItems = document.querySelectorAll('li');
72
+ const firstGroupItem = createEventTarget(groupItems[0]);
73
+ firstGroupItem.focus();
74
+ firstGroupItem.keydown({
75
key: 'ArrowDown',
76
});
77
expect(document.activeElement.textContent).toBe('Item 2');
78
79
- const secondListItem = createEventTarget(document.activeElement);
80
- secondListItem.keydown({
79
+ const secondGroupItem = createEventTarget(document.activeElement);
80
+ secondGroupItem.keydown({
81
key: 'ArrowDown',
82
});
83
expect(document.activeElement.textContent).toBe('Item 3');
84
85
- const thirdListItem = createEventTarget(document.activeElement);
86
- thirdListItem.keydown({
85
+ const thirdGroupItem = createEventTarget(document.activeElement);
86
+ thirdGroupItem.keydown({
87
key: 'ArrowDown',
88
});
89
expect(document.activeElement.textContent).toBe('Item 3');
90
- thirdListItem.keydown({
90
+ thirdGroupItem.keydown({
91
key: 'ArrowRight',
92
});
93
expect(document.activeElement.textContent).toBe('Item 3');
94
- thirdListItem.keydown({
94
+ thirdGroupItem.keydown({
95
key: 'ArrowLeft',
96
});
97
expect(document.activeElement.textContent).toBe('Item 3');
98
// Should be a no-op due to modifier
99
- thirdListItem.keydown({
99
+ thirdGroupItem.keydown({
100
key: 'ArrowUp',
101
altKey: true,
102
});
@@ -104,80 +104,80 @@ describe('FocusList', () => {
104
});
105
106
it('handles keyboard arrow operations (landscape)', () => {
107
- const Test = createFocusListComponent();
107
+ const Test = createFocusGroupComponent();
108
109
ReactDOM.render(<Test portrait={false} />, container);
110
- const listItems = document.querySelectorAll('li');
111
- const firstListItem = createEventTarget(listItems[0]);
112
- firstListItem.focus();
113
- firstListItem.keydown({
110
+ const GroupItems = document.querySelectorAll('li');
111
+ const firstGroupItem = createEventTarget(GroupItems[0]);
112
+ firstGroupItem.focus();
113
+ firstGroupItem.keydown({
114
key: 'ArrowRight',
115
});
116
expect(document.activeElement.textContent).toBe('Item 2');
117
118
- const secondListItem = createEventTarget(document.activeElement);
119
- secondListItem.keydown({
118
+ const secondGroupItem = createEventTarget(document.activeElement);
119
+ secondGroupItem.keydown({
120
key: 'ArrowRight',
121
});
122
expect(document.activeElement.textContent).toBe('Item 3');
123
124
- const thirdListItem = createEventTarget(document.activeElement);
125
- thirdListItem.keydown({
124
+ const thirdGroupItem = createEventTarget(document.activeElement);
125
+ thirdGroupItem.keydown({
126
key: 'ArrowRight',
127
});
128
expect(document.activeElement.textContent).toBe('Item 3');
129
- thirdListItem.keydown({
129
+ thirdGroupItem.keydown({
130
key: 'ArrowUp',
131
});
132
expect(document.activeElement.textContent).toBe('Item 3');
133
- thirdListItem.keydown({
133
+ thirdGroupItem.keydown({
134
key: 'ArrowDown',
135
});
136
expect(document.activeElement.textContent).toBe('Item 3');
137
});
138
139
it('handles keyboard arrow operations (portrait) with wrapping enabled', () => {
140
- const Test = createFocusListComponent();
140
+ const Test = createFocusGroupComponent();
141
142
ReactDOM.render(<Test portrait={true} wrap={true} />, container);
143
- const listItems = document.querySelectorAll('li');
144
- let firstListItem = createEventTarget(listItems[0]);
145
- firstListItem.focus();
146
- firstListItem.keydown({
143
+ const GroupItems = document.querySelectorAll('li');
144
+ let firstGroupItem = createEventTarget(GroupItems[0]);
145
+ firstGroupItem.focus();
146
+ firstGroupItem.keydown({
147
key: 'ArrowDown',
148
});
149
expect(document.activeElement.textContent).toBe('Item 2');
150
151
- const secondListItem = createEventTarget(document.activeElement);
152
- secondListItem.keydown({
151
+ const secondGroupItem = createEventTarget(document.activeElement);
152
+ secondGroupItem.keydown({
153
key: 'ArrowDown',
154
});
155
expect(document.activeElement.textContent).toBe('Item 3');
156
157
- const thirdListItem = createEventTarget(document.activeElement);
158
- thirdListItem.keydown({
157
+ const thirdGroupItem = createEventTarget(document.activeElement);
158
+ thirdGroupItem.keydown({
159
key: 'ArrowDown',
160
});
161
expect(document.activeElement.textContent).toBe('Item 1');
162
163
- firstListItem = createEventTarget(document.activeElement);
164
- firstListItem.keydown({
163
+ firstGroupItem = createEventTarget(document.activeElement);
164
+ firstGroupItem.keydown({
165
key: 'ArrowUp',
166
});
167
expect(document.activeElement.textContent).toBe('Item 3');
168
});
169
170
it('handles keyboard arrow operations (portrait) with allowModifiers', () => {
171
- const Test = createFocusListComponent();
171
+ const Test = createFocusGroupComponent();
172
173
ReactDOM.render(
174
<Test portrait={true} allowModifiers={true} />,
175
container,
176
);
177
- const listItems = document.querySelectorAll('li');
178
- let firstListItem = createEventTarget(listItems[0]);
179
- firstListItem.focus();
180
- firstListItem.keydown({
177
+ const GroupItems = document.querySelectorAll('li');
178
+ let firstGroupItem = createEventTarget(GroupItems[0]);
179
+ firstGroupItem.focus();
180
+ firstGroupItem.keydown({
181
key: 'ArrowDown',
182
altKey: true,
183
});
@@ -185,7 +185,7 @@ describe('FocusList', () => {
185
});
186
187
it('handles keyboard arrow operations mixed with tabbing', () => {
188
- const [FocusList, FocusItem] = createFocusList(TabbableScope);
188
+ const [FocusGroup, FocusItem] = createFocusGroup(TabbableScope);
189
const beforeRef = React.createRef();
190
const afterRef = React.createRef();
191
@@ -193,7 +193,7 @@ describe('FocusList', () => {
193
return (
194
<>
195
<input placeholder="Before" ref={beforeRef} />
196
- <FocusList tabScope={TabbableScope} portrait={true}>
196
+ <FocusGroup tabScope={TabbableScope} portrait={true}>
197
<ul>
198
<FocusItem>
199
<li>
@@ -226,7 +226,7 @@ describe('FocusList', () => {
226
</li>
227
</FocusItem>
228
</ul>
229
- </FocusList>
229
+ </FocusGroup>
230
<input placeholder="After" ref={afterRef} />
231
</>
232
);
packages/react-interactions/accessibility/src/__tests__/FocusManager-test.internal.js
+15
-245
@@ -7,12 +7,10 @@
7
* @flow
8
*/
9
10
-import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
11
-
10
let React;
11
let ReactFeatureFlags;
12
let FocusManager;
15
-let FocusControl;
13
+let TabbableScope;
14
15
describe('FocusManager', () => {
16
beforeEach(() => {
@@ -20,8 +18,8 @@ describe('FocusManager', () => {
18
ReactFeatureFlags = require('shared/ReactFeatureFlags');
19
ReactFeatureFlags.enableScopeAPI = true;
20
ReactFeatureFlags.enableFlareAPI = true;
23
- FocusManager = require('../FocusManager').default;
24
- FocusControl = require('../FocusControl');
21
+ FocusManager = require('../FocusManager');
22
+ TabbableScope = require('../TabbableScope').default;
23
React = require('react');
24
});
25
@@ -40,235 +38,7 @@ describe('FocusManager', () => {
38
container = null;
39
});
40
43
- it('handles tab operations by default', () => {
44
- const inputRef = React.createRef();
45
- const input2Ref = React.createRef();
46
- const buttonRef = React.createRef();
47
- const button2Ref = React.createRef();
48
- const divRef = React.createRef();
49
-
50
- const Test = () => (
51
- <FocusManager>
52
- <input ref={inputRef} />
53
- <button ref={buttonRef} />
54
- <div ref={divRef} tabIndex={0} />
55
- <input ref={input2Ref} tabIndex={-1} />
56
- <button ref={button2Ref} />
57
- </FocusManager>
58
- );
59
-
60
- ReactDOM.render(<Test />, container);
61
- inputRef.current.focus();
62
- createEventTarget(document.activeElement).tabNext();
63
- expect(document.activeElement).toBe(buttonRef.current);
64
- createEventTarget(document.activeElement).tabNext();
65
- expect(document.activeElement).toBe(divRef.current);
66
- createEventTarget(document.activeElement).tabNext();
67
- expect(document.activeElement).toBe(button2Ref.current);
68
- createEventTarget(document.activeElement).tabPrevious();
69
- expect(document.activeElement).toBe(divRef.current);
70
- });
71
-
72
- it('handles autoFocus', () => {
73
- const buttonRef = React.createRef();
74
-
75
- const Test = () => (
76
- <FocusManager autoFocus={true}>
77
- <input tabIndex={-1} />
78
- <button ref={buttonRef} />
79
- </FocusManager>
80
- );
81
-
82
- ReactDOM.render(<Test />, container);
83
- expect(document.activeElement).toBe(buttonRef.current);
84
- });
85
-
86
- it('handles restoreFocus', () => {
87
- const difRef = React.createRef();
88
- const buttonRef = React.createRef();
89
-
90
- const Test = ({flag}) => {
91
- return (
92
- <div ref={difRef} tabIndex={0}>
93
- {flag ? (
94
- <FocusManager autoFocus={true} restoreFocus={true}>
95
- <button ref={buttonRef} />
96
- </FocusManager>
97
- ) : null}
98
- </div>
99
- );
100
- };
101
-
102
- ReactDOM.render(<Test flag={false} />, container);
103
- difRef.current.focus();
104
- expect(document.activeElement).toBe(difRef.current);
105
- ReactDOM.render(<Test flag={true} />, container);
106
- expect(document.activeElement).toBe(buttonRef.current);
107
- ReactDOM.render(<Test flag={false} />, container);
108
- expect(document.activeElement).toBe(difRef.current);
109
- });
110
-
111
- it('handles containFocus', () => {
112
- const inputRef = React.createRef();
113
- const input2Ref = React.createRef();
114
- const input3Ref = React.createRef();
115
- const buttonRef = React.createRef();
116
- const button2Ref = React.createRef();
117
-
118
- const Test = () => (
119
- <div>
120
- <FocusManager containFocus={true}>
121
- <input ref={inputRef} tabIndex={-1} />
122
- <button ref={buttonRef} id={1} />
123
- <button ref={button2Ref} id={2} />
124
- <input ref={input2Ref} tabIndex={-1} />
125
- </FocusManager>
126
- <input ref={input3Ref} />
127
- </div>
128
- );
129
-
130
- ReactDOM.render(<Test />, container);
131
- buttonRef.current.focus();
132
- createEventTarget(document.activeElement).tabNext();
133
- expect(document.activeElement).toBe(button2Ref.current);
134
- createEventTarget(document.activeElement).tabNext();
135
- expect(document.activeElement).toBe(buttonRef.current);
136
- createEventTarget(document.activeElement).tabNext();
137
- expect(document.activeElement).toBe(button2Ref.current);
138
- createEventTarget(document.activeElement).tabPrevious();
139
- expect(document.activeElement).toBe(buttonRef.current);
140
- createEventTarget(document.activeElement).tabPrevious();
141
- expect(document.activeElement).toBe(button2Ref.current);
142
- // Focus should be restored to the contained area
143
- const rAF = window.requestAnimationFrame;
144
- window.requestAnimationFrame = x => setTimeout(x);
145
- input3Ref.current.focus();
146
- jest.advanceTimersByTime(1);
147
- window.requestAnimationFrame = rAF;
148
- expect(document.activeElement).toBe(button2Ref.current);
149
- });
150
-
151
- it('works with nested FocusManagers', () => {
152
- const inputRef = React.createRef();
153
- const input2Ref = React.createRef();
154
- const buttonRef = React.createRef();
155
- const button2Ref = React.createRef();
156
- const button3Ref = React.createRef();
157
- const button4Ref = React.createRef();
158
-
159
- const Test = () => (
160
- <FocusManager>
161
- <input ref={inputRef} tabIndex={-1} />
162
- <button ref={buttonRef} id={1} />
163
- <FocusManager>
164
- <button ref={button2Ref} id={2} />
165
- <button ref={button3Ref} id={3} />
166
- </FocusManager>
167
- <input ref={input2Ref} tabIndex={-1} />
168
- <button ref={button4Ref} id={4} />
169
- </FocusManager>
170
- );
171
-
172
- ReactDOM.render(<Test />, container);
173
- buttonRef.current.focus();
174
- expect(document.activeElement).toBe(buttonRef.current);
175
- createEventTarget(document.activeElement).tabNext();
176
- expect(document.activeElement).toBe(button2Ref.current);
177
- createEventTarget(document.activeElement).tabNext();
178
- expect(document.activeElement).toBe(button3Ref.current);
179
- createEventTarget(document.activeElement).tabNext();
180
- expect(document.activeElement).toBe(button4Ref.current);
181
- createEventTarget(document.activeElement).tabPrevious();
182
- expect(document.activeElement).toBe(button3Ref.current);
183
- createEventTarget(document.activeElement).tabPrevious();
184
- expect(document.activeElement).toBe(button2Ref.current);
185
- });
186
-
187
- it('handles containFocus (nested FocusManagers)', () => {
188
- const inputRef = React.createRef();
189
- const input2Ref = React.createRef();
190
- const buttonRef = React.createRef();
191
- const button2Ref = React.createRef();
192
- const button3Ref = React.createRef();
193
- const button4Ref = React.createRef();
194
-
195
- const Test = () => (
196
- <FocusManager>
197
- <input ref={inputRef} tabIndex={-1} />
198
- <button ref={buttonRef} id={1} />
199
- <FocusManager containFocus={true}>
200
- <button ref={button2Ref} id={2} />
201
- <button ref={button3Ref} id={3} />
202
- </FocusManager>
203
- <input ref={input2Ref} tabIndex={-1} />
204
- <button ref={button4Ref} id={4} />
205
- </FocusManager>
206
- );
207
-
208
- ReactDOM.render(<Test />, container);
209
- buttonRef.current.focus();
210
- expect(document.activeElement).toBe(buttonRef.current);
211
- createEventTarget(document.activeElement).tabNext();
212
- expect(document.activeElement).toBe(button2Ref.current);
213
- createEventTarget(document.activeElement).tabNext();
214
- expect(document.activeElement).toBe(button3Ref.current);
215
- createEventTarget(document.activeElement).tabNext();
216
- expect(document.activeElement).toBe(button2Ref.current);
217
- createEventTarget(document.activeElement).tabPrevious();
218
- expect(document.activeElement).toBe(button3Ref.current);
219
- createEventTarget(document.activeElement).tabPrevious();
220
- expect(document.activeElement).toBe(button2Ref.current);
221
- });
222
-
223
- it('should work as expected with suspense fallbacks', () => {
224
- const buttonRef = React.createRef();
225
- const button2Ref = React.createRef();
226
- const button3Ref = React.createRef();
227
- const button4Ref = React.createRef();
228
- const button5Ref = React.createRef();
229
-
230
- function SuspendedComponent() {
231
- throw new Promise(() => {
232
- // Never resolve
233
- });
234
- }
235
-
236
- function Component() {
237
- return (
238
- <React.Fragment>
239
- <button ref={button5Ref} id={5} />
240
- <SuspendedComponent />
241
- </React.Fragment>
242
- );
243
- }
244
-
245
- const Test = () => (
246
- <FocusManager>
247
- <button ref={buttonRef} id={1} />
248
- <button ref={button2Ref} id={2} />
249
- <React.Suspense fallback={<button ref={button3Ref} id={3} />}>
250
- <Component />
251
- </React.Suspense>
252
- <button ref={button4Ref} id={4} />
253
- </FocusManager>
254
- );
255
-
256
- ReactDOM.render(<Test />, container);
257
- buttonRef.current.focus();
258
- expect(document.activeElement).toBe(buttonRef.current);
259
- createEventTarget(document.activeElement).tabNext();
260
- expect(document.activeElement).toBe(button2Ref.current);
261
- createEventTarget(document.activeElement).tabNext();
262
- expect(document.activeElement).toBe(button3Ref.current);
263
- createEventTarget(document.activeElement).tabNext();
264
- expect(document.activeElement).toBe(button4Ref.current);
265
- createEventTarget(document.activeElement).tabPrevious();
266
- expect(document.activeElement).toBe(button3Ref.current);
267
- createEventTarget(document.activeElement).tabPrevious();
268
- expect(document.activeElement).toBe(button2Ref.current);
269
- });
270
-
271
- it('allows for imperative tab focus control using FocusControl', () => {
41
+ it('allows for imperative tab focus control with a scope', () => {
42
const firstFocusControllerRef = React.createRef();
43
const secondFocusControllerRef = React.createRef();
44
const buttonRef = React.createRef();
@@ -277,16 +47,16 @@ describe('FocusManager', () => {
47
48
const Test = () => (
49
<div>
280
- <FocusManager ref={firstFocusControllerRef}>
50
+ <TabbableScope ref={firstFocusControllerRef}>
51
<input tabIndex={-1} />
52
<button ref={buttonRef} />
53
<button ref={button2Ref} />
54
<input tabIndex={-1} />
285
- </FocusManager>
286
- <FocusManager ref={secondFocusControllerRef}>
55
+ </TabbableScope>
56
+ <TabbableScope ref={secondFocusControllerRef}>
57
<input tabIndex={-1} />
58
<div ref={divRef} tabIndex={0} />
289
- </FocusManager>
59
+ </TabbableScope>
60
</div>
61
);
62
@@ -294,21 +64,21 @@ describe('FocusManager', () => {
64
const firstFocusController = firstFocusControllerRef.current;
65
const secondFocusController = secondFocusControllerRef.current;
66
297
- FocusControl.focusFirst(firstFocusController);
67
+ FocusManager.focusFirst(firstFocusController);
68
expect(document.activeElement).toBe(buttonRef.current);
299
- FocusControl.focusNext(firstFocusController);
69
+ FocusManager.focusNext(firstFocusController);
70
expect(document.activeElement).toBe(button2Ref.current);
301
- FocusControl.focusPrevious(firstFocusController);
71
+ FocusManager.focusPrevious(firstFocusController);
72
expect(document.activeElement).toBe(buttonRef.current);
73
304
- const nextController = FocusControl.getNextScope(firstFocusController);
74
+ const nextController = FocusManager.getNextScope(firstFocusController);
75
expect(nextController).toBe(secondFocusController);
306
- FocusControl.focusFirst(nextController);
76
+ FocusManager.focusFirst(nextController);
77
expect(document.activeElement).toBe(divRef.current);
78
309
- const previousController = FocusControl.getPreviousScope(nextController);
79
+ const previousController = FocusManager.getPreviousScope(nextController);
80
expect(previousController).toBe(firstFocusController);
311
- FocusControl.focusFirst(previousController);
81
+ FocusManager.focusFirst(previousController);
82
expect(document.activeElement).toBe(buttonRef.current);
83
});
84
});
packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js
+1
-1
@@ -8,7 +8,7 @@
8
*/
9
10
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
11
-import {emulateBrowserTab} from '../emulateBrowserTab';
11
+import {emulateBrowserTab} from '../shared/emulateBrowserTab';
12
13
let React;
14
let ReactFeatureFlags;
packages/react-interactions/accessibility/src/shared/emulateBrowserTab.js
renamed
+2
-4
@@ -12,7 +12,7 @@
12
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
13
14
// This function is used by the a11y modules for testing
15
-export function emulateBrowserTab(backwards) {
15
+export function emulateBrowserTab(backwards: boolean): void {
16
const activeElement = document.activeElement;
17
const focusedElem = createEventTarget(activeElement);
18
let defaultPrevented = false;
@@ -29,9 +29,7 @@ export function emulateBrowserTab(backwards) {
29
document.querySelectorAll(
30
'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
31
),
32
- ).filter(
33
- elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
34
- );
32
+ ).filter(elem => elem.tabIndex > -1 && !elem.disabled);
33
const idx = focusableElems.indexOf(activeElement);
34
if (idx !== -1) {
35
focusableElems[backwards ? idx - 1 : idx + 1].focus();
packages/react-interactions/accessibility/src/shared/getTabbableNodes.js
new
+31
@@ -0,0 +1,31 @@
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
+export default function getTabbableNodes(scope: ReactScopeMethods) {
13
+ const tabbableNodes = scope.getScopedNodes();
14
+ if (tabbableNodes === null || tabbableNodes.length === 0) {
15
+ return [null, null, null, 0, null];
16
+ }
17
+ const firstTabbableElem = tabbableNodes[0];
18
+ const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1];
19
+ const currentIndex = tabbableNodes.indexOf(document.activeElement);
20
+ let focusedElement = null;
21
+ if (currentIndex !== -1) {
22
+ focusedElement = tabbableNodes[currentIndex];
23
+ }
24
+ return [
25
+ tabbableNodes,
26
+ firstTabbableElem,
27
+ lastTabbableElem,
28
+ currentIndex,
29
+ focusedElement,
30
+ ];
31
+}
packages/react-interactions/accessibility/src/shared/setElementCanTab.js
new
+50
@@ -0,0 +1,50 @@
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
+const tabIndexDesc = Object.getOwnPropertyDescriptor(
11
+ HTMLElement.prototype,
12
+ 'tabIndex',
13
+);
14
+const tabIndexSetter = (tabIndexDesc: any).set;
15
+
16
+export default function setElementCanTab(
17
+ elem: HTMLElement,
18
+ canTab: boolean,
19
+): void {
20
+ let tabIndexState = (elem: any)._tabIndexState;
21
+ if (!tabIndexState) {
22
+ tabIndexState = {
23
+ value: elem.tabIndex,
24
+ canTab,
25
+ };
26
+ (elem: any)._tabIndexState = tabIndexState;
27
+ if (!canTab) {
28
+ elem.tabIndex = -1;
29
+ }
30
+ // We track the tabIndex value so we can restore the correct
31
+ // tabIndex after we're done with it.
32
+ // $FlowFixMe: Flow comoplains that we are missing value?
33
+ Object.defineProperty(elem, 'tabIndex', {
34
+ enumerable: false,
35
+ configurable: true,
36
+ get() {
37
+ return tabIndexState.canTab ? tabIndexState.value : -1;
38
+ },
39
+ set(val) {
40
+ if (tabIndexState.canTab) {
41
+ tabIndexSetter.call(elem, val);
42
+ }
43
+ tabIndexState.value = val;
44
+ },
45
+ });
46
+ } else if (tabIndexState.canTab !== canTab) {
47
+ tabIndexSetter.call(elem, canTab ? tabIndexState.value : -1);
48
+ tabIndexState.canTab = canTab;
49
+ }
50
+}
scripts/rollup/bundles.js
+10
-11
@@ -682,22 +682,21 @@ const bundles = [
682
{
683
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
684
moduleType: NON_FIBER_RENDERER,
685
- entry: 'react-interactions/accessibility/focus-manager',
686
- global: 'ReactFocusManager',
685
+ entry: 'react-interactions/accessibility/focus-contain',
686
+ global: 'ReactFocusContain',
687
externals: [
688
'react',
689
- 'react-interactions/events/keyboard',
689
'react-interactions/events/focus',
691
- 'react-interactions/accessibility/tabbable-scope',
692
- 'react-interactions/accessibility/focus-control',
690
+ 'react-interactions/events/keyboard',
691
+ 'react-interactions/accessibility/focus-manager',
692
],
693
},
694
695
{
696
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
697
moduleType: NON_FIBER_RENDERER,
699
- entry: 'react-interactions/accessibility/focus-control',
700
- global: 'ReactFocusControl',
698
+ entry: 'react-interactions/accessibility/focus-manager',
699
+ global: 'ReactFocusManager',
700
externals: ['react'],
701
},
702
@@ -712,13 +711,13 @@ const bundles = [
711
{
712
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
713
moduleType: NON_FIBER_RENDERER,
715
- entry: 'react-interactions/accessibility/focus-list',
716
- global: 'ReactFocusList',
714
+ entry: 'react-interactions/accessibility/focus-group',
715
+ global: 'ReactFocusGroup',
716
externals: [
717
'react',
718
'react-interactions/events/keyboard',
719
'react-interactions/accessibility/tabbable-scope',
721
- 'react-interactions/accessibility/focus-control',
720
+ 'react-interactions/accessibility/focus-manager',
721
],
722
},
723
];
@@ -728,7 +727,7 @@ const fbBundleExternalsMap = {
727
'react-interactions/events/keyboard': 'ReactEventsKeyboard',
728
'react-interactions/events/tap': 'ReactEventsTap',
729
'react-interactions/accessibility/tabbable-scope': 'ReactTabbableScope',
731
- 'react-interactions/accessibility/focus-control': 'ReactFocusControl',
730
+ 'react-interactions/accessibility/focus-manager': 'ReactFocusManager',
731
};
732
733
// Based on deep-freeze by substack (public domain)