Просмотр исходного кода

fix(hydration): clear inline teleport children when missing targets stop owning the main view

daiwei 1 неделя назад
Родитель
Сommit
1e828e4890

+ 79 - 0
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -4413,6 +4413,85 @@ describe('Vapor Mode hydration', () => {
       expect('Failed to locate Teleport target').toHaveBeenWarned()
       expect('Failed to locate Teleport target').toHaveBeenWarned()
     })
     })
 
 
+    test('disabled teleport with null target should preserve trailing sibling when re-enabled without target', async () => {
+      const data = ref({
+        disabled: true,
+        target: undefined as string | undefined,
+        tail: 'tail',
+      })
+
+      const { container } = await mountWithHydration(
+        '<!--[--><!--teleport start--><div>content</div><!--teleport end--><span>tail</span><!--]-->',
+        `<teleport :to="data.target" :disabled="data.disabled">
+          <div>content</div>
+        </teleport>
+        <span>{{data.tail}}</span>`,
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        '<!--[--><!--teleport start--><div>content</div><!--teleport end--><span>tail</span><!--]-->',
+      )
+      expect(`Hydration text mismatch`).not.toHaveBeenWarned()
+
+      data.value.tail = 'tail-updated'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        '<!--[--><!--teleport start--><div>content</div><!--teleport end--><span>tail-updated</span><!--]-->',
+      )
+      expect('Invalid Teleport target').not.toHaveBeenWarned()
+
+      data.value.disabled = false
+      await nextTick()
+      expect('Invalid Teleport target').toHaveBeenWarned()
+      expect(container.innerHTML).toBe(
+        '<!--[--><!--teleport start--><!--teleport end--><span>tail-updated</span><!--]-->',
+      )
+    })
+
+    test('enabled teleport with null target should preserve trailing sibling when toggling disabled', async () => {
+      const data = ref({
+        disabled: false,
+        target: '#non-existent-target-hydrate-sibling' as string | undefined,
+        tail: 'tail',
+      })
+
+      const { container } = await mountWithHydration(
+        '<!--[--><!--teleport start--><!--teleport end--><span>tail</span><!--]-->',
+        `<teleport :to="data.target" :disabled="data.disabled">
+          <div>content</div>
+        </teleport>
+        <span>{{data.tail}}</span>`,
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        '<!--[--><!--teleport start--><!--teleport end--><span>tail</span><!--]-->',
+      )
+      expect('Failed to locate Teleport target').toHaveBeenWarned()
+
+      data.value.tail = 'tail-updated'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        '<!--[--><!--teleport start--><!--teleport end--><span>tail-updated</span><!--]-->',
+      )
+
+      data.value.disabled = true
+      data.value.target = undefined
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        '<!--[--><!--teleport start--><div>content</div><!--teleport end--><span>tail-updated</span><!--]-->',
+      )
+
+      data.value.disabled = false
+      data.value.target = '#non-existent-target-hydrate-sibling'
+      await nextTick()
+      expect('Invalid Teleport target on mount').toHaveBeenWarned()
+      expect(container.innerHTML).toBe(
+        '<!--[--><!--teleport start--><!--teleport end--><span>tail-updated</span><!--]-->',
+      )
+    })
+
     test('enabled teleport with null target should delay child setup until target becomes available', async () => {
     test('enabled teleport with null target should delay child setup until target becomes available', async () => {
       const version = ref('one')
       const version = ref('one')
       const target = ref<any>('#non-existent-target-hydrate-late')
       const target = ref<any>('#non-existent-target-hydrate-late')

+ 25 - 0
packages/runtime-vapor/src/components/Teleport.ts

@@ -259,6 +259,20 @@ export class TeleportFragment extends VaporFragment {
     }
     }
   }
   }
 
 
+  private clearMainViewChildren(): void {
+    if (!this.placeholder || !this.anchor) return
+
+    let node = this.placeholder.nextSibling
+    while (node && node !== this.anchor) {
+      const next = node.nextSibling
+      remove(node, parentNode(node)!)
+      node = next
+    }
+
+    this.isMounted = false
+    this.mountContainer = null
+  }
+
   private handlePropsUpdate(): void {
   private handlePropsUpdate(): void {
     // not mounted yet
     // not mounted yet
     if (!this.parent || isHydrating) return
     if (!this.parent || isHydrating) return
@@ -270,6 +284,17 @@ export class TeleportFragment extends VaporFragment {
     }
     }
     // mount into target container
     // mount into target container
     else {
     else {
+      // Align with initial enabled-null-target hydration: once Teleport leaves
+      // disabled mode, its children should no longer stay mounted inline in the
+      // main view if there is no valid target to move them into.
+      if (
+        this.placeholder &&
+        this.anchor &&
+        this.placeholder.nextSibling !== this.anchor
+      ) {
+        this.clearMainViewChildren()
+      }
+
       if (
       if (
         isTeleportDeferred(this.resolvedProps!) ||
         isTeleportDeferred(this.resolvedProps!) ||
         // force defer when the parent is not connected to the DOM,
         // force defer when the parent is not connected to the DOM,