diff --git a/fixtures/view-transition/src/components/NestedParentExit.css b/fixtures/view-transition/src/components/NestedParentExit.css new file mode 100644 index 000000000000..a5b71e6c36d5 --- /dev/null +++ b/fixtures/view-transition/src/components/NestedParentExit.css @@ -0,0 +1,150 @@ +.nested-parent-exit { + width: 280px; + min-height: 280px; + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #ccc; +} + +.nested-parent-exit-swipe { + min-height: 200px; +} + +.nested-parent-exit-label { + margin: 0 0 0.75rem; + font-size: 13px; + color: #666; +} + +.nested-parent-exit-panel { + min-height: 240px; +} + +.nested-parent-exit .feed-item { + margin-bottom: 0.75rem; + cursor: pointer; +} + +.nested-parent-exit .feed-item-title { + margin: 0 0 0.15rem; + font-size: 15px; + font-weight: 600; +} + +.nested-parent-exit .feed-item p, +.nested-parent-exit .detail-view p { + margin: 0; + font-size: 13px; + color: #666; +} + +.nested-parent-exit .back-button { + margin-bottom: 0.75rem; + padding: 0; + border: none; + background: none; + cursor: pointer; + font: inherit; + color: inherit; + text-decoration: underline; +} + +@keyframes nested-exit-left { + from { + opacity: 1; + translate: 0 0; + } + to { + opacity: 0; + translate: -120% 0; + } +} + +::view-transition-old(.nested-exit-left) { + animation: nested-exit-left 450ms ease-out forwards; +} + +@keyframes nested-enter-from-left { + from { + opacity: 0; + translate: -120% 0; + } + to { + opacity: 1; + translate: 0 0; + } +} + +::view-transition-new(.nested-enter-from-left) { + animation: nested-enter-from-left 450ms ease-out 650ms both; +} + +::view-transition-group(.nested-shared-post-forward) { + animation-duration: 600ms; + animation-delay: 300ms; + animation-timing-function: ease-in-out; + animation-fill-mode: both; +} + +::view-transition-old(.nested-shared-post-forward), +::view-transition-new(.nested-shared-post-forward) { + animation-delay: 300ms; + animation-duration: 600ms; + animation-fill-mode: both; +} + +::view-transition-group(.nested-shared-post-back) { + animation-duration: 600ms; + animation-timing-function: ease-in-out; +} + +::view-transition-group(.nested-shared-inner-forward) { + animation-duration: 500ms; + animation-delay: 400ms; + animation-timing-function: ease-in-out; + animation-fill-mode: both; +} + +::view-transition-old(.nested-shared-inner-forward), +::view-transition-new(.nested-shared-inner-forward) { + animation-delay: 400ms; + animation-duration: 500ms; + animation-fill-mode: both; +} + +::view-transition-group(.nested-shared-inner-back) { + animation-duration: 500ms; + animation-delay: 100ms; + animation-timing-function: ease-in-out; + animation-fill-mode: both; +} + +@keyframes nested-back-btn-enter { + from { + opacity: 0; + translate: -20px 0; + } + to { + opacity: 1; + translate: 0 0; + } +} + +@keyframes nested-back-btn-exit { + from { + opacity: 1; + translate: 0 0; + } + to { + opacity: 0; + translate: -20px 0; + } +} + +::view-transition-new(.nested-back-btn-enter):only-child { + animation: nested-back-btn-enter 300ms ease-out 650ms both; +} + +::view-transition-old(.nested-back-btn-exit):only-child { + animation: nested-back-btn-exit 200ms ease-in forwards; +} diff --git a/fixtures/view-transition/src/components/NestedParentExit.js b/fixtures/view-transition/src/components/NestedParentExit.js new file mode 100644 index 000000000000..7dbe2973c747 --- /dev/null +++ b/fixtures/view-transition/src/components/NestedParentExit.js @@ -0,0 +1,174 @@ +import React, { + ViewTransition, + useState, + useOptimistic, + startTransition, + addTransitionType, +} from 'react'; +import SwipeRecognizer from './SwipeRecognizer.js'; +import './NestedParentExit.css'; + +const items = [ + {id: 1, title: 'First Post', body: 'Hello from the first post.'}, + {id: 2, title: 'Second Post', body: 'Hello from the second post.'}, + {id: 3, title: 'Third Post', body: 'Hello from the third post.'}, +]; + +function logGestureParent(kind, title, _timeline, _options, _instance, types) { + // eslint-disable-next-line no-console + console.log(`[NestedParentExit] onGestureParent${kind}`, title, types); +} + +function FeedItem({item, index, activeIndex, onSelect}) { + const isActive = activeIndex === index; + + return ( + + logGestureParent('Exit', item.title, ...args) + } + onGestureParentEnter={(...args) => + logGestureParent('Enter', item.title, ...args) + }> +
onSelect(item, index)}> + +
{item.title}
+
+

{item.body}

+
+
+ ); +} + +function Detail({item, onBack}) { + return ( + +
+ + + + +
{item.title}
+
+

{item.body}

+
+
+ ); +} + +const initialNav = {selected: null, activeIndex: null}; + +export default function NestedParentExit() { + const [nav, setNav] = useState(initialNav); + const [optimisticNav, navigateByGesture] = useOptimistic( + nav, + (state, direction) => { + if (direction === 'left' && state.selected === null) { + return {selected: items[0], activeIndex: 0}; + } + if (direction === 'right' && state.selected !== null) { + return { + selected: null, + activeIndex: + state.activeIndex ?? + items.findIndex(i => i.id === state.selected.id), + }; + } + return state; + } + ); + + const {selected, activeIndex} = optimisticNav; + + function goToDetail(item, index) { + setNav({selected: item, activeIndex: index}); + startTransition(() => { + addTransitionType('nav-forward'); + }); + } + + function goBack() { + const current = selected; + if (current == null) { + return; + } + const backIndex = items.findIndex(i => i.id === current.id); + setNav({selected: null, activeIndex: backIndex}); + startTransition(() => { + addTransitionType('nav-back'); + }); + } + + function swipeAction() { + if (nav.selected === null) { + goToDetail(items[0], 0); + } else { + goBack(); + } + } + + return ( +
+

+ Parent Exit/Enter — click a post or swipe (scroll the strip below) +

+
+ { + addTransitionType( + direction === 'left' ? 'nav-forward' : 'nav-back' + ); + navigateByGesture(direction); + }} + direction={selected ? 'right' : 'left'}> + +
+ {selected ? ( + + ) : ( + <> + {items.map((item, index) => ( + + ))} + + )} +
+
+
+
+
+ ); +} diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 60faa09732d9..37f5624e9470 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -20,6 +20,7 @@ import './Page.css'; import transitions from './Transitions.module.css'; import NestedReveal from './NestedReveal.js'; +import NestedParentExit from './NestedParentExit.js'; async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -322,6 +323,7 @@ export default function Page({url, navigate}) { + ); } diff --git a/packages/react-dom/src/__tests__/ReactDOMViewTransition-test.js b/packages/react-dom/src/__tests__/ReactDOMViewTransition-test.js index a44b3f5235ee..bd50ba2db3a3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMViewTransition-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMViewTransition-test.js @@ -461,5 +461,478 @@ describe('ReactDOMViewTransition', () => { onEnter.mock.calls.length + enterCallsAfterFallback, ).toBeGreaterThanOrEqual(1); }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('does not fire onExit/onEnter on nested ViewTransition when the subtree is removed as one unit', async () => { + const onParentExit = jest.fn(); + const onParentEnter = jest.fn(); + const onNestedExit = jest.fn(); + const onNestedEnter = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + +
+ +
Item
+
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentEnter.mockClear(); + onNestedEnter.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentEnter).toHaveBeenCalledTimes(1); + expect(onNestedEnter).not.toHaveBeenCalled(); + + onParentExit.mockClear(); + onNestedExit.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentExit).toHaveBeenCalledTimes(1); + expect(onNestedExit).not.toHaveBeenCalled(); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('fires onParentExit when ancestor ViewTransition exits', async () => { + const onParentExit = jest.fn(); + const onNestedExit = jest.fn(); + const onParentExitNested = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + +
+ +
Item
+
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentExit.mockClear(); + onNestedExit.mockClear(); + onParentExitNested.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentExit).toHaveBeenCalledTimes(1); + expect(onNestedExit).not.toHaveBeenCalled(); + expect(onParentExitNested).toHaveBeenCalledTimes(1); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('fires onParentEnter when ancestor ViewTransition enters', async () => { + const onParentEnter = jest.fn(); + const onNestedEnter = jest.fn(); + const onParentEnterNested = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + +
+ +
Item
+
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentEnter.mockClear(); + onNestedEnter.mockClear(); + onParentEnterNested.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentEnter).toHaveBeenCalledTimes(1); + expect(onNestedEnter).not.toHaveBeenCalled(); + expect(onParentEnterNested).toHaveBeenCalledTimes(1); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('breaks parentExit chain when intermediate ViewTransition lacks parentExit', async () => { + const onParentExit1 = jest.fn(); + const onParentExit2 = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + +
+ +
+ +
+ +
Deep
+
+
+
+
+
+ +
Shallow
+
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentExit1.mockClear(); + onParentExit2.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentExit1).not.toHaveBeenCalled(); + expect(onParentExit2).toHaveBeenCalledTimes(1); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('does not fire onParentEnter when ancestor exits', async () => { + const onParentEnter = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + +
+ + +
Item
+
+
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentEnter.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentEnter).not.toHaveBeenCalled(); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('does not fire onParentExit when ancestor shares instead of exiting', async () => { + const onShare = jest.fn(); + const onParentExit = jest.fn(); + + function App({page}) { + if (page === 'a') { + return ( + + +
Page A
+
+
+ ); + } + return ( + + +
Page B
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onShare.mockClear(); + onParentExit.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onShare).toHaveBeenCalledTimes(1); + expect(onParentExit).not.toHaveBeenCalled(); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('does not fire onParentEnter when ancestor shares instead of entering', async () => { + const onShare = jest.fn(); + const onParentEnter = jest.fn(); + + function App({page}) { + if (page === 'a') { + return ( + + +
Page A
+
+
+ ); + } + return ( + + +
Page B
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onShare.mockClear(); + onParentEnter.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onShare).toHaveBeenCalledTimes(1); + expect(onParentEnter).not.toHaveBeenCalled(); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('does not fire onParentExit when ancestor exit is none', async () => { + const onParentExit = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + + +
Item
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentExit.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentExit).not.toHaveBeenCalled(); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('does not fire onParentEnter when ancestor enter is none', async () => { + const onParentEnter = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + + +
Item
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentEnter.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentEnter).not.toHaveBeenCalled(); + }); + + // @gate enableViewTransition && enableViewTransitionParentEnterExit + it('relays parentExit chain through unstyled parentExit', async () => { + const onParentExit = jest.fn(); + + function App({show}) { + if (!show) { + return null; + } + return ( + +
+ + +
Item
+
+
+
+
+ ); + } + + const root = ReactDOMClient.createRoot(container); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + onParentExit.mockClear(); + + await act(() => { + startTransition(() => { + root.render(); + }); + }); + + expect(onParentExit).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 32e593958457..558ba4e3bad3 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -75,6 +75,7 @@ import { restoreUpdateViewTransitionForGesture, appearingViewTransitions, commitEnterViewTransitions, + commitParentExitViewTransitions, measureNestedViewTransitions, measureUpdateViewTransition, viewTransitionCancelableChildren, @@ -93,6 +94,7 @@ import { import { enableProfilerTimer, enableComponentPerformanceTrack, + enableViewTransitionParentEnterExit, } from 'shared/ReactFeatureFlags'; import {trackAnimatingTask} from './ReactProfilerTimer'; import {scheduleGestureTransitionEvent} from './ReactFiberWorkLoop'; @@ -327,6 +329,9 @@ function applyExitViewTransition(placement: Fiber): void { scheduleGestureTransitionEvent(placement, props.onGestureShare); } else { scheduleGestureTransitionEvent(placement, props.onGestureExit); + if (enableViewTransitionParentEnterExit) { + commitParentExitViewTransitions(placement, true); + } } } } diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 760270010dbc..2e816ca527e0 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -21,6 +21,7 @@ import { NoFlags, Update, ViewTransitionStatic, + ViewTransitionStaticParent, AffectedParentLayout, ViewTransitionNamedStatic, } from './ReactFiberFlags'; @@ -47,6 +48,7 @@ import { enableComponentPerformanceTrack, enableProfilerTimer, enableViewTransitionForPersistenceMode, + enableViewTransitionParentEnterExit, } from 'shared/ReactFeatureFlags'; export let shouldStartViewTransition: boolean = false; @@ -324,6 +326,104 @@ function commitAppearingPairViewTransitions(placement: Fiber): void { } } +export function commitParentEnterViewTransitions( + parent: Fiber, + gesture: boolean, +): void { + let child = parent.child; + while (child !== null) { + if (child.tag === OffscreenComponent && child.memoizedState !== null) { + // Skip hidden subtrees. + } else if (child.tag === ViewTransitionComponent) { + const props: ViewTransitionProps = child.memoizedProps; + if (props.parentEnter !== undefined) { + const state: ViewTransitionState = child.stateNode; + const name = getViewTransitionName(props, state); + const className: ?string = getViewTransitionClassName( + props.default, + props.parentEnter, + ); + if (className !== 'none') { + applyViewTransitionToHostInstances( + child, + name, + className, + null, + false, + ); + if (gesture) { + scheduleGestureTransitionEvent(child, props.onGestureParentEnter); + } else { + scheduleViewTransitionEvent(child, props.onParentEnter); + } + } + commitParentEnterViewTransitions(child, gesture); + } + } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) { + commitParentEnterViewTransitions(child, gesture); + } + child = child.sibling; + } +} + +export function commitParentExitViewTransitions( + parent: Fiber, + gesture: boolean, +): void { + let child = parent.child; + while (child !== null) { + if (child.tag === OffscreenComponent && child.memoizedState !== null) { + // Skip hidden subtrees. + } else if (child.tag === ViewTransitionComponent) { + const props: ViewTransitionProps = child.memoizedProps; + if (props.parentExit !== undefined) { + const state: ViewTransitionState = child.stateNode; + const name = getViewTransitionName(props, state); + const className: ?string = getViewTransitionClassName( + props.default, + props.parentExit, + ); + if (className !== 'none') { + applyViewTransitionToHostInstances( + child, + name, + className, + null, + false, + ); + if (gesture) { + scheduleGestureTransitionEvent(child, props.onGestureParentExit); + } else { + scheduleViewTransitionEvent(child, props.onParentExit); + } + } + commitParentExitViewTransitions(child, gesture); + } + } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) { + commitParentExitViewTransitions(child, gesture); + } + child = child.sibling; + } +} + +function restoreParentEnterOrExitViewTransitions(parent: Fiber): void { + let child = parent.child; + while (child !== null) { + if (child.tag === OffscreenComponent && child.memoizedState !== null) { + // Skip hidden subtrees. + } else if (child.tag === ViewTransitionComponent) { + const props: ViewTransitionProps = child.memoizedProps; + if (props.parentEnter !== undefined || props.parentExit !== undefined) { + restoreViewTransitionOnHostInstances(child.child, false); + restoreParentEnterOrExitViewTransitions(child); + } + } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) { + restoreParentEnterOrExitViewTransitions(child); + } + child = child.sibling; + } +} + export function commitEnterViewTransitions( placement: Fiber, gesture: boolean, @@ -359,6 +459,9 @@ export function commitEnterViewTransitions( } else { scheduleViewTransitionEvent(placement, props.onEnter); } + if (enableViewTransitionParentEnterExit) { + commitParentEnterViewTransitions(placement, gesture); + } } } } else { @@ -487,6 +590,9 @@ export function commitExitViewTransitions(deletion: Fiber): void { scheduleViewTransitionEvent(deletion, props.onShare); } else { scheduleViewTransitionEvent(deletion, props.onExit); + if (enableViewTransitionParentEnterExit) { + commitParentExitViewTransitions(deletion, false); + } } } if (appearingViewTransitions !== null) { @@ -617,6 +723,9 @@ export function restoreEnterOrExitViewTransitions(fiber: Fiber): void { const instance: ViewTransitionState = fiber.stateNode; instance.paired = null; restoreViewTransitionOnHostInstances(fiber.child, false); + if (enableViewTransitionParentEnterExit) { + restoreParentEnterOrExitViewTransitions(fiber); + } restorePairedViewTransitions(fiber); } else if ((fiber.subtreeFlags & ViewTransitionStatic) !== NoFlags) { let child = fiber.child; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 6fc4297e1b33..acb1551b455d 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -40,6 +40,7 @@ import { passChildrenWhenCloningPersistedNodes, disableLegacyMode, enableViewTransition, + enableViewTransitionParentEnterExit, enableSuspenseyImages, } from 'shared/ReactFeatureFlags'; @@ -98,6 +99,7 @@ import { ShouldSuspendCommit, Cloned, ViewTransitionStatic, + ViewTransitionStaticParent, Hydrate, PortalStatic, } from './ReactFiberFlags'; @@ -2060,6 +2062,17 @@ function completeWork( // bubble up to the parent tree to indicate that there's a child that // might need an exit View Transition upon unmount. workInProgress.flags |= ViewTransitionStatic; + if (enableViewTransitionParentEnterExit) { + const props = workInProgress.pendingProps; + if ( + props.parentEnter !== undefined || + props.parentExit !== undefined + ) { + workInProgress.flags |= ViewTransitionStaticParent; + } else { + workInProgress.flags &= ~ViewTransitionStaticParent; + } + } bubbleProperties(workInProgress); } return null; diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index 9f85897fb05c..859ea1fd594b 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -83,6 +83,10 @@ export const ViewTransitionNamedStatic = // ViewTransitionStatic tracks whether there are an ViewTransition components from // the nearest HostComponent down. It resets at every HostComponent level. export const ViewTransitionStatic = /* */ 0b0000010000000000000000000000000; +// ViewTransitionStaticParent tracks whether there are ViewTransition components +// with parentEnter/parentExit props. Unlike ViewTransitionStatic, this is NOT +// cleared by HostComponents so it can be used to skip subtrees in parent walks. +export const ViewTransitionStaticParent = /* */ 0b1000000000000000000000000000000; // Tracks whether a HostPortal is present in the tree. export const PortalStatic = /* */ 0b0000100000000000000000000000000; @@ -140,6 +144,7 @@ export const StaticMask = RefStatic | MaySuspendCommit | ViewTransitionStatic | + ViewTransitionStaticParent | ViewTransitionNamedStatic | PortalStatic | Forked; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 5d2d6efaa7df..d6c3bf25a47c 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -80,6 +80,8 @@ export const enableTaint = __EXPERIMENTAL__; export const enableViewTransition: boolean = true; +export const enableViewTransitionParentEnterExit = __EXPERIMENTAL__; + export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition = __EXPERIMENTAL__; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 032d70c3ce36..22f6d42421f5 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -313,6 +313,8 @@ export type ViewTransitionProps = { exit?: ViewTransitionClass, share?: ViewTransitionClass, update?: ViewTransitionClass, + parentEnter?: ViewTransitionClass, + parentExit?: ViewTransitionClass, onEnter?: ( instance: ViewTransitionInstance, types: Array, @@ -321,6 +323,14 @@ export type ViewTransitionProps = { instance: ViewTransitionInstance, types: Array, ) => void | (() => void), + onParentEnter?: ( + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onParentExit?: ( + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), onShare?: ( instance: ViewTransitionInstance, types: Array, @@ -341,6 +351,18 @@ export type ViewTransitionProps = { instance: ViewTransitionInstance, types: Array, ) => void | (() => void), + onGestureParentEnter?: ( + timeline: GestureProvider, + options: GestureOptionsRequired, + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onGestureParentExit?: ( + timeline: GestureProvider, + options: GestureOptionsRequired, + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), onGestureShare?: ( timeline: GestureProvider, options: GestureOptionsRequired, diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 96d13e9ec461..a8a357265d2a 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -71,6 +71,7 @@ export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionParentEnterExit: boolean = true; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 3c452162ab98..19cba62427ba 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -60,6 +60,7 @@ export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionParentEnterExit: boolean = false; export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 309de96b4951..9dcfe483c7f7 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -61,6 +61,7 @@ export const enableYieldingBeforePassive: boolean = true; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionParentEnterExit = __EXPERIMENTAL__; export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index af8dd955d0ba..622255565463 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -56,6 +56,7 @@ export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive = false; export const enableThrottledScheduling = false; export const enableViewTransition = true; +export const enableViewTransitionParentEnterExit = false; export const enableViewTransitionForPersistenceMode = false; export const enableGestureTransition = false; export const enableScrollEndPolyfill = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index e57495ed4e53..2a737242d49d 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -67,6 +67,7 @@ export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionParentEnterExit = __EXPERIMENTAL__; export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index d3340f6eb940..7b3ce7f31cf8 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -32,6 +32,7 @@ export const enableInfiniteRenderLoopDetectionForceThrow: boolean = __VARIANT__; export const enableFastAddPropertiesInDiffing: boolean = __VARIANT__; export const enableSuspenseyImages: boolean = __VARIANT__; export const enableViewTransition: boolean = __VARIANT__; +export const enableViewTransitionParentEnterExit: boolean = __VARIANT__; export const enableScrollEndPolyfill: boolean = __VARIANT__; export const enableFragmentRefs: boolean = __VARIANT__; export const enableFragmentRefsScrollIntoView: boolean = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 062cbaa4268a..d080724d4672 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -100,6 +100,7 @@ export const disableLegacyMode: boolean = true; export const enableEagerAlternateStateNodeCleanup: boolean = true; +export const enableViewTransitionParentEnterExit: boolean = false; export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false;