|
| 1 | +<script lang="ts"> |
| 2 | +import type { CheckboxProps } from '@/ui/components/Checkbox/Checkbox.vue' |
| 3 | +import type { AcceptableValue, ComponentConfig } from '@/ui/types/utils' |
| 4 | +import type { CheckboxGroupRootEmits, CheckboxGroupRootProps } from 'reka-ui' |
| 5 | +import Checkbox from '@/ui/components/Checkbox/Checkbox.vue' |
| 6 | +import { useFormField } from '@/ui/composables/useFormField' |
| 7 | +import theme from '@/ui/theme/checkbox-group' |
| 8 | +import { get } from '@/ui/utils/get' |
| 9 | +import { omit } from '@/ui/utils/omit' |
| 10 | +import { reactivePick } from '@vueuse/shared' |
| 11 | +import { CheckboxGroupRoot, useForwardProps, useForwardPropsEmits } from 'reka-ui' |
| 12 | +import { tv } from 'tailwind-variants' |
| 13 | +import { computed, useId } from 'vue' |
| 14 | +
|
| 15 | +type CheckboxGroup = ComponentConfig<typeof theme> |
| 16 | +
|
| 17 | +export type CheckboxGroupValue = AcceptableValue |
| 18 | +
|
| 19 | +export type CheckboxGroupItem = { |
| 20 | + label?: string |
| 21 | + description?: string |
| 22 | + disabled?: boolean |
| 23 | + value?: string |
| 24 | + [key: string]: any |
| 25 | +} | CheckboxGroupValue |
| 26 | +
|
| 27 | +export interface CheckboxGroupProps<T extends CheckboxGroupItem = CheckboxGroupItem> extends Pick<CheckboxGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'>, Pick<CheckboxProps, 'color' | 'variant' | 'indicator' | 'icon'> { |
| 28 | + as?: any |
| 29 | + legend?: string |
| 30 | + valueKey?: string |
| 31 | + labelKey?: string |
| 32 | + descriptionKey?: string |
| 33 | + items?: T[] |
| 34 | + size?: CheckboxGroup['variants']['size'] |
| 35 | + orientation?: CheckboxGroupRootProps['orientation'] |
| 36 | + class?: any |
| 37 | + ui?: CheckboxGroup['slots'] & CheckboxProps['ui'] |
| 38 | +} |
| 39 | +
|
| 40 | +export type CheckboxGroupEmits = CheckboxGroupRootEmits & { |
| 41 | + change: [payload: Event] |
| 42 | +} |
| 43 | +
|
| 44 | +type SlotProps<T extends CheckboxGroupItem> = (props: { item: T & { id: string } }) => any |
| 45 | +
|
| 46 | +export interface CheckboxGroupSlots<T extends CheckboxGroupItem = CheckboxGroupItem> { |
| 47 | + legend: (props?: object) => any |
| 48 | + label: SlotProps<T> |
| 49 | + description: SlotProps<T> |
| 50 | +} |
| 51 | +</script> |
| 52 | + |
| 53 | +<script setup lang="ts" generic="T extends CheckboxGroupItem"> |
| 54 | +const props = withDefaults(defineProps<CheckboxGroupProps<T>>(), { |
| 55 | + valueKey: 'value', |
| 56 | + labelKey: 'label', |
| 57 | + descriptionKey: 'description', |
| 58 | + orientation: 'vertical', |
| 59 | +}) |
| 60 | +const emits = defineEmits<CheckboxGroupEmits>() |
| 61 | +const slots = defineSlots<CheckboxGroupSlots<T>>() |
| 62 | +
|
| 63 | +const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop', 'required'), emits) |
| 64 | +const checkboxProps = useForwardProps(reactivePick(props, 'variant', 'indicator', 'icon')) |
| 65 | +const proxySlots = omit(slots, ['legend']) |
| 66 | +
|
| 67 | +const { color, name, size, id: _id, disabled, ariaAttrs } = useFormField<CheckboxGroupProps<T>>(props, { bind: false }) |
| 68 | +const id = _id.value ?? useId() |
| 69 | +
|
| 70 | +const ui = computed(() => tv(theme)({ |
| 71 | + size: size.value, |
| 72 | + required: props.required, |
| 73 | + orientation: props.orientation, |
| 74 | +})) |
| 75 | +
|
| 76 | +function normalizeItem(item: any) { |
| 77 | + if (item === null) { |
| 78 | + return { |
| 79 | + id: `${id}:null`, |
| 80 | + value: undefined, |
| 81 | + label: undefined, |
| 82 | + } |
| 83 | + } |
| 84 | +
|
| 85 | + if (typeof item === 'string' || typeof item === 'number') { |
| 86 | + return { |
| 87 | + id: `${id}:${item}`, |
| 88 | + value: String(item), |
| 89 | + label: String(item), |
| 90 | + } |
| 91 | + } |
| 92 | +
|
| 93 | + const value = get(item, props.valueKey as string) |
| 94 | + const label = get(item, props.labelKey as string) |
| 95 | + const description = get(item, props.descriptionKey as string) |
| 96 | +
|
| 97 | + return { |
| 98 | + ...item, |
| 99 | + value, |
| 100 | + label, |
| 101 | + description, |
| 102 | + id: `${id}:${value}`, |
| 103 | + } |
| 104 | +} |
| 105 | +
|
| 106 | +const normalizedItems = computed(() => { |
| 107 | + if (!props.items) { |
| 108 | + return [] |
| 109 | + } |
| 110 | + return props.items.map(normalizeItem) |
| 111 | +}) |
| 112 | +
|
| 113 | +function onUpdate(value: any) { |
| 114 | + // @ts-expect-error - 'target' does not exist in type 'EventInit' |
| 115 | + const event = new Event('change', { target: { value } }) |
| 116 | + emits('change', event) |
| 117 | +} |
| 118 | +</script> |
| 119 | + |
| 120 | +<!-- eslint-disable vue/no-template-shadow --> |
| 121 | +<template> |
| 122 | + <CheckboxGroupRoot |
| 123 | + :id="id" |
| 124 | + v-bind="rootProps" |
| 125 | + :name="name" |
| 126 | + :disabled="disabled" |
| 127 | + :class="ui.root({ class: [props.class, props.ui?.root] })" |
| 128 | + @update:model-value="onUpdate" |
| 129 | + > |
| 130 | + <fieldset :class="ui.fieldset({ class: props.ui?.fieldset })" v-bind="ariaAttrs"> |
| 131 | + <legend v-if="legend || !!slots.legend" :class="ui.legend({ class: props.ui?.legend })"> |
| 132 | + <slot name="legend"> |
| 133 | + {{ legend }} |
| 134 | + </slot> |
| 135 | + </legend> |
| 136 | + |
| 137 | + <Checkbox |
| 138 | + v-for="item in normalizedItems" |
| 139 | + :key="item.value" |
| 140 | + v-bind="{ ...item, ...checkboxProps }" |
| 141 | + :color="color" |
| 142 | + :size="size" |
| 143 | + :name="name" |
| 144 | + :disabled="item.disabled || disabled" |
| 145 | + :ui="props.ui ? omit(props.ui, ['root']) : undefined" |
| 146 | + :class="ui.item({ class: props.ui?.item })" |
| 147 | + > |
| 148 | + <template v-for="(_, name) in proxySlots" #[name]> |
| 149 | + <slot :name="(name as keyof CheckboxGroupSlots<T>)" :item="item" /> |
| 150 | + </template> |
| 151 | + </Checkbox> |
| 152 | + </fieldset> |
| 153 | + </CheckboxGroupRoot> |
| 154 | +</template> |
0 commit comments