Skip to content

Commit

Permalink
feat(slot): new component (#259)
Browse files Browse the repository at this point in the history
* feat(slot): new component

* fix: ts-lint

* fix: ts-lint

* refactor: slot

* chore: fix readme

* refactor: slot

* refactor: slot

* chore: add more stories
  • Loading branch information
productdevbook committed Aug 2, 2023
1 parent ddf7077 commit 247d535
Show file tree
Hide file tree
Showing 13 changed files with 641 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@oku-ui/progress": "workspace:^",
"@oku-ui/provide": "workspace:^",
"@oku-ui/separator": "workspace:^",
"@oku-ui/slot": "workspace:^",
"@oku-ui/switch": "workspace:^",
"@oku-ui/toggle": "workspace:^",
"@oku-ui/use-composable": "workspace:^",
Expand Down
13 changes: 13 additions & 0 deletions packages/components/slot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# `slot`

<span><a href="https://www.npmjs.com/package/@oku-ui/slot "><img src="https://img.shields.io/npm/v/@oku-ui/slot?style=flat&colorA=18181B&colorB=28CF8D" alt="Version"></a> </span> | <span> <a href="https://www.npmjs.com/package/@oku-ui/slot"> <img src="https://img.shields.io/npm/dm/@oku-ui/slot?style=flat&colorA=18181B&colorB=28CF8D" alt="Downloads"> </a> </span> | <span> <a href="https://oku-ui.com/primitives/components/slot"><img src="https://img.shields.io/badge/Open%20Documentation-18181B" alt="Website"></a> </span>

## Installation

```sh
$ pnpm add @oku-ui/slot
```

## Usage

soon docs
12 changes: 12 additions & 0 deletions packages/components/slot/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
{
builder: 'mkdist',
input: './src/',
pattern: ['**/!(*.test|*.stories).ts'],
},
],
declaration: true,
})
49 changes: 49 additions & 0 deletions packages/components/slot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@oku-ui/slot",
"type": "module",
"version": "0.2.3",
"license": "MIT",
"source": "src/index.ts",
"funding": "https://github.com/sponsors/productdevbook",
"homepage": "https://oku-ui.com/primitives",
"repository": {
"type": "git",
"url": "git+https://github.com/oku-ui/primitives.git",
"directory": "packages/components/slot"
},
"bugs": {
"url": "https://github.com/oku-ui/primitives/issues"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=18"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
},
"peerDependencies": {
"vue": "^3.3.0"
},
"dependencies": {
"@oku-ui/primitive": "latest",
"@oku-ui/use-composable": "latest",
"@oku-ui/utils": "latest"
},
"devDependencies": {
"tsconfig": "workspace:^"
},
"publishConfig": {
"access": "public"
}
}
4 changes: 4 additions & 0 deletions packages/components/slot/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
OkuSlot,
OkuSlottable,
} from './slot'
223 changes: 223 additions & 0 deletions packages/components/slot/src/slot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { h, ref } from 'vue'
import type { Component } from 'vue'
import { OkuSlot, OkuSlottable } from './slot'

const ButtonTest: Component = {
components: {
OkuSlot,
OkuSlottable,
},
inheritAttrs: false,
props: {
asChild: {
type: Boolean,
default: false,
},
},
setup(props, { slots }) {
return () => {
const Tag: any = props.asChild ? OkuSlot : 'button'
return h(
Tag,
null,
[
slots.iconLeft && slots.iconLeft(),
h(OkuSlottable, {}, {
default: () => slots.default && slots.default(),
}),
slots.iconRight && slots.iconRight(),
],
)
}
},

}

describe('given a Button with Slottable', () => {
describe('without asChild', () => {
it('should render a button with icon on the left/right', () => {
const _component: Component = {
components: {
ButtonTest,
},
setup() {
return () => h(ButtonTest, null, {
iconLeft: () => h('span', {}, 'left'),
iconRight: () => h('span', {}, 'right'),
default: () => [
'Button ',
h('em', {}, 'text'),
],
})
},
}

const wrapper = mount(_component)
expect(wrapper.html()).equal('<button><span>left</span>Button <em>text</em><span>right</span></button>')
})
})

describe('with asChild', () => {
it('should render a button with icon on the left/right', () => {
const _component: Component = {
components: {
ButtonTest,
},
setup() {
return () => h(ButtonTest, {
asChild: true,
},
{
iconLeft: () => h('span', {}, 'left'),
iconRight: () => h('span', {}, 'right'),
default: () => h('a', {
href: 'https://oku-ui.com',
}, {
default: () => [
'Button ',
h('em', {}, 'text'),
],
}),
})
},
}

const wrapper = mount(_component)
expect(wrapper.html()).equal('<a href="https://oku-ui.com"><span>left</span>Button <em>text</em><span>right</span></a>')
})
})
})

describe('given a slotted Trigger', () => {
it('with onClick on itself', async () => {
const _component: Component = {
components: {
OkuSlot,
},
setup() {
const click = ref(false)
const onClick = () => {
click.value = true
}
return () => h(OkuSlot, {
onClick,
}, {
default: () => h('button', {
type: 'button',
class: 't',
}, click.value ? 'Clicked' : 'Click me'),
})
},
}

const wrapper = mount(_component)
expect(wrapper.html()).equal('<button type="button" class="t">Click me</button>')
await wrapper.trigger('click')
expect(wrapper.html()).equal('<button type="button" class="t">Clicked</button>')
})

it('with onClick on the child', async () => {
const _component: Component = {
components: {
OkuSlot,
},
setup() {
const click = ref(false)
const onClick = () => {
click.value = true
}
return () => h(OkuSlot, {}, {
default: () => h('button', {
type: 'button',
onClick,
}, click.value ? 'Clicked' : 'Click me'),
})
},
}

const wrapper = mount(_component)
expect(wrapper.html()).equal('<button type="button">Click me</button>')
await wrapper.trigger('click')
expect(wrapper.html()).equal('<button type="button">Clicked</button>')
})

it('with onClick on the child and on itself', async () => {
const _component: Component = {
components: {
OkuSlot,
},
setup() {
const click = ref(false)
const onClick = () => {
click.value = true
}
return () => h(OkuSlot, {
onClick,
}, {
default: () => h('button', {
type: 'button',
onClick,
}, click.value ? 'Clicked' : 'Click me'),
})
},
}

const wrapper = mount(_component)
expect(wrapper.html()).equal('<button type="button">Click me</button>')
await wrapper.trigger('click')
expect(wrapper.html()).equal('<button type="button">Clicked</button>')
})

it('with onClick on itself AND undefined onClick on the child', async () => {
const _component: Component = {
components: {
OkuSlot,
},
setup() {
const click = ref(false)
const onClick = () => {
click.value = true
}
return () => h(OkuSlot, {
onClick,
}, {
default: () => h('button', {
type: 'button',
}, click.value ? 'Clicked' : 'Click me'),
})
},
}

const wrapper = mount(_component)
expect(wrapper.html()).equal('<button type="button">Click me</button>')
await wrapper.trigger('click')
expect(wrapper.html()).equal('<button type="button">Clicked</button>')
})

it('with undefined onClick on itself AND onClick on the child', async () => {
const _component: Component = {
components: {
OkuSlot,
},
setup() {
const click = ref(false)
const onClick = () => {
click.value = true
}
return () => h(OkuSlot, {}, {
default: () => h('button', {
type: 'button',
onClick,
}, click.value ? 'Clicked' : 'Click me'),
})
},
}

const wrapper = mount(_component)
expect(wrapper.html()).equal('<button type="button">Click me</button>')
await wrapper.trigger('click')
expect(wrapper.html()).equal('<button type="button">Clicked</button>')
})
})
55 changes: 55 additions & 0 deletions packages/components/slot/src/slot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { cloneVNode, createVNode, defineComponent, mergeProps } from 'vue'
import { useComposeRefs, useForwardRef } from '@oku-ui/use-composable'
import { isSlottable } from './utils'

const NAME = 'OkuSlot'

const OkuSlot = defineComponent({
name: NAME,
inheritAttrs: false,
setup(props, { attrs, slots }) {
const forwarded = useForwardRef()
const composedRefs = useComposeRefs(forwarded)

return () => {
const defaultSlot = slots.default?.()
const slottable = defaultSlot?.find(isSlottable)

if (slottable && defaultSlot?.length) {
// TODO: default TS type problem
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const newParentElement = slottable.children?.default?.()[0]

const newChildren = defaultSlot.map((child) => {
if (child === slottable)
return newParentElement.children

else return child
})

return createVNode(newParentElement.type, {
...mergeProps(attrs, props, newParentElement.props), ref: composedRefs,
}, {
default: () => newChildren,
})
}
else if (slots.default) {
return cloneVNode(slots.default?.()[0], { ...mergeProps(attrs, props), ref: composedRefs }, true)
}
else {
return null
}
}
},
})

const OkuSlottable = defineComponent({
name: 'OkuSlottable',
inheritAttrs: false,
setup(_, { slots }) {
return slots.default || (() => null) // Ensure it returns a function even if the default slot is not provided
},
})

export { OkuSlot, OkuSlottable }
Loading

0 comments on commit 247d535

Please sign in to comment.