mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-29 11:20:36 +01:00
fix(Link): consistent behavior between nuxt, vue and inertia (#4134)
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import theme from '#build/ui/button'
|
import theme from '#build/ui/button'
|
||||||
import type { LinkProps } from './Link.vue'
|
|
||||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||||
import type { AvatarProps } from '../types'
|
import type { LinkProps, AvatarProps } from '../types'
|
||||||
import type { ComponentConfig } from '../types/utils'
|
import type { ComponentConfig } from '../types/utils'
|
||||||
|
|
||||||
type Button = ComponentConfig<typeof theme, AppConfig, 'button'>
|
type Button = ComponentConfig<typeof theme, AppConfig, 'button'>
|
||||||
@@ -123,14 +122,13 @@ const ui = computed(() => tv({
|
|||||||
v-slot="{ active, ...slotProps }"
|
v-slot="{ active, ...slotProps }"
|
||||||
:type="type"
|
:type="type"
|
||||||
:disabled="disabled || isLoading"
|
:disabled="disabled || isLoading"
|
||||||
:class="ui.base({ class: [props.ui?.base, props.class] })"
|
|
||||||
v-bind="omit(linkProps, ['type', 'disabled', 'onClick'])"
|
v-bind="omit(linkProps, ['type', 'disabled', 'onClick'])"
|
||||||
custom
|
custom
|
||||||
>
|
>
|
||||||
<ULinkBase
|
<ULinkBase
|
||||||
v-bind="slotProps"
|
v-bind="slotProps"
|
||||||
:class="ui.base({
|
:class="ui.base({
|
||||||
class: [props.class, props.ui?.base],
|
class: [props.ui?.base, props.class],
|
||||||
active,
|
active,
|
||||||
...(active && activeVariant ? { variant: activeVariant } : {}),
|
...(active && activeVariant ? { variant: activeVariant } : {}),
|
||||||
...(active && activeColor ? { color: activeColor } : {})
|
...(active && activeColor ? { color: activeColor } : {})
|
||||||
|
|||||||
@@ -89,11 +89,12 @@ export interface LinkSlots {
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { isEqual, diff } from 'ohash/utils'
|
import { isEqual } from 'ohash/utils'
|
||||||
import { useForwardProps } from 'reka-ui'
|
import { useForwardProps } from 'reka-ui'
|
||||||
import { reactiveOmit } from '@vueuse/core'
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
import { useRoute, useAppConfig } from '#imports'
|
import { useRoute, useAppConfig } from '#imports'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
|
import { isPartiallyEqual } from '../utils/link'
|
||||||
import ULinkBase from './LinkBase.vue'
|
import ULinkBase from './LinkBase.vue'
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
defineOptions({ inheritAttrs: false })
|
||||||
@@ -111,7 +112,7 @@ defineSlots<LinkSlots>()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const appConfig = useAppConfig() as Link['AppConfig']
|
const appConfig = useAppConfig() as Link['AppConfig']
|
||||||
|
|
||||||
const nuxtLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'raw', 'class'))
|
const nuxtLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to', 'href', 'raw', 'custom', 'class'))
|
||||||
|
|
||||||
const ui = computed(() => tv({
|
const ui = computed(() => tv({
|
||||||
extend: tv(theme),
|
extend: tv(theme),
|
||||||
@@ -125,19 +126,7 @@ const ui = computed(() => tv({
|
|||||||
}, appConfig.ui?.link || {})
|
}, appConfig.ui?.link || {})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function isPartiallyEqual(item1: any, item2: any) {
|
const to = computed(() => props.to ?? props.href)
|
||||||
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
|
|
||||||
if (q.type === 'added') {
|
|
||||||
filtered.add(q.key)
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}, new Set<string>())
|
|
||||||
|
|
||||||
const item1Filtered = Object.fromEntries(Object.entries(item1).filter(([key]) => !diffedKeys.has(key)))
|
|
||||||
const item2Filtered = Object.fromEntries(Object.entries(item2).filter(([key]) => !diffedKeys.has(key)))
|
|
||||||
|
|
||||||
return isEqual(item1Filtered, item2Filtered)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
||||||
if (props.active !== undefined) {
|
if (props.active !== undefined) {
|
||||||
@@ -177,7 +166,7 @@ function resolveLinkClass({ route, isActive, isExactActive }: any) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink v-slot="{ href, navigate, route: linkRoute, rel, target, isExternal, isActive, isExactActive }" v-bind="nuxtLinkProps" custom>
|
<NuxtLink v-slot="{ href, navigate, route: linkRoute, rel, target, isExternal, isActive, isExactActive }" v-bind="nuxtLinkProps" :to="to" custom>
|
||||||
<template v-if="custom">
|
<template v-if="custom">
|
||||||
<slot
|
<slot
|
||||||
v-bind="{
|
v-bind="{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ComponentConfig } from '../../types/utils'
|
|||||||
|
|
||||||
type Link = ComponentConfig<typeof theme, AppConfig, 'link'>
|
type Link = ComponentConfig<typeof theme, AppConfig, 'link'>
|
||||||
|
|
||||||
interface NuxtLinkProps extends Omit<InertiaLinkProps, 'href'> {
|
interface NuxtLinkProps extends Omit<InertiaLinkProps, 'href' | 'onClick'> {
|
||||||
activeClass?: string
|
activeClass?: string
|
||||||
/**
|
/**
|
||||||
* Route Location the link should navigate to when clicked on.
|
* Route Location the link should navigate to when clicked on.
|
||||||
@@ -62,10 +62,11 @@ import { computed } from 'vue'
|
|||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { useForwardProps } from 'reka-ui'
|
import { useForwardProps } from 'reka-ui'
|
||||||
import { reactiveOmit } from '@vueuse/core'
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
import { usePage, Link as InertiaLink } from '@inertiajs/vue3'
|
import { usePage } from '@inertiajs/vue3'
|
||||||
import { hasProtocol } from 'ufo'
|
import { hasProtocol } from 'ufo'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { tv } from '../../utils/tv'
|
import { tv } from '../../utils/tv'
|
||||||
|
import ULinkBase from '../../components/LinkBase.vue'
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
@@ -78,9 +79,11 @@ const props = withDefaults(defineProps<LinkProps>(), {
|
|||||||
})
|
})
|
||||||
defineSlots<LinkSlots>()
|
defineSlots<LinkSlots>()
|
||||||
|
|
||||||
|
const page = usePage()
|
||||||
|
|
||||||
const appConfig = useAppConfig() as Link['AppConfig']
|
const appConfig = useAppConfig() as Link['AppConfig']
|
||||||
|
|
||||||
const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'activeClass', 'inactiveClass', 'to', 'raw', 'class'))
|
const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'activeClass', 'inactiveClass', 'to', 'href', 'raw', 'custom', 'class'))
|
||||||
|
|
||||||
const ui = computed(() => tv({
|
const ui = computed(() => tv({
|
||||||
extend: tv(theme),
|
extend: tv(theme),
|
||||||
@@ -94,14 +97,42 @@ const ui = computed(() => tv({
|
|||||||
}, appConfig.ui?.link || {})
|
}, appConfig.ui?.link || {})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const href = computed(() => props.to ?? props.href)
|
||||||
|
|
||||||
const isExternal = computed(() => {
|
const isExternal = computed(() => {
|
||||||
if (props.external) return true
|
if (props.external) {
|
||||||
if (!props.to) return false
|
return true
|
||||||
return typeof props.to === 'string' && hasProtocol(props.to, { acceptRelative: true })
|
}
|
||||||
|
|
||||||
|
if (!href.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof href.value === 'string' && hasProtocol(href.value, { acceptRelative: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLinkActive = computed(() => {
|
||||||
|
if (props.active !== undefined) {
|
||||||
|
return props.active
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!href.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.exact && page.url === href.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.exact && page.url.startsWith(href.value)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const linkClass = computed(() => {
|
const linkClass = computed(() => {
|
||||||
const active = isActive.value
|
const active = isLinkActive.value
|
||||||
|
|
||||||
if (props.raw) {
|
if (props.raw) {
|
||||||
return [props.class, active ? props.activeClass : props.inactiveClass]
|
return [props.class, active ? props.activeClass : props.inactiveClass]
|
||||||
@@ -109,74 +140,36 @@ const linkClass = computed(() => {
|
|||||||
|
|
||||||
return ui.value({ class: props.class, active, disabled: props.disabled })
|
return ui.value({ class: props.class, active, disabled: props.disabled })
|
||||||
})
|
})
|
||||||
|
|
||||||
const page = usePage()
|
|
||||||
const url = computed(() => props.to ?? props.href ?? '')
|
|
||||||
|
|
||||||
const isActive = computed(() => props.active || (!!url.value && (props.exact ? url.value === props.href : page?.url.startsWith(url.value))))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="!isExternal && !!url">
|
<template v-if="custom">
|
||||||
<InertiaLink v-bind="routerLinkProps" :href="url">
|
<slot
|
||||||
<template v-if="custom">
|
|
||||||
<slot
|
|
||||||
v-bind="{
|
|
||||||
...$attrs,
|
|
||||||
as,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
href: url,
|
|
||||||
active: isActive
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<ULinkBase
|
|
||||||
v-else
|
|
||||||
v-bind="{
|
|
||||||
...$attrs,
|
|
||||||
as,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
href: url,
|
|
||||||
active: isActive
|
|
||||||
}"
|
|
||||||
:class="linkClass"
|
|
||||||
>
|
|
||||||
<slot :active="isActive" />
|
|
||||||
</ULinkBase>
|
|
||||||
</InertiaLink>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<template v-if="custom">
|
|
||||||
<slot
|
|
||||||
v-bind="{
|
|
||||||
...$attrs,
|
|
||||||
as,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
href: to,
|
|
||||||
target: isExternal ? '_blank' : undefined,
|
|
||||||
active: isActive
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<ULinkBase
|
|
||||||
v-else
|
|
||||||
v-bind="{
|
v-bind="{
|
||||||
...$attrs,
|
...$attrs,
|
||||||
|
...routerLinkProps,
|
||||||
as,
|
as,
|
||||||
type,
|
type,
|
||||||
disabled,
|
disabled,
|
||||||
href: url,
|
href,
|
||||||
target: isExternal ? '_blank' : undefined,
|
active: isLinkActive,
|
||||||
active: isActive
|
isExternal
|
||||||
}"
|
}"
|
||||||
:is-external="isExternal"
|
/>
|
||||||
:class="linkClass"
|
|
||||||
>
|
|
||||||
<slot :active="isActive" />
|
|
||||||
</ULinkBase>
|
|
||||||
</template>
|
</template>
|
||||||
|
<ULinkBase
|
||||||
|
v-else
|
||||||
|
v-bind="{
|
||||||
|
...$attrs,
|
||||||
|
...routerLinkProps,
|
||||||
|
as,
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
href,
|
||||||
|
isExternal
|
||||||
|
}"
|
||||||
|
:class="linkClass"
|
||||||
|
>
|
||||||
|
<slot :active="isLinkActive" />
|
||||||
|
</ULinkBase>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
77
src/runtime/inertia/components/LinkBase.vue
Normal file
77
src/runtime/inertia/components/LinkBase.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { LinkProps } from '../../types'
|
||||||
|
|
||||||
|
export interface LinkBaseProps {
|
||||||
|
as?: string
|
||||||
|
type?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onClick?: ((e: MouseEvent) => void | Promise<void>) | Array<((e: MouseEvent) => void | Promise<void>)>
|
||||||
|
href?: string
|
||||||
|
target?: LinkProps['target']
|
||||||
|
active?: boolean
|
||||||
|
isExternal?: boolean
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
import { Link as InertiaLink } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<LinkBaseProps>(), {
|
||||||
|
as: 'button',
|
||||||
|
type: 'button'
|
||||||
|
})
|
||||||
|
|
||||||
|
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>
|
||||||
|
<InertiaLink
|
||||||
|
v-if="!!href && !isExternal && !disabled"
|
||||||
|
:href="href"
|
||||||
|
v-bind="{
|
||||||
|
target: target || (isExternal ? '_blank' : undefined),
|
||||||
|
...$attrs
|
||||||
|
}"
|
||||||
|
@click="onClickWrapper"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</InertiaLink>
|
||||||
|
<Primitive
|
||||||
|
v-else
|
||||||
|
v-bind="href ? {
|
||||||
|
'as': 'a',
|
||||||
|
'href': disabled ? undefined : href,
|
||||||
|
'aria-disabled': disabled ? 'true' : undefined,
|
||||||
|
'role': disabled ? 'link' : undefined,
|
||||||
|
'tabindex': disabled ? -1 : undefined,
|
||||||
|
'target': target || (isExternal ? '_blank' : undefined),
|
||||||
|
...$attrs
|
||||||
|
} : as === 'button' ? {
|
||||||
|
as,
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
...$attrs
|
||||||
|
} : {
|
||||||
|
as,
|
||||||
|
...$attrs
|
||||||
|
}"
|
||||||
|
@click="onClickWrapper"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { reactivePick } from '@vueuse/core'
|
import { reactivePick } from '@vueuse/core'
|
||||||
|
import { isEqual, diff } from 'ohash/utils'
|
||||||
import type { LinkProps } from '../types'
|
import type { LinkProps } from '../types'
|
||||||
|
|
||||||
export function pickLinkProps(link: LinkProps & { [key: string]: any }) {
|
export function pickLinkProps(link: LinkProps & { [key: string]: any }) {
|
||||||
@@ -19,3 +20,17 @@ export function pickLinkProps(link: LinkProps & { [key: string]: any }) {
|
|||||||
|
|
||||||
return reactivePick(link, ...propsToInclude)
|
return reactivePick(link, ...propsToInclude)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPartiallyEqual(item1: any, item2: any) {
|
||||||
|
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
|
||||||
|
if (q.type === 'added') {
|
||||||
|
filtered.add(q.key)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}, new Set<string>())
|
||||||
|
|
||||||
|
const item1Filtered = Object.fromEntries(Object.entries(item1).filter(([key]) => !diffedKeys.has(key)))
|
||||||
|
const item2Filtered = Object.fromEntries(Object.entries(item2).filter(([key]) => !diffedKeys.has(key)))
|
||||||
|
|
||||||
|
return isEqual(item1Filtered, item2Filtered)
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,15 +87,17 @@ export interface LinkSlots {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, getCurrentInstance } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { isEqual, diff } from 'ohash/utils'
|
import { isEqual } from 'ohash/utils'
|
||||||
import { useForwardProps } from 'reka-ui'
|
import { useForwardProps } from 'reka-ui'
|
||||||
import { reactiveOmit } from '@vueuse/core'
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
import { hasProtocol } from 'ufo'
|
import { hasProtocol } from 'ufo'
|
||||||
import { useRoute, RouterLink } from 'vue-router'
|
import { useRoute, RouterLink } from 'vue-router'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { tv } from '../../utils/tv'
|
import { tv } from '../../utils/tv'
|
||||||
|
import { isPartiallyEqual } from '../../utils/link'
|
||||||
|
import ULinkBase from '../../components/LinkBase.vue'
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
@@ -109,25 +111,11 @@ const props = withDefaults(defineProps<LinkProps>(), {
|
|||||||
})
|
})
|
||||||
defineSlots<LinkSlots>()
|
defineSlots<LinkSlots>()
|
||||||
|
|
||||||
// Check if vue-router is available by checking for the injection key
|
const route = useRoute()
|
||||||
const hasRouter = computed(() => {
|
|
||||||
const app = getCurrentInstance()?.appContext.app
|
|
||||||
return !!(app?.config?.globalProperties?.$router)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Only try to get route if router exists
|
|
||||||
const route = computed(() => {
|
|
||||||
if (!hasRouter.value) return null
|
|
||||||
try {
|
|
||||||
return useRoute()
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const appConfig = useAppConfig() as Link['AppConfig']
|
const appConfig = useAppConfig() as Link['AppConfig']
|
||||||
|
|
||||||
const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to', 'raw', 'class'))
|
const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to', 'href', 'raw', 'custom', 'class'))
|
||||||
|
|
||||||
const ui = computed(() => tv({
|
const ui = computed(() => tv({
|
||||||
extend: tv(theme),
|
extend: tv(theme),
|
||||||
@@ -141,23 +129,18 @@ const ui = computed(() => tv({
|
|||||||
}, appConfig.ui?.link || {})
|
}, appConfig.ui?.link || {})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function isPartiallyEqual(item1: any, item2: any) {
|
const to = computed(() => props.to ?? props.href)
|
||||||
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
|
|
||||||
if (q.type === 'added') {
|
|
||||||
filtered.add(q.key)
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}, new Set<string>())
|
|
||||||
|
|
||||||
const item1Filtered = Object.fromEntries(Object.entries(item1).filter(([key]) => !diffedKeys.has(key)))
|
|
||||||
const item2Filtered = Object.fromEntries(Object.entries(item2).filter(([key]) => !diffedKeys.has(key)))
|
|
||||||
|
|
||||||
return isEqual(item1Filtered, item2Filtered)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isExternal = computed(() => {
|
const isExternal = computed(() => {
|
||||||
if (!props.to) return false
|
if (props.external) {
|
||||||
return typeof props.to === 'string' && hasProtocol(props.to, { acceptRelative: true })
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!to.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
||||||
@@ -165,17 +148,17 @@ function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
|||||||
return props.active
|
return props.active
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.to || !route.value) {
|
if (!to.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.exactQuery === 'partial') {
|
if (props.exactQuery === 'partial') {
|
||||||
if (!isPartiallyEqual(linkRoute.query, route.value.query)) return false
|
if (!isPartiallyEqual(linkRoute.query, route.query)) return false
|
||||||
} else if (props.exactQuery === true) {
|
} else if (props.exactQuery === true) {
|
||||||
if (!isEqual(linkRoute.query, route.value.query)) return false
|
if (!isEqual(linkRoute.query, route.query)) return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.exactHash && linkRoute.hash !== route.value.hash) {
|
if (props.exactHash && linkRoute.hash !== route.hash) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,8 +185,8 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="hasRouter && !isExternal">
|
<template v-if="!isExternal && !!to">
|
||||||
<RouterLink v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive }" v-bind="routerLinkProps" :to="to || '#'" custom>
|
<RouterLink v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive }" v-bind="routerLinkProps" :to="to" custom>
|
||||||
<template v-if="custom">
|
<template v-if="custom">
|
||||||
<slot
|
<slot
|
||||||
v-bind="{
|
v-bind="{
|
||||||
@@ -212,7 +195,7 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
|
|||||||
as,
|
as,
|
||||||
type,
|
type,
|
||||||
disabled,
|
disabled,
|
||||||
href: to ? href : undefined,
|
href,
|
||||||
navigate,
|
navigate,
|
||||||
active: isLinkActive({ route: linkRoute, isActive, isExactActive })
|
active: isLinkActive({ route: linkRoute, isActive, isExactActive })
|
||||||
}"
|
}"
|
||||||
@@ -226,7 +209,7 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
|
|||||||
as,
|
as,
|
||||||
type,
|
type,
|
||||||
disabled,
|
disabled,
|
||||||
href: to ? href : undefined,
|
href,
|
||||||
navigate
|
navigate
|
||||||
}"
|
}"
|
||||||
:class="resolveLinkClass({ route: linkRoute, isActive, isExactActive })"
|
:class="resolveLinkClass({ route: linkRoute, isActive, isExactActive })"
|
||||||
@@ -246,7 +229,8 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
|
|||||||
disabled,
|
disabled,
|
||||||
href: to,
|
href: to,
|
||||||
target: isExternal ? '_blank' : undefined,
|
target: isExternal ? '_blank' : undefined,
|
||||||
active: false
|
active,
|
||||||
|
isExternal
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -258,12 +242,12 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
|
|||||||
type,
|
type,
|
||||||
disabled,
|
disabled,
|
||||||
href: (to as string),
|
href: (to as string),
|
||||||
target: isExternal ? '_blank' : undefined
|
target: isExternal ? '_blank' : undefined,
|
||||||
|
isExternal
|
||||||
}"
|
}"
|
||||||
:is-external="isExternal"
|
|
||||||
:class="resolveLinkClass()"
|
:class="resolveLinkClass()"
|
||||||
>
|
>
|
||||||
<slot :active="false" />
|
<slot :active="active" />
|
||||||
</ULinkBase>
|
</ULinkBase>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ export default (options: Required<ModuleOptions>) => ({
|
|||||||
variants: {
|
variants: {
|
||||||
active: {
|
active: {
|
||||||
true: 'text-primary',
|
true: 'text-primary',
|
||||||
false: ['text-muted hover:text-default', options.theme.transitions && 'transition-colors']
|
false: 'text-muted'
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
true: 'cursor-not-allowed opacity-75'
|
true: 'cursor-not-allowed opacity-75'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
compoundVariants: [{
|
||||||
|
active: false,
|
||||||
|
disabled: false,
|
||||||
|
class: ['hover:text-default', options.theme.transitions && 'transition-colors']
|
||||||
|
}]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ describe('Link', () => {
|
|||||||
['with to', { props: { to: '/' } }],
|
['with to', { props: { to: '/' } }],
|
||||||
['with type', { props: { type: 'submit' as const } }],
|
['with type', { props: { type: 'submit' as const } }],
|
||||||
['with disabled', { props: { disabled: true } }],
|
['with disabled', { props: { disabled: true } }],
|
||||||
['with raw', { props: { raw: true } }],
|
|
||||||
['with class', { props: { class: 'font-medium' } }],
|
|
||||||
['with activeClass', { props: { active: true, activeClass: 'text-highlighted' } }],
|
['with activeClass', { props: { active: true, activeClass: 'text-highlighted' } }],
|
||||||
['with inactiveClass', { props: { active: false, inactiveClass: 'hover:text-primary' } }],
|
['with inactiveClass', { props: { active: false, inactiveClass: 'hover:text-primary' } }],
|
||||||
|
['with raw', { props: { raw: true } }],
|
||||||
|
['with raw activeClass', { props: { raw: true, active: true, activeClass: 'text-highlighted' } }],
|
||||||
|
['with raw inactiveClass', { props: { raw: true, active: false, inactiveClass: 'hover:text-primary' } }],
|
||||||
|
['with class', { props: { class: 'font-medium' } }],
|
||||||
// Slots
|
// Slots
|
||||||
['with default slot', { slots: { default: () => 'Default slot' } }]
|
['with default slot', { slots: { default: () => 'Default slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: LinkProps, slots?: Partial<LinkSlots> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: LinkProps, slots?: Partial<LinkSlots> }) => {
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ exports[`Link > renders with class correctly 1`] = `"<button type="button" class
|
|||||||
|
|
||||||
exports[`Link > renders with default slot correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted hover:text-default transition-colors">Default slot</button>"`;
|
exports[`Link > renders with default slot correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted hover:text-default transition-colors">Default slot</button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with disabled correctly 1`] = `"<button type="button" disabled="" class="focus-visible:outline-primary text-muted hover:text-default transition-colors cursor-not-allowed opacity-75"></button>"`;
|
exports[`Link > renders with disabled correctly 1`] = `"<button type="button" disabled="" class="focus-visible:outline-primary text-muted cursor-not-allowed opacity-75"></button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with inactiveClass correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted transition-colors hover:text-primary"></button>"`;
|
exports[`Link > renders with inactiveClass correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></button>"`;
|
||||||
|
|
||||||
|
exports[`Link > renders with raw activeClass correctly 1`] = `"<button type="button" class="text-highlighted"></button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with raw correctly 1`] = `"<button type="button" class=""></button>"`;
|
exports[`Link > renders with raw correctly 1`] = `"<button type="button" class=""></button>"`;
|
||||||
|
|
||||||
|
exports[`Link > renders with raw inactiveClass correctly 1`] = `"<button type="button" class="hover:text-primary"></button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with to correctly 1`] = `"<a href="/" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;
|
exports[`Link > renders with to correctly 1`] = `"<a href="/" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;
|
||||||
|
|
||||||
exports[`Link > renders with type correctly 1`] = `"<button type="submit" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></button>"`;
|
exports[`Link > renders with type correctly 1`] = `"<button type="submit" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></button>"`;
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ exports[`Link > renders with class correctly 1`] = `"<button type="button" class
|
|||||||
|
|
||||||
exports[`Link > renders with default slot correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted hover:text-default transition-colors">Default slot</button>"`;
|
exports[`Link > renders with default slot correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted hover:text-default transition-colors">Default slot</button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with disabled correctly 1`] = `"<button type="button" disabled="" class="focus-visible:outline-primary text-muted hover:text-default transition-colors cursor-not-allowed opacity-75"></button>"`;
|
exports[`Link > renders with disabled correctly 1`] = `"<button type="button" disabled="" class="focus-visible:outline-primary text-muted cursor-not-allowed opacity-75"></button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with inactiveClass correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted transition-colors hover:text-primary"></button>"`;
|
exports[`Link > renders with inactiveClass correctly 1`] = `"<button type="button" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></button>"`;
|
||||||
|
|
||||||
|
exports[`Link > renders with raw activeClass correctly 1`] = `"<button type="button" class="text-highlighted"></button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with raw correctly 1`] = `"<button type="button" class=""></button>"`;
|
exports[`Link > renders with raw correctly 1`] = `"<button type="button" class=""></button>"`;
|
||||||
|
|
||||||
|
exports[`Link > renders with raw inactiveClass correctly 1`] = `"<button type="button" class="hover:text-primary"></button>"`;
|
||||||
|
|
||||||
exports[`Link > renders with to correctly 1`] = `"<a href="/" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;
|
exports[`Link > renders with to correctly 1`] = `"<a href="/" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;
|
||||||
|
|
||||||
exports[`Link > renders with type correctly 1`] = `"<button type="submit" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></button>"`;
|
exports[`Link > renders with type correctly 1`] = `"<button type="submit" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></button>"`;
|
||||||
|
|||||||
Reference in New Issue
Block a user