Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
36 changes: 36 additions & 0 deletions docs/content/docs/components/checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ A control that allows the user to toggle between checked and not checked.
'Supports indeterminate state.',
'Full keyboard navigation.',
'Can be controlled or uncontrolled.',
'Supports custom true/false values.',
]"
/>

Expand Down Expand Up @@ -103,6 +104,41 @@ Wrapper around `CheckboxRoot` to support array of `modelValue`

## Examples

### Custom Values

Use the `trueValue` and `falseValue` props to specify custom values for the checked and unchecked states instead of the default `true`/`false`.

```vue line=5-6,11-12
<script setup>
import { Icon } from '@iconify/vue'
import { CheckboxIndicator, CheckboxRoot } from 'reka-ui'

// With string values
const acceptTerms = ref('no')

// With number values
const permission = ref(0)
</script>

<template>
<!-- String values -->
<CheckboxRoot v-model="acceptTerms" true-value="yes" false-value="no">
<CheckboxIndicator>
<Icon icon="radix-icons:check" />
</CheckboxIndicator>
</CheckboxRoot>
<span>Value: {{ acceptTerms }}</span> <!-- "yes" or "no" -->

<!-- Number values -->
<CheckboxRoot v-model="permission" :true-value="1" :false-value="0">
<CheckboxIndicator>
<Icon icon="radix-icons:check" />
</CheckboxIndicator>
</CheckboxRoot>
<span>Value: {{ permission }}</span> <!-- 1 or 0 -->
</template>
```

### Indeterminate

You can set the checkbox to `indeterminate` by taking control of its state.
Expand Down
38 changes: 37 additions & 1 deletion docs/content/docs/components/switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ A control that allows the user to toggle between checked and not checked.
## Features

<Highlights
:features="['Full keyboard navigation.', 'Can be controlled or uncontrolled.']"
:features="[
'Full keyboard navigation.',
'Can be controlled or uncontrolled.',
'Supports custom true/false values.',
]"
/>

## Installation
Expand Down Expand Up @@ -82,6 +86,38 @@ The thumb that is used to visually indicate whether the switch is on or off.
]"
/>

## Examples

### Custom Values

Use the `trueValue` and `falseValue` props to specify custom values for the on and off states instead of the default `true`/`false`.

```vue line=4-5,9-10
<script setup>
import { SwitchRoot, SwitchThumb } from 'reka-ui'

// With string values
const status = ref('inactive')

// With number values
const enabled = ref(0)
</script>

<template>
<!-- String values -->
<SwitchRoot v-model="status" true-value="active" false-value="inactive">
<SwitchThumb />
</SwitchRoot>
<span>Status: {{ status }}</span> <!-- "active" or "inactive" -->

<!-- Number values -->
<SwitchRoot v-model="enabled" :true-value="1" :false-value="0">
<SwitchThumb />
</SwitchRoot>
<span>Enabled: {{ enabled }}</span> <!-- 1 or 0 -->
</template>
```

## Accessibility

Adheres to the [`switch` role requirements](https://www.w3.org/WAI/ARIA/apg/patterns/switch).
Expand Down
45 changes: 32 additions & 13 deletions packages/core/src/Checkbox/CheckboxRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { useVModel } from '@vueuse/core'
import { createContext, isNullish, isValueEqualOrExist, useFormControl, useForwardExpose } from '@/shared'
import { injectCheckboxGroupRootContext } from './CheckboxGroupRoot.vue'

export interface CheckboxRootProps extends PrimitiveProps, FormFieldProps {
export interface CheckboxRootProps<T = boolean> extends PrimitiveProps, FormFieldProps {
/** The value of the checkbox when it is initially rendered. Use when you do not need to control its value. */
defaultValue?: boolean | 'indeterminate'
defaultValue?: T | 'indeterminate'
/** The controlled value of the checkbox. Can be binded with v-model. */
modelValue?: boolean | 'indeterminate' | null
modelValue?: T | 'indeterminate' | null
/** When `true`, prevents the user from interacting with the checkbox */
disabled?: boolean
/**
Expand All @@ -21,11 +21,19 @@ export interface CheckboxRootProps extends PrimitiveProps, FormFieldProps {
value?: AcceptableValue
/** Id of the element */
id?: string
/**
* The value used when the checkbox is checked. Defaults to `true`.
*/
trueValue?: T
/**
* The value used when the checkbox is unchecked. Defaults to `false`.
*/
falseValue?: T
}

export type CheckboxRootEmits = {
export type CheckboxRootEmits<T = boolean> = {
/** Event handler called when the value of the checkbox changes. */
'update:modelValue': [value: boolean | 'indeterminate']
'update:modelValue': [value: T | 'indeterminate']
}

interface CheckboxRootContext {
Expand All @@ -37,7 +45,7 @@ export const [injectCheckboxRootContext, provideCheckboxRootContext]
= createContext<CheckboxRootContext>('CheckboxRoot')
</script>

<script setup lang="ts">
<script setup lang="ts" generic="T = boolean">
import { isEqual } from 'ohash'
import { computed } from 'vue'
import { Primitive } from '@/Primitive'
Expand All @@ -49,12 +57,14 @@ defineOptions({
inheritAttrs: false,
})

const props = withDefaults(defineProps<CheckboxRootProps>(), {
const props = withDefaults(defineProps<CheckboxRootProps<T>>(), {
modelValue: undefined,
value: 'on',
as: 'button',
trueValue: (() => true) as unknown as undefined,
falseValue: (() => false) as unknown as undefined,
})
const emits = defineEmits<CheckboxRootEmits>()
const emits = defineEmits<CheckboxRootEmits<T>>()

defineSlots<{
default?: (props: {
Expand All @@ -69,19 +79,23 @@ const { forwardRef, currentElement } = useForwardExpose()

const checkboxGroupContext = injectCheckboxGroupRootContext(null)

const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue,
const modelValue = useVModel(props as any, 'modelValue', emits as any, {
defaultValue: props.defaultValue ?? props.falseValue,
passive: (props.modelValue === undefined) as false,
}) as Ref<CheckedState>
}) as Ref<T | 'indeterminate'>

const disabled = computed(() => checkboxGroupContext?.disabled.value || props.disabled)

const isChecked = computed(() => modelValue.value === props.trueValue)

const checkboxState = computed<CheckedState>(() => {
if (!isNullish(checkboxGroupContext?.modelValue.value)) {
return isValueEqualOrExist(checkboxGroupContext.modelValue.value, props.value)
}
else {
return modelValue.value === 'indeterminate' ? 'indeterminate' : modelValue.value
if (modelValue.value === 'indeterminate')
return 'indeterminate'
return isChecked.value
}
})

Expand All @@ -98,7 +112,12 @@ function handleClick() {
checkboxGroupContext.modelValue.value = modelValueArray
}
else {
modelValue.value = isIndeterminate(modelValue.value) ? true : !modelValue.value
if (modelValue.value === 'indeterminate') {
modelValue.value = props.trueValue as T
}
else {
modelValue.value = isChecked.value ? props.falseValue as T : props.trueValue as T
}
}
}

Expand Down
54 changes: 54 additions & 0 deletions packages/core/src/Checkbox/story/Checkbox.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { CheckboxIndicator, CheckboxRoot } from '..'

const checkboxOne = ref<boolean | 'indeterminate'>('indeterminate')
const checkboxThree = ref(false)
const customStringState = ref<'yes' | 'no'>('no')
const customNumberState = ref<1 | 0>(0)
</script>

<template>
Expand Down Expand Up @@ -77,5 +79,57 @@ const checkboxThree = ref(false)
</label>
</div>
</Variant>

<Variant title="Custom string values">
<div class="flex flex-col gap-4">
<label
class="flex flex-row gap-4 items-center"
>
<CheckboxRoot
v-model="customStringState"
true-value="yes"
false-value="no"
class="shadow-blackA7 hover:bg-violet3 flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-[4px] bg-white shadow-[0_2px_10px] outline-none focus-within:shadow-[0_0_0_2px_black]"
>
<CheckboxIndicator
class="bg-white h-full w-full rounded flex items-center justify-center"
>
<Icon
icon="radix-icons:check"
class="h-4 w-4 text-black"
/>
</CheckboxIndicator>
</CheckboxRoot>
<span class="select-none">Accept terms</span>
</label>
<span class="text-sm">v-model value: "{{ customStringState }}"</span>
</div>
</Variant>

<Variant title="Custom number values">
<div class="flex flex-col gap-4">
<label
class="flex flex-row gap-4 items-center"
>
<CheckboxRoot
v-model="customNumberState"
:true-value="1"
:false-value="0"
class="shadow-blackA7 hover:bg-violet3 flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-[4px] bg-white shadow-[0_2px_10px] outline-none focus-within:shadow-[0_0_0_2px_black]"
>
<CheckboxIndicator
class="bg-white h-full w-full rounded flex items-center justify-center"
>
<Icon
icon="radix-icons:check"
class="h-4 w-4 text-black"
/>
</CheckboxIndicator>
</CheckboxRoot>
<span class="select-none">Enable feature</span>
</label>
<span class="text-sm">v-model value: {{ customNumberState }}</span>
</div>
</Variant>
</Story>
</template>
52 changes: 52 additions & 0 deletions packages/core/src/Switch/Switch.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ref } from 'vue'
import { SwitchRoot, SwitchThumb } from '.'

const switchState = ref(true)
const customStringState = ref<'on' | 'off'>('off')
const customNumberState = ref<1 | 0>(0)
</script>

<template>
Expand All @@ -29,5 +31,55 @@ const switchState = ref(true)
</SwitchRoot>
</div>
</Variant>

<Variant title="Custom string values">
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<label
class="text-white text-[15px] leading-none pr-[15px]"
for="custom-string"
>
Status
</label>
<SwitchRoot
id="custom-string"
v-model="customStringState"
true-value="on"
false-value="off"
class="w-[42px] h-[25px] focus-within:outline focus-within:outline-black flex bg-black/50 shadow-sm rounded-full relative data-[state=checked]:bg-black cursor-default"
>
<SwitchThumb
class="block w-[21px] h-[21px] my-auto bg-white shadow-sm rounded-full transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]"
/>
</SwitchRoot>
</div>
<span class="text-white text-sm">v-model value: "{{ customStringState }}"</span>
</div>
</Variant>

<Variant title="Custom number values">
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<label
class="text-white text-[15px] leading-none pr-[15px]"
for="custom-number"
>
Permission
</label>
<SwitchRoot
id="custom-number"
v-model="customNumberState"
:true-value="1"
:false-value="0"
class="w-[42px] h-[25px] focus-within:outline focus-within:outline-black flex bg-black/50 shadow-sm rounded-full relative data-[state=checked]:bg-black cursor-default"
>
<SwitchThumb
class="block w-[21px] h-[21px] my-auto bg-white shadow-sm rounded-full transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]"
/>
</SwitchRoot>
</div>
<span class="text-white text-sm">v-model value: {{ customNumberState }}</span>
</div>
</Variant>
</Story>
</template>
Loading