Link
A wrapper around with extra props.
Demo
Related Theme
This requires the following theme to be installed:
Component
vue
<script lang="ts">
import theme from '@/ui/theme/link'
import { Primitive } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'
const link = tv(theme)
export interface LinkProps {
as?: string
type?: string
active?: boolean
disabled?: boolean
href?: string
target?: string
rel?: string
class?: any
raw?: boolean
onClick?: ((e: MouseEvent) => void | Promise<void>) | Array<((e: MouseEvent) => void | Promise<void>)>
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<LinkProps>(), {
as: 'button',
type: 'button',
})
const ui = computed(() => link({
active: props.active,
disabled: props.disabled,
class: props.class,
}))
const resolvedClass = computed(() => {
if (props.raw)
return props.class
return ui.value
})
function onClickWrapper(e: MouseEvent) {
if (props.disabled) {
e.stopPropagation()
e.preventDefault()
return
}
if (props.onClick) {
for (const onClick of Array.isArray(props.onClick) ? props.onClick : [props.onClick]) {
onClick(e)
}
}
}
</script>
<template>
<Primitive
v-bind="href ? {
'as': 'a',
'href': disabled ? undefined : href,
'aria-disabled': disabled ? 'true' : undefined,
'role': disabled ? 'link' : undefined,
'tabindex': disabled ? -1 : undefined,
} : as === 'button' ? {
as,
type,
disabled,
} : {
as,
}"
:rel="rel"
:target="target"
:class="resolvedClass"
@click="onClickWrapper"
>
<slot />
</Primitive>
</template>vue
<script lang="ts">
import theme from '@/UI/Theme/link'
import { Primitive } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'
const link = tv(theme)
export interface LinkProps {
as?: string
type?: string
active?: boolean
disabled?: boolean
href?: string
target?: string
rel?: string
class?: any
raw?: boolean
onClick?: ((e: MouseEvent) => void | Promise<void>) | Array<((e: MouseEvent) => void | Promise<void>)>
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<LinkProps>(), {
as: 'button',
type: 'button',
})
const ui = computed(() => link({
active: props.active,
disabled: props.disabled,
class: props.class,
}))
const resolvedClass = computed(() => {
if (props.raw)
return props.class
return ui.value
})
function onClickWrapper(e: MouseEvent) {
if (props.disabled) {
e.stopPropagation()
e.preventDefault()
return
}
if (props.onClick) {
for (const onClick of Array.isArray(props.onClick) ? props.onClick : [props.onClick]) {
onClick(e)
}
}
}
</script>
<template>
<Primitive
v-bind="href ? {
'as': 'a',
'href': disabled ? undefined : href,
'aria-disabled': disabled ? 'true' : undefined,
'role': disabled ? 'link' : undefined,
'tabindex': disabled ? -1 : undefined,
} : as === 'button' ? {
as,
type,
disabled,
} : {
as,
}"
:rel="rel"
:target="target"
:class="resolvedClass"
@click="onClickWrapper"
>
<slot />
</Primitive>
</template>Theme
ts
export default {
base: '',
variants: {
active: {
true: '',
false: '',
},
disabled: {
true: '',
},
},
}View Nuxt UI theme
ts
export default {
base: 'focus-visible:outline-primary',
variants: {
active: {
true: 'text-primary',
false: 'text-muted hover:text-default transition-colors',
},
disabled: {
true: 'cursor-not-allowed opacity-75',
},
},
}Test
To test this component, you can use the following test file:
ts
import type { RenderOptions } from '@testing-library/vue'
import Link from '@/ui/components/Link.vue'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('link', () => {
it.each<[string, RenderOptions<typeof Link>]>([
// Props
['with as', { props: { as: 'div' } }],
['with to', { props: { href: '/' } }],
['with type', { props: { type: 'submit' as const } }],
['with disabled', { props: { disabled: true } }],
['with raw', { props: { raw: true } }],
['with class', { props: { class: 'font-medium' } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],
])('renders %s correctly', (name, options) => {
render(Link, {
attrs: {
'data-testid': 'link',
},
...options,
})
expect(screen.getByTestId('link')).matchSnapshot()
})
})ts
import type { RenderOptions } from '@testing-library/vue'
import Link from '@/UI/Components/Link.vue'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('link', () => {
it.each<[string, RenderOptions<typeof Link>]>([
// Props
['with as', { props: { as: 'div' } }],
['with to', { props: { href: '/' } }],
['with type', { props: { type: 'submit' as const } }],
['with disabled', { props: { disabled: true } }],
['with raw', { props: { raw: true } }],
['with class', { props: { class: 'font-medium' } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],
])('renders %s correctly', (name, options) => {
render(Link, {
attrs: {
'data-testid': 'link',
},
...options,
})
expect(screen.getByTestId('link')).matchSnapshot()
})
})