mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[Fizz] Apply View Transition Name and Class to SSR:ed View Transitions (#33332)
Stacked on #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.
This commit is contained in:
committed by
GitHub
parent
5d49b79c99
commit
c8b7499df9
@@ -23,8 +23,6 @@ export default function render(url, res) {
|
||||
const {pipe, abort} = renderToPipeableStream(
|
||||
<App assets={assets} initialURL={url} />,
|
||||
{
|
||||
// TODO: Temporary hack. Detect from attributes instead.
|
||||
bootstrapScriptContent: 'window._useVT = true;',
|
||||
bootstrapScripts: [assets['main.js']],
|
||||
onShellReady() {
|
||||
// If something errored before we started streaming, we set the error code appropriately.
|
||||
|
||||
@@ -200,21 +200,41 @@ export default function Page({url, navigate}) {
|
||||
<div>!!</div>
|
||||
</ViewTransition>
|
||||
</Activity>
|
||||
<Suspense fallback="Loading">
|
||||
<Suspense
|
||||
fallback={
|
||||
<ViewTransition>
|
||||
<div>
|
||||
<ViewTransition name="shared-reveal">
|
||||
<h2>█████</h2>
|
||||
</ViewTransition>
|
||||
<p>████</p>
|
||||
<p>███████</p>
|
||||
<p>████</p>
|
||||
<p>██</p>
|
||||
<p>██████</p>
|
||||
<p>███</p>
|
||||
<p>████</p>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
}>
|
||||
<ViewTransition>
|
||||
<p>these</p>
|
||||
<p>rows</p>
|
||||
<p>exist</p>
|
||||
<p>to</p>
|
||||
<p>test</p>
|
||||
<p>scrolling</p>
|
||||
<p>content</p>
|
||||
<p>out</p>
|
||||
<p>of</p>
|
||||
{portal}
|
||||
<p>the</p>
|
||||
<p>viewport</p>
|
||||
<Suspend />
|
||||
<div>
|
||||
<p>these</p>
|
||||
<p>rows</p>
|
||||
<ViewTransition name="shared-reveal">
|
||||
<h2>exist</h2>
|
||||
</ViewTransition>
|
||||
<p>to</p>
|
||||
<p>test</p>
|
||||
<p>scrolling</p>
|
||||
<p>content</p>
|
||||
<p>out</p>
|
||||
<p>of</p>
|
||||
{portal}
|
||||
<p>the</p>
|
||||
<p>viewport</p>
|
||||
<Suspend />
|
||||
</div>
|
||||
</ViewTransition>
|
||||
</Suspense>
|
||||
{show ? <Component /> : null}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ export const clientRenderBoundary =
|
||||
export const completeBoundary =
|
||||
'$RB=[];$RV=function(c){$RT=performance.now();for(var a=0;a<c.length;a+=2){var b=c[a],h=c[a+1],e=b.parentNode;if(e){var f=b.previousSibling,g=0;do{if(b&&8===b.nodeType){var d=b.data;if("/$"===d||"/&"===d)if(0===g)break;else g--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||g++}d=b.nextSibling;e.removeChild(b);b=d}while(b);for(;h.firstChild;)e.insertBefore(h.firstChild,b);f.data="$";f._reactRetry&&f._reactRetry()}}c.length=0};$RC=function(c,a){if(a=document.getElementById(a))if(a.parentNode.removeChild(a),c=document.getElementById(c))c.previousSibling.data="$~",$RB.push(c,a),2===$RB.length&&setTimeout($RV.bind(null,$RB),("number"!==typeof $RT?0:$RT)+300-performance.now())};';
|
||||
export const completeBoundaryUpgradeToViewTransitions =
|
||||
'$RV=function(c,a){try{var d=document.__reactViewTransition;if(d){d.finished.finally($RV.bind(null,a));return}if(window._useVT){var b=document.__reactViewTransition=document.startViewTransition({update:c.bind(null,a),types:[]});b.ready.finally(function(){});b.finished.finally(function(){document.__reactViewTransition===b&&(document.__reactViewTransition=null)});$RB=[];return}}catch(e){}c(a)}.bind(null,$RV);';
|
||||
'$RV=function(w,f){function h(b,d){var k=b.getAttribute(d);k&&(d=b.style,l.push(b,d.viewTransitionName,d.viewTransitionClass),"auto"!==k&&(d.viewTransitionClass=k),(b=b.getAttribute("vt-name"))||(b="\\u00abT"+F++ +"\\u00bb"),d.viewTransitionName=b,x=!0)}var x=!1,F=0,l=[];try{var e=document.__reactViewTransition;if(e){e.finished.finally($RV.bind(null,f));return}var m=new Map;for(e=1;e<f.length;e+=2)for(var g=f[e].querySelectorAll("[vt-share]"),c=0;c<g.length;c++){var a=g[c];m.set(a.getAttribute("vt-name"),a)}for(g=0;g<f.length;g+=2){var y=f[g],t=y.parentNode;if(t){var r=t.getBoundingClientRect();if(r.left||r.top||r.width||r.height){a=y;for(e=0;a;){if(8===a.nodeType){var p=a.data;if("/$"===p)if(0===e)break;else e--;else"$"!==p&&"$?"!==p&&"$~"!==p&&"$!"!==p||e++}else if(1===a.nodeType){c=a;var z=c.getAttribute("vt-name"),u=m.get(z);h(c,u?"vt-share":"vt-exit");u&&(h(u,"vt-share"),m.set(z,null));var A=c.querySelectorAll("[vt-share]");for(c=0;c<A.length;c++){var B=A[c],C=B.getAttribute("vt-name"),D=m.get(C);\nD&&(h(B,"vt-share"),h(D,"vt-share"),m.set(C,null))}}a=a.nextSibling}for(var q=f[g+1].firstElementChild;q;)null!==m.get(q.getAttribute("vt-name"))&&h(q,"vt-enter"),q=q.nextElementSibling;a=t;do for(var n=a.firstElementChild;n;){var E=n.getAttribute("vt-update");E&&"none"!==E&&!l.includes(n)&&h(n,"vt-update");n=n.nextElementSibling}while((a=a.parentNode)&&1===a.nodeType&&"none"!==a.getAttribute("vt-update"))}}}if(x){var v=document.__reactViewTransition=document.startViewTransition({update:w.bind(null,\nf),types:[]});v.ready.finally(function(){for(var b=l.length-3;0<=b;b-=3){var d=l[b],k=d.style;k.viewTransitionName=l[b+1];k.viewTransitionClass=l[b+1];""===d.getAttribute("style")&&d.removeAttribute("style")}});v.finished.finally(function(){document.__reactViewTransition===v&&(document.__reactViewTransition=null)});$RB=[];return}}catch(b){}w(f)}.bind(null,$RV);';
|
||||
export const completeBoundaryWithStyles =
|
||||
'$RM=new Map;$RR=function(n,w,p){function u(q){this._p=null;q()}for(var r=new Map,t=document,h,b,e=t.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=e[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&$RM.set(b.getAttribute("href"),b),r.set(b.dataset.precedence,h=b));e=0;b=[];var l,a;for(k=!0;;){if(k){var f=p[e++];if(!f){k=!1;e=0;continue}var c=!1,m=0;var d=f[m++];if(a=$RM.get(d)){var g=a._p;c=!0}else{a=t.createElement("link");a.href=d;a.rel=\n"stylesheet";for(a.dataset.precedence=l=f[m++];g=f[m++];)a.setAttribute(g,f[m++]);g=a._p=new Promise(function(q,x){a.onload=u.bind(a,q);a.onerror=u.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!g||d&&!matchMedia(d).matches||b.push(g);if(c)continue}else{a=v[e++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=r.get(l)||h;c===h&&(h=a);r.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=t.head,c.insertBefore(a,c.firstChild))}if(p=document.getElementById(n))p.previousSibling.data=\n"$~";Promise.all(b).then($RC.bind(null,n,w),$RX.bind(null,n,"CSS failed to load"))};';
|
||||
export const completeSegment =
|
||||
|
||||
Vendored
+169
-2
@@ -3,6 +3,7 @@
|
||||
// Shared implementation and constants between the inline script and external
|
||||
// runtime instruction sets.
|
||||
|
||||
const ELEMENT_NODE = 1;
|
||||
const COMMENT_NODE = 8;
|
||||
const ACTIVITY_START_DATA = '&';
|
||||
const ACTIVITY_END_DATA = '/&';
|
||||
@@ -84,6 +85,35 @@ export function revealCompletedBoundariesWithViewTransitions(
|
||||
revealBoundaries,
|
||||
batch,
|
||||
) {
|
||||
let shouldStartViewTransition = false;
|
||||
let autoNameIdx = 0;
|
||||
const restoreQueue = [];
|
||||
function applyViewTransitionName(element, classAttributeName) {
|
||||
const className = element.getAttribute(classAttributeName);
|
||||
if (!className) {
|
||||
return;
|
||||
}
|
||||
// Add any elements we apply a name to a queue to be reverted when we start.
|
||||
const elementStyle = element.style;
|
||||
restoreQueue.push(
|
||||
element,
|
||||
elementStyle['viewTransitionName'],
|
||||
elementStyle['viewTransitionClass'],
|
||||
);
|
||||
if (className !== 'auto') {
|
||||
elementStyle['viewTransitionClass'] = className;
|
||||
}
|
||||
let name = element.getAttribute('vt-name');
|
||||
if (!name) {
|
||||
// Auto-generate a name for this one.
|
||||
// TODO: We don't have a prefix to pick from here but maybe we don't need it
|
||||
// since it's only applicable temporarily during this specific animation.
|
||||
const idPrefix = '';
|
||||
name = '\u00AB' + idPrefix + 'T' + autoNameIdx++ + '\u00BB';
|
||||
}
|
||||
elementStyle['viewTransitionName'] = name;
|
||||
shouldStartViewTransition = true;
|
||||
}
|
||||
try {
|
||||
const existingTransition = document['__reactViewTransition'];
|
||||
if (existingTransition) {
|
||||
@@ -91,7 +121,132 @@ export function revealCompletedBoundariesWithViewTransitions(
|
||||
existingTransition.finished.finally(window['$RV'].bind(null, batch));
|
||||
return;
|
||||
}
|
||||
const shouldStartViewTransition = window['_useVT']; // TODO: Detect.
|
||||
// First collect all entering names that might form pairs exiting names.
|
||||
const appearingViewTransitions = new Map();
|
||||
for (let i = 1; i < batch.length; i += 2) {
|
||||
const contentNode = batch[i];
|
||||
const appearingElements = contentNode.querySelectorAll('[vt-share]');
|
||||
for (let j = 0; j < appearingElements.length; j++) {
|
||||
const appearingElement = appearingElements[j];
|
||||
appearingViewTransitions.set(
|
||||
appearingElement.getAttribute('vt-name'),
|
||||
appearingElement,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Next we'll find the nodes that we're going to animate and apply names to them..
|
||||
for (let i = 0; i < batch.length; i += 2) {
|
||||
const suspenseIdNode = batch[i];
|
||||
const parentInstance = suspenseIdNode.parentNode;
|
||||
if (!parentInstance) {
|
||||
// We may have client-rendered this boundary already. Skip it.
|
||||
continue;
|
||||
}
|
||||
const parentRect = parentInstance.getBoundingClientRect();
|
||||
if (
|
||||
!parentRect.left &&
|
||||
!parentRect.top &&
|
||||
!parentRect.width &&
|
||||
!parentRect.height
|
||||
) {
|
||||
// If the parent instance is display: none then we don't animate this boundary.
|
||||
// This can happen when this boundary is actually a child of a different boundary that
|
||||
// isn't yet revealed or is about to be revealed, but in that case that boundary
|
||||
// should do the exit/enter and not this one. Conveniently this also lets us skip
|
||||
// this if it's just in a hidden tree in general.
|
||||
// TODO: Should we skip it if it's out of viewport? It's possible that it gets
|
||||
// brought into the viewport by changing size.
|
||||
// TODO: There's a another case where an inner boundary is inside a fallback that
|
||||
// is about to be deleted. In that case we should not run exit animations on the inner.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply exit animations to the immediate elements inside the fallback.
|
||||
let node = suspenseIdNode;
|
||||
let depth = 0;
|
||||
while (node) {
|
||||
if (node.nodeType === COMMENT_NODE) {
|
||||
const data = node.data;
|
||||
if (data === SUSPENSE_END_DATA) {
|
||||
if (depth === 0) {
|
||||
break;
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
} else if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === SUSPENSE_QUEUED_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
} else if (node.nodeType === ELEMENT_NODE) {
|
||||
const exitElement = node;
|
||||
const exitName = exitElement.getAttribute('vt-name');
|
||||
const pairedElement = appearingViewTransitions.get(exitName);
|
||||
applyViewTransitionName(
|
||||
exitElement,
|
||||
pairedElement ? 'vt-share' : 'vt-exit',
|
||||
);
|
||||
if (pairedElement) {
|
||||
// Activate the other side as well.
|
||||
applyViewTransitionName(pairedElement, 'vt-share');
|
||||
appearingViewTransitions.set(exitName, null); // mark claimed
|
||||
}
|
||||
// Next we'll look inside this element for pairs to trigger "share".
|
||||
const disappearingElements =
|
||||
exitElement.querySelectorAll('[vt-share]');
|
||||
for (let j = 0; j < disappearingElements.length; j++) {
|
||||
const disappearingElement = disappearingElements[j];
|
||||
const name = disappearingElement.getAttribute('vt-name');
|
||||
const appearingElement = appearingViewTransitions.get(name);
|
||||
if (appearingElement) {
|
||||
applyViewTransitionName(disappearingElement, 'vt-share');
|
||||
applyViewTransitionName(appearingElement, 'vt-share');
|
||||
appearingViewTransitions.set(name, null); // mark claimed
|
||||
}
|
||||
}
|
||||
}
|
||||
node = node.nextSibling;
|
||||
}
|
||||
|
||||
// Apply enter animations to the new nodes about to be inserted.
|
||||
const contentNode = batch[i + 1];
|
||||
let enterElement = contentNode.firstElementChild;
|
||||
while (enterElement) {
|
||||
const paired =
|
||||
appearingViewTransitions.get(enterElement.getAttribute('vt-name')) ===
|
||||
null;
|
||||
if (!paired) {
|
||||
applyViewTransitionName(enterElement, 'vt-enter');
|
||||
}
|
||||
enterElement = enterElement.nextElementSibling;
|
||||
}
|
||||
|
||||
// Apply update animations to any parents and siblings that might be affected.
|
||||
let ancestorElement = parentInstance;
|
||||
do {
|
||||
let childElement = ancestorElement.firstElementChild;
|
||||
while (childElement) {
|
||||
// TODO: Bail out if we can
|
||||
const updateClassName = childElement.getAttribute('vt-update');
|
||||
if (
|
||||
updateClassName &&
|
||||
updateClassName !== 'none' &&
|
||||
!restoreQueue.includes(childElement)
|
||||
) {
|
||||
// If we have already handled this element as part of another exit/enter/share, don't override.
|
||||
applyViewTransitionName(childElement, 'vt-update');
|
||||
}
|
||||
childElement = childElement.nextElementSibling;
|
||||
}
|
||||
} while (
|
||||
(ancestorElement = ancestorElement.parentNode) &&
|
||||
ancestorElement.nodeType === ELEMENT_NODE &&
|
||||
ancestorElement.getAttribute('vt-update') !== 'none'
|
||||
);
|
||||
}
|
||||
if (shouldStartViewTransition) {
|
||||
const transition = (document['__reactViewTransition'] = document[
|
||||
'startViewTransition'
|
||||
@@ -100,7 +255,19 @@ export function revealCompletedBoundariesWithViewTransitions(
|
||||
types: [], // TODO: Add a hard coded type for Suspense reveals.
|
||||
}));
|
||||
transition.ready.finally(() => {
|
||||
// TODO
|
||||
// Restore all the names/classes that we applied to what they were before.
|
||||
// We do it in reverse order in case there were duplicates so the first one wins.
|
||||
for (let i = restoreQueue.length - 3; i >= 0; i -= 3) {
|
||||
const element = restoreQueue[i];
|
||||
const elementStyle = element.style;
|
||||
const previousName = restoreQueue[i + 1];
|
||||
elementStyle['viewTransitionName'] = previousName;
|
||||
const previousClassName = restoreQueue[i + 1];
|
||||
elementStyle['viewTransitionClass'] = previousClassName;
|
||||
if (element.getAttribute('style') === '') {
|
||||
element.removeAttribute('style');
|
||||
}
|
||||
}
|
||||
});
|
||||
transition.finished.finally(() => {
|
||||
if (document['__reactViewTransition'] === transition) {
|
||||
|
||||
Reference in New Issue
Block a user