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?.(),
+ )
+ },
+})