Skip to content
Merged
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
58 changes: 52 additions & 6 deletions src/runtime/components/Link.vue
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ interface NuxtLinkDefaultSlotProps {
<script setup lang="ts">
import { computed } from 'vue'
import { isEqual } from 'ohash/utils'
import { useForwardProps } from 'reka-ui'
import { useForwardProps, Slot } from 'reka-ui'
import { defu } from 'defu'
import { reactiveOmit } from '@vueuse/core'
import { hasProtocol } from 'ufo'
import { useRoute, useAppConfig } from '#imports'
import { mergeClasses } from '../utils'
import { tv } from '../utils/tv'
Expand Down Expand Up @@ -146,11 +147,30 @@ const ui = computed(() => tv({

const to = computed(() => props.to ?? props.href)

function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
const isInternalLink = computed(() => {
if (!to.value) return false
if (props.external) return false
if (typeof to.value !== 'string') return true
if (hasProtocol(to.value, { acceptRelative: true })) return false
if (props.target && props.target !== '_self') return false
return true
})

const externalRel = computed(() => {
if (props.noRel) return null
if (props.rel) return props.rel
return 'noopener noreferrer'
})

function isLinkActive({ route: linkRoute, isActive, isExactActive }: any = {}) {
if (props.active !== undefined) {
return props.active
}

if (!to.value) {
return false
}

if (props.exactQuery === 'partial') {
if (!isPartiallyEqual(linkRoute.query, route.query)) return false
} else if (props.exactQuery === true) {
Expand All @@ -172,7 +192,7 @@ function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
return false
}

function resolveLinkClass({ route, isActive, isExactActive }: any) {
function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
const active = isLinkActive({ route, isActive, isExactActive })

if (props.raw) {
Expand All @@ -184,8 +204,8 @@ function resolveLinkClass({ route, isActive, isExactActive }: any) {
</script>

<template>
<NuxtLink v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive, ...rest }" v-bind="nuxtLinkProps" :to="to" custom>
<template v-if="custom">
<NuxtLink v-if="isInternalLink" v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive, ...rest }" v-bind="nuxtLinkProps" :to="to" custom>
<Slot v-if="custom">
<slot
v-bind="{
...$attrs,
Expand All @@ -201,7 +221,7 @@ function resolveLinkClass({ route, isActive, isExactActive }: any) {
active: isLinkActive({ route: linkRoute, isActive, isExactActive })
}"
/>
</template>
</Slot>
<ULinkBase
v-else
v-bind="{
Expand All @@ -221,4 +241,30 @@ function resolveLinkClass({ route, isActive, isExactActive }: any) {
<slot :active="isLinkActive({ route: linkRoute, isActive, isExactActive })" />
</ULinkBase>
</NuxtLink>

<Slot v-else-if="custom">
<slot
v-bind="{
...$attrs,
as,
type,
disabled,
...(to ? { href: String(to), target: props.target, rel: externalRel, isExternal: true } : {}),
active: active ?? false
}"
/>
</Slot>
<ULinkBase
v-else
v-bind="{
...$attrs,
as,
type,
disabled,
...(to ? { href: String(to), target: props.target, rel: externalRel, isExternal: true } : {})
}"
:class="resolveLinkClass()"
>
<slot :active="active ?? false" />
</ULinkBase>
</template>
6 changes: 3 additions & 3 deletions src/runtime/vue/overrides/inertia/Link.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface LinkSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { defu } from 'defu'
import { useForwardProps } from 'reka-ui'
import { useForwardProps, Slot } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { usePage } from '@inertiajs/vue3'
import { hasProtocol } from 'ufo'
Expand Down Expand Up @@ -179,7 +179,7 @@ const linkClass = computed(() => {
</script>

<template>
<template v-if="custom">
<Slot v-if="custom">
<slot
v-bind="{
...$attrs,
Expand All @@ -194,7 +194,7 @@ const linkClass = computed(() => {
isExternal
}"
/>
</template>
</Slot>
<ULinkBase
v-else
v-bind="{
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/vue/overrides/none/Link.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface LinkSlots {
<script setup lang="ts">
import { computed, inject } from 'vue'
import { defu } from 'defu'
import { Slot } from 'reka-ui'
import { hasProtocol } from 'ufo'
import { useAppConfig } from '#imports'
import { mergeClasses } from '../../../utils'
Expand Down Expand Up @@ -169,7 +170,7 @@ const navigate = handleNavigation
</script>

<template>
<template v-if="custom">
<Slot v-if="custom">
<slot
v-bind="{
...$attrs,
Expand All @@ -184,7 +185,7 @@ const navigate = handleNavigation
active: isLinkActive
}"
/>
</template>
</Slot>
<ULinkBase
v-else
v-bind="{
Expand Down
94 changes: 45 additions & 49 deletions src/runtime/vue/overrides/vue-router/Link.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export interface LinkSlots {
import { computed } from 'vue'
import { defu } from 'defu'
import { isEqual } from 'ohash/utils'
import { useForwardProps } from 'reka-ui'
import { useForwardProps, Slot } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { hasProtocol } from 'ufo'
import { useRoute, RouterLink } from 'vue-router'
Expand Down Expand Up @@ -181,27 +181,9 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {

<!-- eslint-disable vue/no-template-shadow -->
<template>
<template v-if="!isExternal && !!to">
<RouterLink v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive }" v-bind="routerLinkProps" :to="to" custom>
<template v-if="custom">
<slot
v-bind="{
...$attrs,
...(exact && isExactActive ? { 'aria-current': props.ariaCurrentValue } : {}),
as,
type,
disabled,
href,
navigate,
rel,
target,
isExternal,
active: isLinkActive({ route: linkRoute, isActive, isExactActive })
}"
/>
</template>
<ULinkBase
v-else
<RouterLink v-if="!isExternal && !!to" v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive }" v-bind="routerLinkProps" :to="to" custom>
<Slot v-if="custom">
<slot
v-bind="{
...$attrs,
...(exact && isExactActive ? { 'aria-current': props.ariaCurrentValue } : {}),
Expand All @@ -212,46 +194,60 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
navigate,
rel,
target,
isExternal
}"
:class="resolveLinkClass({ route: linkRoute, isActive, isExactActive })"
>
<slot :active="isLinkActive({ route: linkRoute, isActive, isExactActive })" />
</ULinkBase>
</RouterLink>
</template>

<template v-else>
<template v-if="custom">
<slot
v-bind="{
...$attrs,
as,
type,
disabled,
href: to,
rel,
target,
active: active ?? false,
isExternal
isExternal,
active: isLinkActive({ route: linkRoute, isActive, isExactActive })
}"
/>
</template>
</Slot>
<ULinkBase
v-else
v-bind="{
...$attrs,
...(exact && isExactActive ? { 'aria-current': props.ariaCurrentValue } : {}),
as,
type,
disabled,
href: (to as string),
href,
navigate,
rel,
target,
isExternal
}"
:class="resolveLinkClass()"
:class="resolveLinkClass({ route: linkRoute, isActive, isExactActive })"
>
<slot :active="active ?? false" />
<slot :active="isLinkActive({ route: linkRoute, isActive, isExactActive })" />
</ULinkBase>
</template>
</RouterLink>

<Slot v-else-if="custom">
<slot
v-bind="{
...$attrs,
as,
type,
disabled,
href: to,
rel,
target,
active: active ?? false,
isExternal
}"
/>
</Slot>
Comment thread
benjamincanac marked this conversation as resolved.
<ULinkBase
v-else
v-bind="{
...$attrs,
as,
type,
disabled,
href: (to as string),
rel,
target,
isExternal
}"
:class="resolveLinkClass()"
>
<slot :active="active ?? false" />
</ULinkBase>
</template>
4 changes: 4 additions & 0 deletions test/components/Link.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ describe('Link', () => {
['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' } }],
['with external to', { props: { to: 'https://example.com' } }],
['with external to and target', { props: { to: 'https://example.com', target: '_blank' } }],
['with internal to and target', { props: { to: '/about', target: '_blank' } }],
['with external prop', { props: { to: '/api/download', external: true } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }]
])
Expand Down
8 changes: 8 additions & 0 deletions test/components/__snapshots__/Link-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ exports[`Link > renders with default slot correctly 1`] = `"<button type="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 external prop correctly 1`] = `"<a href="/api/download" rel="noopener noreferrer" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

exports[`Link > renders with external to and target correctly 1`] = `"<a href="https://example.com" rel="noopener noreferrer" target="_blank" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

exports[`Link > renders with external to correctly 1`] = `"<a href="https://example.com" rel="noopener noreferrer" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

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 internal to and target correctly 1`] = `"<a href="/about" rel="noopener noreferrer" target="_blank" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

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>"`;
Expand Down
8 changes: 8 additions & 0 deletions test/components/__snapshots__/Link.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ exports[`Link > renders with default slot correctly 1`] = `"<button type="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 external prop correctly 1`] = `"<a href="/api/download" rel="noopener noreferrer" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

exports[`Link > renders with external to and target correctly 1`] = `"<a href="https://example.com" rel="noopener noreferrer" target="_blank" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

exports[`Link > renders with external to correctly 1`] = `"<a href="https://example.com" rel="noopener noreferrer" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

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 internal to and target correctly 1`] = `"<a href="/about" rel="noopener noreferrer" target="_blank" class="focus-visible:outline-primary text-muted hover:text-default transition-colors"></a>"`;

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>"`;
Expand Down
Loading