Skip to content
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
32 changes: 32 additions & 0 deletions docs/content/docs/2.components/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,38 @@ export default defineAppConfig({
```
::

### Link with external icon

You can display an external icon with the `external-icon` prop to indicate that the link will be open in a new tab.

::component-code
---
ignore:
- target
props:
to: https://github.com/nuxt/ui
target: _blank
externalIcon: true
slots:
default: Button
---
::

The default icon for external link (`appConfig.ui.icons.external`) can be replaced with any [Icon](/docs/components/icon).

::component-code
---
ignore:
- target
props:
to: https://github.com/nuxt/ui
target: _blank
externalIcon: i-lucide-external-link
slots:
default: Button
---
::

### Loading

Use the `loading` prop to show a loading icon and disable the Button.
Expand Down
1 change: 1 addition & 0 deletions playgrounds/nuxt/app/pages/components/button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function onClick() {
<Matrix v-slot="props" :attrs="attrs">
<UButton label="Button" v-bind="props" />
<UButton label="Link" to="/" v-bind="props" />
<UButton label="Link with external icon" external-icon target="_blank" to="https://nuxt.com" v-bind="props" />
<UButton label="Disabled" disabled v-bind="props" />
<UButton label="Disabled link" to="#" disabled v-bind="props" />
<UButton label="Loading" loading v-bind="props" />
Expand Down
16 changes: 15 additions & 1 deletion src/runtime/components/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import theme from '#build/ui/button'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { LinkProps } from './Link.vue'
import type { AvatarProps } from './Avatar.vue'
import type { IconProps } from './Icon.vue'
import type { ComponentConfig } from '../types/tv'

type Button = ComponentConfig<typeof theme, AppConfig, 'button'>
Expand All @@ -21,6 +22,13 @@ export interface ButtonProps extends UseComponentIconsProps, Omit<LinkProps, 'ra
*/
variant?: Button['variants']['variant']
activeVariant?: Button['variants']['variant']
/**
* The icon displayed when the button is an external link.
* Set to `true` to show the external icon.
* @defaultValue false
* @IconifyIcon
*/
externalIcon?: boolean | IconProps['name']
/**
* @defaultValue 'md'
*/
Expand Down Expand Up @@ -60,7 +68,9 @@ import UAvatar from './Avatar.vue'
import ULink from './Link.vue'
import ULinkBase from './LinkBase.vue'

const _props = defineProps<ButtonProps>()
const _props = withDefaults(defineProps<ButtonProps>(), {
externalIcon: false
})
const slots = defineSlots<ButtonSlots>()

const props = useComponentProps('button', _props)
Expand Down Expand Up @@ -149,6 +159,10 @@ const ui = computed(() => tv({
</span>
</slot>

<div v-if="props.target === '_blank' && props.externalIcon !== false" data-slot="externalIconContainer" :class="ui.externalIconContainer({ class: props.ui?.externalIconContainer, active })">
<UIcon :name="typeof props.externalIcon === 'string' ? props.externalIcon : appConfig.ui.icons.external" data-slot="externalIcon" :class="ui.externalIcon({ class: props.ui?.externalIcon, active })" />
</div>

<slot name="trailing" :ui="ui">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" data-slot="trailingIcon" :class="ui.trailingIcon({ class: props.ui?.trailingIcon, active })" />
</slot>
Expand Down
9 changes: 8 additions & 1 deletion src/theme/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export default (options: Required<ModuleOptions>) => ({
leadingIcon: 'shrink-0',
leadingAvatar: 'shrink-0',
leadingAvatarSize: '',
trailingIcon: 'shrink-0'
trailingIcon: 'shrink-0',
externalIconContainer: 'h-full -ms-1',
externalIcon: 'inline-block size-3 align-top'
},
variants: {
...fieldGroupVariant,
Expand All @@ -29,30 +31,35 @@ export default (options: Required<ModuleOptions>) => ({
base: 'px-2 py-1 text-xs gap-1',
leadingIcon: 'size-4',
leadingAvatarSize: '3xs',
externalIcon: 'size-2',
trailingIcon: 'size-4'
},
sm: {
base: 'px-2.5 py-1.5 text-xs gap-1.5',
leadingIcon: 'size-4',
leadingAvatarSize: '3xs',
externalIcon: 'size-2',
trailingIcon: 'size-4'
},
md: {
base: 'px-2.5 py-1.5 text-sm gap-1.5',
leadingIcon: 'size-5',
leadingAvatarSize: '2xs',
externalIcon: 'size-3',
trailingIcon: 'size-5'
},
lg: {
base: 'px-3 py-2 text-sm gap-2',
leadingIcon: 'size-5',
leadingAvatarSize: '2xs',
externalIcon: 'size-3',
trailingIcon: 'size-5'
},
xl: {
base: 'px-3 py-2 text-base gap-2',
leadingIcon: 'size-6',
leadingAvatarSize: 'xs',
externalIcon: 'size-3',
trailingIcon: 'size-6'
}
},
Expand Down
3 changes: 3 additions & 0 deletions test/components/Button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ describe('Button', () => {
['with leadingIcon', { props: { leadingIcon: 'i-lucide-arrow-left' } }],
['with trailing and icon', { props: { trailing: true, icon: 'i-lucide-arrow-right' } }],
['with trailingIcon', { props: { trailingIcon: 'i-lucide-arrow-right' } }],
['with externalIcon', { props: { label: 'Button', to: 'https://nuxt.com', target: '_blank', externalIcon: true } }],
['with externalIcon false', { props: { label: 'Button', to: 'https://nuxt.com', target: '_blank', externalIcon: false } }],
['with externalIcon custom', { props: { label: 'Button', to: 'https://nuxt.com', target: '_blank', externalIcon: 'i-lucide-external-link' } }],
['with avatar', { props: { avatar: { src: 'https://github.com/benjamincanac.png' } } }],
['with avatar and leadingIcon', { props: { avatar: { src: 'https://github.com/benjamincanac.png' }, leadingIcon: 'i-lucide-arrow-left' } }],
['with avatar and trailingIcon', { props: { avatar: { src: 'https://github.com/benjamincanac.png' }, trailingIcon: 'i-lucide-arrow-right' } }],
Expand Down
5 changes: 5 additions & 0 deletions test/components/__snapshots__/Alert-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`Alert > renders with actions correctly 1`] = `
<div data-slot="actions" class="flex flex-wrap gap-1.5 shrink-0 items-start mt-2.5"><button type="button" data-slot="base" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2 py-1 text-xs gap-1 text-inverted bg-primary hover:bg-primary/75 active:bg-primary/75 disabled:bg-primary aria-disabled:bg-primary outline-primary/25 focus-visible:outline-3">
<!--v-if--><span data-slot="label" class="truncate">Action</span>
<!--v-if-->
<!--v-if-->
</button></div>
</div>
<!--v-if-->
Expand Down Expand Up @@ -62,6 +63,7 @@ exports[`Alert > renders with close correctly 1`] = `
<!--v-if--><button type="button" data-slot="close" aria-label="Close" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors text-sm gap-1.5 text-muted hover:text-default active:text-default disabled:text-muted aria-disabled:text-muted outline-inverted/25 focus-visible:outline-3 p-0"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="leadingIcon" class="shrink-0 size-5"></svg>
<!--v-if-->
<!--v-if-->
<!--v-if-->
</button>
</div>
</div>"
Expand Down Expand Up @@ -91,6 +93,7 @@ exports[`Alert > renders with closeIcon correctly 1`] = `
<!--v-if--><button type="button" data-slot="close" aria-label="Close" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors text-sm gap-1.5 text-muted hover:text-default active:text-default disabled:text-muted aria-disabled:text-muted outline-inverted/25 focus-visible:outline-3 p-0"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="leadingIcon" class="shrink-0 size-5"></svg>
<!--v-if-->
<!--v-if-->
<!--v-if-->
</button>
</div>
</div>"
Expand Down Expand Up @@ -201,6 +204,7 @@ exports[`Alert > renders with orientation horizontal correctly 1`] = `
<div data-slot="actions" class="flex flex-wrap gap-1.5 shrink-0 items-center"><button type="button" data-slot="base" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2 py-1 text-xs gap-1 text-inverted bg-primary hover:bg-primary/75 active:bg-primary/75 disabled:bg-primary aria-disabled:bg-primary outline-primary/25 focus-visible:outline-3">
<!--v-if--><span data-slot="label" class="truncate">Action</span>
<!--v-if-->
<!--v-if-->
</button>
<!--v-if-->
</div>
Expand All @@ -215,6 +219,7 @@ exports[`Alert > renders with orientation vertical correctly 1`] = `
<div data-slot="actions" class="flex flex-wrap gap-1.5 shrink-0 items-start mt-2.5"><button type="button" data-slot="base" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2 py-1 text-xs gap-1 text-inverted bg-primary hover:bg-primary/75 active:bg-primary/75 disabled:bg-primary aria-disabled:bg-primary outline-primary/25 focus-visible:outline-3">
<!--v-if--><span data-slot="label" class="truncate">Action</span>
<!--v-if-->
<!--v-if-->
</button></div>
</div>
<!--v-if-->
Expand Down
5 changes: 5 additions & 0 deletions test/components/__snapshots__/Alert.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`Alert > renders with actions correctly 1`] = `
<div data-slot="actions" class="flex flex-wrap gap-1.5 shrink-0 items-start mt-2.5"><button type="button" data-slot="base" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2 py-1 text-xs gap-1 text-inverted bg-primary hover:bg-primary/75 active:bg-primary/75 disabled:bg-primary aria-disabled:bg-primary outline-primary/25 focus-visible:outline-3">
<!--v-if--><span data-slot="label" class="truncate">Action</span>
<!--v-if-->
<!--v-if-->
</button></div>
</div>
<!--v-if-->
Expand Down Expand Up @@ -62,6 +63,7 @@ exports[`Alert > renders with close correctly 1`] = `
<!--v-if--><button type="button" data-slot="close" aria-label="Close" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors text-sm gap-1.5 text-muted hover:text-default active:text-default disabled:text-muted aria-disabled:text-muted outline-inverted/25 focus-visible:outline-3 p-0"><span class="iconify i-lucide:x shrink-0 size-5" aria-hidden="true" data-slot="leadingIcon"></span>
<!--v-if-->
<!--v-if-->
<!--v-if-->
</button>
</div>
</div>"
Expand Down Expand Up @@ -91,6 +93,7 @@ exports[`Alert > renders with closeIcon correctly 1`] = `
<!--v-if--><button type="button" data-slot="close" aria-label="Close" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors text-sm gap-1.5 text-muted hover:text-default active:text-default disabled:text-muted aria-disabled:text-muted outline-inverted/25 focus-visible:outline-3 p-0"><span class="iconify i-lucide:trash shrink-0 size-5" aria-hidden="true" data-slot="leadingIcon"></span>
<!--v-if-->
<!--v-if-->
<!--v-if-->
</button>
</div>
</div>"
Expand Down Expand Up @@ -201,6 +204,7 @@ exports[`Alert > renders with orientation horizontal correctly 1`] = `
<div data-slot="actions" class="flex flex-wrap gap-1.5 shrink-0 items-center"><button type="button" data-slot="base" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2 py-1 text-xs gap-1 text-inverted bg-primary hover:bg-primary/75 active:bg-primary/75 disabled:bg-primary aria-disabled:bg-primary outline-primary/25 focus-visible:outline-3">
<!--v-if--><span data-slot="label" class="truncate">Action</span>
<!--v-if-->
<!--v-if-->
</button>
<!--v-if-->
</div>
Expand All @@ -215,6 +219,7 @@ exports[`Alert > renders with orientation vertical correctly 1`] = `
<div data-slot="actions" class="flex flex-wrap gap-1.5 shrink-0 items-start mt-2.5"><button type="button" data-slot="base" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2 py-1 text-xs gap-1 text-inverted bg-primary hover:bg-primary/75 active:bg-primary/75 disabled:bg-primary aria-disabled:bg-primary outline-primary/25 focus-visible:outline-3">
<!--v-if--><span data-slot="label" class="truncate">Action</span>
<!--v-if-->
<!--v-if-->
</button></div>
</div>
<!--v-if-->
Expand Down
Loading
Loading