@samitouri / QOS-React-2 / commits / b33633d932

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