mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
b55d319559
part of https://github.com/facebook/react/pull/26571 merging separately to improve tracking of files renames in git Rename HostConfig files to FiberConfig to clarify they are configs for Fiber and not Fizz/Flight. This better conforms to the naming used in Flight and now Fizz of `ReactFlightServerConfig` and `ReactFizzConfig`
609 lines
16 KiB
JavaScript
609 lines
16 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
|
import type {Instance} from './ReactFiberConfig';
|
|
|
|
import {
|
|
HostComponent,
|
|
HostHoistable,
|
|
HostSingleton,
|
|
HostText,
|
|
} from 'react-reconciler/src/ReactWorkTags';
|
|
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
|
import {
|
|
findFiberRoot,
|
|
getBoundingRect,
|
|
getInstanceFromNode,
|
|
getTextContent,
|
|
isHiddenSubtree,
|
|
matchAccessibilityRole,
|
|
setFocusIfFocusable,
|
|
setupIntersectionObserver,
|
|
supportsTestSelectors,
|
|
} from './ReactFiberConfig';
|
|
|
|
let COMPONENT_TYPE: symbol | number = 0b000;
|
|
let HAS_PSEUDO_CLASS_TYPE: symbol | number = 0b001;
|
|
let ROLE_TYPE: symbol | number = 0b010;
|
|
let TEST_NAME_TYPE: symbol | number = 0b011;
|
|
let TEXT_TYPE: symbol | number = 0b100;
|
|
|
|
if (typeof Symbol === 'function' && Symbol.for) {
|
|
const symbolFor = Symbol.for;
|
|
COMPONENT_TYPE = symbolFor('selector.component');
|
|
HAS_PSEUDO_CLASS_TYPE = symbolFor('selector.has_pseudo_class');
|
|
ROLE_TYPE = symbolFor('selector.role');
|
|
TEST_NAME_TYPE = symbolFor('selector.test_id');
|
|
TEXT_TYPE = symbolFor('selector.text');
|
|
}
|
|
|
|
type Type = symbol | number;
|
|
|
|
type ComponentSelector = {
|
|
$$typeof: Type,
|
|
value: React$AbstractComponent<empty, mixed>,
|
|
};
|
|
|
|
type HasPseudoClassSelector = {
|
|
$$typeof: Type,
|
|
value: Array<Selector>,
|
|
};
|
|
|
|
type RoleSelector = {
|
|
$$typeof: Type,
|
|
value: string,
|
|
};
|
|
|
|
type TextSelector = {
|
|
$$typeof: Type,
|
|
value: string,
|
|
};
|
|
|
|
type TestNameSelector = {
|
|
$$typeof: Type,
|
|
value: string,
|
|
};
|
|
|
|
type Selector =
|
|
| ComponentSelector
|
|
| HasPseudoClassSelector
|
|
| RoleSelector
|
|
| TextSelector
|
|
| TestNameSelector;
|
|
|
|
export function createComponentSelector(
|
|
component: React$AbstractComponent<empty, mixed>,
|
|
): ComponentSelector {
|
|
return {
|
|
$$typeof: COMPONENT_TYPE,
|
|
value: component,
|
|
};
|
|
}
|
|
|
|
export function createHasPseudoClassSelector(
|
|
selectors: Array<Selector>,
|
|
): HasPseudoClassSelector {
|
|
return {
|
|
$$typeof: HAS_PSEUDO_CLASS_TYPE,
|
|
value: selectors,
|
|
};
|
|
}
|
|
|
|
export function createRoleSelector(role: string): RoleSelector {
|
|
return {
|
|
$$typeof: ROLE_TYPE,
|
|
value: role,
|
|
};
|
|
}
|
|
|
|
export function createTextSelector(text: string): TextSelector {
|
|
return {
|
|
$$typeof: TEXT_TYPE,
|
|
value: text,
|
|
};
|
|
}
|
|
|
|
export function createTestNameSelector(id: string): TestNameSelector {
|
|
return {
|
|
$$typeof: TEST_NAME_TYPE,
|
|
value: id,
|
|
};
|
|
}
|
|
|
|
function findFiberRootForHostRoot(hostRoot: Instance): Fiber {
|
|
const maybeFiber = getInstanceFromNode((hostRoot: any));
|
|
if (maybeFiber != null) {
|
|
if (typeof maybeFiber.memoizedProps['data-testname'] !== 'string') {
|
|
throw new Error(
|
|
'Invalid host root specified. Should be either a React container or a node with a testname attribute.',
|
|
);
|
|
}
|
|
|
|
return ((maybeFiber: any): Fiber);
|
|
} else {
|
|
const fiberRoot = findFiberRoot(hostRoot);
|
|
|
|
if (fiberRoot === null) {
|
|
throw new Error(
|
|
'Could not find React container within specified host subtree.',
|
|
);
|
|
}
|
|
|
|
// The Flow type for FiberRoot is a little funky.
|
|
// createFiberRoot() cheats this by treating the root as :any and adding stateNode lazily.
|
|
return ((fiberRoot: any).stateNode.current: Fiber);
|
|
}
|
|
}
|
|
|
|
function matchSelector(fiber: Fiber, selector: Selector): boolean {
|
|
const tag = fiber.tag;
|
|
switch (selector.$$typeof) {
|
|
case COMPONENT_TYPE:
|
|
if (fiber.type === selector.value) {
|
|
return true;
|
|
}
|
|
break;
|
|
case HAS_PSEUDO_CLASS_TYPE:
|
|
return hasMatchingPaths(
|
|
fiber,
|
|
((selector: any): HasPseudoClassSelector).value,
|
|
);
|
|
case ROLE_TYPE:
|
|
if (
|
|
tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton
|
|
) {
|
|
const node = fiber.stateNode;
|
|
if (
|
|
matchAccessibilityRole(node, ((selector: any): RoleSelector).value)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
case TEXT_TYPE:
|
|
if (
|
|
tag === HostComponent ||
|
|
tag === HostText ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton
|
|
) {
|
|
const textContent = getTextContent(fiber);
|
|
if (
|
|
textContent !== null &&
|
|
textContent.indexOf(((selector: any): TextSelector).value) >= 0
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
case TEST_NAME_TYPE:
|
|
if (
|
|
tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton
|
|
) {
|
|
const dataTestID = fiber.memoizedProps['data-testname'];
|
|
if (
|
|
typeof dataTestID === 'string' &&
|
|
dataTestID.toLowerCase() ===
|
|
((selector: any): TestNameSelector).value.toLowerCase()
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error('Invalid selector type specified.');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function selectorToString(selector: Selector): string | null {
|
|
switch (selector.$$typeof) {
|
|
case COMPONENT_TYPE:
|
|
const displayName = getComponentNameFromType(selector.value) || 'Unknown';
|
|
return `<${displayName}>`;
|
|
case HAS_PSEUDO_CLASS_TYPE:
|
|
return `:has(${selectorToString(selector) || ''})`;
|
|
case ROLE_TYPE:
|
|
return `[role="${((selector: any): RoleSelector).value}"]`;
|
|
case TEXT_TYPE:
|
|
return `"${((selector: any): TextSelector).value}"`;
|
|
case TEST_NAME_TYPE:
|
|
return `[data-testname="${((selector: any): TestNameSelector).value}"]`;
|
|
default:
|
|
throw new Error('Invalid selector type specified.');
|
|
}
|
|
}
|
|
|
|
function findPaths(root: Fiber, selectors: Array<Selector>): Array<Fiber> {
|
|
const matchingFibers: Array<Fiber> = [];
|
|
|
|
const stack = [root, 0];
|
|
let index = 0;
|
|
while (index < stack.length) {
|
|
const fiber = ((stack[index++]: any): Fiber);
|
|
const tag = fiber.tag;
|
|
let selectorIndex = ((stack[index++]: any): number);
|
|
let selector = selectors[selectorIndex];
|
|
|
|
if (
|
|
(tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton) &&
|
|
isHiddenSubtree(fiber)
|
|
) {
|
|
continue;
|
|
} else {
|
|
while (selector != null && matchSelector(fiber, selector)) {
|
|
selectorIndex++;
|
|
selector = selectors[selectorIndex];
|
|
}
|
|
}
|
|
|
|
if (selectorIndex === selectors.length) {
|
|
matchingFibers.push(fiber);
|
|
} else {
|
|
let child = fiber.child;
|
|
while (child !== null) {
|
|
stack.push(child, selectorIndex);
|
|
child = child.sibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
return matchingFibers;
|
|
}
|
|
|
|
// Same as findPaths but with eager bailout on first match
|
|
function hasMatchingPaths(root: Fiber, selectors: Array<Selector>): boolean {
|
|
const stack = [root, 0];
|
|
let index = 0;
|
|
while (index < stack.length) {
|
|
const fiber = ((stack[index++]: any): Fiber);
|
|
const tag = fiber.tag;
|
|
let selectorIndex = ((stack[index++]: any): number);
|
|
let selector = selectors[selectorIndex];
|
|
|
|
if (
|
|
(tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton) &&
|
|
isHiddenSubtree(fiber)
|
|
) {
|
|
continue;
|
|
} else {
|
|
while (selector != null && matchSelector(fiber, selector)) {
|
|
selectorIndex++;
|
|
selector = selectors[selectorIndex];
|
|
}
|
|
}
|
|
|
|
if (selectorIndex === selectors.length) {
|
|
return true;
|
|
} else {
|
|
let child = fiber.child;
|
|
while (child !== null) {
|
|
stack.push(child, selectorIndex);
|
|
child = child.sibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function findAllNodes(
|
|
hostRoot: Instance,
|
|
selectors: Array<Selector>,
|
|
): Array<Instance> {
|
|
if (!supportsTestSelectors) {
|
|
throw new Error('Test selector API is not supported by this renderer.');
|
|
}
|
|
|
|
const root = findFiberRootForHostRoot(hostRoot);
|
|
const matchingFibers = findPaths(root, selectors);
|
|
|
|
const instanceRoots: Array<Instance> = [];
|
|
|
|
const stack = Array.from(matchingFibers);
|
|
let index = 0;
|
|
while (index < stack.length) {
|
|
const node = ((stack[index++]: any): Fiber);
|
|
const tag = node.tag;
|
|
if (
|
|
tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton
|
|
) {
|
|
if (isHiddenSubtree(node)) {
|
|
continue;
|
|
}
|
|
instanceRoots.push(node.stateNode);
|
|
} else {
|
|
let child = node.child;
|
|
while (child !== null) {
|
|
stack.push(child);
|
|
child = child.sibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
return instanceRoots;
|
|
}
|
|
|
|
export function getFindAllNodesFailureDescription(
|
|
hostRoot: Instance,
|
|
selectors: Array<Selector>,
|
|
): string | null {
|
|
if (!supportsTestSelectors) {
|
|
throw new Error('Test selector API is not supported by this renderer.');
|
|
}
|
|
|
|
const root = findFiberRootForHostRoot(hostRoot);
|
|
|
|
let maxSelectorIndex: number = 0;
|
|
const matchedNames = [];
|
|
|
|
// The logic of this loop should be kept in sync with findPaths()
|
|
const stack = [root, 0];
|
|
let index = 0;
|
|
while (index < stack.length) {
|
|
const fiber = ((stack[index++]: any): Fiber);
|
|
const tag = fiber.tag;
|
|
let selectorIndex = ((stack[index++]: any): number);
|
|
const selector = selectors[selectorIndex];
|
|
|
|
if (
|
|
(tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton) &&
|
|
isHiddenSubtree(fiber)
|
|
) {
|
|
continue;
|
|
} else if (matchSelector(fiber, selector)) {
|
|
matchedNames.push(selectorToString(selector));
|
|
selectorIndex++;
|
|
|
|
if (selectorIndex > maxSelectorIndex) {
|
|
maxSelectorIndex = selectorIndex;
|
|
}
|
|
}
|
|
|
|
if (selectorIndex < selectors.length) {
|
|
let child = fiber.child;
|
|
while (child !== null) {
|
|
stack.push(child, selectorIndex);
|
|
child = child.sibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (maxSelectorIndex < selectors.length) {
|
|
const unmatchedNames = [];
|
|
for (let i = maxSelectorIndex; i < selectors.length; i++) {
|
|
unmatchedNames.push(selectorToString(selectors[i]));
|
|
}
|
|
|
|
return (
|
|
'findAllNodes was able to match part of the selector:\n' +
|
|
` ${matchedNames.join(' > ')}\n\n` +
|
|
'No matching component was found for:\n' +
|
|
` ${unmatchedNames.join(' > ')}`
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export type BoundingRect = {
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
};
|
|
|
|
export function findBoundingRects(
|
|
hostRoot: Instance,
|
|
selectors: Array<Selector>,
|
|
): Array<BoundingRect> {
|
|
if (!supportsTestSelectors) {
|
|
throw new Error('Test selector API is not supported by this renderer.');
|
|
}
|
|
|
|
const instanceRoots = findAllNodes(hostRoot, selectors);
|
|
|
|
const boundingRects: Array<BoundingRect> = [];
|
|
for (let i = 0; i < instanceRoots.length; i++) {
|
|
boundingRects.push(getBoundingRect(instanceRoots[i]));
|
|
}
|
|
|
|
for (let i = boundingRects.length - 1; i > 0; i--) {
|
|
const targetRect = boundingRects[i];
|
|
const targetLeft = targetRect.x;
|
|
const targetRight = targetLeft + targetRect.width;
|
|
const targetTop = targetRect.y;
|
|
const targetBottom = targetTop + targetRect.height;
|
|
|
|
for (let j = i - 1; j >= 0; j--) {
|
|
if (i !== j) {
|
|
const otherRect = boundingRects[j];
|
|
const otherLeft = otherRect.x;
|
|
const otherRight = otherLeft + otherRect.width;
|
|
const otherTop = otherRect.y;
|
|
const otherBottom = otherTop + otherRect.height;
|
|
|
|
// Merging all rects to the minimums set would be complicated,
|
|
// but we can handle the most common cases:
|
|
// 1. completely overlapping rects
|
|
// 2. adjacent rects that are the same width or height (e.g. items in a list)
|
|
//
|
|
// Even given the above constraints,
|
|
// we still won't end up with the fewest possible rects without doing multiple passes,
|
|
// but it's good enough for this purpose.
|
|
|
|
if (
|
|
targetLeft >= otherLeft &&
|
|
targetTop >= otherTop &&
|
|
targetRight <= otherRight &&
|
|
targetBottom <= otherBottom
|
|
) {
|
|
// Complete overlapping rects; remove the inner one.
|
|
boundingRects.splice(i, 1);
|
|
break;
|
|
} else if (
|
|
targetLeft === otherLeft &&
|
|
targetRect.width === otherRect.width &&
|
|
!(otherBottom < targetTop) &&
|
|
!(otherTop > targetBottom)
|
|
) {
|
|
// Adjacent vertical rects; merge them.
|
|
if (otherTop > targetTop) {
|
|
otherRect.height += otherTop - targetTop;
|
|
otherRect.y = targetTop;
|
|
}
|
|
if (otherBottom < targetBottom) {
|
|
otherRect.height = targetBottom - otherTop;
|
|
}
|
|
|
|
boundingRects.splice(i, 1);
|
|
break;
|
|
} else if (
|
|
targetTop === otherTop &&
|
|
targetRect.height === otherRect.height &&
|
|
!(otherRight < targetLeft) &&
|
|
!(otherLeft > targetRight)
|
|
) {
|
|
// Adjacent horizontal rects; merge them.
|
|
if (otherLeft > targetLeft) {
|
|
otherRect.width += otherLeft - targetLeft;
|
|
otherRect.x = targetLeft;
|
|
}
|
|
if (otherRight < targetRight) {
|
|
otherRect.width = targetRight - otherLeft;
|
|
}
|
|
|
|
boundingRects.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return boundingRects;
|
|
}
|
|
|
|
export function focusWithin(
|
|
hostRoot: Instance,
|
|
selectors: Array<Selector>,
|
|
): boolean {
|
|
if (!supportsTestSelectors) {
|
|
throw new Error('Test selector API is not supported by this renderer.');
|
|
}
|
|
|
|
const root = findFiberRootForHostRoot(hostRoot);
|
|
const matchingFibers = findPaths(root, selectors);
|
|
|
|
const stack = Array.from(matchingFibers);
|
|
let index = 0;
|
|
while (index < stack.length) {
|
|
const fiber = ((stack[index++]: any): Fiber);
|
|
const tag = fiber.tag;
|
|
if (isHiddenSubtree(fiber)) {
|
|
continue;
|
|
}
|
|
if (
|
|
tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton
|
|
) {
|
|
const node = fiber.stateNode;
|
|
if (setFocusIfFocusable(node)) {
|
|
return true;
|
|
}
|
|
}
|
|
let child = fiber.child;
|
|
while (child !== null) {
|
|
stack.push(child);
|
|
child = child.sibling;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const commitHooks: Array<Function> = [];
|
|
|
|
export function onCommitRoot(): void {
|
|
if (supportsTestSelectors) {
|
|
commitHooks.forEach(commitHook => commitHook());
|
|
}
|
|
}
|
|
|
|
export type IntersectionObserverOptions = Object;
|
|
|
|
export type ObserveVisibleRectsCallback = (
|
|
intersections: Array<{ratio: number, rect: BoundingRect}>,
|
|
) => void;
|
|
|
|
export function observeVisibleRects(
|
|
hostRoot: Instance,
|
|
selectors: Array<Selector>,
|
|
callback: (intersections: Array<{ratio: number, rect: BoundingRect}>) => void,
|
|
options?: IntersectionObserverOptions,
|
|
): {disconnect: () => void} {
|
|
if (!supportsTestSelectors) {
|
|
throw new Error('Test selector API is not supported by this renderer.');
|
|
}
|
|
|
|
const instanceRoots = findAllNodes(hostRoot, selectors);
|
|
|
|
const {disconnect, observe, unobserve} = setupIntersectionObserver(
|
|
instanceRoots,
|
|
callback,
|
|
options,
|
|
);
|
|
|
|
// When React mutates the host environment, we may need to change what we're listening to.
|
|
const commitHook = () => {
|
|
const nextInstanceRoots = findAllNodes(hostRoot, selectors);
|
|
|
|
instanceRoots.forEach(target => {
|
|
if (nextInstanceRoots.indexOf(target) < 0) {
|
|
unobserve(target);
|
|
}
|
|
});
|
|
|
|
nextInstanceRoots.forEach(target => {
|
|
if (instanceRoots.indexOf(target) < 0) {
|
|
observe(target);
|
|
}
|
|
});
|
|
};
|
|
|
|
commitHooks.push(commitHook);
|
|
|
|
return {
|
|
disconnect: () => {
|
|
// Stop listening for React mutations:
|
|
const index = commitHooks.indexOf(commitHook);
|
|
if (index >= 0) {
|
|
commitHooks.splice(index, 1);
|
|
}
|
|
|
|
// Disconnect the host observer:
|
|
disconnect();
|
|
},
|
|
};
|
|
}
|