diff --git a/e2e/docs/components/route-link.md b/e2e/docs/components/route-link.md index ec2656aa50..48dd4e0010 100644 --- a/e2e/docs/components/route-link.md +++ b/e2e/docs/components/route-link.md @@ -28,13 +28,19 @@ - text - text +- text +- text - text - text +- text +- text ### Class - text - text +- text +- text ### Attrs @@ -42,11 +48,17 @@ - text - text - text +- text +- text +- text +- text ### Slots - text - texttext2 +- text +- texttext2 ### Hash and query @@ -56,9 +68,24 @@ - text - text - text +- text +- text +- text +- text +- text +- text - text - text - text - text - text - text + +### Relative + +- text +- text +- text +- text +- text +- text diff --git a/e2e/tests/components/route-link.spec.ts b/e2e/tests/components/route-link.spec.ts index c72e932ff2..e555744cac 100644 --- a/e2e/tests/components/route-link.spec.ts +++ b/e2e/tests/components/route-link.spec.ts @@ -38,6 +38,10 @@ test('should render active status correctly', async ({ page }) => { const CONFIGS = [ 'route-link route-link-active', 'route-link route-link-active', + 'route-link route-link-active', + 'route-link route-link-active', + 'route-link', + 'route-link', 'route-link', 'route-link', ] @@ -53,6 +57,8 @@ test('should render class correctly', async ({ page }) => { const CONFIGS = [ 'route-link custom-class', 'route-link route-link-active custom-class', + 'route-link custom-class', + 'route-link route-link-active custom-class', ] for (const [index, className] of CONFIGS.entries()) { @@ -80,6 +86,22 @@ test('should render attributes correctly', async ({ page }) => { attrName: 'aria-label', attrValue: 'test', }, + { + attrName: 'title', + attrValue: 'Title', + }, + { + attrName: 'target', + attrValue: '_blank', + }, + { + attrName: 'rel', + attrValue: 'noopener', + }, + { + attrName: 'aria-label', + attrValue: 'test', + }, ] for (const [index, { attrName, attrValue }] of CONFIGS.entries()) { @@ -99,6 +121,14 @@ test('should render slots correctly', async ({ page }) => { spansCount: 2, spansText: ['text', 'text2'], }, + { + spansCount: 1, + spansText: ['text'], + }, + { + spansCount: 2, + spansText: ['text', 'text2'], + }, ] for (const [index, { spansCount, spansText }] of CONFIGS.entries()) { const children = await page @@ -114,6 +144,12 @@ test('should render slots correctly', async ({ page }) => { test('should render hash and query correctly', async ({ page }) => { const CONFIGS = [ + `${BASE}#hash`, + `${BASE}?query`, + `${BASE}?query#hash`, + `${BASE}?query=1#hash`, + `${BASE}?query=1&query=2#hash`, + `${BASE}#hash?query=1&query=2`, `${BASE}#hash`, `${BASE}?query`, `${BASE}?query#hash`, @@ -134,3 +170,20 @@ test('should render hash and query correctly', async ({ page }) => { ).toHaveAttribute('href', href) } }) + +test('should render relative links correctly', async ({ page }) => { + const CONFIGS = [ + BASE, + `${BASE}404.html`, + `${BASE}components/not-exist.html`, + BASE, + `${BASE}404.html`, + `${BASE}components/not-exist.html`, + ] + + for (const [index, href] of CONFIGS.entries()) { + await expect( + page.locator('.e2e-theme-content #relative + ul > li a').nth(index), + ).toHaveAttribute('href', href) + } +}) diff --git a/packages/client/src/components/RouteLink.ts b/packages/client/src/components/RouteLink.ts index a246bba061..be3dccd24c 100644 --- a/packages/client/src/components/RouteLink.ts +++ b/packages/client/src/components/RouteLink.ts @@ -1,8 +1,8 @@ -import { h } from 'vue' -import type { FunctionalComponent, HTMLAttributes, VNode } from 'vue' -import { useRouter } from 'vue-router' +import { removeLeadingSlash } from '@vuepress/shared' +import { computed, defineComponent, h } from 'vue' +import type { SlotsType, VNode } from 'vue' +import { useRoute, useRouter } from 'vue-router' import { resolveRoutePath } from '../router/index.js' -import { withBase } from '../utils/index.js' /** * Forked from https://github.com/vuejs/router/blob/941b2131e80550009e5221d4db9f366b1fea3fd5/packages/router/src/RouterLink.ts#L293 @@ -23,7 +23,7 @@ const guardEvent = (event: MouseEvent): boolean | void => { return true } -export interface RouteLinkProps extends HTMLAttributes { +export interface RouteLinkProps { /** * Whether the link is active to have an active class * @@ -53,42 +53,61 @@ export interface RouteLinkProps extends HTMLAttributes { * * It's recommended to use `RouteLink` in VuePress. */ -export const RouteLink: FunctionalComponent< - RouteLinkProps, - Record, - { - default: () => string | VNode | (string | VNode)[] - } -> = ( - { active = false, activeClass = 'route-link-active', to, ...attrs }, - { slots }, -) => { - const router = useRouter() - const resolvedPath = resolveRoutePath(to) +export const RouteLink = defineComponent({ + name: 'RouteLink', - const path = - // only anchor or query - resolvedPath.startsWith('#') || resolvedPath.startsWith('?') - ? resolvedPath - : withBase(resolvedPath) + props: { + /** + * The route path to link to + */ + to: { + type: String, + required: true, + }, - return h( - 'a', - { - ...attrs, - class: ['route-link', { [activeClass]: active }], - href: path, - onClick: (event: MouseEvent = {} as MouseEvent) => { - guardEvent(event) ? router.push(to).catch() : Promise.resolve() - }, + /** + * Whether the link is active to have an active class + * + * Notice that the active status is not automatically determined according to the current route. + */ + active: Boolean, + + /** + * The class to add when the link is active + */ + activeClass: { + type: String, + default: 'route-link-active', }, - slots.default?.(), - ) -} + }, -RouteLink.displayName = 'RouteLink' -RouteLink.props = { - active: Boolean, - activeClass: String, - to: String, -} + slots: Object as SlotsType<{ + default: () => string | VNode | (string | VNode)[] + }>, + + setup(props, { slots }) { + const router = useRouter() + const route = useRoute() + + const path = computed(() => + props.to.startsWith('#') || props.to.startsWith('?') + ? props.to + : `${__VUEPRESS_BASE__}${removeLeadingSlash(resolveRoutePath(props.to, route.path))}`, + ) + + return () => + h( + 'a', + { + class: ['route-link', { [props.activeClass]: props.active }], + href: path.value, + onClick: (event: MouseEvent = {} as MouseEvent) => { + if (guardEvent(event)) { + router.push(props.to).catch() + } + }, + }, + slots.default?.(), + ) + }, +})