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
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
Use the `size` prop to change the size of the RadioGroup.
@@ -197,6 +202,57 @@ externalTypes:
- RadioGroupItem[]
props:
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'
items:
- 'System'

View File

@@ -2,6 +2,8 @@
import theme from '#build/ui/radio-group'
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 = [
'Option 1',
@@ -23,27 +25,36 @@ const itemsWithDescription = [
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 ms-[100px]">
<URadioGroup :items="items" default-value="1" />
<URadioGroup :items="items" color="neutral" default-value="1" />
<URadioGroup :items="items" color="error" default-value="2" />
<URadioGroup :items="literalOptions" />
<URadioGroup :items="items" label="Disabled" disabled />
<URadioGroup :items="items" orientation="horizontal" class="ms-[-91px]" />
<USelect v-model="variant" :items="variants" />
<div class="flex flex-wrap gap-4 ms-[100px]">
<URadioGroup :variant="variant" :items="items" default-value="1" />
<URadioGroup :variant="variant" :items="items" color="neutral" default-value="1" />
<URadioGroup :variant="variant" :items="items" color="error" default-value="2" />
<URadioGroup :variant="variant" :items="literalOptions" />
<URadioGroup :variant="variant" :items="items" disabled />
</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]">
<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 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 class="flex gap-4">
<URadioGroup :items="items" legend="Legend" />
<URadioGroup :items="items" legend="Legend" required />
<URadioGroup :items="items">
<URadioGroup :variant="variant" :items="items" legend="Legend" />
<URadioGroup :variant="variant" :items="items" legend="Legend" required />
<URadioGroup :variant="variant" :items="items">
<template #legend>
<span class="italic font-bold">
With slots
@@ -56,6 +67,6 @@ const itemsWithDescription = [
</template>
</URadioGroup>
</div>
<URadioGroup :items="items" legend="Legend" orientation="horizontal" required />
<URadioGroup :variant="variant" :items="items" legend="Legend" orientation="horizontal" required />
</div>
</template>

View File

@@ -49,6 +49,10 @@ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> exte
* @defaultValue 'md'
*/
size?: RadioGroupVariants['size']
/**
* @defaultValue 'list'
*/
variant?: RadioGroupVariants['variant']
/**
* @defaultValue 'primary'
*/
@@ -58,6 +62,11 @@ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> exte
* @defaultValue 'vertical'
*/
orientation?: RadioGroupRootProps['orientation']
/**
* Position of the indicator.
* @defaultValue 'start'
*/
indicator?: RadioGroupVariants['indicator']
class?: any
ui?: Partial<typeof radioGroup.slots>
}
@@ -101,7 +110,9 @@ const ui = computed(() => radioGroup({
color: color.value,
disabled: disabled.value,
required: props.required,
orientation: props.orientation
orientation: props.orientation,
variant: props.variant,
indicator: props.indicator
}))
function normalizeItem(item: any) {
@@ -167,7 +178,7 @@ function onUpdate(value: any) {
{{ legend }}
</slot>
</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 })">
<RadioGroupItem
:id="item.id"
@@ -180,16 +191,18 @@ function onUpdate(value: any) {
</div>
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
<Label :class="ui.label({ class: props.ui?.label })" :for="item.id">
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">{{ item.label }}</slot>
</Label>
<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>
</component>
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
{{ item.description }}
</slot>
</p>
</div>
</div>
</component>
</fieldset>
</RadioGroupRoot>
</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',
indicator: 'flex items-center justify-center size-full rounded-full after:bg-(--ui-bg) after:rounded-full',
container: 'flex items-center',
wrapper: 'ms-2',
wrapper: 'w-full',
label: 'block font-medium text-(--ui-text)',
description: 'text-(--ui-text-muted)'
},
@@ -24,6 +24,16 @@ export default (options: Required<ModuleOptions>) => ({
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: {
horizontal: {
fieldset: 'flex-row',
@@ -33,6 +43,20 @@ export default (options: Required<ModuleOptions>) => ({
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: {
xs: {
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: {
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', () => {
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 = [
{ value: '1', label: 'Option 1' },
@@ -28,8 +30,10 @@ describe('RadioGroup', () => {
['with description', { props: { items: items.map((opt, count) => ({ ...opt, description: `Description ${count}` })) } }],
['with required', { props: { ...props, legend: 'Legend', required: true } }],
...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]),
['with color neutral', { props: { color: 'neutral', defaultValue: '1' } }],
['with orientation', { props: { ...props, orientation: 'horizontal' } }],
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant, defaultValue: '1' } }]),
...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 as', { props: { ...props, as: 'section' } }],
['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