feat: add Primitive/Slot polymorphic-rendering utility
First piece of the reka-ui removal: a small asChild/Slot implementation to replace reka-ui's Primitive component.
This commit is contained in:
parent
d56ccce2fe
commit
dc3aa2c816
3 changed files with 83 additions and 0 deletions
21
packages/library/src/lib/primitive.ts
Normal file
21
packages/library/src/lib/primitive.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Component } from 'vue'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import Slot from './slot'
|
||||
|
||||
export const Primitive = defineComponent({
|
||||
name: 'Primitive',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
as: {
|
||||
type: [String, Object, Function] as unknown as () => string | Component,
|
||||
default: 'div',
|
||||
},
|
||||
asChild: { type: Boolean, default: false },
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
return () => {
|
||||
const Tag = props.asChild ? Slot : props.as
|
||||
return h(Tag, attrs, slots)
|
||||
}
|
||||
},
|
||||
})
|
||||
17
packages/library/src/lib/slot.ts
Normal file
17
packages/library/src/lib/slot.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { VNode } from 'vue'
|
||||
import { cloneVNode, defineComponent, mergeProps } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Slot',
|
||||
inheritAttrs: false,
|
||||
setup(_, { slots, attrs }) {
|
||||
return () => {
|
||||
const children = slots.default?.() ?? []
|
||||
if (children.length !== 1) {
|
||||
throw new Error('Slot requires exactly one child element')
|
||||
}
|
||||
const child = children[0] as VNode
|
||||
return cloneVNode(child, mergeProps(attrs, (child.props ?? {}) as Record<string, unknown>))
|
||||
}
|
||||
},
|
||||
})
|
||||
45
packages/library/tests/primitive.spec.ts
Normal file
45
packages/library/tests/primitive.spec.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { h } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { Primitive } from '../src/lib/primitive'
|
||||
|
||||
describe('Primitive', () => {
|
||||
it('renders the "as" tag with forwarded attrs when asChild is false', () => {
|
||||
const wrapper = mount(Primitive, {
|
||||
props: { as: 'a' },
|
||||
attrs: { href: '/somewhere', class: 'link' },
|
||||
slots: { default: () => 'Go' },
|
||||
})
|
||||
const el = wrapper.get('a')
|
||||
expect(el.attributes('href')).toBe('/somewhere')
|
||||
expect(el.classes()).toContain('link')
|
||||
expect(el.text()).toBe('Go')
|
||||
})
|
||||
|
||||
it('clones the single child and merges attrs onto it when asChild is true', async () => {
|
||||
let clicked = false
|
||||
const wrapper = mount(Primitive, {
|
||||
props: { asChild: true },
|
||||
attrs: { class: 'from-parent', 'data-slot': 'button', onClick: () => { clicked = true } },
|
||||
slots: {
|
||||
default: () => h('button', { class: 'from-child', type: 'button' }, 'Click'),
|
||||
},
|
||||
})
|
||||
const el = wrapper.get('button')
|
||||
expect(el.attributes('data-slot')).toBe('button')
|
||||
expect(el.attributes('type')).toBe('button')
|
||||
expect(el.classes()).toContain('from-parent')
|
||||
expect(el.classes()).toContain('from-child')
|
||||
await el.trigger('click')
|
||||
expect(clicked).toBe(true)
|
||||
})
|
||||
|
||||
it('throws if asChild is true with zero or multiple children', () => {
|
||||
expect(() =>
|
||||
mount(Primitive, {
|
||||
props: { asChild: true },
|
||||
slots: { default: () => [h('span', 'a'), h('span', 'b')] },
|
||||
}),
|
||||
).toThrow()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue