import type {ViewTransitionProps} from 'shared/ReactTypes';
import type {Instance, InstanceMeasurement, Props} from './ReactFiberConfig';
import type {Fiber} from './ReactInternalTypes';
import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
import {
HostComponent,
OffscreenComponent,
ViewTransitionComponent,
} from './ReactWorkTags';
import {
NoFlags,
Update,
ViewTransitionStatic,
AffectedParentLayout,
ViewTransitionNamedStatic,
} from './ReactFiberFlags';
import {
supportsMutation,
applyViewTransitionName,
restoreViewTransitionName,
measureInstance,
measureClonedInstance,
hasInstanceChanged,
hasInstanceAffectedParent,
wasInstanceInViewport,
} from './ReactFiberConfig';
import {scheduleViewTransitionEvent} from './ReactFiberWorkLoop';
import {
getViewTransitionName,
getViewTransitionClassName,
} from './ReactFiberViewTransitionComponent';
export let shouldStartViewTransition: boolean = false;
export function resetShouldStartViewTransition(): void {
shouldStartViewTransition = false;
}
export let appearingViewTransitions: Map<string, ViewTransitionState> | null =
null;
export function resetAppearingViewTransitions(): void {
appearingViewTransitions = null;
}
export function trackAppearingViewTransition(
name: string,
state: ViewTransitionState,
): void {
if (appearingViewTransitions === null) {
appearingViewTransitions = new Map();
}
appearingViewTransitions.set(name, state);
}
export function trackEnterViewTransitions(placement: Fiber): void {
if (
placement.tag === ViewTransitionComponent ||
(placement.subtreeFlags & ViewTransitionStatic) !== NoFlags
) {
shouldStartViewTransition = true;
}
}
export let viewTransitionCancelableChildren: null | Array<
Instance | string | Props,
> = null;
export function pushViewTransitionCancelableScope(): null | Array<
Instance | string | Props,
> {
const prevChildren = viewTransitionCancelableChildren;
viewTransitionCancelableChildren = null;
return prevChildren;
}
export function popViewTransitionCancelableScope(
prevChildren: null | Array<Instance | string | Props>,
): void {
viewTransitionCancelableChildren = prevChildren;
}
let viewTransitionHostInstanceIdx = 0;
export function applyViewTransitionToHostInstances(
child: null | Fiber,
name: string,
className: ?string,
collectMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
viewTransitionHostInstanceIdx = 0;
return applyViewTransitionToHostInstancesRecursive(
child,
name,
className,
collectMeasurements,
stopAtNestedViewTransitions,
);
}
function applyViewTransitionToHostInstancesRecursive(
child: null | Fiber,
name: string,
className: ?string,
collectMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
if (!supportsMutation) {
return false;
}
let inViewport = false;
while (child !== null) {
if (child.tag === HostComponent) {
const instance: Instance = child.stateNode;
if (collectMeasurements !== null) {
const measurement = measureInstance(instance);
collectMeasurements.push(measurement);
if (wasInstanceInViewport(measurement)) {
inViewport = true;
}
} else if (!inViewport) {
if (wasInstanceInViewport(measureInstance(instance))) {
inViewport = true;
}
}
shouldStartViewTransition = true;
applyViewTransitionName(
instance,
viewTransitionHostInstanceIdx === 0
? name
:
name + '_' + viewTransitionHostInstanceIdx,
className,
);
viewTransitionHostInstanceIdx++;
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
} else if (
child.tag === ViewTransitionComponent &&
stopAtNestedViewTransitions
) {
} else {
if (
applyViewTransitionToHostInstancesRecursive(
child.child,
name,
className,
collectMeasurements,
stopAtNestedViewTransitions,
)
) {
inViewport = true;
}
}
child = child.sibling;
}
return inViewport;
}
function restoreViewTransitionOnHostInstances(
child: null | Fiber,
stopAtNestedViewTransitions: boolean,
): void {
if (!supportsMutation) {
return;
}
while (child !== null) {
if (child.tag === HostComponent) {
const instance: Instance = child.stateNode;
restoreViewTransitionName(instance, child.memoizedProps);
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
} else if (
child.tag === ViewTransitionComponent &&
stopAtNestedViewTransitions
) {
} else {
restoreViewTransitionOnHostInstances(
child.child,
stopAtNestedViewTransitions,
);
}
child = child.sibling;
}
}
function commitAppearingPairViewTransitions(placement: Fiber): void {
if ((placement.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
return;
}
let child = placement.child;
while (child !== null) {
if (child.tag === OffscreenComponent && child.memoizedState === null) {
} else {
commitAppearingPairViewTransitions(child);
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
) {
const instance: ViewTransitionState = child.stateNode;
if (instance.paired) {
const props: ViewTransitionProps = child.memoizedProps;
if (props.name == null || props.name === 'auto') {
throw new Error(
'Found a pair with an auto name. This is a bug in React.',
);
}
const name = props.name;
const className: ?string = getViewTransitionClassName(
props.default,
props.share,
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
child.child,
name,
className,
null,
false,
);
if (!inViewport) {
restoreViewTransitionOnHostInstances(child.child, false);
}
}
}
}
}
child = child.sibling;
}
}
export function commitEnterViewTransitions(
placement: Fiber,
gesture: boolean,
): void {
if (placement.tag === ViewTransitionComponent) {
const state: ViewTransitionState = placement.stateNode;
const props: ViewTransitionProps = placement.memoizedProps;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.default,
state.paired ? props.share : props.enter,
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
placement.child,
name,
className,
null,
false,
);
if (!inViewport) {
restoreViewTransitionOnHostInstances(placement.child, false);
} else {
commitAppearingPairViewTransitions(placement);
if (!state.paired) {
if (gesture) {
} else {
scheduleViewTransitionEvent(placement, props.onEnter);
}
}
}
} else {
commitAppearingPairViewTransitions(placement);
}
} else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = placement.child;
while (child !== null) {
commitEnterViewTransitions(child, gesture);
child = child.sibling;
}
} else {
commitAppearingPairViewTransitions(placement);
}
}
function commitDeletedPairViewTransitions(deletion: Fiber): void {
if (
appearingViewTransitions === null ||
appearingViewTransitions.size === 0
) {
return;
}
const pairs = appearingViewTransitions;
if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
return;
}
let child = deletion.child;
while (child !== null) {
if (child.tag === OffscreenComponent && child.memoizedState === null) {
} else {
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
) {
const props: ViewTransitionProps = child.memoizedProps;
const name = props.name;
if (name != null && name !== 'auto') {
const pair = pairs.get(name);
if (pair !== undefined) {
const className: ?string = getViewTransitionClassName(
props.default,
props.share,
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
child.child,
name,
className,
null,
false,
);
if (!inViewport) {
restoreViewTransitionOnHostInstances(child.child, false);
} else {
const oldInstance: ViewTransitionState = child.stateNode;
const newInstance: ViewTransitionState = pair;
newInstance.paired = oldInstance;
oldInstance.paired = newInstance;
scheduleViewTransitionEvent(child, props.onShare);
}
}
pairs.delete(name);
if (pairs.size === 0) {
break;
}
}
}
}
commitDeletedPairViewTransitions(child);
}
child = child.sibling;
}
}
export function commitExitViewTransitions(deletion: Fiber): void {
if (deletion.tag === ViewTransitionComponent) {
const props: ViewTransitionProps = deletion.memoizedProps;
const name = getViewTransitionName(props, deletion.stateNode);
const pair =
appearingViewTransitions !== null
? appearingViewTransitions.get(name)
: undefined;
const className: ?string = getViewTransitionClassName(
props.default,
pair !== undefined ? props.share : props.exit,
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
deletion.child,
name,
className,
null,
false,
);
if (!inViewport) {
restoreViewTransitionOnHostInstances(deletion.child, false);
} else if (pair !== undefined) {
const oldInstance: ViewTransitionState = deletion.stateNode;
const newInstance: ViewTransitionState = pair;
newInstance.paired = oldInstance;
oldInstance.paired = newInstance;
appearingViewTransitions.delete(name);
scheduleViewTransitionEvent(deletion, props.onShare);
} else {
scheduleViewTransitionEvent(deletion, props.onExit);
}
}
if (appearingViewTransitions !== null) {
commitDeletedPairViewTransitions(deletion);
}
} else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = deletion.child;
while (child !== null) {
commitExitViewTransitions(child);
child = child.sibling;
}
} else {
if (appearingViewTransitions !== null) {
commitDeletedPairViewTransitions(deletion);
}
}
}
export function commitBeforeUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
): void {
const oldProps: ViewTransitionProps = current.memoizedProps;
const oldName = getViewTransitionName(oldProps, current.stateNode);
const newProps: ViewTransitionProps = finishedWork.memoizedProps;
const className: ?string = getViewTransitionClassName(
newProps.default,
newProps.update,
);
if (className === 'none') {
return;
}
applyViewTransitionToHostInstances(
current.child,
oldName,
className,
(current.memoizedState = []),
true,
);
}
export function commitNestedViewTransitions(changedParent: Fiber): void {
let child = changedParent.child;
while (child !== null) {
if (child.tag === ViewTransitionComponent) {
const props: ViewTransitionProps = child.memoizedProps;
const name = getViewTransitionName(props, child.stateNode);
const className: ?string = getViewTransitionClassName(
props.default,
props.update,
);
child.flags &= ~Update;
if (className !== 'none') {
applyViewTransitionToHostInstances(
child.child,
name,
className,
(child.memoizedState = []),
false,
);
}
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
commitNestedViewTransitions(child);
}
child = child.sibling;
}
}
function restorePairedViewTransitions(parent: Fiber): void {
if ((parent.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
return;
}
let child = parent.child;
while (child !== null) {
if (child.tag === OffscreenComponent && child.memoizedState === null) {
} else {
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
) {
const instance: ViewTransitionState = child.stateNode;
if (instance.paired !== null) {
instance.paired = null;
restoreViewTransitionOnHostInstances(child.child, false);
}
}
restorePairedViewTransitions(child);
}
child = child.sibling;
}
}
export function restoreEnterOrExitViewTransitions(fiber: Fiber): void {
if (fiber.tag === ViewTransitionComponent) {
const instance: ViewTransitionState = fiber.stateNode;
instance.paired = null;
restoreViewTransitionOnHostInstances(fiber.child, false);
restorePairedViewTransitions(fiber);
} else if ((fiber.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = fiber.child;
while (child !== null) {
restoreEnterOrExitViewTransitions(child);
child = child.sibling;
}
} else {
restorePairedViewTransitions(fiber);
}
}
export function restoreUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
): void {
restoreViewTransitionOnHostInstances(current.child, true);
restoreViewTransitionOnHostInstances(finishedWork.child, true);
}
export function restoreUpdateViewTransitionForGesture(
current: Fiber,
finishedWork: Fiber,
): void {
restoreViewTransitionOnHostInstances(current.child, true);
}
export function restoreNestedViewTransitions(changedParent: Fiber): void {
let child = changedParent.child;
while (child !== null) {
if (child.tag === ViewTransitionComponent) {
restoreViewTransitionOnHostInstances(child.child, false);
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
restoreNestedViewTransitions(child);
}
child = child.sibling;
}
}
export function measureViewTransitionHostInstances(
parentViewTransition: Fiber,
child: null | Fiber,
newName: string,
oldName: string,
className: ?string,
previousMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
viewTransitionHostInstanceIdx = 0;
return measureViewTransitionHostInstancesRecursive(
parentViewTransition,
child,
newName,
oldName,
className,
previousMeasurements,
stopAtNestedViewTransitions,
);
}
function measureViewTransitionHostInstancesRecursive(
parentViewTransition: Fiber,
child: null | Fiber,
newName: string,
oldName: string,
className: ?string,
previousMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
if (!supportsMutation) {
return true;
}
let inViewport = false;
while (child !== null) {
if (child.tag === HostComponent) {
const instance: Instance = child.stateNode;
if (
previousMeasurements !== null &&
viewTransitionHostInstanceIdx < previousMeasurements.length
) {
const previousMeasurement =
previousMeasurements[viewTransitionHostInstanceIdx];
const nextMeasurement = measureInstance(instance);
if (
wasInstanceInViewport(previousMeasurement) ||
wasInstanceInViewport(nextMeasurement)
) {
inViewport = true;
}
if (
(parentViewTransition.flags & Update) === NoFlags &&
hasInstanceChanged(previousMeasurement, nextMeasurement)
) {
parentViewTransition.flags |= Update;
}
if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) {
parentViewTransition.flags |= AffectedParentLayout;
}
} else {
parentViewTransition.flags |= AffectedParentLayout;
}
if ((parentViewTransition.flags & Update) !== NoFlags) {
applyViewTransitionName(
instance,
viewTransitionHostInstanceIdx === 0
? newName
:
newName + '_' + viewTransitionHostInstanceIdx,
className,
);
}
if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
if (viewTransitionCancelableChildren === null) {
viewTransitionCancelableChildren = [];
}
viewTransitionCancelableChildren.push(
instance,
oldName,
child.memoizedProps,
);
}
viewTransitionHostInstanceIdx++;
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
} else if (
child.tag === ViewTransitionComponent &&
stopAtNestedViewTransitions
) {
parentViewTransition.flags |= child.flags & AffectedParentLayout;
} else {
if (
measureViewTransitionHostInstancesRecursive(
parentViewTransition,
child.child,
newName,
oldName,
className,
previousMeasurements,
stopAtNestedViewTransitions,
)
) {
inViewport = true;
}
}
child = child.sibling;
}
return inViewport;
}
export function measureUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
gesture: boolean,
): boolean {
const oldFiber = gesture ? finishedWork : current;
const newFiber = gesture ? current : finishedWork;
const props: ViewTransitionProps = newFiber.memoizedProps;
const state: ViewTransitionState = newFiber.stateNode;
const newName = getViewTransitionName(props, state);
const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
const className: ?string = getViewTransitionClassName(
props.default,
props.update,
);
if (className === 'none') {
return false;
}
let previousMeasurements: null | Array<InstanceMeasurement>;
if (gesture) {
const clones = state.clones;
if (clones === null) {
previousMeasurements = null;
} else {
previousMeasurements = clones.map(measureClonedInstance);
}
} else {
previousMeasurements = oldFiber.memoizedState;
oldFiber.memoizedState = null;
}
const inViewport = measureViewTransitionHostInstances(
finishedWork,
newFiber.child,
newName,
oldName,
className,
previousMeasurements,
true,
);
const previousCount =
previousMeasurements === null ? 0 : previousMeasurements.length;
if (viewTransitionHostInstanceIdx !== previousCount) {
finishedWork.flags |= AffectedParentLayout;
}
return inViewport;
}
export function measureNestedViewTransitions(
changedParent: Fiber,
gesture: boolean,
): void {
let child = changedParent.child;
while (child !== null) {
if (child.tag === ViewTransitionComponent) {
const props: ViewTransitionProps = child.memoizedProps;
const state: ViewTransitionState = child.stateNode;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.default,
props.update,
);
let previousMeasurements: null | Array<InstanceMeasurement>;
if (gesture) {
const clones = state.clones;
if (clones === null) {
previousMeasurements = null;
} else {
previousMeasurements = clones.map(measureClonedInstance);
}
} else {
previousMeasurements = child.memoizedState;
child.memoizedState = null;
}
const inViewport = measureViewTransitionHostInstances(
child,
child.child,
name,
name,
className,
previousMeasurements,
false,
);
if ((child.flags & Update) === NoFlags || !inViewport) {
} else {
if (gesture) {
} else {
scheduleViewTransitionEvent(child, props.onUpdate);
}
}
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
measureNestedViewTransitions(child, gesture);
}
child = child.sibling;
}
}