Skip to content
This repository was archived by the owner on Sep 25, 2025. It is now read-only.

Commit 3d05447

Browse files
committed
refacor: radiop-group
1 parent 99dc22a commit 3d05447

File tree

5 files changed

+954
-354
lines changed

5 files changed

+954
-354
lines changed

.playground/src/pages/components/radio-group.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ const value = ref()
1717
<template>
1818
<div class="flex flex-col gap-4">
1919
<RadioGroup v-model="value" :items="items" :color="color" />
20+
21+
<RadioGroup v-model="value" :items="items" :color="color" variant="table" />
2022
</div>
2123
</template>

src/components/RadioGroup/RadioGroup.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { describe, expect, it } from 'vitest'
66

77
describe('radioGroup', () => {
88
const sizes = Object.keys(theme.variants.size) as any
9+
const variants = Object.keys(theme.variants.variant) as any
10+
const indicators = Object.keys(theme.variants.indicator) as any
911

1012
const items = [
1113
{ value: '1', label: 'Option 1' },
@@ -25,9 +27,12 @@ describe('radioGroup', () => {
2527
['with disabled', { props: { ...props, disabled: true } }],
2628
['with description', { props: { items: items.map((opt, count) => ({ ...opt, description: `Description ${count}` })) } }],
2729
['with required', { props: { ...props, legend: 'Legend', required: true } }],
28-
...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]),
29-
['with color neutral', { props: { color: 'neutral', defaultValue: '1' } }],
30-
['with orientation', { props: { ...props, orientation: 'horizontal' } }],
30+
...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size, defaultValue: '1' } }]),
31+
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant, defaultValue: '1' } }]),
32+
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { ...props, variant, color: 'neutral', defaultValue: '1' } }]),
33+
...variants.map((variant: string) => [`with horizontal variant ${variant}`, { props: { ...props, variant, orientation: 'horizontal', defaultValue: '1' } }]),
34+
...indicators.map((indicator: string) => [`with indicator ${indicator}`, { props: { ...props, indicator, defaultValue: '1' } }]),
35+
['with ariaLabel', { props, attrs: { 'aria-label': 'Aria label' } }],
3136
['with as', { props: { ...props, as: 'section' } }],
3237
['with class', { props: { ...props, class: 'absolute' } }],
3338
['with ui', { props: { ...props, ui: { wrapper: 'ms-4' } } }],

src/components/RadioGroup/RadioGroup.vue

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,55 @@
11
<script lang="ts">
2-
import type { AcceptableValue, RadioGroupRootEmits, RadioGroupRootProps } from 'reka-ui'
3-
import type { VariantProps } from 'tailwind-variants'
2+
import type { AcceptableValue, ComponentConfig } from '@/ui/types/utils'
3+
import type { RadioGroupRootEmits, RadioGroupRootProps } from 'reka-ui'
44
import { useFormField } from '@/ui/composables/useFormField'
55
import theme from '@/ui/theme/radio-group'
66
import { get } from '@/ui/utils/get'
77
import { reactivePick } from '@vueuse/shared'
8-
import { Label, RadioGroupIndicator, RadioGroupRoot, RadioGroupItem as RRadioGroupItem, useForwardPropsEmits } from 'reka-ui'
8+
import { Label, RadioGroupIndicator, RadioGroupRoot, RadioGroupItem as RekaRadiopGroupItem, useForwardPropsEmits } from 'reka-ui'
99
import { tv } from 'tailwind-variants'
1010
import { computed, useId } from 'vue'
1111
12-
const radioGroup = tv(theme)
12+
type RadioGroup = ComponentConfig<typeof theme>
1313
14-
type RadioGroupVariants = VariantProps<typeof radioGroup>
15-
16-
export interface RadioGroupItem {
14+
export type RadioGroupValue = AcceptableValue
15+
export type RadioGroupItem = {
1716
label?: string
1817
description?: string
1918
disabled?: boolean
2019
value?: string
21-
}
20+
[key: string]: any
21+
} | RadioGroupValue
2222
23-
export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
23+
export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
2424
as?: any
2525
legend?: string
2626
valueKey?: string
2727
labelKey?: string
2828
descriptionKey?: string
2929
items?: T[]
30-
size?: RadioGroupVariants['size']
31-
color?: RadioGroupVariants['color']
32-
/**
33-
* The orientation the radio buttons are laid out.
34-
* @defaultValue 'vertical'
35-
*/
30+
size?: RadioGroup['variants']['size']
31+
variant?: RadioGroup['variants']['variant']
32+
color?: RadioGroup['variants']['color']
3633
orientation?: RadioGroupRootProps['orientation']
34+
indicator?: RadioGroup['variants']['indicator']
3735
class?: any
38-
ui?: Partial<typeof radioGroup.slots>
36+
ui?: RadioGroup['slots']
3937
}
4038
4139
export type RadioGroupEmits = RadioGroupRootEmits & {
4240
change: [payload: Event]
4341
}
4442
45-
type SlotProps<T> = (props: { item: T, modelValue?: AcceptableValue }) => any
43+
type SlotProps<T extends RadioGroupItem> = (props: { item: T & { id: string }, modelValue?: RadioGroupValue }) => any
4644
47-
export interface RadioGroupSlots<T> {
45+
export interface RadioGroupSlots<T extends RadioGroupItem = RadioGroupItem> {
4846
legend: (props?: object) => any
4947
label: SlotProps<T>
5048
description: SlotProps<T>
5149
}
5250
</script>
5351

54-
<script setup lang="ts" generic="T extends RadioGroupItem | AcceptableValue">
52+
<script setup lang="ts" generic="T extends RadioGroupItem">
5553
const props = withDefaults(defineProps<RadioGroupProps<T>>(), {
5654
valueKey: 'value',
5755
labelKey: 'label',
@@ -63,23 +61,33 @@ const slots = defineSlots<RadioGroupSlots<T>>()
6361
6462
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop', 'required'), emits)
6563
66-
const { color, name, size, id: _id, disabled, ariaAttrs } = useFormField<RadioGroupProps<T>>(props)
64+
const { color, name, size, id: _id, disabled, ariaAttrs } = useFormField<RadioGroupProps<T>>(props, { bind: false })
6765
const id = _id.value ?? useId()
6866
69-
const ui = computed(() => radioGroup({
67+
const ui = computed(() => tv(theme)({
7068
size: size.value,
7169
color: color.value,
7270
disabled: disabled.value,
7371
required: props.required,
7472
orientation: props.orientation,
73+
variant: props.variant,
74+
indicator: props.indicator,
7575
}))
7676
7777
function normalizeItem(item: any) {
78-
if (['string', 'number', 'boolean'].includes(typeof item)) {
78+
if (item === null) {
79+
return {
80+
id: `${id}:null`,
81+
value: undefined,
82+
label: undefined,
83+
}
84+
}
85+
86+
if (typeof item === 'string' || typeof item === 'number') {
7987
return {
8088
id: `${id}:${item}`,
81-
value: item,
82-
label: item,
89+
value: String(item),
90+
label: String(item),
8391
}
8492
}
8593
@@ -127,29 +135,32 @@ function onUpdate(value: any) {
127135
{{ legend }}
128136
</slot>
129137
</legend>
130-
<div v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })">
138+
139+
<component :is="variant === 'list' ? 'div' : Label" v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })">
131140
<div :class="ui.container({ class: props.ui?.container })">
132-
<RRadioGroupItem
141+
<RekaRadiopGroupItem
133142
:id="item.id"
134143
:value="item.value"
135-
:disabled="disabled"
136-
:class="ui.base({ class: props.ui?.base })"
144+
:disabled="item.disabled"
145+
:class="ui.base({ class: props.ui?.base, disabled: item.disabled })"
137146
>
138147
<RadioGroupIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
139-
</RRadioGroupItem>
148+
</RekaRadiopGroupItem>
140149
</div>
141150

142-
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
143-
<Label :class="ui.label({ class: props.ui?.label })" :for="item.id">
144-
<slot name="label" :item="item" :model-value="modelValue">{{ item.label }}</slot>
145-
</Label>
151+
<div v-if="(item.label || !!slots.label) || (item.description || !!slots.description)" :class="ui.wrapper({ class: props.ui?.wrapper })">
152+
<component :is="variant === 'list' ? Label : 'p'" v-if="item.label || !!slots.label" :for="item.id" :class="ui.label({ class: props.ui?.label })">
153+
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">
154+
{{ item.label }}
155+
</slot>
156+
</component>
146157
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
147-
<slot name="description" :item="item" :model-value="modelValue">
158+
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
148159
{{ item.description }}
149160
</slot>
150161
</p>
151162
</div>
152-
</div>
163+
</component>
153164
</fieldset>
154165
</RadioGroupRoot>
155166
</template>

0 commit comments

Comments
 (0)