main
js 347 lines 9.62 KB
Raw
1 import React, {
2 addTransitionType,
3 ViewTransition,
4 Activity,
5 useLayoutEffect,
6 useEffect,
7 useInsertionEffect,
8 useState,
9 useId,
10 useOptimistic,
11 startTransition,
12 Suspense,
13 } from 'react';
14
15 import {createPortal} from 'react-dom';
16
17 import SwipeRecognizer from './SwipeRecognizer.js';
18
19 import './Page.css';
20
21 import transitions from './Transitions.module.css';
22 import NestedReveal from './NestedReveal.js';
23 import NestedParentExit from './NestedParentExit.js';
24
25 async function sleep(ms) {
26 return new Promise(resolve => setTimeout(resolve, ms));
27 }
28
29 const a = (
30 <div key="a">
31 <ViewTransition>
32 <div>a</div>
33 </ViewTransition>
34 </div>
35 );
36
37 const b = (
38 <div key="b">
39 <ViewTransition>
40 <div>b</div>
41 </ViewTransition>
42 </div>
43 );
44
45 function ExampleCard({title, children}) {
46 return (
47 <div className="example-card">
48 {title ? <h2 className="example-card-title">{title}</h2> : null}
49 <div className="example-card-content">{children}</div>
50 </div>
51 );
52 }
53
54 function Component() {
55 // Test inserting fonts with style tags using useInsertionEffect. This is not recommended but
56 // used to test that gestures etc works with useInsertionEffect so that stylesheet based
57 // libraries can be properly supported.
58 useInsertionEffect(() => {
59 const style = document.createElement('style');
60 style.textContent = `
61 .roboto-font {
62 font-family: "Roboto", serif;
63 font-optical-sizing: auto;
64 font-weight: 100;
65 font-style: normal;
66 font-variation-settings:
67 "wdth" 100;
68 }
69 `;
70 document.head.appendChild(style);
71 return () => {
72 document.head.removeChild(style);
73 };
74 }, []);
75 return (
76 <ViewTransition
77 default={
78 transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
79 }>
80 <p className="roboto-font">Slide In from Left, Slide Out to Right</p>
81 <p>
82 <img
83 src="https://react.dev/_next/image?url=%2Fimages%2Fteam%2Fsebmarkbage.jpg&w=3840&q=75"
84 width="400"
85 height="248"
86 />
87 </p>
88 </ViewTransition>
89 );
90 }
91
92 function Id() {
93 // This is just testing that Id inside a ViewTransition can hydrate correctly.
94 return <span id={useId()} />;
95 }
96
97 let wait;
98 export function resetPageReveal() {
99 wait = undefined;
100 }
101 function Suspend() {
102 if (!wait) wait = sleep(500);
103 return React.use(wait);
104 }
105
106 export default function Page({url, navigate}) {
107 const [renderedUrl, optimisticNavigate] = useOptimistic(
108 url,
109 (state, direction) => {
110 return direction === 'left' ? '/?a' : '/?b';
111 }
112 );
113 const show = renderedUrl === '/?b';
114 function onTransition(viewTransition, types) {
115 const keyframes = [
116 {rotate: '0deg', transformOrigin: '30px 8px'},
117 {rotate: '360deg', transformOrigin: '30px 8px'},
118 ];
119 const animation1 = viewTransition.old.animate(keyframes, 250);
120 const animation2 = viewTransition.new.animate(keyframes, 250);
121 return () => {
122 animation1.cancel();
123 animation2.cancel();
124 };
125 }
126
127 function onGestureTransition(
128 timeline,
129 {rangeStart, rangeEnd},
130 viewTransition,
131 types
132 ) {
133 const keyframes = [
134 {rotate: '0deg', transformOrigin: '30px 8px'},
135 {rotate: '360deg', transformOrigin: '30px 8px'},
136 ];
137 const reverse = rangeStart > rangeEnd;
138 if (timeline instanceof AnimationTimeline) {
139 // Native Timeline
140 const options = {
141 timeline: timeline,
142 direction: reverse ? 'normal' : 'reverse',
143 rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
144 rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
145 };
146 const animation1 = viewTransition.old.animate(keyframes, options);
147 const animation2 = viewTransition.new.animate(keyframes, options);
148 return () => {
149 animation1.cancel();
150 animation2.cancel();
151 };
152 } else {
153 // Custom Timeline
154 const options = {
155 direction: reverse ? 'normal' : 'reverse',
156 // We set the delay and duration to represent the span of the range.
157 delay: reverse ? rangeEnd : rangeStart,
158 duration: reverse ? rangeStart - rangeEnd : rangeEnd - rangeStart,
159 };
160 const animation1 = viewTransition.old.animate(keyframes, options);
161 const animation2 = viewTransition.new.animate(keyframes, options);
162 // Let the custom timeline take control of driving the animations.
163 const cleanup1 = timeline.animate(animation1);
164 const cleanup2 = timeline.animate(animation2);
165 return () => {
166 animation1.cancel();
167 animation2.cancel();
168 cleanup1();
169 cleanup2();
170 };
171 }
172 }
173
174 function swipeAction() {
175 navigate(show ? '/?a' : '/?b');
176 }
177
178 const [counter, setCounter] = useState(0);
179
180 useEffect(() => {
181 const timer = setInterval(() => setCounter(c => c + 1), 1000);
182 return () => clearInterval(timer);
183 }, []);
184
185 useLayoutEffect(() => {
186 // Calling a default update should not interrupt ViewTransitions but
187 // a flushSync will.
188 // Promise.resolve().then(() => {
189 // flushSync(() => {
190 // setCounter(c => c + 10);
191 // });
192 // });
193 }, [show]);
194
195 const [showModal, setShowModal] = useState(false);
196 const portal = showModal ? (
197 createPortal(
198 <div className="portal">
199 Portal: {!show ? 'A' : 'B'}
200 <ViewTransition>
201 <div>{!show ? 'A' : 'B'}</div>
202 </ViewTransition>
203 </div>,
204 document.body
205 )
206 ) : (
207 <button
208 onClick={() =>
209 startTransition(async () => {
210 await sleep(2000);
211 setShowModal(true);
212 })
213 }>
214 Show Modal
215 </button>
216 );
217
218 const exclamation = (
219 <ViewTransition
220 name="exclamation"
221 onShare={onTransition}
222 onGestureShare={onGestureTransition}>
223 <span>
224 <div>!</div>
225 </span>
226 </ViewTransition>
227 );
228 return (
229 <div className="examples">
230 <ExampleCard title="Navigation & Gestures">
231 <SwipeRecognizer
232 action={swipeAction}
233 gesture={direction => {
234 addTransitionType(
235 direction === 'left' ? 'navigation-forward' : 'navigation-back'
236 );
237 optimisticNavigate(direction);
238 }}
239 direction={show ? 'left' : 'right'}>
240 <button
241 className="button"
242 onClick={() => {
243 navigate(url === '/?b' ? '/?a' : '/?b');
244 }}>
245 {url === '/?b' ? 'Goto A' : 'Goto B'}
246 </button>
247 <ViewTransition default="none">
248 <div>
249 <ViewTransition>
250 <div>
251 <ViewTransition default={transitions['slide-on-nav']}>
252 <h1>{!show ? 'A' : 'B' + counter}</h1>
253 </ViewTransition>
254 </div>
255 </ViewTransition>
256 <ViewTransition
257 default={{
258 'navigation-back': transitions['slide-right'],
259 'navigation-forward': transitions['slide-left'],
260 }}>
261 <h1>{!show ? 'A' + counter : 'B'}</h1>
262 </ViewTransition>
263 {
264 // Using url instead of renderedUrl here lets us only update this on commit.
265 url === '/?b' ? (
266 <div>
267 {a}
268 {b}
269 </div>
270 ) : (
271 <div>
272 {b}
273 {a}
274 </div>
275 )
276 }
277 <ViewTransition>
278 {show ? (
279 <div>hello{exclamation}</div>
280 ) : (
281 <section>Loading</section>
282 )}
283 </ViewTransition>
284 <p>
285 <Id />
286 </p>
287 {show ? null : (
288 <ViewTransition>
289 <div>world{exclamation}</div>
290 </ViewTransition>
291 )}
292 <Activity mode={show ? 'visible' : 'hidden'}>
293 <ViewTransition>
294 <div>!!</div>
295 </ViewTransition>
296 </Activity>
297 <Suspense
298 fallback={
299 <ViewTransition>
300 <div>
301 <ViewTransition name="shared-reveal">
302 <h2>█████</h2>
303 </ViewTransition>
304 <p>████</p>
305 <p>███████</p>
306 <p>████</p>
307 <p>██</p>
308 <p>██████</p>
309 <p>███</p>
310 <p>████</p>
311 </div>
312 </ViewTransition>
313 }>
314 <ViewTransition>
315 <div>
316 <p>these</p>
317 <p>rows</p>
318 <ViewTransition name="shared-reveal">
319 <h2>exist</h2>
320 </ViewTransition>
321 <p>to</p>
322 <p>test</p>
323 <p>scrolling</p>
324 <p>content</p>
325 <p>out</p>
326 <p>of</p>
327 {portal}
328 <p>the</p>
329 <p>viewport</p>
330 <Suspend />
331 </div>
332 </ViewTransition>
333 {show ? <Component /> : null}
334 </Suspense>
335 </div>
336 </ViewTransition>
337 </SwipeRecognizer>
338 </ExampleCard>
339 <ExampleCard title="Nested Suspense Reveal">
340 <NestedReveal />
341 </ExampleCard>
342 <ExampleCard title="Parent Enter / Exit (SSR)">
343 <NestedParentExit />
344 </ExampleCard>
345 </div>
346 );
347 }