feat(nuxt3): add <NuxtLink> component (#3544)

Co-authored-by: pooya parsa <pyapar@gmail.com>
This commit is contained in:
Lucie 2022-03-14 14:36:32 +01:00 committed by GitHub
parent 3c96417a65
commit 4cefce44a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 612 additions and 20 deletions

View File

@ -0,0 +1,116 @@
# Routing
## `<NuxtLink>`
Nuxt provides `<NuxtLink>` component to handle any kind of links within your application.
`<NuxtLink>` component is a drop-in replacement for both Vue Router's `<RouterLink />` component and HTML's `<a>` tag. It intelligently determines whether the link is _internal_ or _external_ and renders it accordingly with available optimizations (prefetching, default attributes, etc.)
## Examples
### Basic usage
In this example, we use `<NuxtLink>` component to link to a website.
```vue [app.vue]
<template>
<NuxtLink to="https://nuxtjs.org">
Nuxt website
</NuxtLink>
<!-- <a href="https://nuxtjs.org" rel="noopener noreferrer">...</a> -->
</template>
```
:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/framework/tree/main/examples/nuxt-link?terminal=dev" blank}
### Internal routing
In this example, we use `<NuxtLink>` component to link to another page of the application.
```vue [pages/index.vue]
<template>
<NuxtLink to="/about">
About page
</NuxtLink>
<!-- <a href="/about">...</a> (+Vue Router & prefetching) -->
</template>
```
:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/framework/tree/main/examples/nuxt-link?terminal=dev" blank}
### `target` and `rel` attributes
In this example, we use `<NuxtLink>` with `target`, `rel`, and `noRel` props.
```vue [app.vue]
<template>
<NuxtLink to="https://twitter.com/nuxt_js" target="_blank">
Nuxt Twitter
</NuxtLink>
<!-- <a href="https://twitter.com/nuxt_js" target="_blank" rel="noopener noreferrer">...</a> -->
<NuxtLink to="https://discord.nuxtjs.org" target="_blank" rel="noopener">
Nuxt Discord
</NuxtLink>
<!-- <a href="https://discord.nuxtjs.org" target="_blank" rel="noopener">...</a> -->
<NuxtLink to="https://github.com/nuxt" no-rel>
Nuxt GitHub
</NuxtLink>
<!-- <a href="https://github.com/nuxt">...</a> -->
<NuxtLink to="/contact" target="_blank">
Contact page opens in another tab
</NuxtLink>
<!-- <a href="/contact" target="_blank" rel="noopener noreferrer">...</a> -->
</template>
```
:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/framework/tree/main/examples/nuxt-link?terminal=dev" blank}
## Props
- **to**: Any URL or a [route location object](https://router.vuejs.org/api/#routelocationraw) from Vue Router
- **href**: An alias for `to`. If used with `to`, `href` will be ignored
- **target**: A `target` attribute value to apply on the link
- **rel**: A `rel` attribute value to apply on the link. Defaults to `"noopener noreferrer"` for external links.
- **noRel**: If set to `true`, no `rel` attribute will be added to the link
- **activeClass**: A class to apply on active links. Works the same as [Vue Router's `active-class` prop](https://router.vuejs.org/api/#active-class) on internal links. Defaults to Vue Router's default (`"router-link-active"`)
- **exactActiveClass**: A class to apply on exact active links. Works the same as [Vue Router's `exact-active-class` prop](https://router.vuejs.org/api/#exact-active-class) on internal links. Defaults to Vue Router's default `"router-link-exact-active"`)
- **replace**: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/#replace) on internal links
- **ariaCurrentValue**: An `aria-current` attribute value to apply on exact active links. Works the same as [Vue Router's `aria-current-value` prop](https://router.vuejs.org/api/#aria-current-value) on internal links
- **external**: Forces the link to be considered as external (`true`) or internal (`false`). This is helpful to handle edge-cases
::alert{icon=👉}
Defaults can be overwriten, see [overwriting defaults](#overwriting-defaults) if you want to change them.
::
## Overwriting defaults
You can overwrite `<NuxtLink>` defaults by creating your own link component using `defineNuxtLink`.
```js [components/MyNuxtLink.js]
export default defineNuxtLink({
name: 'MyNuxtLink',
/* see signature below for more */
})
```
You can then use `<MyNuxtLink />` component as usual with your new defaults.
:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/framework/tree/main/examples/nuxt-link-pages?terminal=dev" blank}
### `defineNuxtLink` signature
```ts
defineNuxtLink({
componentName?: string;
externalRelAttribute?: string;
activeClass?: string;
exactActiveClass?: string;
}) => Component
```
- **componentName**: A name for the defined `<NuxtLink>` component.- **externalRelAttribute**: A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable
- **activeClass**: A default class to apply on active links. Works the same as [Vue Router's `linkActiveClass` option](https://router.vuejs.org/api/#linkactiveclass). Defaults to Vue Router's default (`"router-link-active"`)
- **exactActiveClass**: A default class to apply on exact active links. Works the same as [Vue Router's `linkExactActiveClass` option](https://router.vuejs.org/api/#linkexactactiveclass). Defaults to Vue Router's default (`"router-link-exact-active"`)

View File

@ -0,0 +1,13 @@
<template>
<NuxtExampleLayout :show-tips="true" example="nuxt-link" class="example">
<NuxtPage />
</NuxtExampleLayout>
</template>
<style>
.example a {
font-family: Arial, Helvetica, sans-serif;
padding: 1rem 10rem;
display: block;
}
</style>

View File

@ -0,0 +1,6 @@
export default defineNuxtLink({
componentName: 'MyNuxtLink',
externalRelAttribute: '',
activeClass: 'active',
exactActiveClass: 'exact-active'
})

View File

@ -0,0 +1,7 @@
import { defineNuxtConfig } from 'nuxt3'
export default defineNuxtConfig({
modules: [
'@nuxt/ui'
]
})

View File

@ -0,0 +1,13 @@
{
"name": "example-nuxt-link",
"private": true,
"scripts": {
"build": "nuxi build",
"dev": "nuxi dev",
"start": "nuxi preview"
},
"devDependencies": {
"@nuxt/ui": "npm:@nuxt/ui-edge@latest",
"nuxt3": "latest"
}
}

View File

@ -0,0 +1,5 @@
<template>
<NuxtLink to="/">
Index page
</NuxtLink>
</template>

View File

@ -0,0 +1,25 @@
<template>
<div>
<NuxtLink to="/about">
About page
</NuxtLink>
<NuxtLink to="https://nuxtjs.org">
Nuxt website
</NuxtLink>
<NuxtLink to="https://twitter.com/nuxt_js" target="_blank">
Nuxt Twitter with a blank target
</NuxtLink>
<NuxtLink to="https://discord.nuxtjs.org" target="_blank" rel="noopener">
Nuxt Discord with a blank target and custom rel value
</NuxtLink>
<NuxtLink to="https://github.com/nuxt" no-rel>
Nuxt GitHub without rel attribute
</NuxtLink>
<MyNuxtLink to="https://nuxtjs.org">
Nuxt website with a custom link component with no default rel attribute
</MyNuxtLink>
<MyNuxtLink to="/">
Index page with a custom link component with a custom active class
</MyNuxtLink>
</div>
</template>

View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -0,0 +1,3 @@
// defineNuxtLink
export { defineNuxtLink } from './nuxt-link'
export type { NuxtLinkOptions, NuxtLinkProps } from './nuxt-link'

View File

@ -0,0 +1,186 @@
import { defineComponent, h, resolveComponent, PropType, computed, DefineComponent } from 'vue'
import { RouteLocationRaw, Router } from 'vue-router'
import { useRouter } from '#app'
const firstNonUndefined = <T>(...args: T[]): T => args.find(arg => arg !== undefined)
const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer'
export type NuxtLinkOptions = {
componentName?: string;
externalRelAttribute?: string | null;
activeClass?: string;
exactActiveClass?: string;
}
export type NuxtLinkProps = {
// Routing
to?: string | RouteLocationRaw;
href?: string | RouteLocationRaw;
external?: boolean;
// Attributes
target?: string;
rel?: string;
noRel?: boolean;
// Styling
activeClass?: string;
exactActiveClass?: string;
// Vue Router's `<RouterLink>` additional props
replace?: boolean;
ariaCurrentValue?: string;
};
export function defineNuxtLink (options: NuxtLinkOptions) {
const componentName = options.componentName || 'NuxtLink'
const checkPropConflicts = (props: NuxtLinkProps, main: string, sub: string): void => {
if (process.dev && props[main] !== undefined && props[sub] !== undefined) {
console.warn(`[${componentName}] \`${main}\` and \`${sub}\` cannot be used together. \`${sub}\` will be ignored.`)
}
}
return defineComponent({
name: componentName,
props: {
// Routing
to: {
type: [String, Object] as PropType<string | RouteLocationRaw>,
default: undefined,
required: false
},
href: {
type: [String, Object] as PropType<string | RouteLocationRaw>,
default: undefined,
required: false
},
// Attributes
target: {
type: String as PropType<string>,
default: undefined,
required: false
},
rel: {
type: String as PropType<string>,
default: undefined,
required: false
},
noRel: {
type: Boolean as PropType<boolean>,
default: undefined,
required: false
},
// Styling
activeClass: {
type: String as PropType<string>,
default: undefined,
required: false
},
exactActiveClass: {
type: String as PropType<string>,
default: undefined,
required: false
},
// Vue Router's `<RouterLink>` additional props
replace: {
type: Boolean as PropType<boolean>,
default: undefined,
required: false
},
ariaCurrentValue: {
type: String as PropType<string>,
default: undefined,
required: false
},
// Edge cases handling
external: {
type: Boolean as PropType<boolean>,
default: undefined,
required: false
},
// Slot API
custom: {
type: Boolean as PropType<boolean>,
default: undefined,
required: false
}
},
setup (props, { slots }) {
const router = useRouter() as Router | undefined
// Resolving `to` value from `to` and `href` props
const to = computed<string | RouteLocationRaw>(() => {
checkPropConflicts(props, 'to', 'href')
return props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
})
// Resolving link type
const isExternal = computed<boolean>(() => {
// External prop is explictly set
if (props.external) {
return true
}
// When `target` prop is set, link is external
if (props.target && props.target !== '_self') {
return true
}
// When `to` is a route object then it's an internal link
if (typeof to.value === 'object') {
return false
}
// Directly check if `to` is an external URL with Regex
// Regex101 expression: {@link https://regex101.com/r/1y7iod/1}
// TODO: Use `ufo.hasProtocol` when issue fixed https://github.com/unjs/ufo/issues/45
return !/^\/(?!\/)/.test(to.value)
})
return () => {
if (!isExternal.value) {
// Internal link
return h(
resolveComponent('RouterLink'),
{
to: to.value,
activeClass: props.activeClass || options.activeClass,
exactActiveClass: props.exactActiveClass || options.exactActiveClass,
replace: props.replace,
ariaCurrentValue: props.ariaCurrentValue
},
// TODO: Slot API
slots.default
)
}
// Resolves `to` value if it's a route location object
// converts `'''` to `null` to prevent the attribute from being added as empty (`href=""`)
const href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null
// Resolves `target` value
const target = props.target || null
// Resolves `rel`
checkPropConflicts(props, 'noRel', 'rel')
const rel = props.noRel
? null
// converts `""` to `null` to prevent the attribute from being added as empty (`rel=""`)
: firstNonUndefined<string | null>(props.rel, options.externalRelAttribute, DEFAULT_EXTERNAL_REL_ATTRIBUTE) || null
return h('a', { href, rel, target }, slots.default())
}
}
}) as unknown as DefineComponent<NuxtLinkProps>
}
export default defineNuxtLink({ componentName: 'NuxtLink' })

View File

@ -2,6 +2,8 @@
export * from './nuxt'
export * from './composables'
export * from './components'
// eslint-disable-next-line import/no-restricted-paths
export type { PageMeta } from '../pages/runtime'
// eslint-disable-next-line import/no-restricted-paths

View File

@ -1,4 +1,4 @@
import { DefineComponent, reactive, h } from 'vue'
import { reactive, h } from 'vue'
import { parseURL, parseQuery } from 'ufo'
import { NuxtApp } from '@nuxt/schema'
import { createError } from 'h3'
@ -6,12 +6,6 @@ import { defineNuxtPlugin } from '..'
import { callWithNuxt } from '../nuxt'
import { clearError, throwError } from '#app'
declare module 'vue' {
export interface GlobalComponents {
NuxtLink: DefineComponent<{ to: String }>
}
}
interface Route {
/** Percentage encoded pathname section of the URL. */
path: string;
@ -34,7 +28,11 @@ interface Route {
meta: Record<string, any>;
}
function getRouteFromPath (fullPath: string) {
function getRouteFromPath (fullPath: string | Record<string, unknown>) {
if (typeof fullPath === 'object') {
throw new TypeError('[nuxt] Route location object cannot be resolved when vue-router is disabled (no pages).')
}
const url = parseURL(fullPath.toString())
return {
path: url.pathname,
@ -46,7 +44,8 @@ function getRouteFromPath (fullPath: string) {
name: undefined,
matched: [],
redirectedFrom: undefined,
meta: {}
meta: {},
href: fullPath
}
}
@ -80,7 +79,7 @@ interface Router {
afterEach: (guard: RouterHooks['navigate:after']) => () => void
onError: (handler: RouterHooks['error']) => () => void
// Routes
resolve: (url: string) => Route
resolve: (url: string | Record<string, unknown>) => Route
addRoute: (parentName: string, route: Route) => void
getRoutes: () => any[]
hasRoute: (name: string) => boolean
@ -174,6 +173,12 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
}
}
nuxtApp.vueApp.component('RouterLink', {
functional: true,
props: { to: String },
setup: (props, { slots }) => () => h('a', { href: props.to, onClick: (e) => { e.preventDefault(); router.push(props.to) } }, slots)
})
if (process.client) {
window.addEventListener('popstate', (event) => {
const location = (event.target as Window).location
@ -214,12 +219,6 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
delete nuxtApp._processingMiddleware
})
nuxtApp.vueApp.component('NuxtLink', {
functional: true,
props: { to: String },
setup: (props, { slots }) => () => h('a', { href: props.to, onClick: (e) => { e.preventDefault(); router.push(props.to) } }, slots)
})
if (process.server) {
nuxtApp.hooks.hookOnce('app:created', async () => {
await router.push(nuxtApp.ssrContext.url)

View File

@ -40,7 +40,8 @@ export const appPreset = defineUnimportPreset({
'addRouteMiddleware',
'throwError',
'clearError',
'useError'
'useError',
'defineNuxtLink'
]
})

View File

@ -90,6 +90,12 @@ async function initNuxt (nuxt: Nuxt) {
filePath: resolve(nuxt.options.appDir, 'components/client-only')
})
// Add <NuxtLink>
addComponent({
name: 'NuxtLink',
filePath: resolve(nuxt.options.appDir, 'components/nuxt-link')
})
for (const m of modulesToInstall) {
if (Array.isArray(m)) {
await installModule(m[0], m[1])

View File

@ -3,7 +3,6 @@ import {
createRouter,
createWebHistory,
createMemoryHistory,
RouterLink,
NavigationGuard
} from 'vue-router'
import { createError } from 'h3'
@ -17,7 +16,6 @@ import { globalMiddleware, namedMiddleware } from '#build/middleware'
declare module 'vue' {
export interface GlobalComponents {
NuxtPage: typeof NuxtPage
NuxtLink: typeof RouterLink
/** @deprecated */
NuxtNestedPage: typeof NuxtPage
/** @deprecated */
@ -27,7 +25,6 @@ declare module 'vue' {
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('NuxtPage', NuxtPage)
nuxtApp.vueApp.component('NuxtLink', RouterLink)
// TODO: remove before release - present for backwards compatibility & intentionally undocumented
nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage)
nuxtApp.vueApp.component('NuxtChild', NuxtPage)

View File

@ -0,0 +1,200 @@
import { expect, describe, it, vi } from 'vitest'
import { RouteLocationRaw } from 'vue-router'
import { NuxtLinkOptions, NuxtLinkProps, defineNuxtLink } from '../src/app/components/nuxt-link'
// Mocks `h()`
vi.mock('vue', async () => {
const vue: Record<string, unknown> = await vi.importActual('vue')
return {
...vue,
resolveComponent: (name: string) => name,
h: (...args) => args
}
})
// Mocks Nuxt `useRouter()`
vi.mock('#app', () => ({
useRouter: () => ({ resolve: ({ to }: { to: string }) => ({ href: to }) })
}))
// Helpers for test lisibility
const EXTERNAL = 'a'
const INTERNAL = 'RouterLink'
// Renders a `<NuxtLink />`
const nuxtLink = (
props: NuxtLinkProps = {},
NuxtLinkOptions: Partial<NuxtLinkOptions> = {}
): { type: string, props: Record<string, unknown>, slots: unknown } => {
const component = defineNuxtLink({ componentName: 'NuxtLink', ...NuxtLinkOptions })
const [type, _props, slots] = (component.setup as unknown as (props: NuxtLinkProps, context: { slots: Record<string, () => unknown> }) =>
() => [string, Record<string, unknown>, unknown])(props, { slots: { default: () => null } })()
return { type, props: _props, slots }
}
describe('nuxt-link:to', () => {
it('renders link with `to` prop', () => {
expect(nuxtLink({ to: '/to' }).props.to).toBe('/to')
})
it('renders link with `href` prop', () => {
expect(nuxtLink({ href: '/href' }).props.to).toBe('/href')
})
it('renders link with `to` prop and warns about `href` prop conflict', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(vi.fn())
expect(nuxtLink({ to: '/to', href: '/href' }).props.to).toBe('/to')
// TODO: Uncomment when `dev` mode for tests is available
// expect(consoleWarnSpy).toHaveBeenCalledOnce()
consoleWarnSpy.mockRestore()
})
it('defaults to `null`', () => {
expect(nuxtLink().props.href).toBe(null)
})
})
describe('nuxt-link:isExternal', () => {
it('returns based on `to` value', () => {
// Internal
expect(nuxtLink({ to: '/foo' }).type).toBe(INTERNAL)
expect(nuxtLink({ to: '/foo/bar' }).type).toBe(INTERNAL)
expect(nuxtLink({ to: '/foo/bar?baz=qux' }).type).toBe(INTERNAL)
// External
expect(nuxtLink({ to: 'https://nuxtjs.org' }).type).toBe(EXTERNAL)
expect(nuxtLink({ to: '//nuxtjs.org' }).type).toBe(EXTERNAL)
expect(nuxtLink({ to: 'tel:0123456789' }).type).toBe(EXTERNAL)
expect(nuxtLink({ to: 'mailto:hello@nuxtlabs.com' }).type).toBe(EXTERNAL)
})
it('returns `false` when `to` is a route location object', () => {
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).type).toBe(INTERNAL)
})
it('honors `external` prop', () => {
expect(nuxtLink({ to: '/to', external: true }).type).toBe(EXTERNAL)
expect(nuxtLink({ to: '/to', external: false }).type).toBe(INTERNAL)
})
it('returns `true` when using the `target` prop', () => {
expect(nuxtLink({ to: '/foo', target: '_blank' }).type).toBe(EXTERNAL)
expect(nuxtLink({ to: '/foo/bar', target: '_blank' }).type).toBe(EXTERNAL)
expect(nuxtLink({ to: '/foo/bar?baz=qux', target: '_blank' }).type).toBe(EXTERNAL)
})
})
describe('nuxt-link:propsOrAttributes', () => {
describe('`isExternal` is `true`', () => {
describe('href', () => {
it('forwards `to` value', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org' }).props.href).toBe('https://nuxtjs.org')
})
it('resolves route location object', () => {
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw, external: true }).props.href).toBe('/to')
})
})
describe('target', () => {
it('forwards `target` prop', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org', target: '_blank' }).props.target).toBe('_blank')
expect(nuxtLink({ to: 'https://nuxtjs.org', target: null }).props.target).toBe(null)
})
it('defaults to `null`', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org' }).props.target).toBe(null)
})
})
describe('rel', () => {
it('uses framework\'s default', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org' }).props.rel).toBe('noopener noreferrer')
})
it('uses user\'s default', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org' }, { externalRelAttribute: 'foo' }).props.rel).toBe('foo')
expect(nuxtLink({ to: 'https://nuxtjs.org' }, { externalRelAttribute: null }).props.rel).toBe(null)
})
it('uses and favors `rel` prop', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org', rel: 'foo' }).props.rel).toBe('foo')
expect(nuxtLink({ to: 'https://nuxtjs.org', rel: 'foo' }, { externalRelAttribute: 'bar' }).props.rel).toBe('foo')
expect(nuxtLink({ to: 'https://nuxtjs.org', rel: null }, { externalRelAttribute: 'bar' }).props.rel).toBe(null)
expect(nuxtLink({ to: 'https://nuxtjs.org', rel: '' }, { externalRelAttribute: 'bar' }).props.rel).toBe(null)
})
it('honors `noRel` prop', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org', noRel: true }).props.rel).toBe(null)
expect(nuxtLink({ to: 'https://nuxtjs.org', noRel: false }).props.rel).toBe('noopener noreferrer')
})
it('honors `noRel` prop and warns about `rel` prop conflict', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(vi.fn())
expect(nuxtLink({ to: 'https://nuxtjs.org', noRel: true, rel: 'foo' }).props.rel).toBe(null)
// TODO: Uncomment when `dev` mode for tests is available
// expect(consoleWarnSpy).toHaveBeenCalledOnce()
consoleWarnSpy.mockRestore()
})
})
})
describe('`isExternal` is `false`', () => {
describe('to', () => {
it('forwards `to` prop', () => {
expect(nuxtLink({ to: '/to' }).props.to).toBe('/to')
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).props.to).toEqual({ to: '/to' })
})
})
describe('activeClass', () => {
it('uses framework\'s default', () => {
expect(nuxtLink({ to: '/to' }).props.activeClass).toBe(undefined)
})
it('uses user\'s default', () => {
expect(nuxtLink({ to: '/to' }, { activeClass: 'activeClass' }).props.activeClass).toBe('activeClass')
})
it('uses and favors `activeClass` prop', () => {
expect(nuxtLink({ to: '/to', activeClass: 'propActiveClass' }).props.activeClass).toBe('propActiveClass')
expect(nuxtLink({ to: '/to', activeClass: 'propActiveClass' }, { activeClass: 'activeClass' }).props.activeClass).toBe('propActiveClass')
})
})
describe('exactActiveClass', () => {
it('uses framework\'s default', () => {
expect(nuxtLink({ to: '/to' }).props.exactActiveClass).toBe(undefined)
})
it('uses user\'s default', () => {
expect(nuxtLink({ to: '/to' }, { exactActiveClass: 'exactActiveClass' }).props.exactActiveClass).toBe('exactActiveClass')
})
it('uses and favors `exactActiveClass` prop', () => {
expect(nuxtLink({ to: '/to', exactActiveClass: 'propExactActiveClass' }).props.exactActiveClass).toBe('propExactActiveClass')
expect(nuxtLink({ to: '/to', exactActiveClass: 'propExactActiveClass' }, { exactActiveClass: 'exactActiveClass' }).props.exactActiveClass).toBe('propExactActiveClass')
})
})
describe('replace', () => {
it('forwards `replace` prop', () => {
expect(nuxtLink({ to: '/to', replace: true }).props.replace).toBe(true)
expect(nuxtLink({ to: '/to', replace: false }).props.replace).toBe(false)
})
})
describe('ariaCurrentValue', () => {
it('forwards `ariaCurrentValue` prop', () => {
expect(nuxtLink({ to: '/to', ariaCurrentValue: 'page' }).props.ariaCurrentValue).toBe('page')
expect(nuxtLink({ to: '/to', ariaCurrentValue: 'step' }).props.ariaCurrentValue).toBe('step')
})
})
})
})

View File

@ -3,6 +3,7 @@ import { defineConfig } from 'vite'
export default defineConfig({
alias: {
'#app': resolve('./packages/nuxt3/src/app/index.ts'),
'@nuxt/test-utils': resolve('./packages/test-utils/src/index.ts')
}
})

View File

@ -10648,6 +10648,15 @@ __metadata:
languageName: unknown
linkType: soft
"example-nuxt-link@workspace:examples/nuxt-link":
version: 0.0.0-use.local
resolution: "example-nuxt-link@workspace:examples/nuxt-link"
dependencies:
"@nuxt/ui": "npm:@nuxt/ui-edge@latest"
nuxt3: latest
languageName: unknown
linkType: soft
"example-use-async-data@workspace:examples/use-async-data":
version: 0.0.0-use.local
resolution: "example-use-async-data@workspace:examples/use-async-data"