feat(RadioGroup): add card and table variants (#3178)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Romain Hamel
2025-03-31 16:34:31 +02:00
committed by GitHub
parent 615fcfd73b
commit 4d138ad671
7 changed files with 1541 additions and 623 deletions

View File

@@ -133,30 +133,6 @@ props:
--- ---
:: ::
### Orientation
Use the `orientation` prop to change the orientation of the RadioGroup. Defaults to `vertical`.
::component-code
---
prettier: true
ignore:
- defaultValue
- items
external:
- items
externalTypes:
- RadioGroupItem[]
props:
orientation: 'horizontal'
defaultValue: 'System'
items:
- 'System'
- 'Light'
- 'Dark'
---
::
### Color ### Color
Use the `color` prop to change the color of the RadioGroup. Use the `color` prop to change the color of the RadioGroup.
@@ -181,6 +157,35 @@ props:
--- ---
:: ::
### Variant
Use the `variant` prop to change the variant of the RadioGroup.
::component-code
---
prettier: true
ignore:
- defaultValue
- items
external:
- items
props:
color: 'primary'
variant: 'table'
defaultValue: 'pro'
items:
- label: 'Pro'
value: 'pro'
description: 'Tailored for indie hackers, freelancers and solo founders.'
- label: 'Startup'
value: 'startup'
description: 'Best suited for small teams, startups and agencies.'
- label: 'Enterprise'
value: 'enterprise'
description: 'Ideal for larger teams and organizations.'
---
::
### Size ### Size
Use the `size` prop to change the size of the RadioGroup. Use the `size` prop to change the size of the RadioGroup.
@@ -197,6 +202,57 @@ externalTypes:
- RadioGroupItem[] - RadioGroupItem[]
props: props:
size: 'xl' size: 'xl'
variant: 'list'
defaultValue: 'System'
items:
- 'System'
- 'Light'
- 'Dark'
---
::
### Orientation
Use the `orientation` prop to change the orientation of the RadioGroup. Defaults to `vertical`.
::component-code
---
prettier: true
ignore:
- defaultValue
- items
external:
- items
externalTypes:
- RadioGroupItem[]
props:
orientation: 'horizontal'
variant: 'list'
defaultValue: 'System'
items:
- 'System'
- 'Light'
- 'Dark'
---
::
### Indicator
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
::component-code
---
prettier: true
ignore:
- defaultValue
- items
external:
- items
externalTypes:
- RadioGroupItem[]
props:
indicator: 'end'
variant: 'card'
defaultValue: 'System' defaultValue: 'System'
items: items:
- 'System' - 'System'

View File

@@ -2,6 +2,8 @@
import theme from '#build/ui/radio-group' import theme from '#build/ui/radio-group'
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size> const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
const variants = Object.keys(theme.variants.variant)
const variant = ref('list' as const)
const literalOptions = [ const literalOptions = [
'Option 1', 'Option 1',
@@ -23,27 +25,36 @@ const itemsWithDescription = [
<template> <template>
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 ms-[100px]"> <USelect v-model="variant" :items="variants" />
<URadioGroup :items="items" default-value="1" />
<URadioGroup :items="items" color="neutral" default-value="1" /> <div class="flex flex-wrap gap-4 ms-[100px]">
<URadioGroup :items="items" color="error" default-value="2" /> <URadioGroup :variant="variant" :items="items" default-value="1" />
<URadioGroup :items="literalOptions" /> <URadioGroup :variant="variant" :items="items" color="neutral" default-value="1" />
<URadioGroup :items="items" label="Disabled" disabled /> <URadioGroup :variant="variant" :items="items" color="error" default-value="2" />
<URadioGroup :items="items" orientation="horizontal" class="ms-[-91px]" /> <URadioGroup :variant="variant" :items="literalOptions" />
<URadioGroup :variant="variant" :items="items" disabled />
</div> </div>
<div class="flex flex-wrap gap-4 ms-[100px]">
<URadioGroup :variant="variant" :items="items" default-value="3" indicator="start" />
<URadioGroup :variant="variant" :items="items" default-value="3" indicator="end" />
<URadioGroup :variant="variant" :items="items" default-value="3" indicator="hidden" />
</div>
<URadioGroup :variant="variant" :items="items" orientation="horizontal" class="ms-[95px]" />
<div class="flex items-center gap-4 ms-[34px]"> <div class="flex items-center gap-4 ms-[34px]">
<URadioGroup v-for="size in sizes" :key="size" :size="size" :items="items" /> <URadioGroup v-for="size in sizes" :key="size" :size="size" :variant="variant" :items="items" />
</div> </div>
<div class="flex items-center gap-4 ms-[74px]"> <div class="flex items-center gap-4 ms-[74px]">
<URadioGroup v-for="size in sizes" :key="size" :size="size" :items="itemsWithDescription" /> <URadioGroup v-for="size in sizes" :key="size" :size="size" :variant="variant" :items="itemsWithDescription" />
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<URadioGroup :items="items" legend="Legend" /> <URadioGroup :variant="variant" :items="items" legend="Legend" />
<URadioGroup :items="items" legend="Legend" required /> <URadioGroup :variant="variant" :items="items" legend="Legend" required />
<URadioGroup :items="items"> <URadioGroup :variant="variant" :items="items">
<template #legend> <template #legend>
<span class="italic font-bold"> <span class="italic font-bold">
With slots With slots
@@ -56,6 +67,6 @@ const itemsWithDescription = [
</template> </template>
</URadioGroup> </URadioGroup>
</div> </div>
<URadioGroup :items="items" legend="Legend" orientation="horizontal" required /> <URadioGroup :variant="variant" :items="items" legend="Legend" orientation="horizontal" required />
</div> </div>
</template> </template>

View File

@@ -49,6 +49,10 @@ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> exte
* @defaultValue 'md' * @defaultValue 'md'
*/ */
size?: RadioGroupVariants['size'] size?: RadioGroupVariants['size']
/**
* @defaultValue 'list'
*/
variant?: RadioGroupVariants['variant']
/** /**
* @defaultValue 'primary' * @defaultValue 'primary'
*/ */
@@ -58,6 +62,11 @@ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> exte
* @defaultValue 'vertical' * @defaultValue 'vertical'
*/ */
orientation?: RadioGroupRootProps['orientation'] orientation?: RadioGroupRootProps['orientation']
/**
* Position of the indicator.
* @defaultValue 'start'
*/
indicator?: RadioGroupVariants['indicator']
class?: any class?: any
ui?: Partial<typeof radioGroup.slots> ui?: Partial<typeof radioGroup.slots>
} }
@@ -101,7 +110,9 @@ const ui = computed(() => radioGroup({
color: color.value, color: color.value,
disabled: disabled.value, disabled: disabled.value,
required: props.required, required: props.required,
orientation: props.orientation orientation: props.orientation,
variant: props.variant,
indicator: props.indicator
})) }))
function normalizeItem(item: any) { function normalizeItem(item: any) {
@@ -167,7 +178,7 @@ function onUpdate(value: any) {
{{ legend }} {{ legend }}
</slot> </slot>
</legend> </legend>
<div v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })"> <component :is="variant === 'list' ? 'div' : Label" v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })">
<div :class="ui.container({ class: props.ui?.container })"> <div :class="ui.container({ class: props.ui?.container })">
<RadioGroupItem <RadioGroupItem
:id="item.id" :id="item.id"
@@ -180,16 +191,18 @@ function onUpdate(value: any) {
</div> </div>
<div :class="ui.wrapper({ class: props.ui?.wrapper })"> <div :class="ui.wrapper({ class: props.ui?.wrapper })">
<Label :class="ui.label({ class: props.ui?.label })" :for="item.id"> <component :is="variant === 'list' ? Label : 'p'" :class="ui.label({ class: props.ui?.label })" :for="item.id">
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">{{ item.label }}</slot> <slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">
</Label> {{ item.label }}
</slot>
</component>
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })"> <p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)"> <slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
{{ item.description }} {{ item.description }}
</slot> </slot>
</p> </p>
</div> </div>
</div> </component>
</fieldset> </fieldset>
</RadioGroupRoot> </RadioGroupRoot>
</template> </template>

View File

@@ -9,7 +9,7 @@ export default (options: Required<ModuleOptions>) => ({
base: 'rounded-full ring ring-inset ring-(--ui-border-accented) focus-visible:outline-2 focus-visible:outline-offset-2', base: 'rounded-full ring ring-inset ring-(--ui-border-accented) focus-visible:outline-2 focus-visible:outline-offset-2',
indicator: 'flex items-center justify-center size-full rounded-full after:bg-(--ui-bg) after:rounded-full', indicator: 'flex items-center justify-center size-full rounded-full after:bg-(--ui-bg) after:rounded-full',
container: 'flex items-center', container: 'flex items-center',
wrapper: 'ms-2', wrapper: 'w-full',
label: 'block font-medium text-(--ui-text)', label: 'block font-medium text-(--ui-text)',
description: 'text-(--ui-text-muted)' description: 'text-(--ui-text-muted)'
}, },
@@ -24,6 +24,16 @@ export default (options: Required<ModuleOptions>) => ({
indicator: 'bg-(--ui-bg-inverted)' indicator: 'bg-(--ui-bg-inverted)'
} }
}, },
variant: {
list: {
},
card: {
item: 'items-center border border-(--ui-border-muted) rounded-lg'
},
table: {
item: 'border border-(--ui-border-muted)'
}
},
orientation: { orientation: {
horizontal: { horizontal: {
fieldset: 'flex-row', fieldset: 'flex-row',
@@ -33,6 +43,20 @@ export default (options: Required<ModuleOptions>) => ({
fieldset: 'flex-col' fieldset: 'flex-col'
} }
}, },
indicator: {
start: {
item: 'flex-row',
base: 'me-2'
},
end: {
item: 'flex-row-reverse',
base: 'ms-2'
},
hidden: {
base: 'sr-only',
wrapper: 'text-center'
}
},
size: { size: {
xs: { xs: {
fieldset: 'gap-0.5', fieldset: 'gap-0.5',
@@ -87,8 +111,62 @@ export default (options: Required<ModuleOptions>) => ({
} }
} }
}, },
compoundVariants: [
{ size: 'xs', variant: ['card', 'table'], class: { item: 'p-2.5' } },
{ size: 'sm', variant: ['card', 'table'], class: { item: 'p-3' } },
{ size: 'md', variant: ['card', 'table'], class: { item: 'p-3.5' } },
{ size: 'lg', variant: ['card', 'table'], class: { item: 'p-4' } },
{ size: 'xl', variant: ['card', 'table'], class: { item: 'p-4.5' } },
{
orientation: 'horizontal',
variant: 'table',
class: {
item: 'first-of-type:rounded-l-lg last-of-type:rounded-r-lg',
fieldset: 'gap-0 -space-x-px'
}
},
{
orientation: 'vertical',
variant: 'table',
class: {
item: 'first-of-type:rounded-t-lg last-of-type:rounded-b-lg',
fieldset: 'gap-0 -space-y-px'
}
},
...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'card',
class: {
item: `has-data-[state=checked]:border-(--ui-${color})`
}
})),
{
color: 'neutral',
variant: 'card',
class: {
item: 'has-data-[state=checked]:border-(--ui-border-elevated)'
}
},
...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'table',
class: {
item: `has-data-[state=checked]:bg-(--ui-${color})/10 has-data-[state=checked]:border-(--ui-${color})/50 has-data-[state=checked]:z-[1]`
}
})),
{
color: 'neutral',
variant: 'table',
class: {
item: 'has-data-[state=checked]:bg-(--ui-bg-elevated) has-data-[state=checked]:border-(--ui-border-inverted)/25 has-data-[state=checked]:z-[1]'
}
}
],
defaultVariants: { defaultVariants: {
size: 'md', size: 'md',
color: 'primary' color: 'primary',
variant: 'list',
orientation: 'vertical',
indicator: 'start'
} }
}) })

View File

@@ -8,6 +8,8 @@ import type { FormInputEvents } from '~/src/module'
describe('RadioGroup', () => { describe('RadioGroup', () => {
const sizes = Object.keys(theme.variants.size) as any const sizes = Object.keys(theme.variants.size) as any
const variants = Object.keys(theme.variants.variant) as any
const indicators = Object.keys(theme.variants.indicator) as any
const items = [ const items = [
{ value: '1', label: 'Option 1' }, { value: '1', label: 'Option 1' },
@@ -28,8 +30,10 @@ describe('RadioGroup', () => {
['with description', { props: { items: items.map((opt, count) => ({ ...opt, description: `Description ${count}` })) } }], ['with description', { props: { items: items.map((opt, count) => ({ ...opt, description: `Description ${count}` })) } }],
['with required', { props: { ...props, legend: 'Legend', required: true } }], ['with required', { props: { ...props, legend: 'Legend', required: true } }],
...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]), ...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]),
['with color neutral', { props: { color: 'neutral', defaultValue: '1' } }], ...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant, defaultValue: '1' } }]),
['with orientation', { props: { ...props, orientation: 'horizontal' } }], ...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { ...props, variant, color: 'neutral', defaultValue: '1' } }]),
...variants.map((variant: string) => [`with horizontal variant ${variant}`, { props: { ...props, variant, orientation: 'horizontal', defaultValue: '1' } }]),
...indicators.map((indicator: string) => [`with indicator ${indicator}`, { props: { ...props, indicator } }]),
['with ariaLabel', { props, attrs: { 'aria-label': 'Aria label' } }], ['with ariaLabel', { props, attrs: { 'aria-label': 'Aria label' } }],
['with as', { props: { ...props, as: 'section' } }], ['with as', { props: { ...props, as: 'section' } }],
['with class', { props: { ...props, class: 'absolute' } }], ['with class', { props: { ...props, class: 'absolute' } }],

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff