Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 5 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
28 changes: 28 additions & 0 deletions docs/content/3.components/radio-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,34 @@ props:
---
::

### Variant

Use the `variant` prop to change the variant of the RadioGroup.

::component-code
---
prettier: true
ignore:
- defaultValue
- items
external:
- items
props:
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.'
---
::

### Legend

Use the `legend` prop to set the legend of the RadioGroup.
Expand Down
31 changes: 18 additions & 13 deletions playground/app/pages/components/radio-group.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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('radio' as const)

const literalOptions = [
'Option 1',
Expand All @@ -23,27 +25,30 @@ 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" label="Disabled" disabled />
</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
Expand All @@ -56,6 +61,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>
60 changes: 36 additions & 24 deletions src/runtime/components/RadioGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultVa
* @defaultValue 'vertical'
*/
orientation?: RadioGroupRootProps['orientation']
variant?: RadioGroupVariants['variant']
class?: any
ui?: Partial<typeof radioGroup.slots>
}
Expand Down Expand Up @@ -85,7 +86,9 @@ const props = withDefaults(defineProps<RadioGroupProps<T>>(), {
const emits = defineEmits<RadioGroupEmits>()
const slots = defineSlots<RadioGroupSlots<T>>()

const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop', 'required'), emits)
const modelValue = defineModel<AcceptableValue>()

const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultValue', 'orientation', 'loop', 'required'), emits)

const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs } = useFormField<RadioGroupProps<T>>(props, { bind: false })
const id = _id.value ?? useId()
Expand All @@ -95,7 +98,8 @@ const ui = computed(() => radioGroup({
color: color.value,
disabled: disabled.value,
required: props.required,
orientation: props.orientation
orientation: props.orientation,
variant: props.variant
}))

function normalizeItem(item: any) {
Expand Down Expand Up @@ -140,8 +144,8 @@ function onUpdate(value: any) {
<template>
<RadioGroupRoot
:id="id"
v-slot="{ modelValue }"
v-bind="rootProps"
:model-value="modelValue"
:name="name"
:disabled="disabled"
:class="ui.root({ class: [props.class, props.ui?.root] })"
Expand All @@ -153,27 +157,35 @@ function onUpdate(value: any) {
{{ legend }}
</slot>
</legend>
<div 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"
:value="item.value"
:disabled="disabled"
:class="ui.base({ class: props.ui?.base })"
>
<RadioGroupIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
</RadioGroupItem>
</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">{{ item.label }}</slot>
</Label>
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
<slot name="description" :item="item" :model-value="modelValue">
{{ item.description }}
</slot>
</p>
<div :class="ui.itemWrapper({ class: props.ui?.item })">
<div
v-for="item in normalizedItems"
:key="item.value"
:class="ui.item({ class: props.ui?.item })"
:data-checked="item.value === modelValue || !modelValue && item.value === defaultValue"
@click.prevent="modelValue = disabled ? modelValue : item.value"
>
<div :class="ui.container({ class: props.ui?.container })">
<RadioGroupItem
:id="item.id"
:value="item.value"
:disabled="disabled"
:class="ui.base({ class: props.ui?.base })"
>
<RadioGroupIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
</RadioGroupItem>
</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">{{ item.label }}</slot>
</Label>
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
<slot name="description" :item="item" :model-value="modelValue">
{{ item.description }}
</slot>
</p>
</div>
</div>
</div>
</fieldset>
Expand Down
60 changes: 56 additions & 4 deletions src/theme/radio-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'relative',
fieldset: 'flex',
fieldset: '',
legend: 'mb-1 block font-medium text-[var(--ui-text)]',
item: 'flex items-start',
itemWrapper: 'flex',
base: 'rounded-full ring ring-inset ring-[var(--ui-border-accented)] focus-visible:outline-2 focus-visible:outline-offset-2',
indicator: 'flex items-center justify-center size-full rounded-full after:bg-[var(--ui-bg)] after:rounded-full',
container: 'flex items-center',
Expand All @@ -24,13 +25,23 @@ export default (options: Required<ModuleOptions>) => ({
indicator: 'bg-[var(--ui-bg-inverted)]'
}
},
variant: {
radio: {},
card: {
base: 'ml-4',
item: 'flex-row-reverse items-center justify-between border-2 border-[var(--ui-border-muted)] rounded-lg'
},
table: {
item: 'border-[var(--ui-border-muted)]'
}
},
orientation: {
horizontal: {
fieldset: 'flex-row',
itemWrapper: 'flex-row',
wrapper: 'me-2'
},
vertical: {
fieldset: 'flex-col'
itemWrapper: 'flex-col'
}
},
size: {
Expand Down Expand Up @@ -87,8 +98,49 @@ export default (options: Required<ModuleOptions>) => ({
}
}
},
compoundVariants: [
{ size: 'xs', variant: 'card', class: { item: 'p-2.5', itemWrapper: 'gap-2' } },
{ size: 'sm', variant: 'card', class: { item: 'p-3', itemWrapper: 'gap-2.5' } },
{ size: 'md', variant: 'card', class: { item: 'p-3.5', itemWrapper: 'gap-2.5' } },
{ size: 'lg', variant: 'card', class: { item: 'p-4', itemWrapper: 'gap-3.5' } },
{ size: 'xl', variant: 'card', class: { item: 'p-4.5', itemWrapper: 'gap-3.5' } },

{ size: 'xs', variant: 'table', class: { item: 'p-2.5', itemWrapper: 'gap-0' } },
{ size: 'sm', variant: 'table', class: { item: 'p-3', itemWrapper: 'gap-0' } },
{ size: 'md', variant: 'table', class: { item: 'p-3.5', itemWrapper: 'gap-0' } },
{ size: 'lg', variant: 'table', class: { item: 'p-4', itemWrapper: 'gap-0' } },
{ size: 'xl', variant: 'table', class: { item: 'p-4.5', itemWrapper: 'gap-0' } },

{ orientation: 'horizontal', variant: 'table', class: { item: 'first:rounded-l-lg last:rounded-r-lg not-first:not-last:border-x-2 first:border-l-2 border-y-2 last:border-r-2' } },
{ orientation: 'vertical', variant: 'table', class: { item: 'first:rounded-t-lg last:rounded-b-lg not-first:not-last:border-y-2 first:border-t-2 border-x-2 last:border-b-2' } },

...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'card',
class: {
item: `data-[checked=true]:border-[var(--ui-${color})]`
}
})),

{
color: 'neutral',
variant: 'card',
class: {
item: 'data-[checked=true]:border-[var(--ui-border-elevated)]'
}
},

...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'table',
class: {
item: `data-[checked=true]:bg-[var(--ui-${color})]/20 data-[checked=true]:border-[var(--ui-${color})]/20`
}
}))
],
defaultVariants: {
size: 'md',
color: 'primary'
color: 'primary',
variant: 'radio'
}
})
2 changes: 2 additions & 0 deletions test/components/RadioGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe('RadioGroup', () => {
...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]),
['with color neutral', { props: { color: 'neutral', defaultValue: '1' } }],
['with orientation', { props: { ...props, orientation: 'horizontal' } }],
['with variant card', { props: { ...props, variant: 'card' } }],
['with variant table', { props: { ...props, variant: 'table' } }],
['with as', { props: { ...props, as: 'section' } }],
['with class', { props: { ...props, class: 'absolute' } }],
['with ui', { props: { ...props, ui: { wrapper: 'ms-4' } } }],
Expand Down
Loading