} from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots'
+import { renderSlot } from '../src/helpers/renderSlot'
+import { setCurrentRenderingInstance } from '../src/componentRenderContext'
describe('component: slots', () => {
function renderWithSlots(slots: any): any {
createApp(App).mount(root)
expect(serializeInner(root)).toBe('foo')
})
+
+ // in-DOM templates use kebab-case slot names
+ describe('in-DOM template kebab-case slot name resolution', () => {
+ beforeEach(() => {
+ __BROWSER__ = true
+ })
+
+ afterEach(() => {
+ __BROWSER__ = false
+ })
+
+ test('should resolve camelCase slot access to kebab-case via slots', () => {
+ const Comp = {
+ setup(_: any, { slots }: any) {
+ // Access with camelCase, but slot is passed with kebab-case
+ return () => slots.dropdownRender()
+ },
+ }
+
+ const App = {
+ setup() {
+ // Parent passes slot with kebab-case name (simulating in-DOM template)
+ return () =>
+ h(Comp, null, { 'dropdown-render': () => 'dropdown content' })
+ },
+ }
+
+ const root = nodeOps.createElement('div')
+ createApp(App).mount(root)
+ expect(serializeInner(root)).toBe('dropdown content')
+ })
+
+ test('should resolve camelCase slot access to kebab-case via slots (PROD)', () => {
+ __DEV__ = false
+ try {
+ const Comp = {
+ setup(_: any, { slots }: any) {
+ // Access with camelCase, but slot is passed with kebab-case
+ return () => slots.dropdownRender()
+ },
+ }
+
+ const App = {
+ setup() {
+ // Parent passes slot with kebab-case name (simulating in-DOM template)
+ return () =>
+ h(Comp, null, { 'dropdown-render': () => 'dropdown content' })
+ },
+ }
+
+ const root = nodeOps.createElement('div')
+ createApp(App).mount(root)
+ expect(serializeInner(root)).toBe('dropdown content')
+ } finally {
+ __DEV__ = true
+ }
+ })
+
+ test('should prefer exact match over kebab-case conversion via slots', () => {
+ const Comp = {
+ setup(_: any, { slots }: any) {
+ return () => slots.dropdownRender()
+ },
+ }
+
+ const App = {
+ setup() {
+ // Both exact match and kebab-case exist
+ return () =>
+ h(Comp, null, {
+ 'dropdown-render': () => 'kebab',
+ dropdownRender: () => 'exact',
+ })
+ },
+ }
+
+ const root = nodeOps.createElement('div')
+ createApp(App).mount(root)
+ // exact match should take priority
+ expect(serializeInner(root)).toBe('exact')
+ })
+
+ // renderSlot tests
+ describe('renderSlot', () => {
+ beforeEach(() => {
+ setCurrentRenderingInstance({ type: {} } as any)
+ })
+
+ afterEach(() => {
+ setCurrentRenderingInstance(null)
+ })
+
+ test('should resolve camelCase slot name to kebab-case via renderSlot', () => {
+ let child: any
+ const vnode = renderSlot(
+ { 'dropdown-render': () => [(child = h('child'))] },
+ 'dropdownRender',
+ )
+ expect(vnode.children).toEqual([child])
+ })
+
+ test('should prefer exact match over kebab-case conversion via renderSlot', () => {
+ let exactChild: any
+ const vnode = renderSlot(
+ {
+ 'dropdown-render': () => [h('kebab')],
+ dropdownRender: () => [(exactChild = h('exact'))],
+ },
+ 'dropdownRender',
+ )
+ expect(vnode.children).toEqual([exactChild])
+ })
+ })
+ })
})
ShapeFlags,
extend,
getGlobalThis,
+ hyphenate,
isArray,
isFunction,
isObject,
},
}
-/**
- * Dev-only
- */
-function getSlotsProxy(instance: ComponentInternalInstance): Slots {
- return new Proxy(instance.slots, {
- get(target, key: string) {
+const createSlotsProxyHandlers = (
+ instance: ComponentInternalInstance,
+): ProxyHandler<InternalSlots> => ({
+ get(target, key: string | symbol) {
+ if (__DEV__) {
track(instance, TrackOpTypes.GET, '$slots')
- return target[key]
- },
- })
-}
+ }
+ // in-DOM templates use kebab-case slot names, only relevant in browser
+ return (
+ target[key as string] ||
+ (__BROWSER__ && typeof key === 'string' && target[hyphenate(key)])
+ )
+ },
+})
export function createSetupContext(
instance: ComponentInternalInstance,
)
},
get slots() {
- return slotsProxy || (slotsProxy = getSlotsProxy(instance))
+ return (
+ slotsProxy ||
+ (slotsProxy = new Proxy(
+ instance.slots,
+ createSlotsProxyHandlers(instance),
+ ))
+ )
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)
} else {
return {
attrs: new Proxy(instance.attrs, attrsProxyHandlers),
- slots: instance.slots,
+ slots: __BROWSER__
+ ? new Proxy(instance.slots, createSlotsProxyHandlers(instance))
+ : instance.slots,
emit: instance.emit,
expose,
}