diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 247086c64ba2d..4a525677bd214 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -461,3 +461,7 @@ export function updateFundamentalComponent(fundamentalInstance) { export function unmountFundamentalComponent(fundamentalInstance) { throw new Error('Not yet implemented.'); } + +export function getInstanceFromNode(node) { + throw new Error('Not yet implemented.'); +} diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index c9425be84026c..2e9224817acb9 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -7,7 +7,11 @@ * @flow */ -import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree'; +import { + precacheFiberNode, + updateFiberProps, + getClosestInstanceFromNode, +} from './ReactDOMComponentTree'; import { createElement, createTextNode, @@ -976,3 +980,7 @@ export function unmountFundamentalComponent( } } } + +export function getInstanceFromNode(node: HTMLElement): null | Object { + return getClosestInstanceFromNode(node) || null; +} diff --git a/packages/react-interactions/accessibility/README.md b/packages/react-interactions/accessibility/README.md index 9e910b18778f3..582820eec122a 100644 --- a/packages/react-interactions/accessibility/README.md +++ b/packages/react-interactions/accessibility/README.md @@ -42,8 +42,8 @@ function MyComponent(props) { ); } -// Using the ref, we can get the host nodes via getScopedNodes() -const divs = divOnlyScope.current.getScopedNodes(); +// Using the ref, we can get the host nodes via getAllNodes() +const divs = divOnlyScope.current.getAllNodes(); // [
DIV 1
,
DIV 2
,
DIV 3
] console.log(divs); @@ -72,7 +72,17 @@ Returns the parent `ReactScopeInterface` of the scope node or `null` if none exi Returns the current `props` object of the scope node. -### getScopedNodes: () => null | Array +### getAllNodes: () => null | Array Returns an array of all child host nodes that successfully match when queried using the -query function passed to the scope. Returns `null` if there are no matching host nodes. \ No newline at end of file +query function passed to the scope. Returns `null` if there are no matching host nodes. + +### getFirstNode: () => null | HTMLElement + +Returns the first child host node that successfully matches when queried using the +query function passed to the scope. Returns `null` if there is no matching host node. + +### containsNode: (node: HTMLElement) => boolean + +Returns `true` or `false` depending on if the given `HTMLElement` is a descendant +of the scope's sub-tree. \ No newline at end of file diff --git a/packages/react-interactions/accessibility/docs/TabbableScope.md b/packages/react-interactions/accessibility/docs/TabbableScope.md index ff2209f6b5849..2bafcad00db21 100644 --- a/packages/react-interactions/accessibility/docs/TabbableScope.md +++ b/packages/react-interactions/accessibility/docs/TabbableScope.md @@ -15,7 +15,7 @@ function FocusableNodeCollector(props) { const scope = scopeRef.current; if (scope) { - const tabFocusableNodes = scope.getScopedNodes(); + const tabFocusableNodes = scope.getAllNodes(); if (tabFocusableNodes && props.onFocusableNodes) { props.onFocusableNodes(tabFocusableNodes); } diff --git a/packages/react-interactions/accessibility/src/FocusContain.js b/packages/react-interactions/accessibility/src/FocusContain.js index 100e747a57d38..60bd3cd2929fc 100644 --- a/packages/react-interactions/accessibility/src/FocusContain.js +++ b/packages/react-interactions/accessibility/src/FocusContain.js @@ -66,10 +66,14 @@ export default function FocusContain({ useLayoutEffect( () => { const scope = scopeRef.current; - if (scope !== null && disabled !== true) { - const elems = scope.getScopedNodes(); - if (elems && elems.indexOf(document.activeElement) === -1) { - elems[0].focus(); + if ( + scope !== null && + disabled !== true && + !scope.containsNode(document.activeElement) + ) { + const fistElem = scope.getFirstNode(); + if (fistElem !== null) { + fistElem.focus(); } } }, diff --git a/packages/react-interactions/accessibility/src/FocusGroup.js b/packages/react-interactions/accessibility/src/FocusGroup.js index 898ee8a89eb07..c3d8e4004edf9 100644 --- a/packages/react-interactions/accessibility/src/FocusGroup.js +++ b/packages/react-interactions/accessibility/src/FocusGroup.js @@ -30,9 +30,9 @@ type FocusGroupProps = {| const {useRef} = React; function focusGroupItem(cell: ReactScopeMethods, event: KeyboardEvent): void { - const tabbableNodes = cell.getScopedNodes(); - if (tabbableNodes !== null && tabbableNodes.length > 0) { - tabbableNodes[0].focus(); + const firstScopedNode = cell.getFirstNode(); + if (firstScopedNode !== null) { + firstScopedNode.focus(); event.preventDefault(); } } @@ -135,7 +135,7 @@ export function createFocusGroup( const tabScope = getGroupProps(currentItem).tabScopeRef.current; if (tabScope) { const activeNode = document.activeElement; - const nodes = tabScope.getScopedNodes(); + const nodes = tabScope.getAllNodes(); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node !== activeNode) { diff --git a/packages/react-interactions/accessibility/src/FocusTable.js b/packages/react-interactions/accessibility/src/FocusTable.js index ebe12652836bc..0f5970c1eb4fa 100644 --- a/packages/react-interactions/accessibility/src/FocusTable.js +++ b/packages/react-interactions/accessibility/src/FocusTable.js @@ -39,9 +39,9 @@ type FocusTableProps = {| const {useRef} = React; function focusScope(cell: ReactScopeMethods, event?: KeyboardEvent): void { - const tabbableNodes = cell.getScopedNodes(); - if (tabbableNodes !== null && tabbableNodes.length > 0) { - tabbableNodes[0].focus(); + const firstScopedNode = cell.getFirstNode(); + if (firstScopedNode !== null) { + firstScopedNode.focus(); if (event) { event.preventDefault(); } @@ -209,7 +209,7 @@ export function createFocusTable( const tabScope = getTableProps(currentCell).tabScopeRef.current; if (tabScope) { const activeNode = document.activeElement; - const nodes = tabScope.getScopedNodes(); + const nodes = tabScope.getAllNodes(); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node !== activeNode) { diff --git a/packages/react-interactions/accessibility/src/__tests__/TabbableScope-test.internal.js b/packages/react-interactions/accessibility/src/__tests__/TabbableScope-test.internal.js index 3815a35209ab7..6926fa48ad4dc 100644 --- a/packages/react-interactions/accessibility/src/__tests__/TabbableScope-test.internal.js +++ b/packages/react-interactions/accessibility/src/__tests__/TabbableScope-test.internal.js @@ -35,7 +35,7 @@ describe('TabbableScope', () => { container = null; }); - it('getScopedNodes() works as intended', () => { + it('getAllNodes() works as intended', () => { const scopeRef = React.createRef(); const nodeRefA = React.createRef(); const nodeRefB = React.createRef(); @@ -58,7 +58,7 @@ describe('TabbableScope', () => { } ReactDOM.render(, container); - let nodes = scopeRef.current.getScopedNodes(); + let nodes = scopeRef.current.getAllNodes(); expect(nodes).toEqual([ nodeRefA.current, nodeRefB.current, diff --git a/packages/react-interactions/accessibility/src/shared/getTabbableNodes.js b/packages/react-interactions/accessibility/src/shared/getTabbableNodes.js index 1ee49390f2163..dd847c117d4b1 100644 --- a/packages/react-interactions/accessibility/src/shared/getTabbableNodes.js +++ b/packages/react-interactions/accessibility/src/shared/getTabbableNodes.js @@ -18,7 +18,7 @@ export default function getTabbableNodes( number, null | HTMLElement, ] { - const tabbableNodes = scope.getScopedNodes(); + const tabbableNodes = scope.getAllNodes(); if (tabbableNodes === null || tabbableNodes.length === 0) { return [null, null, null, 0, null]; } diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 4a66a4a216122..4b71f19f5557c 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -491,3 +491,7 @@ export function unmountFundamentalComponent(fundamentalInstance) { export function cloneFundamentalInstance(fundamentalInstance) { throw new Error('Not yet implemented.'); } + +export function getInstanceFromNode(node) { + throw new Error('Not yet implemented.'); +} diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index dd9e7e0e08a1c..4714fb3dd1b98 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -530,3 +530,7 @@ export function updateFundamentalComponent(fundamentalInstance) { export function unmountFundamentalComponent(fundamentalInstance) { throw new Error('Not yet implemented.'); } + +export function getInstanceFromNode(node) { + throw new Error('Not yet implemented.'); +} diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index a5040ee8d655a..9e3a2342bd152 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -432,6 +432,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { hidden: instance.hidden, }; }, + + getInstanceFromNode() { + throw new Error('Not yet implemented.'); + }, }; const hostConfig = useMutation diff --git a/packages/react-reconciler/src/ReactFiberScope.js b/packages/react-reconciler/src/ReactFiberScope.js index 54d6bff3d862a..fa8e849a3e323 100644 --- a/packages/react-reconciler/src/ReactFiberScope.js +++ b/packages/react-reconciler/src/ReactFiberScope.js @@ -14,7 +14,7 @@ import type { ReactScopeMethods, } from 'shared/ReactTypes'; -import {getPublicInstance} from './ReactFiberHostConfig'; +import {getPublicInstance, getInstanceFromNode} from './ReactFiberHostConfig'; import { HostComponent, @@ -54,6 +54,29 @@ function collectScopedNodes( } } +function collectFirstScopedNode( + node: Fiber, + fn: (type: string | Object, props: Object) => boolean, +): null | Object { + if (enableScopeAPI) { + if (node.tag === HostComponent) { + const {type, memoizedProps} = node; + if (fn(type, memoizedProps) === true) { + return getPublicInstance(node.stateNode); + } + } + let child = node.child; + + if (isFiberSuspenseAndTimedOut(node)) { + child = getSuspenseFallbackChild(node); + } + if (child !== null) { + return collectFirstScopedNodeFromChildren(child, fn); + } + } + return null; +} + function collectScopedNodesFromChildren( startingChild: Fiber, fn: (type: string | Object, props: Object) => boolean, @@ -66,6 +89,21 @@ function collectScopedNodesFromChildren( } } +function collectFirstScopedNodeFromChildren( + startingChild: Fiber, + fn: (type: string | Object, props: Object) => boolean, +): Object | null { + let child = startingChild; + while (child !== null) { + const scopedNode = collectFirstScopedNode(child, fn); + if (scopedNode !== null) { + return scopedNode; + } + child = child.sibling; + } + return null; +} + function collectNearestScopeMethods( node: Fiber, scope: ReactScope, @@ -151,7 +189,7 @@ export function createScopeMethods( const currentFiber = ((instance.fiber: any): Fiber); return currentFiber.memoizedProps; }, - getScopedNodes(): null | Array { + getAllNodes(): null | Array { const currentFiber = ((instance.fiber: any): Fiber); const child = currentFiber.child; const scopedNodes = []; @@ -160,5 +198,27 @@ export function createScopeMethods( } return scopedNodes.length === 0 ? null : scopedNodes; }, + getFirstNode(): null | Object { + const currentFiber = ((instance.fiber: any): Fiber); + const child = currentFiber.child; + if (child !== null) { + return collectFirstScopedNodeFromChildren(child, fn); + } + return null; + }, + containsNode(node: Object): boolean { + let fiber = getInstanceFromNode(node); + while (fiber !== null) { + if ( + fiber.tag === ScopeComponent && + fiber.type === scope && + fiber.stateNode === instance + ) { + return true; + } + fiber = fiber.return; + } + return false; + }, }; } diff --git a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js index ac686350d99b1..67c1a088b7818 100644 --- a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js @@ -38,7 +38,7 @@ describe('ReactScope', () => { container = null; }); - it('getScopedNodes() works as intended', () => { + it('getAllNodes() works as intended', () => { const TestScope = React.unstable_createScope((type, props) => true); const scopeRef = React.createRef(); const divRef = React.createRef(); @@ -62,16 +62,98 @@ describe('ReactScope', () => { } ReactDOM.render(, container); - let nodes = scopeRef.current.getScopedNodes(); + let nodes = scopeRef.current.getAllNodes(); expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]); ReactDOM.render(, container); - nodes = scopeRef.current.getScopedNodes(); + nodes = scopeRef.current.getAllNodes(); expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]); ReactDOM.render(null, container); expect(scopeRef.current).toBe(null); }); - it('mixed getParent() and getScopedNodes() works as intended', () => { + it('getFirstNode() works as intended', () => { + const TestScope = React.unstable_createScope((type, props) => true); + const scopeRef = React.createRef(); + const divRef = React.createRef(); + const spanRef = React.createRef(); + const aRef = React.createRef(); + + function Test({toggle}) { + return toggle ? ( + +
DIV
+ SPAN + A +
+ ) : ( + + A +
DIV
+ SPAN +
+ ); + } + + ReactDOM.render(, container); + let node = scopeRef.current.getFirstNode(); + expect(node).toEqual(divRef.current); + ReactDOM.render(, container); + node = scopeRef.current.getFirstNode(); + expect(node).toEqual(aRef.current); + ReactDOM.render(null, container); + expect(scopeRef.current).toBe(null); + }); + + it('containsNode() works as intended', () => { + const TestScope = React.unstable_createScope((type, props) => true); + const scopeRef = React.createRef(); + const divRef = React.createRef(); + const spanRef = React.createRef(); + const aRef = React.createRef(); + const outerSpan = React.createRef(); + const emRef = React.createRef(); + + function Test({toggle}) { + return toggle ? ( +
+ SPAN + +
DIV
+ SPAN + A +
+ EM +
+ ) : ( +
+ + A +
DIV
+ SPAN + EM +
+ SPAN +
+ ); + } + + ReactDOM.render(, container); + expect(scopeRef.current.containsNode(divRef.current)).toBe(true); + expect(scopeRef.current.containsNode(spanRef.current)).toBe(true); + expect(scopeRef.current.containsNode(aRef.current)).toBe(true); + expect(scopeRef.current.containsNode(outerSpan.current)).toBe(false); + expect(scopeRef.current.containsNode(emRef.current)).toBe(false); + ReactDOM.render(, container); + expect(scopeRef.current.containsNode(divRef.current)).toBe(true); + expect(scopeRef.current.containsNode(spanRef.current)).toBe(true); + expect(scopeRef.current.containsNode(aRef.current)).toBe(true); + expect(scopeRef.current.containsNode(outerSpan.current)).toBe(false); + expect(scopeRef.current.containsNode(emRef.current)).toBe(true); + ReactDOM.render(, container); + expect(scopeRef.current.containsNode(emRef.current)).toBe(false); + }); + + it('mixed getParent() and getAllNodes() works as intended', () => { const TestScope = React.unstable_createScope((type, props) => true); const TestScope2 = React.unstable_createScope((type, props) => true); const refA = React.createRef(); @@ -108,14 +190,14 @@ describe('ReactScope', () => { ReactDOM.render(, container); const dParent = refD.current.getParent(); expect(dParent).not.toBe(null); - expect(dParent.getScopedNodes()).toEqual([ + expect(dParent.getAllNodes()).toEqual([ divA.current, spanB.current, divB.current, ]); const cParent = refC.current.getParent(); expect(cParent).not.toBe(null); - expect(cParent.getScopedNodes()).toEqual([ + expect(cParent.getAllNodes()).toEqual([ spanA.current, divA.current, spanB.current, @@ -196,7 +278,7 @@ describe('ReactScope', () => { ); container.innerHTML = html; ReactDOM.hydrate(, container); - const nodes = scopeRef.current.getScopedNodes(); + const nodes = scopeRef.current.getAllNodes(); expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]); }); @@ -250,7 +332,7 @@ describe('ReactScope', () => { ReactTestRenderer = require('react-test-renderer'); }); - it('getScopedNodes() works as intended', () => { + it('getAllNodes() works as intended', () => { const TestScope = React.unstable_createScope((type, props) => true); const scopeRef = React.createRef(); const divRef = React.createRef(); @@ -278,14 +360,49 @@ describe('ReactScope', () => { return element; }, }); - let nodes = scopeRef.current.getScopedNodes(); + let nodes = scopeRef.current.getAllNodes(); expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]); renderer.update(); - nodes = scopeRef.current.getScopedNodes(); + nodes = scopeRef.current.getAllNodes(); expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]); }); - it('mixed getParent() and getScopedNodes() works as intended', () => { + it('getFirstNode() works as intended', () => { + const TestScope = React.unstable_createScope((type, props) => true); + const scopeRef = React.createRef(); + const divRef = React.createRef(); + const spanRef = React.createRef(); + const aRef = React.createRef(); + + function Test({toggle}) { + return toggle ? ( + +
DIV
+ SPAN + A +
+ ) : ( + + A +
DIV
+ SPAN +
+ ); + } + + const renderer = ReactTestRenderer.create(, { + createNodeMock: element => { + return element; + }, + }); + let node = scopeRef.current.getFirstNode(); + expect(node).toEqual(divRef.current); + renderer.update(); + node = scopeRef.current.getFirstNode(); + expect(node).toEqual(aRef.current); + }); + + it('mixed getParent() and getAllNodes() works as intended', () => { const TestScope = React.unstable_createScope((type, props) => true); const TestScope2 = React.unstable_createScope((type, props) => true); const refA = React.createRef(); @@ -326,14 +443,14 @@ describe('ReactScope', () => { }); const dParent = refD.current.getParent(); expect(dParent).not.toBe(null); - expect(dParent.getScopedNodes()).toEqual([ + expect(dParent.getAllNodes()).toEqual([ divA.current, spanB.current, divB.current, ]); const cParent = refC.current.getParent(); expect(cParent).not.toBe(null); - expect(cParent.getScopedNodes()).toEqual([ + expect(cParent.getAllNodes()).toEqual([ spanA.current, divA.current, spanB.current, diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index cb35932b20ab2..bcc228bcd693c 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -69,6 +69,7 @@ export const mountFundamentalComponent = $$$hostConfig.mountFundamentalComponent; export const shouldUpdateFundamentalComponent = $$$hostConfig.shouldUpdateFundamentalComponent; +export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode; // ------------------- // Mutation diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index e6df01d0e726b..8cbf8bc64918f 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -29,6 +29,7 @@ export type Instance = {| props: Object, isHidden: boolean, children: Array, + internalInstanceHandle: Object, rootContainerInstance: Container, tag: 'INSTANCE', |}; @@ -155,6 +156,7 @@ export function createInstance( props: propsToUse, isHidden: false, children: [], + internalInstanceHandle, rootContainerInstance, tag: 'INSTANCE', }; @@ -351,3 +353,7 @@ export function unmountFundamentalComponent( onUnmount(null, instance, props, state); } } + +export function getInstanceFromNode(node: Object) { + throw new Error('Not yet implemented.'); +} diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 2e8a891bc2ead..cfef154de6204 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -170,7 +170,9 @@ export type ReactScopeMethods = {| getChildrenFromRoot(): null | Array, getParent(): null | ReactScopeMethods, getProps(): Object, - getScopedNodes(): null | Array, + getAllNodes(): null | Array, + getFirstNode(): null | Object, + containsNode(Object): boolean, |}; export type ReactScopeInstance = {|