main
js 230 lines 7.22 KB
Raw
1 import React, {
2 ViewTransition,
3 Suspense,
4 use,
5 useState,
6 useOptimistic,
7 startTransition,
8 addTransitionType,
9 } from 'react';
10 import SwipeRecognizer from './SwipeRecognizer.js';
11 import './NestedParentExit.css';
12
13 const items = [
14 {id: 1, title: 'First Post', body: 'Hello from the first post.'},
15 {id: 2, title: 'Second Post', body: 'Hello from the second post.'},
16 {id: 3, title: 'Third Post', body: 'Hello from the third post.'},
17 ];
18
19 let feedRevealPromise = null;
20
21 export function resetFeedReveal() {
22 feedRevealPromise = null;
23 }
24
25 function FeedReveal() {
26 if (feedRevealPromise === null) {
27 feedRevealPromise = new Promise(resolve => setTimeout(resolve, 1000));
28 }
29 use(feedRevealPromise);
30 return null;
31 }
32
33 function FeedSkeleton() {
34 return (
35 <ViewTransition exit="nested-feed-exit">
36 <div className="nested-feed">
37 {items.map(item => (
38 // Mirrors FeedItem: each skeleton row relays the exit (level 1), and
39 // its title/body placeholders relay again (level 2) so they animate
40 // out separately from the row — up and to the right. This exercises
41 // the nested parentExit relay through the SSR streaming reveal.
42 <ViewTransition key={item.id} parentExit="nested-exit-left">
43 <div className="feed-item feed-item-skeleton">
44 <ViewTransition parentExit="nested-title-exit-up">
45 <div className="feed-item-title skeleton-line skeleton-title" />
46 </ViewTransition>
47 <ViewTransition parentExit="nested-body-exit-right">
48 <div className="skeleton-line skeleton-body" />
49 </ViewTransition>
50 </div>
51 </ViewTransition>
52 ))}
53 </div>
54 </ViewTransition>
55 );
56 }
57
58 function logGestureParent(kind, title, _timeline, _options, _instance, types) {
59 // eslint-disable-next-line no-console
60 console.log(`[NestedParentExit] onGestureParent${kind}`, title, types);
61 }
62
63 function FeedItem({item, index, activeIndex, onSelect}) {
64 const isActive = activeIndex === index;
65
66 return (
67 <ViewTransition
68 name={'nested-post-' + item.id}
69 share={{
70 'nav-forward': 'nested-shared-post-forward',
71 'nav-back': 'nested-shared-post-back',
72 }}
73 parentExit={isActive ? undefined : 'nested-exit-left'}
74 parentEnter={isActive ? undefined : 'nested-enter-from-left'}
75 onGestureParentExit={(...args) =>
76 logGestureParent('Exit', item.title, ...args)
77 }
78 onGestureParentEnter={(...args) =>
79 logGestureParent('Enter', item.title, ...args)
80 }>
81 <div className="feed-item" onClick={() => onSelect(item, index)}>
82 <ViewTransition
83 name={'nested-title-' + item.id}
84 share={{
85 'nav-forward': 'nested-shared-inner-forward',
86 'nav-back': 'nested-shared-inner-back',
87 }}
88 parentExit={isActive ? undefined : 'nested-title-exit-up'}
89 parentEnter={isActive ? undefined : 'nested-title-enter-from-up'}>
90 <div className="feed-item-title">{item.title}</div>
91 </ViewTransition>
92 <ViewTransition
93 parentExit={isActive ? undefined : 'nested-body-exit-right'}
94 parentEnter={isActive ? undefined : 'nested-body-enter-from-right'}>
95 <p>{item.body}</p>
96 </ViewTransition>
97 </div>
98 </ViewTransition>
99 );
100 }
101
102 function Detail({item, onBack}) {
103 return (
104 <ViewTransition
105 name={'nested-post-' + item.id}
106 share={{
107 'nav-forward': 'nested-shared-post-forward',
108 'nav-back': 'nested-shared-post-back',
109 }}>
110 <div className="detail-view">
111 <ViewTransition
112 enter="nested-back-btn-enter"
113 exit="nested-back-btn-exit">
114 <button className="back-button" onClick={onBack}>
115 Back
116 </button>
117 </ViewTransition>
118 <ViewTransition
119 name={'nested-title-' + item.id}
120 share={{
121 'nav-forward': 'nested-shared-inner-forward',
122 'nav-back': 'nested-shared-inner-back',
123 }}>
124 <div className="feed-item-title">{item.title}</div>
125 </ViewTransition>
126 <p>{item.body}</p>
127 </div>
128 </ViewTransition>
129 );
130 }
131
132 const initialNav = {selected: null, activeIndex: null};
133
134 export default function NestedParentExit() {
135 const [nav, setNav] = useState(initialNav);
136 const [optimisticNav, navigateByGesture] = useOptimistic(
137 nav,
138 (state, direction) => {
139 if (direction === 'left' && state.selected === null) {
140 return {selected: items[0], activeIndex: 0};
141 }
142 if (direction === 'right' && state.selected !== null) {
143 return {
144 selected: null,
145 activeIndex:
146 state.activeIndex ??
147 items.findIndex(i => i.id === state.selected.id),
148 };
149 }
150 return state;
151 }
152 );
153
154 const {selected, activeIndex} = optimisticNav;
155
156 function goToDetail(item, index) {
157 startTransition(() => {
158 addTransitionType('nav-forward');
159 setNav({selected: item, activeIndex: index});
160 });
161 }
162
163 function goBack() {
164 const current = selected;
165 if (current == null) {
166 return;
167 }
168 const backIndex = items.findIndex(i => i.id === current.id);
169 startTransition(() => {
170 addTransitionType('nav-back');
171 setNav({selected: null, activeIndex: backIndex});
172 });
173 }
174
175 function swipeAction() {
176 if (nav.selected === null) {
177 goToDetail(items[0], 0);
178 } else {
179 goBack();
180 }
181 }
182
183 return (
184 <div className="nested-parent-exit">
185 <p className="nested-parent-exit-label">
186 Parent Exit/Enter click a post or swipe (scroll the strip below)
187 </p>
188 <div className="nested-parent-exit-swipe swipe-recognizer">
189 <SwipeRecognizer
190 action={swipeAction}
191 gesture={direction => {
192 addTransitionType(
193 direction === 'left' ? 'nav-forward' : 'nav-back'
194 );
195 navigateByGesture(direction);
196 }}
197 direction={selected ? 'right' : 'left'}>
198 <ViewTransition key={selected ? 'detail' : 'feed'} update="none">
199 <div className="nested-parent-exit-panel">
200 {selected ? (
201 <Detail item={selected} onBack={goBack} />
202 ) : (
203 <Suspense fallback={<FeedSkeleton />}>
204 {/* The entering ViewTransition is the direct child of the
205 Suspense content, so it activates an enter scope and the
206 feed items below relay parentEnter (emitting
207 vt-parent-enter annotations in Fizz). */}
208 <ViewTransition enter="nested-feed-enter">
209 <div className="nested-feed">
210 {items.map((item, index) => (
211 <FeedItem
212 key={item.id}
213 item={item}
214 index={index}
215 activeIndex={activeIndex}
216 onSelect={goToDetail}
217 />
218 ))}
219 <FeedReveal />
220 </div>
221 </ViewTransition>
222 </Suspense>
223 )}
224 </div>
225 </ViewTransition>
226 </SwipeRecognizer>
227 </div>
228 </div>
229 );
230 }