Parcourir la source

fix(transition): align interop transition vnode identity with vdom

daiwei il y a 1 mois
Parent
commit
efda719263

+ 49 - 0
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -73,6 +73,55 @@ describe('vdomInterop', () => {
       expect(componentBlock.$key).toBe('bar')
       expect(componentBlock.vnode.key).toBe('bar')
     })
+
+    test('preserves single slot vnode key on interop fragments', async () => {
+      const key = ref('foo')
+
+      const CompA = defineComponent({
+        setup() {
+          return () => h('div', 'A')
+        },
+      })
+      const CompB = defineComponent({
+        setup() {
+          return () => h('div', 'B')
+        },
+      })
+      const current = shallowRef<any>(CompA)
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createSlot('default') as any
+        },
+      })
+
+      const Parent = defineComponent({
+        setup() {
+          return () =>
+            h(VaporChild as any, null, {
+              default: () => [h(current.value, { key: key.value })],
+            })
+        },
+      })
+
+      const app = createApp(Parent)
+      app.use(vaporInteropPlugin)
+      const vapor = (app._context as any).vapor
+      const originalVdomSlot = vapor.vdomSlot
+      let frag: any
+      vapor.vdomSlot = (...args: any[]) => (frag = originalVdomSlot(...args))
+
+      const host = document.createElement('div')
+      app.mount(host)
+
+      expect(frag.$key).toBe('_defaultfoo')
+
+      key.value = 'bar'
+      current.value = CompB
+      await nextTick()
+
+      expect(frag.$key).toBe('_defaultbar')
+    })
   })
 
   describe('fragment nodes', () => {

+ 40 - 3
packages/runtime-vapor/src/components/Transition.ts

@@ -7,10 +7,12 @@ import {
   type TransitionProps,
   TransitionPropsValidators,
   type TransitionState,
+  type VNode,
   baseResolveTransitionHooks,
   checkTransitionMode,
   currentInstance,
   getComponentName,
+  getTransitionRawChildren,
   isAsyncWrapper,
   isTemplateNode,
   leaveCbKey,
@@ -44,6 +46,7 @@ import {
   isHydrating,
   setCurrentHydrationNode,
 } from '../dom/hydration'
+import { isInteropEnabled } from '../vdomInteropState'
 
 const displayName = 'VaporTransition'
 export type ResolvedTransitionBlock = (
@@ -139,7 +142,10 @@ function getTransitionType(block: ResolvedTransitionBlock): any {
   const type = transitionTypeMap.get(block)
   if (type !== undefined) return type
   if (block instanceof Element) return block.localName
-  if (isFragment(block) && block.vnode) return block.vnode.type
+  if (isFragment(block) && block.vnode) {
+    const children = getTransitionRawChildren([block.vnode])
+    if (children.length === 1) return children[0].type
+  }
   return block
 }
 
@@ -163,6 +169,10 @@ function getLeaveElement(
   if (block instanceof Element) {
     return block as TransitionElement
   }
+  if (isInteropEnabled && isFragment(block) && block.vnode) {
+    const el = getTransitionElementFromVNode(block.vnode)
+    if (el) return el as TransitionElement
+  }
   if (
     isFragment(block) &&
     !isArray(block.nodes) &&
@@ -407,9 +417,12 @@ export function resolveTransitionBlock(
       if (!__DEV__) break
     }
   } else if (isFragment(block)) {
-    if (block.insert) {
+    if (isInteropEnabled && block.vnode) {
       child = block
-      if (block.vnode) transitionTypeMap.set(child, block.vnode.type)
+      const children = getTransitionRawChildren([block.vnode])
+      if (children.length === 1) {
+        transitionTypeMap.set(child, children[0].type)
+      }
     } else {
       // collect fragments for setting transition hooks
       if (onFragment) onFragment(block)
@@ -430,3 +443,27 @@ export function setTransitionHooks(
   }
   block.$transition = hooks
 }
+
+export function getVNodeKey(
+  vnode: VNode | undefined,
+): VNode['key'] | undefined {
+  if (!vnode) return
+  const children = getTransitionRawChildren([vnode])
+  return children.length === 1 ? children[0].key : undefined
+}
+
+export function getTransitionElementFromVNode(
+  vnode: VNode | undefined,
+): Element | undefined {
+  if (!vnode) return
+  if (vnode.component) {
+    return getTransitionElementFromVNode(vnode.component.subTree)
+  }
+  if (vnode.el instanceof Element) {
+    return vnode.el
+  }
+  const children = getTransitionRawChildren([vnode])
+  if (children.length === 1 && children[0] !== vnode) {
+    return getTransitionElementFromVNode(children[0])
+  }
+}

+ 3 - 8
packages/runtime-vapor/src/components/TransitionGroup.ts

@@ -22,6 +22,7 @@ import { renderEffect } from '../renderEffect'
 import {
   type ResolvedTransitionBlock,
   ensureTransitionHooksRegistered,
+  getTransitionElementFromVNode,
   resolveTransitionHooks,
   setTransitionHooks,
 } from './Transition'
@@ -257,14 +258,8 @@ function getTransitionElement(
   if (block instanceof Element) return block
 
   // vdom interop
-  if (
-    isInteropEnabled &&
-    isFragment(block) &&
-    block.vnode &&
-    !isArray(block.nodes) &&
-    (block.nodes instanceof Element || isFragment(block.nodes))
-  ) {
-    return getTransitionElement(block.nodes)
+  if (isInteropEnabled && isFragment(block) && block.vnode) {
+    return getTransitionElementFromVNode(block.vnode)
   }
 }
 

+ 37 - 14
packages/runtime-vapor/src/vdomInterop.ts

@@ -101,7 +101,10 @@ import {
   renderSlotFallback,
 } from './fragment'
 import type { NodeRef } from './apiTemplateRef'
-import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
+import {
+  getVNodeKey,
+  setTransitionHooks as setVaporTransitionHooks,
+} from './components/Transition'
 import { setInteropEnabled } from './vdomInteropState'
 import {
   type KeepAliveInstance,
@@ -559,19 +562,22 @@ function resolveVNodeNodes(vnode: VNode): Block {
   return vnode.el as Block
 }
 
+function appendVnodeUpdatedHook(vnode: VNode, hook: () => void): void {
+  const props = (vnode.props ||= {})
+  const existing = props.onVnodeUpdated
+  props.onVnodeUpdated = existing
+    ? isArray(existing)
+      ? [...existing, hook]
+      : [existing, hook]
+    : hook
+}
+
 function trackFragmentVNodeUpdates(frag: VaporFragment, vnode: VNode): void {
   const refresh = () => {
     frag.nodes = resolveVNodeNodes(vnode)
     if (frag.onUpdated) frag.onUpdated.forEach(m => m())
   }
-
-  const props = (vnode.props ||= {})
-  const existing = props.onVnodeUpdated
-  props.onVnodeUpdated = existing
-    ? isArray(existing)
-      ? [...existing, refresh]
-      : [existing, refresh]
-    : refresh
+  appendVnodeUpdatedHook(vnode, refresh)
 }
 
 /**
@@ -903,12 +909,21 @@ function ensureRendererBridge(
 }
 
 function trackSlotVNodeUpdates(frag: VaporFragment, vnode: VNode): void {
-  trackFragmentVNodeUpdates(frag, vnode)
-  if (vnode.type === Fragment && isArray(vnode.children)) {
-    vnode.children.forEach(child => {
-      if (isVNode(child)) trackSlotVNodeUpdates(frag, child)
-    })
+  const refresh = () => {
+    frag.nodes = resolveVNodeNodes(vnode)
+    if (frag.onUpdated) frag.onUpdated.forEach(m => m())
   }
+
+  const track = (node: VNode) => {
+    appendVnodeUpdatedHook(node, refresh)
+    if (node.type === Fragment && isArray(node.children)) {
+      node.children.forEach(child => {
+        if (isVNode(child)) track(child)
+      })
+    }
+  }
+
+  track(vnode)
 }
 
 /**
@@ -1021,6 +1036,8 @@ function renderVDOMSlot(
 
           if (isHydrating) {
             if (isVNode(resolved)) {
+              frag.vnode = resolved
+              frag.$key = getVNodeKey(resolved)
               trackSlotVNodeUpdates(frag, resolved)
               hydrateVNode(resolved, parentComponent as any)
               currentVNode = resolved
@@ -1039,6 +1056,8 @@ function renderVDOMSlot(
           }
 
           if (isVNode(resolved)) {
+            frag.vnode = resolved
+            frag.$key = getVNodeKey(resolved)
             trackSlotVNodeUpdates(frag, resolved)
             if (currentBlock) {
               remove(currentBlock, parentNode)
@@ -1060,6 +1079,8 @@ function renderVDOMSlot(
           }
 
           if (resolved) {
+            frag.vnode = null
+            frag.$key = undefined
             if (currentVNode) {
               internals.um(currentVNode, parentComponent as any, null, true)
               currentVNode = null
@@ -1083,6 +1104,8 @@ function renderVDOMSlot(
           }
 
           // mark as empty
+          frag.vnode = null
+          frag.$key = undefined
           frag.nodes = []
         } finally {
           setCurrentSlotOwner(prevSlotOwner)