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

[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 = {|