Sfoglia il codice sorgente

fix(transition-group): correct move translation under scale via element rect (#14360)

close #14356

revert #6108
re-fix #6105
reference #9733
edison 2 mesi fa
parent
commit
0243a792ac

+ 25 - 10
packages/runtime-dom/src/components/TransitionGroup.ts

@@ -150,10 +150,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
                 instance,
               ),
             )
-            positionMap.set(child, {
-              left: (child.el as HTMLElement).offsetLeft,
-              top: (child.el as HTMLElement).offsetTop,
-            })
+            positionMap.set(child, getPosition(child.el as HTMLElement))
           }
         }
       }
@@ -194,10 +191,7 @@ function callPendingCbs(c: VNode) {
 }
 
 function recordPosition(c: VNode) {
-  newPositionMap.set(c, {
-    left: (c.el as HTMLElement).offsetLeft,
-    top: (c.el as HTMLElement).offsetTop,
-  })
+  newPositionMap.set(c, getPosition(c.el as HTMLElement))
 }
 
 function applyTranslation(c: VNode): VNode | undefined {
@@ -206,13 +200,34 @@ function applyTranslation(c: VNode): VNode | undefined {
   const dx = oldPos.left - newPos.left
   const dy = oldPos.top - newPos.top
   if (dx || dy) {
-    const s = (c.el as HTMLElement).style
-    s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
+    const el = c.el as HTMLElement
+    const s = el.style
+    const rect = el.getBoundingClientRect()
+    let scaleX = 1
+    let scaleY = 1
+    if (el.offsetWidth) scaleX = rect.width / el.offsetWidth
+    if (el.offsetHeight) scaleY = rect.height / el.offsetHeight
+    if (!Number.isFinite(scaleX) || scaleX === 0) scaleX = 1
+    if (!Number.isFinite(scaleY) || scaleY === 0) scaleY = 1
+    // Avoid division noise when scale is effectively 1.
+    if (Math.abs(scaleX - 1) < 0.01) scaleX = 1
+    if (Math.abs(scaleY - 1) < 0.01) scaleY = 1
+    s.transform = s.webkitTransform = `translate(${dx / scaleX}px,${
+      dy / scaleY
+    }px)`
     s.transitionDuration = '0s'
     return c
   }
 }
 
+function getPosition(el: HTMLElement): Position {
+  const rect = el.getBoundingClientRect()
+  return {
+    left: rect.left,
+    top: rect.top,
+  }
+}
+
 function hasCSSTransform(
   el: ElementWithTransition,
   root: Node,

+ 92 - 0
packages/vue/__tests__/e2e/TransitionGroup.spec.ts

@@ -297,6 +297,98 @@ describe('e2e: TransitionGroup', () => {
     E2E_TIMEOUT,
   )
 
+  test(
+    'move while entering',
+    async () => {
+      await page().evaluate(duration => {
+        const { createApp, ref, onMounted } = (window as any).Vue
+        createApp({
+          template: `
+              <transition-group name="toasts" tag="div" id="toasts">
+                <div class="toast" v-for="toast in list" :key="toast.id">
+                  {{ toast.text }} #{{ toast.id }}
+                </div>
+              </transition-group>
+              <button id="addBtn" @click="add">button</button>
+            `,
+          setup: () => {
+            const list = ref([])
+            let id = 0
+            const add = () => {
+              if (list.value.length > 3) {
+                list.value.splice(0, 1)
+              }
+              list.value.push({
+                id,
+                type: 'error',
+                text: 'Test message',
+              })
+              id++
+            }
+
+            onMounted(() => {
+              const styleNode = document.createElement('style')
+              styleNode.innerHTML = `
+                #toasts {
+                  position: absolute;
+                  bottom: 0;
+                  left: 0;
+                }
+                #toasts > .toast {
+                  width: 150px;
+                  margin-bottom: 10px;
+                  height: 30px;
+                  color: white;
+                  background: black;
+                }
+                .toasts-leave-active {
+                  position: absolute;
+                }
+                .toasts-move { transition: transform ${duration}ms ease; }
+              `
+              document.body.appendChild(styleNode)
+            })
+
+            return { list, add }
+          },
+        }).mount('#app')
+      }, duration)
+
+      const overlapDelay = Math.max(10, Math.floor(duration / 2))
+      const { midTop, finalTop } = await page().evaluate(
+        ({ overlapDelay, duration, buffer }) => {
+          ;(document.querySelector('#addBtn') as any)!.click()
+          return new Promise<{ midTop: number; finalTop: number }>(resolve => {
+            setTimeout(() => {
+              ;(document.querySelector('#addBtn') as any)!.click()
+              Promise.resolve().then(() => {
+                const nodes = Array.from(
+                  document.querySelectorAll('#toasts .toast'),
+                ) as HTMLElement[]
+                const firstToast = nodes.find(node =>
+                  node.textContent?.includes('#0'),
+                )
+                const midTop = firstToast
+                  ? firstToast.getBoundingClientRect().top
+                  : NaN
+                setTimeout(() => {
+                  const finalTop = firstToast
+                    ? firstToast.getBoundingClientRect().top
+                    : NaN
+                  resolve({ midTop, finalTop })
+                }, duration + buffer)
+              })
+            }, overlapDelay)
+          })
+        },
+        { overlapDelay, duration, buffer },
+      )
+
+      expect(midTop).toBeGreaterThan(finalTop)
+    },
+    E2E_TIMEOUT,
+  )
+
   test(
     'dynamic name',
     async () => {