Skip to content

Commit 1936c87

Browse files
authored
fix(transition-group): measure nested groups against their moving outer item (#15848)
fix #5385 fix #1531
1 parent 9ea9b6c commit 1936c87

2 files changed

Lines changed: 564 additions & 19 deletions

File tree

‎packages/runtime-dom/src/components/TransitionGroup.ts‎

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from './Transition'
1212
import { type VShowElement, vShowHidden } from '../directives/vShow'
1313
import {
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+
3846
const positionMap = new WeakMap<VNode, Position>()
3947
const newPositionMap = new WeakMap<VNode, Position>()
4048
const moveCbKey = Symbol('_moveCb')
4149
const 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

4354
export 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

203240
function 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

237311
function hasCSSTransform(

0 commit comments

Comments
 (0)