feat(Button): handle active state

Resolves #3417
This commit is contained in:
Benjamin Canac
2025-03-05 18:51:16 +01:00
parent e3ce1f7a41
commit bd2d4848d2
6 changed files with 258 additions and 33 deletions

View File

@@ -29,23 +29,6 @@ props:
---
::
### Link
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
::component-code
---
ignore:
- label
- target
props:
to: https://github.com/nuxt/ui
target: _blank
slots:
default: Button
---
::
### Color
Use the `color` prop to change the color of the Button.
@@ -160,6 +143,96 @@ props:
---
::
### Link
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
::component-code
---
ignore:
- target
props:
to: https://github.com/nuxt/ui
target: _blank
slots:
default: Button
---
::
When the Button is a link or when using the `active` prop, you can use the `active-color` and `active-variant` props to customize the active state.
::component-code
---
prettier: true
ignore:
- color
- variant
items:
activeColor:
- primary
- secondary
- success
- info
- warning
- error
- neutral
activeVariant:
- solid
- outline
- soft
- subtle
- ghost
- link
props:
active: true
color: neutral
variant: outline
activeColor: primary
activeVariant: solid
slots:
default: |
Button
---
Button
::
You can also use the `active-class` and `inactive-class` props to customize the active state.
::component-code
---
props:
active: true
activeClass: 'font-bold'
inactiveClass: 'font-light'
slots:
default: Button
---
Button
::
::tip
You can configure these styles globally in your `app.config.ts` file under the `ui.button.variants.active` key.
```ts
export default defineAppConfig({
ui: {
button: {
variants: {
active: {
true: {
base: 'font-bold'
}
}
}
}
}
})
```
::
### Loading
Use the `loading` prop to show a loading icon and disable the Button.

View File

@@ -21,10 +21,12 @@ export interface ButtonProps extends UseComponentIconsProps, Omit<LinkProps, 'ra
* @defaultValue 'primary'
*/
color?: ButtonVariants['color']
activeColor?: ButtonVariants['color']
/**
* @defaultValue 'solid'
*/
variant?: ButtonVariants['variant']
activeVariant?: ButtonVariants['variant']
/**
* @defaultValue 'md'
*/
@@ -59,7 +61,11 @@ import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import ULink from './Link.vue'
const props = defineProps<ButtonProps>()
const props = withDefaults(defineProps<ButtonProps>(), {
active: undefined,
activeClass: '',
inactiveClass: ''
})
const slots = defineSlots<ButtonSlots>()
const linkProps = useForwardProps(pickLinkProps(props))
@@ -87,7 +93,19 @@ const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponen
computed(() => ({ ...props, loading: isLoading.value }))
)
const ui = computed(() => button({
const ui = computed(() => tv({
extend: button,
variants: {
active: {
true: {
base: props.activeClass
},
false: {
base: props.inactiveClass
}
}
}
})({
color: props.color,
variant: props.variant,
size: buttonSize.value,
@@ -102,26 +120,37 @@ const ui = computed(() => button({
<template>
<ULink
v-slot="{ active, ...slotProps }"
:type="type"
:disabled="disabled || isLoading"
:class="ui.base({ class: [props.class, props.ui?.base] })"
v-bind="omit(linkProps, ['type', 'disabled'])"
raw
@click="onClickWrapper"
custom
>
<slot name="leading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
</slot>
<ULinkBase
v-bind="slotProps"
:class="ui.base({
class: [props.class, props.ui?.base],
active,
...(active && activeVariant ? { variant: activeVariant } : {}),
...(active && activeColor ? { color: activeColor } : {})
})"
@click="onClickWrapper"
>
<slot name="leading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon, active })" />
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar, active })" />
</slot>
<slot>
<span v-if="label" :class="ui.label({ class: props.ui?.label })">
{{ label }}
</span>
</slot>
<slot>
<span v-if="label" :class="ui.label({ class: props.ui?.label, active })">
{{ label }}
</span>
</slot>
<slot name="trailing">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
<slot name="trailing">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon, active })" />
</slot>
</ULinkBase>
</ULink>
</template>

View File

@@ -73,6 +73,14 @@ export default (options: Required<ModuleOptions>) => ({
},
loading: {
true: ''
},
active: {
true: {
base: ''
},
false: {
base: ''
}
}
},
compoundVariants: [...(options.theme.colors || []).map((color: string) => ({

View File

@@ -20,6 +20,7 @@ describe('Button', () => {
...sizes.map((size: string) => [`with size ${size}`, { props: { label: 'Button', size } }]),
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { label: 'Button', variant } }]),
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { label: 'Button', variant, color: 'neutral' } }]),
...variants.map((activeVariant: string) => [`with active variant ${activeVariant}`, { props: { label: 'Button', active: true, activeVariant } }]),
['with icon', { props: { icon: 'i-lucide-rocket' } }],
['with leading and icon', { props: { leading: true, icon: 'i-lucide-arrow-left' } }],
['with leadingIcon', { props: { leadingIcon: 'i-lucide-arrow-left' } }],
@@ -39,6 +40,8 @@ describe('Button', () => {
['with square', { props: { label: 'Button', square: true } }],
['with as', { props: { label: 'Button', as: 'div' } }],
['with class', { props: { label: 'Button', class: 'rounded-full font-bold' } }],
['with activeClass', { props: { label: 'Button', active: true, activeClass: 'font-bold' } }],
['with inactiveClass', { props: { label: 'Button', active: false, inactiveClass: 'font-light' } }],
['with ui', { props: { label: 'Button', ui: { label: 'font-bold' } } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],

View File

@@ -1,5 +1,54 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Button > renders with active variant ghost correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) hover:bg-(--ui-primary)/10 focus-visible:bg-(--ui-primary)/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant link correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) hover:text-(--ui-primary)/75 disabled:text-(--ui-primary) aria-disabled:text-(--ui-primary) focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant outline correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 ring ring-inset ring-(--ui-primary)/50 text-(--ui-primary) hover:bg-(--ui-primary)/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant soft correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) bg-(--ui-primary)/10 hover:bg-(--ui-primary)/15 focus-visible:bg-(--ui-primary)/15 disabled:bg-(--ui-primary)/10 aria-disabled:bg-(--ui-primary)/10">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant solid correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant subtle correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) ring ring-inset ring-(--ui-primary)/25 bg-(--ui-primary)/10 hover:bg-(--ui-primary)/15 disabled:bg-(--ui-primary)/10 aria-disabled:bg-(--ui-primary)/10 focus-visible:ring-2 focus-visible:ring-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with activeClass correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors font-bold px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with as correctly 1`] = `
"<div class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
@@ -69,6 +118,13 @@ exports[`Button > renders with icon correctly 1`] = `
</button>"
`;
exports[`Button > renders with inactiveClass correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors font-light px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with label correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>

View File

@@ -1,5 +1,54 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Button > renders with active variant ghost correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) hover:bg-(--ui-primary)/10 focus-visible:bg-(--ui-primary)/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant link correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) hover:text-(--ui-primary)/75 disabled:text-(--ui-primary) aria-disabled:text-(--ui-primary) focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant outline correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 ring ring-inset ring-(--ui-primary)/50 text-(--ui-primary) hover:bg-(--ui-primary)/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant soft correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) bg-(--ui-primary)/10 hover:bg-(--ui-primary)/15 focus-visible:bg-(--ui-primary)/15 disabled:bg-(--ui-primary)/10 aria-disabled:bg-(--ui-primary)/10">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant solid correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with active variant subtle correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-primary) ring ring-inset ring-(--ui-primary)/25 bg-(--ui-primary)/10 hover:bg-(--ui-primary)/15 disabled:bg-(--ui-primary)/10 aria-disabled:bg-(--ui-primary)/10 focus-visible:ring-2 focus-visible:ring-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with activeClass correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors font-bold px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with as correctly 1`] = `
"<div class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
@@ -69,6 +118,13 @@ exports[`Button > renders with icon correctly 1`] = `
</button>"
`;
exports[`Button > renders with inactiveClass correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors font-light px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>
<!--v-if-->
</button>"
`;
exports[`Button > renders with label correctly 1`] = `
"<button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-(--ui-bg) bg-(--ui-primary) hover:bg-(--ui-primary)/75 disabled:bg-(--ui-primary) aria-disabled:bg-(--ui-primary) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ui-primary)">
<!--v-if--><span class="truncate">Button</span>