Skip to content

Commit 8ce15b0

Browse files
authored
[Fizz] Apply View Transition Name and Class to SSR:ed View Transitions (facebook#33332)
Stacked on facebook#33330. This walks the element tree to activate the various classes under different scenarios. There are some edge case things that are a little different since we can't express every scenario without virtual nodes. The main thing that's still missing though is avoiding animating updates if it can be contained to a layout or enter/exit/share if they're out of the viewport. I.e. layout stuff.
1 parent 91ac1fe commit 8ce15b0

File tree

4 files changed

+204
-19
lines changed

4 files changed

+204
-19
lines changed

fixtures/view-transition/server/render.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ export default function render(url, res) {
2323
const {pipe, abort} = renderToPipeableStream(
2424
<App assets={assets} initialURL={url} />,
2525
{
26-
// TODO: Temporary hack. Detect from attributes instead.
27-
bootstrapScriptContent: 'window._useVT = true;',
2826
bootstrapScripts: [assets['main.js']],
2927
onShellReady() {
3028
// If something errored before we started streaming, we set the error code appropriately.

fixtures/view-transition/src/components/Page.js

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,21 +200,41 @@ export default function Page({url, navigate}) {
200200
<div>!!</div>
201201
</ViewTransition>
202202
</Activity>
203-
<Suspense fallback="Loading">
203+
<Suspense
204+
fallback={
205+
<ViewTransition>
206+
<div>
207+
<ViewTransition name="shared-reveal">
208+
<h2>█████</h2>
209+
</ViewTransition>
210+
<p>████</p>
211+
<p>███████</p>
212+
<p>████</p>
213+
<p>██</p>
214+
<p>██████</p>
215+
<p>███</p>
216+
<p>████</p>
217+
</div>
218+
</ViewTransition>
219+
}>
204220
<ViewTransition>
205-
<p>these</p>
206-
<p>rows</p>
207-
<p>exist</p>
208-
<p>to</p>
209-
<p>test</p>
210-
<p>scrolling</p>
211-
<p>content</p>
212-
<p>out</p>
213-
<p>of</p>
214-
{portal}
215-
<p>the</p>
216-
<p>viewport</p>
217-
<Suspend />
221+
<div>
222+
<p>these</p>
223+
<p>rows</p>
224+
<ViewTransition name="shared-reveal">
225+
<h2>exist</h2>
226+
</ViewTransition>
227+
<p>to</p>
228+
<p>test</p>
229+
<p>scrolling</p>
230+
<p>content</p>
231+
<p>out</p>
232+
<p>of</p>
233+
{portal}
234+
<p>the</p>
235+
<p>viewport</p>
236+
<Suspend />
237+
</div>
218238
</ViewTransition>
219239
</Suspense>
220240
{show ? <Component /> : null}

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Shared implementation and constants between the inline script and external
44
// runtime instruction sets.
55

6+
const ELEMENT_NODE = 1;
67
const COMMENT_NODE = 8;
78
const ACTIVITY_START_DATA = '&';
89
const ACTIVITY_END_DATA = '/&';
@@ -84,14 +85,168 @@ export function revealCompletedBoundariesWithViewTransitions(
8485
revealBoundaries,
8586
batch,
8687
) {
88+
let shouldStartViewTransition = false;
89+
let autoNameIdx = 0;
90+
const restoreQueue = [];
91+
function applyViewTransitionName(element, classAttributeName) {
92+
const className = element.getAttribute(classAttributeName);
93+
if (!className) {
94+
return;
95+
}
96+
// Add any elements we apply a name to a queue to be reverted when we start.
97+
const elementStyle = element.style;
98+
restoreQueue.push(
99+
element,
100+
elementStyle['viewTransitionName'],
101+
elementStyle['viewTransitionClass'],
102+
);
103+
if (className !== 'auto') {
104+
elementStyle['viewTransitionClass'] = className;
105+
}
106+
let name = element.getAttribute('vt-name');
107+
if (!name) {
108+
// Auto-generate a name for this one.
109+
// TODO: We don't have a prefix to pick from here but maybe we don't need it
110+
// since it's only applicable temporarily during this specific animation.
111+
const idPrefix = '';
112+
name = '\u00AB' + idPrefix + 'T' + autoNameIdx++ + '\u00BB';
113+
}
114+
elementStyle['viewTransitionName'] = name;
115+
shouldStartViewTransition = true;
116+
}
87117
try {
88118
const existingTransition = document['__reactViewTransition'];
89119
if (existingTransition) {
90120
// Retry after the previous ViewTransition finishes.
91121
existingTransition.finished.finally(window['$RV'].bind(null, batch));
92122
return;
93123
}
94-
const shouldStartViewTransition = window['_useVT']; // TODO: Detect.
124+
// First collect all entering names that might form pairs exiting names.
125+
const appearingViewTransitions = new Map();
126+
for (let i = 1; i < batch.length; i += 2) {
127+
const contentNode = batch[i];
128+
const appearingElements = contentNode.querySelectorAll('[vt-share]');
129+
for (let j = 0; j < appearingElements.length; j++) {
130+
const appearingElement = appearingElements[j];
131+
appearingViewTransitions.set(
132+
appearingElement.getAttribute('vt-name'),
133+
appearingElement,
134+
);
135+
}
136+
}
137+
// Next we'll find the nodes that we're going to animate and apply names to them..
138+
for (let i = 0; i < batch.length; i += 2) {
139+
const suspenseIdNode = batch[i];
140+
const parentInstance = suspenseIdNode.parentNode;
141+
if (!parentInstance) {
142+
// We may have client-rendered this boundary already. Skip it.
143+
continue;
144+
}
145+
const parentRect = parentInstance.getBoundingClientRect();
146+
if (
147+
!parentRect.left &&
148+
!parentRect.top &&
149+
!parentRect.width &&
150+
!parentRect.height
151+
) {
152+
// If the parent instance is display: none then we don't animate this boundary.
153+
// This can happen when this boundary is actually a child of a different boundary that
154+
// isn't yet revealed or is about to be revealed, but in that case that boundary
155+
// should do the exit/enter and not this one. Conveniently this also lets us skip
156+
// this if it's just in a hidden tree in general.
157+
// TODO: Should we skip it if it's out of viewport? It's possible that it gets
158+
// brought into the viewport by changing size.
159+
// TODO: There's a another case where an inner boundary is inside a fallback that
160+
// is about to be deleted. In that case we should not run exit animations on the inner.
161+
continue;
162+
}
163+
164+
// Apply exit animations to the immediate elements inside the fallback.
165+
let node = suspenseIdNode;
166+
let depth = 0;
167+
while (node) {
168+
if (node.nodeType === COMMENT_NODE) {
169+
const data = node.data;
170+
if (data === SUSPENSE_END_DATA) {
171+
if (depth === 0) {
172+
break;
173+
} else {
174+
depth--;
175+
}
176+
} else if (
177+
data === SUSPENSE_START_DATA ||
178+
data === SUSPENSE_PENDING_START_DATA ||
179+
data === SUSPENSE_QUEUED_START_DATA ||
180+
data === SUSPENSE_FALLBACK_START_DATA
181+
) {
182+
depth++;
183+
}
184+
} else if (node.nodeType === ELEMENT_NODE) {
185+
const exitElement = node;
186+
const exitName = exitElement.getAttribute('vt-name');
187+
const pairedElement = appearingViewTransitions.get(exitName);
188+
applyViewTransitionName(
189+
exitElement,
190+
pairedElement ? 'vt-share' : 'vt-exit',
191+
);
192+
if (pairedElement) {
193+
// Activate the other side as well.
194+
applyViewTransitionName(pairedElement, 'vt-share');
195+
appearingViewTransitions.set(exitName, null); // mark claimed
196+
}
197+
// Next we'll look inside this element for pairs to trigger "share".
198+
const disappearingElements =
199+
exitElement.querySelectorAll('[vt-share]');
200+
for (let j = 0; j < disappearingElements.length; j++) {
201+
const disappearingElement = disappearingElements[j];
202+
const name = disappearingElement.getAttribute('vt-name');
203+
const appearingElement = appearingViewTransitions.get(name);
204+
if (appearingElement) {
205+
applyViewTransitionName(disappearingElement, 'vt-share');
206+
applyViewTransitionName(appearingElement, 'vt-share');
207+
appearingViewTransitions.set(name, null); // mark claimed
208+
}
209+
}
210+
}
211+
node = node.nextSibling;
212+
}
213+
214+
// Apply enter animations to the new nodes about to be inserted.
215+
const contentNode = batch[i + 1];
216+
let enterElement = contentNode.firstElementChild;
217+
while (enterElement) {
218+
const paired =
219+
appearingViewTransitions.get(enterElement.getAttribute('vt-name')) ===
220+
null;
221+
if (!paired) {
222+
applyViewTransitionName(enterElement, 'vt-enter');
223+
}
224+
enterElement = enterElement.nextElementSibling;
225+
}
226+
227+
// Apply update animations to any parents and siblings that might be affected.
228+
let ancestorElement = parentInstance;
229+
do {
230+
let childElement = ancestorElement.firstElementChild;
231+
while (childElement) {
232+
// TODO: Bail out if we can
233+
const updateClassName = childElement.getAttribute('vt-update');
234+
if (
235+
updateClassName &&
236+
updateClassName !== 'none' &&
237+
!restoreQueue.includes(childElement)
238+
) {
239+
// If we have already handled this element as part of another exit/enter/share, don't override.
240+
applyViewTransitionName(childElement, 'vt-update');
241+
}
242+
childElement = childElement.nextElementSibling;
243+
}
244+
} while (
245+
(ancestorElement = ancestorElement.parentNode) &&
246+
ancestorElement.nodeType === ELEMENT_NODE &&
247+
ancestorElement.getAttribute('vt-update') !== 'none'
248+
);
249+
}
95250
if (shouldStartViewTransition) {
96251
const transition = (document['__reactViewTransition'] = document[
97252
'startViewTransition'
@@ -100,7 +255,19 @@ export function revealCompletedBoundariesWithViewTransitions(
100255
types: [], // TODO: Add a hard coded type for Suspense reveals.
101256
}));
102257
transition.ready.finally(() => {
103-
// TODO
258+
// Restore all the names/classes that we applied to what they were before.
259+
// We do it in reverse order in case there were duplicates so the first one wins.
260+
for (let i = restoreQueue.length - 3; i >= 0; i -= 3) {
261+
const element = restoreQueue[i];
262+
const elementStyle = element.style;
263+
const previousName = restoreQueue[i + 1];
264+
elementStyle['viewTransitionName'] = previousName;
265+
const previousClassName = restoreQueue[i + 1];
266+
elementStyle['viewTransitionClass'] = previousClassName;
267+
if (element.getAttribute('style') === '') {
268+
element.removeAttribute('style');
269+
}
270+
}
104271
});
105272
transition.finished.finally(() => {
106273
if (document['__reactViewTransition'] === transition) {

0 commit comments

Comments
 (0)