33// Shared implementation and constants between the inline script and external
44// runtime instruction sets.
55
6+ const ELEMENT_NODE = 1 ;
67const COMMENT_NODE = 8 ;
78const ACTIVITY_START_DATA = '&' ;
89const 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