Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Transition Types #32105

Merged
merged 4 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions fixtures/view-transition/src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {
useLayoutEffect,
useEffect,
useState,
unstable_addTransitionType as addTransitionType,
} from 'react';

import Chrome from './Chrome';
Expand Down Expand Up @@ -35,11 +36,23 @@ export default function App({assets, initialURL}) {
if (!event.canIntercept) {
return;
}
const navigationType = event.navigationType;
const previousIndex = window.navigation.currentEntry.index;
const newURL = new URL(event.destination.url);
event.intercept({
handler() {
let promise;
startTransition(() => {
addTransitionType('navigation-' + navigationType);
if (navigationType === 'traverse') {
// For traverse types it's useful to distinguish going back or forward.
const nextIndex = event.destination.index;
if (nextIndex > previousIndex) {
addTransitionType('navigation-forward');
} else if (nextIndex < previousIndex) {
addTransitionType('navigation-back');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of how a canonical router implementation could add these types.

We could even go as far as just listening to Navigation Events ourselves in React and have these built-in but it's probably better to leave that up to routers since the intercepts could be doing things we're not aware of.

}
}
promise = new Promise(resolve => {
setRouterState({
url: newURL.pathname + newURL.search,
Expand Down
12 changes: 11 additions & 1 deletion fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function Component() {

export default function Page({url, navigate}) {
const show = url === '/?b';
function onTransition(viewTransition) {
function onTransition(viewTransition, types) {
const keyframes = [
{rotate: '0deg', transformOrigin: '30px 8px'},
{rotate: '360deg', transformOrigin: '30px 8px'},
Expand All @@ -59,6 +59,16 @@ export default function Page({url, navigate}) {
</button>
<ViewTransition className="none">
<div>
<ViewTransition className={transitions['slide-on-nav']}>
<h1>{!show ? 'A' : 'B'}</h1>
</ViewTransition>
<ViewTransition
className={{
'navigation-back': transitions['slide-right'],
'navigation-forward': transitions['slide-left'],
}}>
<h1>{!show ? 'A' : 'B'}</h1>
</ViewTransition>
{show ? (
<div>
{a}
Expand Down
55 changes: 54 additions & 1 deletion fixtures/view-transition/src/components/Transitions.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@
}
}

@keyframes exit-slide-left {
@keyframes enter-slide-left {
0% {
opacity: 0;
translate: 200px 0;
}
100% {
opacity: 1;
translate: 0 0;
}
}

@keyframes exit-slide-right {
0% {
opacity: 1;
translate: 0 0;
Expand All @@ -20,9 +31,51 @@
}
}

@keyframes exit-slide-left {
0% {
opacity: 1;
translate: 0 0;
}
100% {
opacity: 0;
translate: -200px 0;
}
}

::view-transition-new(.slide-right) {
animation: enter-slide-right ease-in 0.25s;
}
::view-transition-old(.slide-right) {
animation: exit-slide-right ease-in 0.25s;
}
::view-transition-new(.slide-left) {
animation: enter-slide-left ease-in 0.25s;
}
::view-transition-old(.slide-left) {
animation: exit-slide-left ease-in 0.25s;
}

::view-transition-new(.enter-slide-right):only-child {
animation: enter-slide-right ease-in 0.25s;
}
::view-transition-old(.exit-slide-left):only-child {
animation: exit-slide-left ease-in 0.25s;
}

:root:active-view-transition-type(navigation-back) {
&::view-transition-new(.slide-on-nav) {
animation: enter-slide-right ease-in 0.25s;
}
&::view-transition-old(.slide-on-nav) {
animation: exit-slide-right ease-in 0.25s;
}
}

:root:active-view-transition-type(navigation-forward) {
&::view-transition-new(.slide-on-nav) {
animation: enter-slide-left ease-in 0.25s;
}
&::view-transition-old(.slide-on-nav) {
animation: exit-slide-left ease-in 0.25s;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
PreinitScriptOptions,
PreinitModuleScriptOptions,
} from 'react-dom/src/shared/ReactDOMTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';

import {NotPending} from '../shared/ReactDOMFormActions';

Expand Down Expand Up @@ -1235,6 +1236,7 @@ const SUSPENSEY_FONT_TIMEOUT = 500;

export function startViewTransition(
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
layoutCallback: () => void,
afterMutationCallback: () => void,
Expand Down Expand Up @@ -1293,7 +1295,7 @@ export function startViewTransition(
afterMutationCallback();
}
},
types: null, // TODO: Provide types.
types: transitionTypes,
});
// $FlowFixMe[prop-missing]
ownerDocument.__reactViewTransition = transition;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-renderer/src/ReactFiberConfigNative.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';

// Modules provided by RN:
import {
Expand Down Expand Up @@ -582,6 +583,7 @@ export function hasInstanceAffectedParent(

export function startViewTransition(
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
layoutCallback: () => void,
afterMutationCallback: () => void,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue'
import type {ReactNodeList} from 'shared/ReactTypes';
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';

import * as Scheduler from 'scheduler/unstable_mock';
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
Expand Down Expand Up @@ -780,6 +781,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {

startViewTransition(
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
afterMutationCallback: () => void,
layoutCallback: () => void,
Expand Down
71 changes: 56 additions & 15 deletions packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,35 @@ import type {ReactNodeList} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {ViewTransitionInstance} from './ReactFiberConfig';

import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
import {
getWorkInProgressRoot,
getPendingTransitionTypes,
} from './ReactFiberWorkLoop';

import {getIsHydrating} from './ReactFiberHydrationContext';

import {getTreeId} from './ReactFiberTreeContext';

export type ViewTransitionClassPerType = {
[transitionType: 'default' | string]: 'none' | string,
};

export type ViewTransitionClass = 'none' | string | ViewTransitionClassPerType;

export type ViewTransitionProps = {
name?: string,
children?: ReactNodeList,
className?: 'none' | string,
enter?: 'none' | string,
exit?: 'none' | string,
layout?: 'none' | string,
share?: 'none' | string,
update?: 'none' | string,
onEnter?: (instance: ViewTransitionInstance) => void,
onExit?: (instance: ViewTransitionInstance) => void,
onLayout?: (instance: ViewTransitionInstance) => void,
onShare?: (instance: ViewTransitionInstance) => void,
onUpdate?: (instance: ViewTransitionInstance) => void,
className?: ViewTransitionClass,
enter?: ViewTransitionClass,
exit?: ViewTransitionClass,
layout?: ViewTransitionClass,
share?: ViewTransitionClass,
update?: ViewTransitionClass,
onEnter?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onExit?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onLayout?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onShare?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onUpdate?: (instance: ViewTransitionInstance, types: Array<string>) => void,
};

export type ViewTransitionState = {
Expand Down Expand Up @@ -82,17 +91,49 @@ export function getViewTransitionName(
return (instance.autoName: any);
}

function getClassNameByType(classByType: ?ViewTransitionClass): ?string {
if (classByType == null || typeof classByType === 'string') {
return classByType;
}
let className: ?string = null;
const activeTypes = getPendingTransitionTypes();
if (activeTypes !== null) {
for (let i = 0; i < activeTypes.length; i++) {
const match = classByType[activeTypes[i]];
if (match != null) {
if (match === 'none') {
// If anything matches "none" that takes precedence over any other
// type that also matches.
return 'none';
}
if (className == null) {
className = match;
} else {
className += ' ' + match;
}
}
}
}
if (className == null) {
// We had no other matches. Match the default for this configuration.
return classByType.default;
}
return className;
}

export function getViewTransitionClassName(
className: ?string,
eventClassName: ?string,
defaultClass: ?ViewTransitionClass,
eventClass: ?ViewTransitionClass,
): ?string {
const className: ?string = getClassNameByType(defaultClass);
const eventClassName: ?string = getClassNameByType(eventClass);
if (eventClassName == null) {
return className;
}
if (eventClassName === 'none') {
return eventClassName;
}
if (className != null) {
if (className != null && className !== 'none') {
return className + ' ' + eventClassName;
}
return eventClassName;
Expand Down
45 changes: 35 additions & 10 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
getViewTransitionName,
type ViewTransitionState,
} from './ReactFiberViewTransitionComponent';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';

import {
enableCreateEventHandleAPI,
Expand Down Expand Up @@ -653,7 +654,9 @@ let pendingEffectsRemainingLanes: Lanes = NoLanes;
let pendingEffectsRenderEndTime: number = -0; // Profiling-only
let pendingPassiveTransitions: Array<Transition> | null = null;
let pendingRecoverableErrors: null | Array<CapturedValue<mixed>> = null;
let pendingViewTransitionEvents: Array<() => void> | null = null;
let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
null;
let pendingTransitionTypes: null | TransitionTypes = null;
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only

Expand Down Expand Up @@ -695,6 +698,10 @@ export function getPendingPassiveEffectsLanes(): Lanes {
return pendingEffectsLanes;
}

export function getPendingTransitionTypes(): null | TransitionTypes {
return pendingTransitionTypes;
}

export function isWorkLoopSuspendedOnData(): boolean {
return (
workInProgressSuspendedReason === SuspendedOnData ||
Expand Down Expand Up @@ -804,7 +811,7 @@ export function requestDeferredLane(): Lane {

export function scheduleViewTransitionEvent(
fiber: Fiber,
callback: ?(instance: ViewTransitionInstance) => void,
callback: ?(instance: ViewTransitionInstance, types: Array<string>) => void,
): void {
if (enableViewTransition) {
if (callback != null) {
Expand Down Expand Up @@ -3348,9 +3355,6 @@ function commitRoot(
pendingEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
pendingRecoverableErrors = recoverableErrors;
if (enableViewTransition) {
pendingViewTransitionEvents = null;
}
pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;
if (enableProfilerTimer) {
pendingEffectsRenderEndTime = completedRenderEndTime;
Expand All @@ -3362,10 +3366,24 @@ function commitRoot(
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
const passiveSubtreeMask =
enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes)
? PassiveTransitionMask
: PassiveMask;
let passiveSubtreeMask;
if (enableViewTransition) {
pendingViewTransitionEvents = null;
if (includesOnlyViewTransitionEligibleLanes(lanes)) {
// Claim any pending Transition Types for this commit.
// This means that multiple roots committing independent View Transitions
// 1) end up staggered because we can only have one at a time.
// 2) only the first one gets all the Transition Types.
pendingTransitionTypes = ReactSharedInternals.V;
ReactSharedInternals.V = null;
passiveSubtreeMask = PassiveTransitionMask;
} else {
pendingTransitionTypes = null;
passiveSubtreeMask = PassiveMask;
}
} else {
passiveSubtreeMask = PassiveMask;
}
if (
// If this subtree rendered with profiling this commit, we need to visit it to log it.
(enableProfilerTimer &&
Expand Down Expand Up @@ -3461,6 +3479,7 @@ function commitRoot(
shouldStartViewTransition &&
startViewTransition(
root.containerInfo,
pendingTransitionTypes,
flushMutationEffects,
flushLayoutEffects,
flushAfterMutationEffects,
Expand Down Expand Up @@ -3708,11 +3727,17 @@ function flushSpawnedWork(): void {
// effects or spawned sync work since this is still part of the previous commit.
// Even though conceptually it's like its own task between layout effets and passive.
const pendingEvents = pendingViewTransitionEvents;
let pendingTypes = pendingTransitionTypes;
pendingTransitionTypes = null;
if (pendingEvents !== null) {
pendingViewTransitionEvents = null;
if (pendingTypes === null) {
// Normalize the type. This is lazily created only for events.
pendingTypes = [];
}
for (let i = 0; i < pendingEvents.length; i++) {
const viewTransitionEvent = pendingEvents[i];
viewTransitionEvent();
viewTransitionEvent(pendingTypes);
}
}
}
Expand Down
Loading
Loading