[react-interactions] Adds more experimental Scope API methods (#17042)
Dominic Gannaway committed
Oct 8, 2019 at 19:32 UTC
b71ab61c8f06b7d1c415a0354db607bf2772473e
17 files changed
+261
-37
packages/react-art/src/ReactARTHostConfig.js
+4
@@ -461,3 +461,7 @@ export function updateFundamentalComponent(fundamentalInstance) {
461
export function unmountFundamentalComponent(fundamentalInstance) {
462
throw new Error('Not yet implemented.');
463
}
464
+
465
+export function getInstanceFromNode(node) {
466
+ throw new Error('Not yet implemented.');
467
+}
packages/react-dom/src/client/ReactDOMHostConfig.js
+9
-1
@@ -7,7 +7,11 @@
7
* @flow
8
*/
9
10
-import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree';
10
+import {
11
+ precacheFiberNode,
12
+ updateFiberProps,
13
+ getClosestInstanceFromNode,
14
+} from './ReactDOMComponentTree';
15
import {
16
createElement,
17
createTextNode,
@@ -976,3 +980,7 @@ export function unmountFundamentalComponent(
980
}
981
}
982
}
983
+
984
+export function getInstanceFromNode(node: HTMLElement): null | Object {
985
+ return getClosestInstanceFromNode(node) || null;
986
+}
packages/react-interactions/accessibility/README.md
+14
-4
@@ -42,8 +42,8 @@ function MyComponent(props) {
42
);
43
}
44
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();
47
48
// [<div>DIV 1</div>, <div>DIV 2</div>, <div>DIV 3</div>]
49
console.log(divs);
@@ -72,7 +72,17 @@ Returns the parent `ReactScopeInterface` of the scope node or `null` if none exi
72
73
Returns the current `props` object of the scope node.
74
75
-### getScopedNodes: () => null | Array<HTMLElement>
75
+### getAllNodes: () => null | Array<HTMLElement>
76
77
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.
\ No newline at end of file
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.
\ No newline at end of file
packages/react-interactions/accessibility/docs/TabbableScope.md
+1
-1
@@ -15,7 +15,7 @@ function FocusableNodeCollector(props) {
15
const scope = scopeRef.current;
16
17
if (scope) {
18
- const tabFocusableNodes = scope.getScopedNodes();
18
+ const tabFocusableNodes = scope.getAllNodes();
19
if (tabFocusableNodes && props.onFocusableNodes) {
20
props.onFocusableNodes(tabFocusableNodes);
21
}
packages/react-interactions/accessibility/src/FocusContain.js
+8
-4
@@ -66,10 +66,14 @@ export default function FocusContain({
66
useLayoutEffect(
67
() => {
68
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();
77
}
78
}
79
},
packages/react-interactions/accessibility/src/FocusGroup.js
+4
-4
@@ -30,9 +30,9 @@ type FocusGroupProps = {|
30
const {useRef} = React;
31
32
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();
36
event.preventDefault();
37
}
38
}
@@ -135,7 +135,7 @@ export function createFocusGroup(
135
const tabScope = getGroupProps(currentItem).tabScopeRef.current;
136
if (tabScope) {
137
const activeNode = document.activeElement;
138
- const nodes = tabScope.getScopedNodes();
138
+ const nodes = tabScope.getAllNodes();
139
for (let i = 0; i < nodes.length; i++) {
140
const node = nodes[i];
141
if (node !== activeNode) {
packages/react-interactions/accessibility/src/FocusTable.js
+4
-4
@@ -39,9 +39,9 @@ type FocusTableProps = {|
39
const {useRef} = React;
40
41
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();
45
if (event) {
46
event.preventDefault();
47
}
@@ -209,7 +209,7 @@ export function createFocusTable(
209
const tabScope = getTableProps(currentCell).tabScopeRef.current;
210
if (tabScope) {
211
const activeNode = document.activeElement;
212
- const nodes = tabScope.getScopedNodes();
212
+ const nodes = tabScope.getAllNodes();
213
for (let i = 0; i < nodes.length; i++) {
214
const node = nodes[i];
215
if (node !== activeNode) {
packages/react-interactions/accessibility/src/__tests__/TabbableScope-test.internal.js
+2
-2
@@ -35,7 +35,7 @@ describe('TabbableScope', () => {
35
container = null;
36
});
37
38
- it('getScopedNodes() works as intended', () => {
38
+ it('getAllNodes() works as intended', () => {
39
const scopeRef = React.createRef();
40
const nodeRefA = React.createRef();
41
const nodeRefB = React.createRef();
@@ -58,7 +58,7 @@ describe('TabbableScope', () => {
58
}
59
60
ReactDOM.render(<Test />, container);
61
- let nodes = scopeRef.current.getScopedNodes();
61
+ let nodes = scopeRef.current.getAllNodes();
62
expect(nodes).toEqual([
63
nodeRefA.current,
64
nodeRefB.current,
packages/react-interactions/accessibility/src/shared/getTabbableNodes.js
+1
-1
@@ -18,7 +18,7 @@ export default function getTabbableNodes(
18
number,
19
null | HTMLElement,
20
] {
21
- const tabbableNodes = scope.getScopedNodes();
21
+ const tabbableNodes = scope.getAllNodes();
22
if (tabbableNodes === null || tabbableNodes.length === 0) {
23
return [null, null, null, 0, null];
24
}
packages/react-native-renderer/src/ReactFabricHostConfig.js
+4
@@ -491,3 +491,7 @@ export function unmountFundamentalComponent(fundamentalInstance) {
491
export function cloneFundamentalInstance(fundamentalInstance) {
492
throw new Error('Not yet implemented.');
493
}
494
+
495
+export function getInstanceFromNode(node) {
496
+ throw new Error('Not yet implemented.');
497
+}
packages/react-native-renderer/src/ReactNativeHostConfig.js
+4
@@ -530,3 +530,7 @@ export function updateFundamentalComponent(fundamentalInstance) {
530
export function unmountFundamentalComponent(fundamentalInstance) {
531
throw new Error('Not yet implemented.');
532
}
533
+
534
+export function getInstanceFromNode(node) {
535
+ throw new Error('Not yet implemented.');
536
+}
packages/react-noop-renderer/src/createReactNoop.js
+4
@@ -432,6 +432,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
432
hidden: instance.hidden,
433
};
434
},
435
+
436
+ getInstanceFromNode() {
437
+ throw new Error('Not yet implemented.');
438
+ },
439
};
440
441
const hostConfig = useMutation
packages/react-reconciler/src/ReactFiberScope.js
+62
-2
@@ -14,7 +14,7 @@ import type {
14
ReactScopeMethods,
15
} from 'shared/ReactTypes';
16
17
-import {getPublicInstance} from './ReactFiberHostConfig';
17
+import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig';
18
19
import {
20
HostComponent,
@@ -54,6 +54,29 @@ function collectScopedNodes(
54
}
55
}
56
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
+
80
function collectScopedNodesFromChildren(
81
startingChild: Fiber,
82
fn: (type: string | Object, props: Object) => boolean,
@@ -66,6 +89,21 @@ function collectScopedNodesFromChildren(
89
}
90
}
91
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
+
107
function collectNearestScopeMethods(
108
node: Fiber,
109
scope: ReactScope,
@@ -151,7 +189,7 @@ export function createScopeMethods(
189
const currentFiber = ((instance.fiber: any): Fiber);
190
return currentFiber.memoizedProps;
191
},
154
- getScopedNodes(): null | Array<Object> {
192
+ getAllNodes(): null | Array<Object> {
193
const currentFiber = ((instance.fiber: any): Fiber);
194
const child = currentFiber.child;
195
const scopedNodes = [];
@@ -160,5 +198,27 @@ export function createScopeMethods(
198
}
199
return scopedNodes.length === 0 ? null : scopedNodes;
200
},
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
+ },
223
};
224
}
packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
+130
-13
@@ -38,7 +38,7 @@ describe('ReactScope', () => {
38
container = null;
39
});
40
41
- it('getScopedNodes() works as intended', () => {
41
+ it('getAllNodes() works as intended', () => {
42
const TestScope = React.unstable_createScope((type, props) => true);
43
const scopeRef = React.createRef();
44
const divRef = React.createRef();
@@ -62,16 +62,98 @@ describe('ReactScope', () => {
62
}
63
64
ReactDOM.render(<Test toggle={true} />, container);
65
- let nodes = scopeRef.current.getScopedNodes();
65
+ let nodes = scopeRef.current.getAllNodes();
66
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
67
ReactDOM.render(<Test toggle={false} />, container);
68
- nodes = scopeRef.current.getScopedNodes();
68
+ nodes = scopeRef.current.getAllNodes();
69
expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]);
70
ReactDOM.render(null, container);
71
expect(scopeRef.current).toBe(null);
72
});
73
74
- it('mixed getParent() and getScopedNodes() works as intended', () => {
74
+ it('getFirstNode() works as intended', () => {
75
+ const TestScope = React.unstable_createScope((type, props) => true);
76
+ const scopeRef = React.createRef();
77
+ const divRef = React.createRef();
78
+ const spanRef = React.createRef();
79
+ const aRef = React.createRef();
80
+
81
+ function Test({toggle}) {
82
+ return toggle ? (
83
+ <TestScope ref={scopeRef}>
84
+ <div ref={divRef}>DIV</div>
85
+ <span ref={spanRef}>SPAN</span>
86
+ <a ref={aRef}>A</a>
87
+ </TestScope>
88
+ ) : (
89
+ <TestScope ref={scopeRef}>
90
+ <a ref={aRef}>A</a>
91
+ <div ref={divRef}>DIV</div>
92
+ <span ref={spanRef}>SPAN</span>
93
+ </TestScope>
94
+ );
95
+ }
96
+
97
+ ReactDOM.render(<Test toggle={true} />, container);
98
+ let node = scopeRef.current.getFirstNode();
99
+ expect(node).toEqual(divRef.current);
100
+ ReactDOM.render(<Test toggle={false} />, container);
101
+ node = scopeRef.current.getFirstNode();
102
+ expect(node).toEqual(aRef.current);
103
+ ReactDOM.render(null, container);
104
+ expect(scopeRef.current).toBe(null);
105
+ });
106
+
107
+ it('containsNode() works as intended', () => {
108
+ const TestScope = React.unstable_createScope((type, props) => true);
109
+ const scopeRef = React.createRef();
110
+ const divRef = React.createRef();
111
+ const spanRef = React.createRef();
112
+ const aRef = React.createRef();
113
+ const outerSpan = React.createRef();
114
+ const emRef = React.createRef();
115
+
116
+ function Test({toggle}) {
117
+ return toggle ? (
118
+ <div>
119
+ <span ref={outerSpan}>SPAN</span>
120
+ <TestScope ref={scopeRef}>
121
+ <div ref={divRef}>DIV</div>
122
+ <span ref={spanRef}>SPAN</span>
123
+ <a ref={aRef}>A</a>
124
+ </TestScope>
125
+ <em ref={emRef}>EM</em>
126
+ </div>
127
+ ) : (
128
+ <div>
129
+ <TestScope ref={scopeRef}>
130
+ <a ref={aRef}>A</a>
131
+ <div ref={divRef}>DIV</div>
132
+ <span ref={spanRef}>SPAN</span>
133
+ <em ref={emRef}>EM</em>
134
+ </TestScope>
135
+ <span ref={outerSpan}>SPAN</span>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ ReactDOM.render(<Test toggle={true} />, container);
141
+ expect(scopeRef.current.containsNode(divRef.current)).toBe(true);
142
+ expect(scopeRef.current.containsNode(spanRef.current)).toBe(true);
143
+ expect(scopeRef.current.containsNode(aRef.current)).toBe(true);
144
+ expect(scopeRef.current.containsNode(outerSpan.current)).toBe(false);
145
+ expect(scopeRef.current.containsNode(emRef.current)).toBe(false);
146
+ ReactDOM.render(<Test toggle={false} />, container);
147
+ expect(scopeRef.current.containsNode(divRef.current)).toBe(true);
148
+ expect(scopeRef.current.containsNode(spanRef.current)).toBe(true);
149
+ expect(scopeRef.current.containsNode(aRef.current)).toBe(true);
150
+ expect(scopeRef.current.containsNode(outerSpan.current)).toBe(false);
151
+ expect(scopeRef.current.containsNode(emRef.current)).toBe(true);
152
+ ReactDOM.render(<Test toggle={true} />, container);
153
+ expect(scopeRef.current.containsNode(emRef.current)).toBe(false);
154
+ });
155
+
156
+ it('mixed getParent() and getAllNodes() works as intended', () => {
157
const TestScope = React.unstable_createScope((type, props) => true);
158
const TestScope2 = React.unstable_createScope((type, props) => true);
159
const refA = React.createRef();
@@ -108,14 +190,14 @@ describe('ReactScope', () => {
190
ReactDOM.render(<Test />, container);
191
const dParent = refD.current.getParent();
192
expect(dParent).not.toBe(null);
111
- expect(dParent.getScopedNodes()).toEqual([
193
+ expect(dParent.getAllNodes()).toEqual([
194
divA.current,
195
spanB.current,
196
divB.current,
197
]);
198
const cParent = refC.current.getParent();
199
expect(cParent).not.toBe(null);
118
- expect(cParent.getScopedNodes()).toEqual([
200
+ expect(cParent.getAllNodes()).toEqual([
201
spanA.current,
202
divA.current,
203
spanB.current,
@@ -196,7 +278,7 @@ describe('ReactScope', () => {
278
);
279
container.innerHTML = html;
280
ReactDOM.hydrate(<Test />, container);
199
- const nodes = scopeRef.current.getScopedNodes();
281
+ const nodes = scopeRef.current.getAllNodes();
282
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
283
});
284
@@ -250,7 +332,7 @@ describe('ReactScope', () => {
332
ReactTestRenderer = require('react-test-renderer');
333
});
334
253
- it('getScopedNodes() works as intended', () => {
335
+ it('getAllNodes() works as intended', () => {
336
const TestScope = React.unstable_createScope((type, props) => true);
337
const scopeRef = React.createRef();
338
const divRef = React.createRef();
@@ -278,14 +360,49 @@ describe('ReactScope', () => {
360
return element;
361
},
362
});
281
- let nodes = scopeRef.current.getScopedNodes();
363
+ let nodes = scopeRef.current.getAllNodes();
364
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
365
renderer.update(<Test toggle={false} />);
284
- nodes = scopeRef.current.getScopedNodes();
366
+ nodes = scopeRef.current.getAllNodes();
367
expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]);
368
});
369
288
- it('mixed getParent() and getScopedNodes() works as intended', () => {
370
+ it('getFirstNode() works as intended', () => {
371
+ const TestScope = React.unstable_createScope((type, props) => true);
372
+ const scopeRef = React.createRef();
373
+ const divRef = React.createRef();
374
+ const spanRef = React.createRef();
375
+ const aRef = React.createRef();
376
+
377
+ function Test({toggle}) {
378
+ return toggle ? (
379
+ <TestScope ref={scopeRef}>
380
+ <div ref={divRef}>DIV</div>
381
+ <span ref={spanRef}>SPAN</span>
382
+ <a ref={aRef}>A</a>
383
+ </TestScope>
384
+ ) : (
385
+ <TestScope ref={scopeRef}>
386
+ <a ref={aRef}>A</a>
387
+ <div ref={divRef}>DIV</div>
388
+ <span ref={spanRef}>SPAN</span>
389
+ </TestScope>
390
+ );
391
+ }
392
+
393
+ const renderer = ReactTestRenderer.create(<Test toggle={true} />, {
394
+ createNodeMock: element => {
395
+ return element;
396
+ },
397
+ });
398
+ let node = scopeRef.current.getFirstNode();
399
+ expect(node).toEqual(divRef.current);
400
+ renderer.update(<Test toggle={false} />);
401
+ node = scopeRef.current.getFirstNode();
402
+ expect(node).toEqual(aRef.current);
403
+ });
404
+
405
+ it('mixed getParent() and getAllNodes() works as intended', () => {
406
const TestScope = React.unstable_createScope((type, props) => true);
407
const TestScope2 = React.unstable_createScope((type, props) => true);
408
const refA = React.createRef();
@@ -326,14 +443,14 @@ describe('ReactScope', () => {
443
});
444
const dParent = refD.current.getParent();
445
expect(dParent).not.toBe(null);
329
- expect(dParent.getScopedNodes()).toEqual([
446
+ expect(dParent.getAllNodes()).toEqual([
447
divA.current,
448
spanB.current,
449
divB.current,
450
]);
451
const cParent = refC.current.getParent();
452
expect(cParent).not.toBe(null);
336
- expect(cParent.getScopedNodes()).toEqual([
453
+ expect(cParent.getAllNodes()).toEqual([
454
spanA.current,
455
divA.current,
456
spanB.current,
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -69,6 +69,7 @@ export const mountFundamentalComponent =
69
$$$hostConfig.mountFundamentalComponent;
70
export const shouldUpdateFundamentalComponent =
71
$$$hostConfig.shouldUpdateFundamentalComponent;
72
+export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
73
74
// -------------------
75
// Mutation
packages/react-test-renderer/src/ReactTestHostConfig.js
+6
@@ -29,6 +29,7 @@ export type Instance = {|
29
props: Object,
30
isHidden: boolean,
31
children: Array<Instance | TextInstance>,
32
+ internalInstanceHandle: Object,
33
rootContainerInstance: Container,
34
tag: 'INSTANCE',
35
|};
@@ -155,6 +156,7 @@ export function createInstance(
156
props: propsToUse,
157
isHidden: false,
158
children: [],
159
+ internalInstanceHandle,
160
rootContainerInstance,
161
tag: 'INSTANCE',
162
};
@@ -351,3 +353,7 @@ export function unmountFundamentalComponent(
353
onUnmount(null, instance, props, state);
354
}
355
}
356
+
357
+export function getInstanceFromNode(node: Object) {
358
+ throw new Error('Not yet implemented.');
359
+}
packages/shared/ReactTypes.js
+3
-1
@@ -170,7 +170,9 @@ export type ReactScopeMethods = {|
170
getChildrenFromRoot(): null | Array<ReactScopeMethods>,
171
getParent(): null | ReactScopeMethods,
172
getProps(): Object,
173
- getScopedNodes(): null | Array<Object>,
173
+ getAllNodes(): null | Array<Object>,
174
+ getFirstNode(): null | Object,
175
+ containsNode(Object): boolean,
176
|};
177
178
export type ReactScopeInstance = {|