[react-interactions] Adds more experimental Scope API methods (#17042)

This commit is contained in:
Dominic Gannaway
2019-10-08 19:32:53 +02:00
committed by GitHub
parent 36ea3cef9e
commit 177752a7b3
17 changed files with 261 additions and 37 deletions
+4
View File
@@ -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.');
}
+9 -1
View File
@@ -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;
}
@@ -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>DIV 1</div>, <div>DIV 2</div>, <div>DIV 3</div>]
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<HTMLElement>
### getAllNodes: () => null | Array<HTMLElement>
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.
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.
@@ -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);
}
@@ -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();
}
}
},
@@ -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) {
@@ -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) {
@@ -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(<Test />, container);
let nodes = scopeRef.current.getScopedNodes();
let nodes = scopeRef.current.getAllNodes();
expect(nodes).toEqual([
nodeRefA.current,
nodeRefB.current,
@@ -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];
}
@@ -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.');
}
@@ -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.');
}
+4
View File
@@ -432,6 +432,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
hidden: instance.hidden,
};
},
getInstanceFromNode() {
throw new Error('Not yet implemented.');
},
};
const hostConfig = useMutation
+62 -2
View File
@@ -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<Object> {
getAllNodes(): null | Array<Object> {
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;
},
};
}
@@ -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(<Test toggle={true} />, container);
let nodes = scopeRef.current.getScopedNodes();
let nodes = scopeRef.current.getAllNodes();
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
ReactDOM.render(<Test toggle={false} />, 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 ? (
<TestScope ref={scopeRef}>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
<a ref={aRef}>A</a>
</TestScope>
) : (
<TestScope ref={scopeRef}>
<a ref={aRef}>A</a>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
</TestScope>
);
}
ReactDOM.render(<Test toggle={true} />, container);
let node = scopeRef.current.getFirstNode();
expect(node).toEqual(divRef.current);
ReactDOM.render(<Test toggle={false} />, 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 ? (
<div>
<span ref={outerSpan}>SPAN</span>
<TestScope ref={scopeRef}>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
<a ref={aRef}>A</a>
</TestScope>
<em ref={emRef}>EM</em>
</div>
) : (
<div>
<TestScope ref={scopeRef}>
<a ref={aRef}>A</a>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
<em ref={emRef}>EM</em>
</TestScope>
<span ref={outerSpan}>SPAN</span>
</div>
);
}
ReactDOM.render(<Test toggle={true} />, 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(<Test toggle={false} />, 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(<Test toggle={true} />, 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(<Test />, 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(<Test />, 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(<Test toggle={false} />);
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 ? (
<TestScope ref={scopeRef}>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
<a ref={aRef}>A</a>
</TestScope>
) : (
<TestScope ref={scopeRef}>
<a ref={aRef}>A</a>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
</TestScope>
);
}
const renderer = ReactTestRenderer.create(<Test toggle={true} />, {
createNodeMock: element => {
return element;
},
});
let node = scopeRef.current.getFirstNode();
expect(node).toEqual(divRef.current);
renderer.update(<Test toggle={false} />);
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,
@@ -69,6 +69,7 @@ export const mountFundamentalComponent =
$$$hostConfig.mountFundamentalComponent;
export const shouldUpdateFundamentalComponent =
$$$hostConfig.shouldUpdateFundamentalComponent;
export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
// -------------------
// Mutation
@@ -29,6 +29,7 @@ export type Instance = {|
props: Object,
isHidden: boolean,
children: Array<Instance | TextInstance>,
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.');
}
+3 -1
View File
@@ -170,7 +170,9 @@ export type ReactScopeMethods = {|
getChildrenFromRoot(): null | Array<ReactScopeMethods>,
getParent(): null | ReactScopeMethods,
getProps(): Object,
getScopedNodes(): null | Array<Object>,
getAllNodes(): null | Array<Object>,
getFirstNode(): null | Object,
containsNode(Object): boolean,
|};
export type ReactScopeInstance = {|