Skip to content

Commit 4cb399a

Browse files
authored
[react-interactions] Modify Scope query mechanism (#17095)
1 parent e7704e2 commit 4cb399a

20 files changed

+263
-213
lines changed

packages/react-interactions/accessibility/docs/FocusContain.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ using the `tabFocus` prop.
1010

1111
```jsx
1212
import FocusContain from 'react-interactions/accessibility/focus-contain';
13-
import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
13+
import tabbableScopeQuery from 'react-interactions/accessibility/tabbable-scope-query';
1414

1515
function MyDialog(props) {
1616
return (
17-
<FocusContain tabScope={TabbableScope} disabled={false}>
17+
<FocusContain scopeQuery={tabbableScopeQuery} disabled={false}>
1818
<div>
1919
<h2>{props.title}<h2>
2020
<p>{props.text}</p>

packages/react-interactions/accessibility/docs/FocusManager.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import {
1414
getPreviousScope,
1515
} from 'react-interactions/accessibility/focus-manager';
1616

17+
function scopeQuery(type) {
18+
return type === 'div';
19+
}
20+
1721
function KeyboardFocusMover(props) {
1822
const scopeRef = useRef(null);
1923

@@ -22,9 +26,9 @@ function KeyboardFocusMover(props) {
2226

2327
if (scope) {
2428
// Focus the first tabbable DOM node in my children
25-
focusFirst(scope);
29+
focusFirst(scopeQuery, scope);
2630
// Then focus the next chilkd
27-
focusNext(scope);
31+
focusNext(scopeQuery, scope);
2832
}
2933
});
3034

packages/react-interactions/accessibility/docs/TabbableScope.md

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# TabbableScopeQuery
2+
3+
`TabbableScopeQuery` is a custom scope implementation that can be used with
4+
`FocusContain`, `FocusGroup`, `FocusTable` and `FocusManager` modules.
5+
6+
## Usage
7+
8+
```jsx
9+
import tabbableScopeQuery from 'react-interactions/accessibility/tabbable-scope-query';
10+
11+
function FocusableNodeCollector(props) {
12+
const scopeRef = useRef(null);
13+
14+
useEffect(() => {
15+
const scope = scopeRef.current;
16+
17+
if (scope) {
18+
const tabFocusableNodes = scope.queryAllNodes(tabbableScopeQuery);
19+
if (tabFocusableNodes && props.onFocusableNodes) {
20+
props.onFocusableNodes(tabFocusableNodes);
21+
}
22+
}
23+
});
24+
25+
return (
26+
<TabbableScope ref={scopeRef}>
27+
{props.children}
28+
</TabbableScope>
29+
);
30+
}
31+
```

packages/react-interactions/accessibility/src/FocusContain.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
* @flow
88
*/
99

10-
import type {ReactScope} from 'shared/ReactTypes';
1110
import type {KeyboardEvent} from 'react-interactions/events/keyboard';
1211

1312
import React from 'react';
@@ -21,15 +20,17 @@ import {
2120
type FocusContainProps = {|
2221
children: React.Node,
2322
disabled?: boolean,
24-
tabScope: ReactScope,
23+
scopeQuery: (type: string | Object, props: Object) => boolean,
2524
|};
2625

2726
const {useLayoutEffect, useRef} = React;
2827

28+
const FocusContainScope = React.unstable_createScope();
29+
2930
export default function FocusContain({
3031
children,
3132
disabled,
32-
tabScope: TabScope,
33+
scopeQuery,
3334
}: FocusContainProps): React.Node {
3435
const scopeRef = useRef(null);
3536
// This ensures tabbing works through the React tree (including Portals and Suspense nodes)
@@ -42,9 +43,9 @@ export default function FocusContain({
4243
const scope = scopeRef.current;
4344
if (scope !== null) {
4445
if (event.shiftKey) {
45-
focusPrevious(scope, event, true);
46+
focusPrevious(scopeQuery, scope, event, true);
4647
} else {
47-
focusNext(scope, event, true);
48+
focusNext(scopeQuery, scope, event, true);
4849
}
4950
}
5051
},
@@ -71,7 +72,7 @@ export default function FocusContain({
7172
disabled !== true &&
7273
!scope.containsNode(document.activeElement)
7374
) {
74-
const fistElem = scope.getFirstNode();
75+
const fistElem = scope.queryFirstNode(scopeQuery);
7576
if (fistElem !== null) {
7677
fistElem.focus();
7778
}
@@ -81,8 +82,8 @@ export default function FocusContain({
8182
);
8283

8384
return (
84-
<TabScope ref={scopeRef} listeners={[keyboard, focusWithin]}>
85+
<FocusContainScope ref={scopeRef} listeners={[keyboard, focusWithin]}>
8586
{children}
86-
</TabScope>
87+
</FocusContainScope>
8788
);
8889
}

packages/react-interactions/accessibility/src/FocusGroup.js

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import type {ReactScope, ReactScopeMethods} from 'shared/ReactTypes';
10+
import type {ReactScopeMethods} from 'shared/ReactTypes';
1111
import type {KeyboardEvent} from 'react-interactions/events/keyboard';
1212

1313
import React from 'react';
@@ -23,14 +23,18 @@ type FocusGroupProps = {|
2323
children: React.Node,
2424
portrait: boolean,
2525
wrap?: boolean,
26-
tabScope?: ReactScope,
26+
tabScopeQuery?: (type: string | Object, props: Object) => boolean,
2727
allowModifiers?: boolean,
2828
|};
2929

3030
const {useRef} = React;
3131

32-
function focusGroupItem(cell: ReactScopeMethods, event: KeyboardEvent): void {
33-
const firstScopedNode = cell.getFirstNode();
32+
function focusGroupItem(
33+
scopeQuery: (type: string | Object, props: Object) => boolean,
34+
cell: ReactScopeMethods,
35+
event: KeyboardEvent,
36+
): void {
37+
const firstScopedNode = cell.queryFirstNode(scopeQuery);
3438
if (firstScopedNode !== null) {
3539
firstScopedNode.focus();
3640
event.preventDefault();
@@ -91,30 +95,25 @@ function hasModifierKey(event: KeyboardEvent): boolean {
9195
}
9296

9397
export function createFocusGroup(
94-
scope: ReactScope,
98+
scopeQuery: (type: string | Object, props: Object) => boolean,
9599
): [(FocusGroupProps) => React.Node, (FocusItemProps) => React.Node] {
96-
const TableScope = React.unstable_createScope(scope.fn);
100+
const TableScope = React.unstable_createScope();
97101

98102
function Group({
99103
children,
100104
portrait,
101105
wrap,
102-
tabScope: TabScope,
106+
tabScopeQuery,
103107
allowModifiers,
104108
}: FocusGroupProps): React.Node {
105-
const tabScopeRef = useRef(null);
106109
return (
107110
<TableScope
108111
type="group"
109112
portrait={portrait}
110113
wrap={wrap}
111-
tabScopeRef={tabScopeRef}
114+
tabScopeQuery={tabScopeQuery}
112115
allowModifiers={allowModifiers}>
113-
{TabScope ? (
114-
<TabScope ref={tabScopeRef}>{children}</TabScope>
115-
) : (
116-
children
117-
)}
116+
{children}
118117
</TableScope>
119118
);
120119
}
@@ -132,19 +131,22 @@ export function createFocusGroup(
132131
const key = event.key;
133132

134133
if (key === 'Tab') {
135-
const tabScope = getGroupProps(currentItem).tabScopeRef.current;
136-
if (tabScope) {
137-
const activeNode = document.activeElement;
138-
const nodes = tabScope.getAllNodes();
139-
for (let i = 0; i < nodes.length; i++) {
140-
const node = nodes[i];
141-
if (node !== activeNode) {
142-
setElementCanTab(node, false);
143-
} else {
144-
setElementCanTab(node, true);
134+
const tabScopeQuery = getGroupProps(currentItem).tabScopeQuery;
135+
if (tabScopeQuery) {
136+
const groupScope = currentItem.getParent();
137+
if (groupScope) {
138+
const activeNode = document.activeElement;
139+
const nodes = groupScope.queryAllNodes(tabScopeQuery);
140+
for (let i = 0; i < nodes.length; i++) {
141+
const node = nodes[i];
142+
if (node !== activeNode) {
143+
setElementCanTab(node, false);
144+
} else {
145+
setElementCanTab(node, true);
146+
}
145147
}
148+
return;
146149
}
147-
return;
148150
}
149151
event.continuePropagation();
150152
return;
@@ -166,7 +168,7 @@ export function createFocusGroup(
166168
currentItem,
167169
);
168170
if (previousGroupItem) {
169-
focusGroupItem(previousGroupItem, event);
171+
focusGroupItem(scopeQuery, previousGroupItem, event);
170172
return;
171173
}
172174
}
@@ -176,7 +178,7 @@ export function createFocusGroup(
176178
if (portrait) {
177179
const nextGroupItem = getNextGroupItem(group, currentItem);
178180
if (nextGroupItem) {
179-
focusGroupItem(nextGroupItem, event);
181+
focusGroupItem(scopeQuery, nextGroupItem, event);
180182
return;
181183
}
182184
}
@@ -189,7 +191,7 @@ export function createFocusGroup(
189191
currentItem,
190192
);
191193
if (previousGroupItem) {
192-
focusGroupItem(previousGroupItem, event);
194+
focusGroupItem(scopeQuery, previousGroupItem, event);
193195
return;
194196
}
195197
}
@@ -199,7 +201,7 @@ export function createFocusGroup(
199201
if (!portrait) {
200202
const nextGroupItem = getNextGroupItem(group, currentItem);
201203
if (nextGroupItem) {
202-
focusGroupItem(nextGroupItem, event);
204+
focusGroupItem(scopeQuery, nextGroupItem, event);
203205
return;
204206
}
205207
}

packages/react-interactions/accessibility/src/FocusManager.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import type {KeyboardEvent} from 'react-interactions/events/keyboard';
1212

1313
import getTabbableNodes from './shared/getTabbableNodes';
1414

15-
export function focusFirst(scope: ReactScopeMethods): void {
16-
const [, firstTabbableElem] = getTabbableNodes(scope);
17-
focusElem(firstTabbableElem);
15+
export function focusFirst(
16+
scopeQuery: (type: string | Object, props: Object) => boolean,
17+
scope: ReactScopeMethods,
18+
): void {
19+
const firstNode = scope.queryFirstNode(scopeQuery);
20+
if (firstNode) {
21+
focusElem(firstNode);
22+
}
1823
}
1924

2025
function focusElem(elem: null | HTMLElement): void {
@@ -24,6 +29,7 @@ function focusElem(elem: null | HTMLElement): void {
2429
}
2530

2631
export function focusNext(
32+
scopeQuery: (type: string | Object, props: Object) => boolean,
2733
scope: ReactScopeMethods,
2834
event?: KeyboardEvent,
2935
contain?: boolean,
@@ -34,7 +40,7 @@ export function focusNext(
3440
lastTabbableElem,
3541
currentIndex,
3642
focusedElement,
37-
] = getTabbableNodes(scope);
43+
] = getTabbableNodes(scopeQuery, scope);
3844

3945
if (focusedElement === null) {
4046
if (event) {
@@ -58,6 +64,7 @@ export function focusNext(
5864
}
5965

6066
export function focusPrevious(
67+
scopeQuery: (type: string | Object, props: Object) => boolean,
6168
scope: ReactScopeMethods,
6269
event?: KeyboardEvent,
6370
contain?: boolean,
@@ -68,7 +75,7 @@ export function focusPrevious(
6875
lastTabbableElem,
6976
currentIndex,
7077
focusedElement,
71-
] = getTabbableNodes(scope);
78+
] = getTabbableNodes(scopeQuery, scope);
7279

7380
if (focusedElement === null) {
7481
if (event) {

0 commit comments

Comments
 (0)