Skip to content

Commit b71ab61

Browse files
authored
[react-interactions] Adds more experimental Scope API methods (#17042)
1 parent 5a71cbe commit b71ab61

File tree

17 files changed

+261
-37
lines changed

17 files changed

+261
-37
lines changed

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,7 @@ export function updateFundamentalComponent(fundamentalInstance) {
461461
export function unmountFundamentalComponent(fundamentalInstance) {
462462
throw new Error('Not yet implemented.');
463463
}
464+
465+
export function getInstanceFromNode(node) {
466+
throw new Error('Not yet implemented.');
467+
}

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
* @flow
88
*/
99

10-
import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree';
10+
import {
11+
precacheFiberNode,
12+
updateFiberProps,
13+
getClosestInstanceFromNode,
14+
} from './ReactDOMComponentTree';
1115
import {
1216
createElement,
1317
createTextNode,
@@ -976,3 +980,7 @@ export function unmountFundamentalComponent(
976980
}
977981
}
978982
}
983+
984+
export function getInstanceFromNode(node: HTMLElement): null | Object {
985+
return getClosestInstanceFromNode(node) || null;
986+
}

packages/react-interactions/accessibility/README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ function MyComponent(props) {
4242
);
4343
}
4444

45-
// Using the ref, we can get the host nodes via getScopedNodes()
46-
const divs = divOnlyScope.current.getScopedNodes();
45+
// Using the ref, we can get the host nodes via getAllNodes()
46+
const divs = divOnlyScope.current.getAllNodes();
4747

4848
// [<div>DIV 1</div>, <div>DIV 2</div>, <div>DIV 3</div>]
4949
console.log(divs);
@@ -72,7 +72,17 @@ Returns the parent `ReactScopeInterface` of the scope node or `null` if none exi
7272

7373
Returns the current `props` object of the scope node.
7474

75-
### getScopedNodes: () => null | Array<HTMLElement>
75+
### getAllNodes: () => null | Array<HTMLElement>
7676

7777
Returns an array of all child host nodes that successfully match when queried using the
78-
query function passed to the scope. Returns `null` if there are no matching host nodes.
78+
query function passed to the scope. Returns `null` if there are no matching host nodes.
79+
80+
### getFirstNode: () => null | HTMLElement
81+
82+
Returns the first child host node that successfully matches when queried using the
83+
query function passed to the scope. Returns `null` if there is no matching host node.
84+
85+
### containsNode: (node: HTMLElement) => boolean
86+
87+
Returns `true` or `false` depending on if the given `HTMLElement` is a descendant
88+
of the scope's sub-tree.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function FocusableNodeCollector(props) {
1515
const scope = scopeRef.current;
1616

1717
if (scope) {
18-
const tabFocusableNodes = scope.getScopedNodes();
18+
const tabFocusableNodes = scope.getAllNodes();
1919
if (tabFocusableNodes && props.onFocusableNodes) {
2020
props.onFocusableNodes(tabFocusableNodes);
2121
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@ export default function FocusContain({
6666
useLayoutEffect(
6767
() => {
6868
const scope = scopeRef.current;
69-
if (scope !== null && disabled !== true) {
70-
const elems = scope.getScopedNodes();
71-
if (elems && elems.indexOf(document.activeElement) === -1) {
72-
elems[0].focus();
69+
if (
70+
scope !== null &&
71+
disabled !== true &&
72+
!scope.containsNode(document.activeElement)
73+
) {
74+
const fistElem = scope.getFirstNode();
75+
if (fistElem !== null) {
76+
fistElem.focus();
7377
}
7478
}
7579
},

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ type FocusGroupProps = {|
3030
const {useRef} = React;
3131

3232
function focusGroupItem(cell: ReactScopeMethods, event: KeyboardEvent): void {
33-
const tabbableNodes = cell.getScopedNodes();
34-
if (tabbableNodes !== null && tabbableNodes.length > 0) {
35-
tabbableNodes[0].focus();
33+
const firstScopedNode = cell.getFirstNode();
34+
if (firstScopedNode !== null) {
35+
firstScopedNode.focus();
3636
event.preventDefault();
3737
}
3838
}
@@ -135,7 +135,7 @@ export function createFocusGroup(
135135
const tabScope = getGroupProps(currentItem).tabScopeRef.current;
136136
if (tabScope) {
137137
const activeNode = document.activeElement;
138-
const nodes = tabScope.getScopedNodes();
138+
const nodes = tabScope.getAllNodes();
139139
for (let i = 0; i < nodes.length; i++) {
140140
const node = nodes[i];
141141
if (node !== activeNode) {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ type FocusTableProps = {|
3939
const {useRef} = React;
4040

4141
function focusScope(cell: ReactScopeMethods, event?: KeyboardEvent): void {
42-
const tabbableNodes = cell.getScopedNodes();
43-
if (tabbableNodes !== null && tabbableNodes.length > 0) {
44-
tabbableNodes[0].focus();
42+
const firstScopedNode = cell.getFirstNode();
43+
if (firstScopedNode !== null) {
44+
firstScopedNode.focus();
4545
if (event) {
4646
event.preventDefault();
4747
}
@@ -209,7 +209,7 @@ export function createFocusTable(
209209
const tabScope = getTableProps(currentCell).tabScopeRef.current;
210210
if (tabScope) {
211211
const activeNode = document.activeElement;
212-
const nodes = tabScope.getScopedNodes();
212+
const nodes = tabScope.getAllNodes();
213213
for (let i = 0; i < nodes.length; i++) {
214214
const node = nodes[i];
215215
if (node !== activeNode) {

packages/react-interactions/accessibility/src/__tests__/TabbableScope-test.internal.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('TabbableScope', () => {
3535
container = null;
3636
});
3737

38-
it('getScopedNodes() works as intended', () => {
38+
it('getAllNodes() works as intended', () => {
3939
const scopeRef = React.createRef();
4040
const nodeRefA = React.createRef();
4141
const nodeRefB = React.createRef();
@@ -58,7 +58,7 @@ describe('TabbableScope', () => {
5858
}
5959

6060
ReactDOM.render(<Test />, container);
61-
let nodes = scopeRef.current.getScopedNodes();
61+
let nodes = scopeRef.current.getAllNodes();
6262
expect(nodes).toEqual([
6363
nodeRefA.current,
6464
nodeRefB.current,

packages/react-interactions/accessibility/src/shared/getTabbableNodes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function getTabbableNodes(
1818
number,
1919
null | HTMLElement,
2020
] {
21-
const tabbableNodes = scope.getScopedNodes();
21+
const tabbableNodes = scope.getAllNodes();
2222
if (tabbableNodes === null || tabbableNodes.length === 0) {
2323
return [null, null, null, 0, null];
2424
}

packages/react-native-renderer/src/ReactFabricHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,7 @@ export function unmountFundamentalComponent(fundamentalInstance) {
491491
export function cloneFundamentalInstance(fundamentalInstance) {
492492
throw new Error('Not yet implemented.');
493493
}
494+
495+
export function getInstanceFromNode(node) {
496+
throw new Error('Not yet implemented.');
497+
}

packages/react-native-renderer/src/ReactNativeHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,3 +530,7 @@ export function updateFundamentalComponent(fundamentalInstance) {
530530
export function unmountFundamentalComponent(fundamentalInstance) {
531531
throw new Error('Not yet implemented.');
532532
}
533+
534+
export function getInstanceFromNode(node) {
535+
throw new Error('Not yet implemented.');
536+
}

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
432432
hidden: instance.hidden,
433433
};
434434
},
435+
436+
getInstanceFromNode() {
437+
throw new Error('Not yet implemented.');
438+
},
435439
};
436440

437441
const hostConfig = useMutation

packages/react-reconciler/src/ReactFiberScope.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
ReactScopeMethods,
1515
} from 'shared/ReactTypes';
1616

17-
import {getPublicInstance} from './ReactFiberHostConfig';
17+
import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
1818

1919
import {
2020
HostComponent,
@@ -54,6 +54,29 @@ function collectScopedNodes(
5454
}
5555
}
5656

57+
function collectFirstScopedNode(
58+
node: Fiber,
59+
fn: (type: string | Object, props: Object) => boolean,
60+
): null | Object {
61+
if (enableScopeAPI) {
62+
if (node.tag === HostComponent) {
63+
const {type, memoizedProps} = node;
64+
if (fn(type, memoizedProps) === true) {
65+
return getPublicInstance(node.stateNode);
66+
}
67+
}
68+
let child = node.child;
69+
70+
if (isFiberSuspenseAndTimedOut(node)) {
71+
child = getSuspenseFallbackChild(node);
72+
}
73+
if (child !== null) {
74+
return collectFirstScopedNodeFromChildren(child, fn);
75+
}
76+
}
77+
return null;
78+
}
79+
5780
function collectScopedNodesFromChildren(
5881
startingChild: Fiber,
5982
fn: (type: string | Object, props: Object) => boolean,
@@ -66,6 +89,21 @@ function collectScopedNodesFromChildren(
6689
}
6790
}
6891

92+
function collectFirstScopedNodeFromChildren(
93+
startingChild: Fiber,
94+
fn: (type: string | Object, props: Object) => boolean,
95+
): Object | null {
96+
let child = startingChild;
97+
while (child !== null) {
98+
const scopedNode = collectFirstScopedNode(child, fn);
99+
if (scopedNode !== null) {
100+
return scopedNode;
101+
}
102+
child = child.sibling;
103+
}
104+
return null;
105+
}
106+
69107
function collectNearestScopeMethods(
70108
node: Fiber,
71109
scope: ReactScope,
@@ -151,7 +189,7 @@ export function createScopeMethods(
151189
const currentFiber = ((instance.fiber: any): Fiber);
152190
return currentFiber.memoizedProps;
153191
},
154-
getScopedNodes(): null | Array<Object> {
192+
getAllNodes(): null | Array<Object> {
155193
const currentFiber = ((instance.fiber: any): Fiber);
156194
const child = currentFiber.child;
157195
const scopedNodes = [];
@@ -160,5 +198,27 @@ export function createScopeMethods(
160198
}
161199
return scopedNodes.length === 0 ? null : scopedNodes;
162200
},
201+
getFirstNode(): null | Object {
202+
const currentFiber = ((instance.fiber: any): Fiber);
203+
const child = currentFiber.child;
204+
if (child !== null) {
205+
return collectFirstScopedNodeFromChildren(child, fn);
206+
}
207+
return null;
208+
},
209+
containsNode(node: Object): boolean {
210+
let fiber = getInstanceFromNode(node);
211+
while (fiber !== null) {
212+
if (
213+
fiber.tag === ScopeComponent &&
214+
fiber.type === scope &&
215+
fiber.stateNode === instance
216+
) {
217+
return true;
218+
}
219+
fiber = fiber.return;
220+
}
221+
return false;
222+
},
163223
};
164224
}

0 commit comments

Comments
 (0)