@@ -11,6 +11,7 @@ import {
1111} from './Transition'
1212import { type VShowElement , vShowHidden } from '../directives/vShow'
1313import {
14+ type ComponentInternalInstance ,
1415 type ComponentOptions ,
1516 DeprecationTypes ,
1617 Fragment ,
@@ -35,10 +36,20 @@ interface Position {
3536 left : number
3637}
3738
39+ // the item of an enclosing group that a nested group is rendered in, sampled
40+ // together with the old positions of the nested group's own children
41+ interface Origin extends Position {
42+ item : Element
43+ willMove : ( ) => boolean
44+ }
45+
3846const positionMap = new WeakMap < VNode , Position > ( )
3947const newPositionMap = new WeakMap < VNode , Position > ( )
4048const moveCbKey = Symbol ( '_moveCb' )
4149const enterCbKey = Symbol ( '_enterCb' )
50+ // groups that have re-rendered but not yet run their move, mapped to whether
51+ // they are going to move their children
52+ const pendingGroups = new WeakMap < ComponentInternalInstance , ( ) => boolean > ( )
4253
4354export type TransitionGroupProps = Omit < TransitionProps , 'mode' > & {
4455 tag ?: string
@@ -72,29 +83,45 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
7283 const state = useTransitionState ( )
7384 let prevChildren : VNode [ ]
7485 let children : VNode [ ]
75-
76- onUpdated ( ( ) => {
77- // children is guaranteed to exist after initial render
78- if ( ! prevChildren . length ) {
79- return
86+ let origins : Origin [ ] | undefined
87+ let moves : boolean | undefined
88+ // probed lazily: the move class is checked on a child after the patch, and a
89+ // nested group may need the answer before this group's own updated hook
90+ const willMove = ( ) => {
91+ if ( moves === undefined ) {
92+ // children is guaranteed to exist after initial render
93+ moves =
94+ prevChildren . length > 0 &&
95+ hasCSSTransform (
96+ prevChildren [ 0 ] . el as ElementWithTransition ,
97+ instance . vnode . el as Node ,
98+ resolveMoveClass ( props ) ,
99+ )
80100 }
81- const moveClass = props . moveClass || `${ props . name || 'v' } -move`
101+ return moves
102+ }
82103
83- if (
84- ! hasCSSTransform (
85- prevChildren [ 0 ] . el as ElementWithTransition ,
86- instance . vnode . el as Node ,
87- moveClass ,
88- )
89- ) {
104+ onUpdated ( ( ) => {
105+ pendingGroups . delete ( instance )
106+ if ( ! willMove ( ) ) {
90107 prevChildren = [ ]
108+ origins = undefined
91109 return
92110 }
111+ const moveClass = resolveMoveClass ( props )
93112
94113 // we divide the work into three loops to avoid mixing DOM reads and writes
95114 // in each iteration - which helps prevent layout thrashing.
96115 prevChildren . forEach ( callPendingCbs )
97- prevChildren . forEach ( recordPosition )
116+ // between them, the enclosing groups that move animate how far the item
117+ // this group is rendered in has moved, so that part is left out here. The
118+ // item is sampled together with this group's children both times, so the
119+ // two samples stay in the same frame whether or not those groups have
120+ // already applied their own translations.
121+ const shift =
122+ origins && getOriginShift ( origins , instance . vnode . el as Node )
123+ origins = undefined
124+ prevChildren . forEach ( c => recordPosition ( c , shift ) )
98125 const movedChildren = prevChildren . filter ( applyTranslation )
99126
100127 // force reflow to put everything in position
@@ -137,6 +164,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
137164 }
138165
139166 prevChildren = [ ]
167+ moves = undefined
140168 if ( children ) {
141169 for ( let i = 0 ; i < children . length ; i ++ ) {
142170 const child = children [ i ]
@@ -175,6 +203,11 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
175203 }
176204 }
177205
206+ if ( prevChildren . length ) {
207+ origins = getOrigins ( instance )
208+ pendingGroups . set ( instance , willMove )
209+ }
210+
178211 return createVNode ( tag , null , children )
179212 }
180213 } ,
@@ -196,8 +229,12 @@ function callPendingCbs(c: VNode) {
196229 }
197230}
198231
199- function recordPosition ( c : VNode ) {
200- newPositionMap . set ( c , getPosition ( c . el as HTMLElement ) )
232+ function resolveMoveClass ( props : TransitionGroupProps ) : string {
233+ return props . moveClass || `${ props . name || 'v' } -move`
234+ }
235+
236+ function recordPosition ( c : VNode , shift ?: Position ) {
237+ newPositionMap . set ( c , getPosition ( c . el as HTMLElement , shift ) )
201238}
202239
203240function applyTranslation ( c : VNode ) : VNode | undefined {
@@ -226,12 +263,49 @@ function applyTranslation(c: VNode): VNode | undefined {
226263 }
227264}
228265
229- function getPosition ( el : HTMLElement ) : Position {
266+ function getPosition ( el : Element , shift ?: Position ) : Position {
230267 const rect = el . getBoundingClientRect ( )
231268 return {
232- left : rect . left ,
233- top : rect . top ,
269+ left : rect . left - ( shift ? shift . left : 0 ) ,
270+ top : rect . top - ( shift ? shift . top : 0 ) ,
271+ }
272+ }
273+
274+ // origins in the enclosing groups that are updating as well, nearest first
275+ function getOrigins ( instance : ComponentInternalInstance ) : Origin [ ] | undefined {
276+ let origins : Origin [ ] | undefined
277+ for ( let group = instance . parent ; group ; group = group . parent ) {
278+ const willMove = pendingGroups . get ( group )
279+ if ( willMove ) {
280+ const item = findItem ( group . vnode . el as Node , instance . vnode . el as Node )
281+ if ( item ) {
282+ ; ( origins || ( origins = [ ] ) ) . push (
283+ extend ( getPosition ( item ) , { item, willMove } ) ,
284+ )
285+ }
286+ }
234287 }
288+ return origins
289+ }
290+
291+ // the item of the group rooted at `root` that `node` is rendered in
292+ function findItem ( root : Node , node : Node | null ) : Element | undefined {
293+ // a group without `tag` renders a fragment, its children share the parent node
294+ const container = root . nodeType === 1 ? root : root . parentNode
295+ while ( node && node . parentNode !== container ) {
296+ node = node . parentNode
297+ }
298+ if ( node && node . nodeType === 1 ) {
299+ return node as Element
300+ }
301+ }
302+
303+ // the nearest origin whose group is going to move it: what farther groups
304+ // animate is part of its movement. An item this group's elements have left
305+ // since no longer carries them.
306+ function getOriginShift ( origins : Origin [ ] , root : Node ) : Position | undefined {
307+ const origin = origins . find ( o => o . willMove ( ) && o . item . contains ( root ) )
308+ return origin && getPosition ( origin . item , origin )
235309}
236310
237311function hasCSSTransform (
0 commit comments