Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions src/runtime/components/Checkbox.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { CheckboxRootProps } from 'reka-ui'
import type { CheckboxRootProps, CheckboxRootEmits } from 'reka-ui'
import type { VNode } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/checkbox'
Expand All @@ -9,7 +9,7 @@ import type { ComponentConfig } from '../types/tv'

type Checkbox = ComponentConfig<typeof theme, AppConfig, 'checkbox'>

export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'required' | 'name' | 'value' | 'id' | 'defaultValue'>, /** @vue-ignore */ Omit<ButtonHTMLAttributes, 'type' | 'disabled' | 'name'> {
export interface CheckboxProps<T = boolean> extends Pick<CheckboxRootProps<T>, 'disabled' | 'required' | 'name' | 'value' | 'id' | 'defaultValue' | 'modelValue' | 'trueValue' | 'falseValue'>, /** @vue-ignore */ Omit<ButtonHTMLAttributes, 'type' | 'disabled' | 'name'> {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/**
* The element or component this component should render as.
* @defaultValue 'div'
Expand Down Expand Up @@ -50,7 +50,7 @@ export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'req
ui?: Checkbox['slots']
}

export type CheckboxEmits = {
export interface CheckboxEmits<T = boolean> extends CheckboxRootEmits<T> {
change: [event: Event]
}
Comment thread
benjamincanac marked this conversation as resolved.

Expand All @@ -60,9 +60,9 @@ export interface CheckboxSlots {
}
</script>

<script setup lang="ts">
<script setup lang="ts" generic="T = boolean">
import { computed, useAttrs, useId } from 'vue'
import { Primitive, CheckboxRoot, CheckboxIndicator, Label, useForwardProps } from 'reka-ui'
import { Primitive, CheckboxRoot, CheckboxIndicator, Label, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useComponentUI } from '../composables/useComponentUI'
Expand All @@ -72,18 +72,16 @@ import UIcon from './Icon.vue'

defineOptions({ inheritAttrs: false })

const props = defineProps<CheckboxProps>()
const props = defineProps<CheckboxProps<T>>()
const slots = defineSlots<CheckboxSlots>()
const emits = defineEmits<CheckboxEmits>()

const modelValue = defineModel<boolean | 'indeterminate'>({ default: undefined })
const emits = defineEmits<CheckboxEmits<T>>()

const appConfig = useAppConfig() as Checkbox['AppConfig']
const uiProp = useComponentUI('checkbox', props)

const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue'))
const rootProps = useForwardPropsEmits(reactivePick(props, 'required', 'value', 'defaultValue', 'modelValue', 'trueValue', 'falseValue'), emits)

const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<CheckboxProps>(props)
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<CheckboxProps<T>>(props)
const id = _id.value ?? useId()

const attrs = useAttrs()
Expand Down Expand Up @@ -118,16 +116,15 @@ function onUpdate(value: any) {
<CheckboxRoot
:id="id"
v-bind="{ ...rootProps, ...forwardedAttrs, ...ariaAttrs }"
v-model="modelValue"
:name="name"
:disabled="disabled"
data-slot="base"
:class="ui.base({ class: uiProp?.base })"
@update:model-value="onUpdate"
>
<template #default="{ modelValue }">
<template #default="{ state }">
<CheckboxIndicator data-slot="indicator" :class="ui.indicator({ class: uiProp?.indicator })">
<UIcon v-if="modelValue === 'indeterminate'" :name="indeterminateIcon || appConfig.ui.icons.minus" data-slot="icon" :class="ui.icon({ class: uiProp?.icon })" />
<UIcon v-if="state === 'indeterminate'" :name="indeterminateIcon || appConfig.ui.icons.minus" data-slot="icon" :class="ui.icon({ class: uiProp?.icon })" />
<UIcon v-else :name="icon || appConfig.ui.icons.check" data-slot="icon" :class="ui.icon({ class: uiProp?.icon })" />
</CheckboxIndicator>
</template>
Expand Down
21 changes: 9 additions & 12 deletions src/runtime/components/Switch.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { SwitchRootProps } from 'reka-ui'
import type { SwitchRootProps, SwitchRootEmits } from 'reka-ui'
import type { VNode } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/switch'
Expand All @@ -9,7 +9,7 @@ import type { ComponentConfig } from '../types/tv'

type Switch = ComponentConfig<typeof theme, AppConfig, 'switch'>

export interface SwitchProps extends Pick<SwitchRootProps, 'disabled' | 'id' | 'name' | 'required' | 'value' | 'defaultValue'>, /** @vue-ignore */ Omit<ButtonHTMLAttributes, 'type' | 'disabled' | 'name'> {
export interface SwitchProps<T = boolean> extends Pick<SwitchRootProps<T>, 'disabled' | 'id' | 'name' | 'required' | 'value' | 'defaultValue' | 'modelValue' | 'trueValue' | 'falseValue'>, /** @vue-ignore */ Omit<ButtonHTMLAttributes, 'type' | 'disabled' | 'name'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
Expand Down Expand Up @@ -47,7 +47,7 @@ export interface SwitchProps extends Pick<SwitchRootProps, 'disabled' | 'id' | '
ui?: Switch['slots']
}

export type SwitchEmits = {
export interface SwitchEmits<T = boolean> extends SwitchRootEmits<T> {
change: [event: Event]
}
Comment thread
benjamincanac marked this conversation as resolved.

Expand All @@ -57,9 +57,9 @@ export interface SwitchSlots {
}
</script>

<script setup lang="ts">
<script setup lang="ts" generic="T = boolean">
import { computed, useAttrs, useId } from 'vue'
import { Primitive, SwitchRoot, SwitchThumb, useForwardProps, Label } from 'reka-ui'
import { Primitive, SwitchRoot, SwitchThumb, useForwardPropsEmits, Label } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useComponentUI } from '../composables/useComponentUI'
Expand All @@ -69,18 +69,16 @@ import UIcon from './Icon.vue'

defineOptions({ inheritAttrs: false })

const props = defineProps<SwitchProps>()
const props = defineProps<SwitchProps<T>>()
const slots = defineSlots<SwitchSlots>()
const emits = defineEmits<SwitchEmits>()

const modelValue = defineModel<boolean>({ default: undefined })
const emits = defineEmits<SwitchEmits<T>>()

const appConfig = useAppConfig() as Switch['AppConfig']
const uiProp = useComponentUI('switch', props)

const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue'))
const rootProps = useForwardPropsEmits(reactivePick(props, 'required', 'value', 'defaultValue', 'modelValue', 'trueValue', 'falseValue'), emits)

const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<SwitchProps>(props)
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<SwitchProps<T>>(props)
const id = _id.value ?? useId()

const attrs = useAttrs()
Expand Down Expand Up @@ -113,7 +111,6 @@ function onUpdate(value: any) {
<SwitchRoot
:id="id"
v-bind="{ ...rootProps, ...forwardedAttrs, ...ariaAttrs }"
v-model="modelValue"
:name="name"
:disabled="disabled || loading"
data-slot="base"
Expand Down
20 changes: 20 additions & 0 deletions test/components/Checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ describe('Checkbox', () => {
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { variant, color: 'neutral', defaultValue: true } }]),
...indicators.map((indicator: string) => [`with indicator ${indicator}`, { props: { indicator, defaultValue: true } }]),
['with ariaLabel', { attrs: { 'aria-label': 'Aria label' } }],
['with trueValue/falseValue as string', { props: { trueValue: 'yes', falseValue: 'no', defaultValue: 'yes' } }],
['with trueValue/falseValue as number', { props: { trueValue: 1, falseValue: 0, defaultValue: 1 } }],
['with trueValue/falseValue unchecked', { props: { trueValue: 'yes', falseValue: 'no', defaultValue: 'no' } }],
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'inline-flex' } }],
['with ui', { props: { ui: { wrapper: 'ms-4' } } }],
Expand Down Expand Up @@ -64,6 +67,23 @@ describe('Checkbox', () => {
await input.vm.$emit('update:modelValue', false)
expect(wrapper.emitted()).toMatchObject({ change: [[{ type: 'change' }]] })
})

test('toggle with custom trueValue/falseValue via click', async () => {
const wrapper = mount(Checkbox, {
props: { trueValue: 'yes', falseValue: 'no', defaultValue: 'no' }
})
const button = wrapper.find('button')

await button.trigger('click')
await flushPromises()
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['yes'])
expect(wrapper.emitted('change')).toHaveLength(1)

await button.trigger('click')
await flushPromises()
expect(wrapper.emitted('update:modelValue')?.[1]).toEqual(['no'])
expect(wrapper.emitted('change')).toHaveLength(2)
})
})

describe('form integration', async () => {
Expand Down
20 changes: 20 additions & 0 deletions test/components/Switch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ describe('Switch', () => {
...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
['with color neutral', { props: { color: 'neutral', defaultValue: true } }],
['with ariaLabel', { attrs: { 'aria-label': 'Aria label' } }],
['with trueValue/falseValue as string', { props: { trueValue: 'on', falseValue: 'off', defaultValue: 'on' } }],
['with trueValue/falseValue as number', { props: { trueValue: 1, falseValue: 0, defaultValue: 1 } }],
['with trueValue/falseValue unchecked', { props: { trueValue: 'on', falseValue: 'off', defaultValue: 'off' } }],
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'inline-flex' } }],
['with ui', { props: { ui: { wrapper: 'ms-4' } } }],
Expand Down Expand Up @@ -63,6 +66,23 @@ describe('Switch', () => {
await input.vm.$emit('update:modelValue', true)
expect(wrapper.emitted()).toMatchObject({ change: [[{ type: 'change' }]] })
})

test('toggle with custom trueValue/falseValue via click', async () => {
const wrapper = mount(Switch, {
props: { trueValue: 'on', falseValue: 'off', defaultValue: 'off' }
})
const button = wrapper.find('button')

await button.trigger('click')
await flushPromises()
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['on'])
expect(wrapper.emitted('change')).toHaveLength(1)

await button.trigger('click')
await flushPromises()
expect(wrapper.emitted('update:modelValue')?.[1]).toEqual(['off'])
expect(wrapper.emitted('change')).toHaveLength(2)
})
})

describe('form integration', async () => {
Expand Down
8 changes: 4 additions & 4 deletions test/components/Table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ describe('Table', () => {

const columns: TableColumn<typeof data[number]>[] = [{
id: 'select',
header: ({ table }) => h(UCheckbox, {
header: ({ table }) => h(UCheckbox<boolean>, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate' | undefined) => table.toggleAllPageRowsSelected(!!value),
'onUpdate:modelValue': value => table.toggleAllPageRowsSelected(!!value),
'label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
cell: ({ row }) => h(UCheckbox<boolean>, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate' | undefined) => row.toggleSelected(!!value),
'onUpdate:modelValue': value => row.toggleSelected(!!value),
'aria-label': 'Select row'
}),
enableSorting: false,
Expand Down
28 changes: 28 additions & 0 deletions test/components/__snapshots__/Checkbox-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,34 @@ exports[`Checkbox > renders with size xs correctly 1`] = `
</div>"
`;

exports[`Checkbox > renders with trueValue/falseValue as number correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0" role="checkbox" type="button" aria-checked="true" aria-required="false" data-state="checked"><span data-state="checked" style="pointer-events: none;" data-slot="indicator" class="flex items-center justify-center size-full text-inverted bg-primary"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="icon" class="shrink-0 size-full"></svg></span>
<!--v-if-->
</button></div>
<!--v-if-->
</div>"
`;

exports[`Checkbox > renders with trueValue/falseValue as string correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0" role="checkbox" type="button" aria-checked="true" aria-required="false" data-state="checked"><span data-state="checked" style="pointer-events: none;" data-slot="indicator" class="flex items-center justify-center size-full text-inverted bg-primary"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="icon" class="shrink-0 size-full"></svg></span>
<!--v-if-->
</button></div>
<!--v-if-->
</div>"
`;

exports[`Checkbox > renders with trueValue/falseValue unchecked correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0" role="checkbox" type="button" aria-checked="false" aria-required="false" data-state="unchecked">
<!---->
<!--v-if-->
</button></div>
<!--v-if-->
</div>"
`;

exports[`Checkbox > renders with ui correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0" role="checkbox" type="button" aria-checked="false" aria-required="false" data-state="unchecked">
Expand Down
28 changes: 28 additions & 0 deletions test/components/__snapshots__/Checkbox.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,34 @@ exports[`Checkbox > renders with size xs correctly 1`] = `
</div>"
`;

exports[`Checkbox > renders with trueValue/falseValue as number correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0-0" role="checkbox" type="button" aria-checked="true" aria-required="false" data-state="checked"><span data-state="checked" style="pointer-events: none;" data-slot="indicator" class="flex items-center justify-center size-full text-inverted bg-primary"><span class="iconify i-lucide:check shrink-0 size-full" aria-hidden="true" data-slot="icon"></span></span>
<!--v-if-->
</button></div>
<!--v-if-->
</div>"
`;

exports[`Checkbox > renders with trueValue/falseValue as string correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0-0" role="checkbox" type="button" aria-checked="true" aria-required="false" data-state="checked"><span data-state="checked" style="pointer-events: none;" data-slot="indicator" class="flex items-center justify-center size-full text-inverted bg-primary"><span class="iconify i-lucide:check shrink-0 size-full" aria-hidden="true" data-slot="icon"></span></span>
<!--v-if-->
</button></div>
<!--v-if-->
</div>"
`;

exports[`Checkbox > renders with trueValue/falseValue unchecked correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0-0" role="checkbox" type="button" aria-checked="false" aria-required="false" data-state="unchecked">
<!---->
<!--v-if-->
</button></div>
<!--v-if-->
</div>"
`;

exports[`Checkbox > renders with ui correctly 1`] = `
"<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0-0" role="checkbox" type="button" aria-checked="false" aria-required="false" data-state="unchecked">
Expand Down
Loading
Loading