Files
Sam ZhouandGitHub 45804af18d [flow] Eliminate usage of more than 1-arg React.AbstractComponent in React codebase (#31314)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

In order to adopt react 19's ref-as-prop model, Flow needs to eliminate
all the places where they are treated differently.
`React.AbstractComponent` is the worst example of this, and we need to
eliminate it.

This PR eliminates them from the react repo, and only keeps the one that
has 1 argument of props.

## How did you test this change?

yarn flow
2024-10-21 16:17:41 -07:00

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$ComponentType<empty>,
};
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$ComponentType<empty>,
): 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();
},
};
}