import React, { ViewTransition, Suspense, use, 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.'}, ]; let feedRevealPromise = null; export function resetFeedReveal() { feedRevealPromise = null; } function FeedReveal() { if (feedRevealPromise === null) { feedRevealPromise = new Promise(resolve => setTimeout(resolve, 1000)); } use(feedRevealPromise); return null; } function FeedSkeleton() { return (
{items.map(item => ( // Mirrors FeedItem: each skeleton row relays the exit (level 1), and // its title/body placeholders relay again (level 2) so they animate // out separately from the row — up and to the right. This exercises // the nested parentExit relay through the SSR streaming reveal.
))}
); } 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) { startTransition(() => { addTransitionType('nav-forward'); setNav({selected: item, activeIndex: index}); }); } function goBack() { const current = selected; if (current == null) { return; } const backIndex = items.findIndex(i => i.id === current.id); startTransition(() => { addTransitionType('nav-back'); setNav({selected: null, activeIndex: backIndex}); }); } 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 ? ( ) : ( }> {/* The entering ViewTransition is the direct child of the Suspense content, so it activates an enter scope and the feed items below relay parentEnter (emitting vt-parent-enter annotations in Fizz). */}
{items.map((item, index) => ( ))}
)}
); }