From dc4dd594fbecce6ed7f44ffa69dc8b5d022287b6 Mon Sep 17 00:00:00 2001 From: Jooies <76929557+joo1es@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:20:25 +0800 Subject: [PATCH] fix(TransitionGroup): use offsetLeft and offsetTop instead of getBoundingClientRect to avoid transform scale affect animation (#6108) close #6105 --- .../src/components/TransitionGroup.ts | 22 +++++--- .../vue/__tests__/e2e/TransitionGroup.spec.ts | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 3ba59f068c..eb86dd4a43 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -29,8 +29,13 @@ import { } from '@vue/runtime-core' import { extend } from '@vue/shared' -const positionMap = new WeakMap() -const newPositionMap = new WeakMap() +interface Position { + top: number + left: number +} + +const positionMap = new WeakMap() +const newPositionMap = new WeakMap() const moveCbKey = Symbol('_moveCb') const enterCbKey = Symbol('_enterCb') @@ -145,10 +150,10 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({ instance, ), ) - positionMap.set( - child, - (child.el as Element).getBoundingClientRect(), - ) + positionMap.set(child, { + left: (child.el as HTMLElement).offsetLeft, + top: (child.el as HTMLElement).offsetTop, + }) } } } @@ -189,7 +194,10 @@ function callPendingCbs(c: VNode) { } function recordPosition(c: VNode) { - newPositionMap.set(c, (c.el as Element).getBoundingClientRect()) + newPositionMap.set(c, { + left: (c.el as HTMLElement).offsetLeft, + top: (c.el as HTMLElement).offsetTop, + }) } function applyTranslation(c: VNode): VNode | undefined { diff --git a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts index 62d89db4e3..09f137b4c2 100644 --- a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts +++ b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts @@ -646,6 +646,62 @@ describe('e2e: TransitionGroup', () => { E2E_TIMEOUT, ) + // #6105 + test( + 'with scale', + async () => { + await page().evaluate(() => { + const { createApp, ref, onMounted } = (window as any).Vue + createApp({ + template: ` +
+
+ +
  • {{item}}
  • +
    + +
    +
    + `, + setup: () => { + const items = ref(['a', 'b', 'c']) + const click = () => { + items.value.reverse() + } + + onMounted(() => { + const styleNode = document.createElement('style') + styleNode.innerHTML = `.v-move { + transition: transform 0.5s ease; + }` + document.body.appendChild(styleNode) + }) + + return { items, click } + }, + }).mount('#app') + }) + + const original_top = await page().$eval('ul li:nth-child(1)', node => { + return node.getBoundingClientRect().top + }) + const new_top = await page().evaluate(() => { + const el = document.querySelector('ul li:nth-child(1)') + const p = new Promise(resolve => { + el!.addEventListener('transitionstart', () => { + const new_top = el!.getBoundingClientRect().top + resolve(new_top) + }) + }) + ;(document.querySelector('#toggleBtn') as any)!.click() + return p + }) + + expect(original_top).toBeLessThan(new_top as number) + }, + E2E_TIMEOUT, + ) + test( 'not leaking after children unmounted', async () => { -- 2.47.3