Skip to content

Commit 7c8f271

Browse files
Mister-Hopemeteorlxy
andauthoredMay 13, 2024··
feat(client): support relative link in RouteLink (#1545)
Co-authored-by: Xinyu Liu <meteor.lxy@foxmail.com>
1 parent c443a95 commit 7c8f271

File tree

5 files changed

+145
-43
lines changed

5 files changed

+145
-43
lines changed
 

‎e2e/docs/components/route-link.md

+27
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,37 @@
2828

2929
- <RouteLink to="/README.md" active="">text</RouteLink>
3030
- <RouteLink to="/README.md" active>text</RouteLink>
31+
- <RouteLink to="/" active="">text</RouteLink>
32+
- <RouteLink to="/" active>text</RouteLink>
3133
- <RouteLink to="/README.md" :active="false">text</RouteLink>
3234
- <RouteLink to="/README.md">text</RouteLink>
35+
- <RouteLink to="/" :active="false">text</RouteLink>
36+
- <RouteLink to="/">text</RouteLink>
3337

3438
### Class
3539

3640
- <RouteLink to="/README.md" class="custom-class">text</RouteLink>
3741
- <RouteLink to="/README.md" active class="custom-class">text</RouteLink>
42+
- <RouteLink to="/" class="custom-class">text</RouteLink>
43+
- <RouteLink to="/" active class="custom-class">text</RouteLink>
3844

3945
### Attrs
4046

4147
- <RouteLink to="/README.md" title="Title">text</RouteLink>
4248
- <RouteLink to="/README.md" target="_blank">text</RouteLink>
4349
- <RouteLink to="/README.md" rel="noopener">text</RouteLink>
4450
- <RouteLink to="/README.md" aria-label="test">text</RouteLink>
51+
- <RouteLink to="/" title="Title">text</RouteLink>
52+
- <RouteLink to="/" target="_blank">text</RouteLink>
53+
- <RouteLink to="/" rel="noopener">text</RouteLink>
54+
- <RouteLink to="/" aria-label="test">text</RouteLink>
4555

4656
### Slots
4757

4858
- <RouteLink to="/README.md"><span>text</span></RouteLink>
4959
- <RouteLink to="/README.md"><span>text</span><span>text2</span></RouteLink>
60+
- <RouteLink to="/"><span>text</span></RouteLink>
61+
- <RouteLink to="/"><span>text</span><span>text2</span></RouteLink>
5062

5163
### Hash and query
5264

@@ -56,9 +68,24 @@
5668
- <RouteLink to="/README.md?query=1#hash">text</RouteLink>
5769
- <RouteLink to="/README.md?query=1&query=2#hash">text</RouteLink>
5870
- <RouteLink to="/README.md#hash?query=1&query=2">text</RouteLink>
71+
- <RouteLink to="/#hash">text</RouteLink>
72+
- <RouteLink to="/?query">text</RouteLink>
73+
- <RouteLink to="/?query#hash">text</RouteLink>
74+
- <RouteLink to="/?query=1#hash">text</RouteLink>
75+
- <RouteLink to="/?query=1&query=2#hash">text</RouteLink>
76+
- <RouteLink to="/#hash?query=1&query=2">text</RouteLink>
5977
- <RouteLink to="#hash">text</RouteLink>
6078
- <RouteLink to="?query">text</RouteLink>
6179
- <RouteLink to="?query#hash">text</RouteLink>
6280
- <RouteLink to="?query=1#hash">text</RouteLink>
6381
- <RouteLink to="?query=1&query=2#hash">text</RouteLink>
6482
- <RouteLink to="#hash?query=1&query=2">text</RouteLink>
83+
84+
### Relative
85+
86+
- <RouteLink to="../README.md">text</RouteLink>
87+
- <RouteLink to="../404.md">text</RouteLink>
88+
- <RouteLink to="not-exist.md">text</RouteLink>
89+
- <RouteLink to="../">text</RouteLink>
90+
- <RouteLink to="../404.html">text</RouteLink>
91+
- <RouteLink to="not-exist.html">text</RouteLink>

‎e2e/tests/components/route-link.spec.ts

+53
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ test('should render active status correctly', async ({ page }) => {
3838
const CONFIGS = [
3939
'route-link route-link-active',
4040
'route-link route-link-active',
41+
'route-link route-link-active',
42+
'route-link route-link-active',
43+
'route-link',
44+
'route-link',
4145
'route-link',
4246
'route-link',
4347
]
@@ -53,6 +57,8 @@ test('should render class correctly', async ({ page }) => {
5357
const CONFIGS = [
5458
'route-link custom-class',
5559
'route-link route-link-active custom-class',
60+
'route-link custom-class',
61+
'route-link route-link-active custom-class',
5662
]
5763

5864
for (const [index, className] of CONFIGS.entries()) {
@@ -80,6 +86,22 @@ test('should render attributes correctly', async ({ page }) => {
8086
attrName: 'aria-label',
8187
attrValue: 'test',
8288
},
89+
{
90+
attrName: 'title',
91+
attrValue: 'Title',
92+
},
93+
{
94+
attrName: 'target',
95+
attrValue: '_blank',
96+
},
97+
{
98+
attrName: 'rel',
99+
attrValue: 'noopener',
100+
},
101+
{
102+
attrName: 'aria-label',
103+
attrValue: 'test',
104+
},
83105
]
84106

85107
for (const [index, { attrName, attrValue }] of CONFIGS.entries()) {
@@ -99,6 +121,14 @@ test('should render slots correctly', async ({ page }) => {
99121
spansCount: 2,
100122
spansText: ['text', 'text2'],
101123
},
124+
{
125+
spansCount: 1,
126+
spansText: ['text'],
127+
},
128+
{
129+
spansCount: 2,
130+
spansText: ['text', 'text2'],
131+
},
102132
]
103133
for (const [index, { spansCount, spansText }] of CONFIGS.entries()) {
104134
const children = await page
@@ -114,6 +144,12 @@ test('should render slots correctly', async ({ page }) => {
114144

115145
test('should render hash and query correctly', async ({ page }) => {
116146
const CONFIGS = [
147+
`${BASE}#hash`,
148+
`${BASE}?query`,
149+
`${BASE}?query#hash`,
150+
`${BASE}?query=1#hash`,
151+
`${BASE}?query=1&query=2#hash`,
152+
`${BASE}#hash?query=1&query=2`,
117153
`${BASE}#hash`,
118154
`${BASE}?query`,
119155
`${BASE}?query#hash`,
@@ -134,3 +170,20 @@ test('should render hash and query correctly', async ({ page }) => {
134170
).toHaveAttribute('href', href)
135171
}
136172
})
173+
174+
test('should render relative links correctly', async ({ page }) => {
175+
const CONFIGS = [
176+
BASE,
177+
`${BASE}404.html`,
178+
`${BASE}components/not-exist.html`,
179+
BASE,
180+
`${BASE}404.html`,
181+
`${BASE}components/not-exist.html`,
182+
]
183+
184+
for (const [index, href] of CONFIGS.entries()) {
185+
await expect(
186+
page.locator('.e2e-theme-content #relative + ul > li a').nth(index),
187+
).toHaveAttribute('href', href)
188+
}
189+
})
+58-40
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { h } from 'vue'
2-
import type { FunctionalComponent, HTMLAttributes, VNode } from 'vue'
3-
import { useRouter } from 'vue-router'
1+
import { computed, defineComponent, h } from 'vue'
2+
import type { SlotsType, VNode } from 'vue'
3+
import { useRoute, useRouter } from 'vue-router'
44
import { resolveRoutePath } from '../router/index.js'
5-
import { withBase } from '../utils/index.js'
65

76
/**
87
* Forked from https://github.com/vuejs/router/blob/941b2131e80550009e5221d4db9f366b1fea3fd5/packages/router/src/RouterLink.ts#L293
@@ -23,7 +22,7 @@ const guardEvent = (event: MouseEvent): boolean | void => {
2322
return true
2423
}
2524

26-
export interface RouteLinkProps extends HTMLAttributes {
25+
export interface RouteLinkProps {
2726
/**
2827
* Whether the link is active to have an active class
2928
*
@@ -53,42 +52,61 @@ export interface RouteLinkProps extends HTMLAttributes {
5352
*
5453
* It's recommended to use `RouteLink` in VuePress.
5554
*/
56-
export const RouteLink: FunctionalComponent<
57-
RouteLinkProps,
58-
Record<never, never>,
59-
{
60-
default: () => string | VNode | (string | VNode)[]
61-
}
62-
> = (
63-
{ active = false, activeClass = 'route-link-active', to, ...attrs },
64-
{ slots },
65-
) => {
66-
const router = useRouter()
67-
const resolvedPath = resolveRoutePath(to)
55+
export const RouteLink = defineComponent({
56+
name: 'RouteLink',
6857

69-
const path =
70-
// only anchor or query
71-
resolvedPath.startsWith('#') || resolvedPath.startsWith('?')
72-
? resolvedPath
73-
: withBase(resolvedPath)
58+
props: {
59+
/**
60+
* The route path to link to
61+
*/
62+
to: {
63+
type: String,
64+
required: true,
65+
},
7466

75-
return h(
76-
'a',
77-
{
78-
...attrs,
79-
class: ['route-link', { [activeClass]: active }],
80-
href: path,
81-
onClick: (event: MouseEvent = {} as MouseEvent) => {
82-
guardEvent(event) ? router.push(to).catch() : Promise.resolve()
83-
},
67+
/**
68+
* Whether the link is active to have an active class
69+
*
70+
* Notice that the active status is not automatically determined according to the current route.
71+
*/
72+
active: Boolean,
73+
74+
/**
75+
* The class to add when the link is active
76+
*/
77+
activeClass: {
78+
type: String,
79+
default: 'route-link-active',
8480
},
85-
slots.default?.(),
86-
)
87-
}
81+
},
8882

89-
RouteLink.displayName = 'RouteLink'
90-
RouteLink.props = {
91-
active: Boolean,
92-
activeClass: String,
93-
to: String,
94-
}
83+
slots: Object as SlotsType<{
84+
default: () => string | VNode | (string | VNode)[]
85+
}>,
86+
87+
setup(props, { slots }) {
88+
const router = useRouter()
89+
const route = useRoute()
90+
91+
const path = computed(() =>
92+
props.to.startsWith('#') || props.to.startsWith('?')
93+
? props.to
94+
: `${__VUEPRESS_BASE__}${resolveRoutePath(props.to, route.path).substring(1)}`,
95+
)
96+
97+
return () =>
98+
h(
99+
'a',
100+
{
101+
class: ['route-link', { [props.activeClass]: props.active }],
102+
href: path.value,
103+
onClick: (event: MouseEvent = {} as MouseEvent) => {
104+
if (guardEvent(event)) {
105+
router.push(props.to).catch()
106+
}
107+
},
108+
},
109+
slots.default?.(),
110+
)
111+
},
112+
})

‎packages/client/src/router/resolveRoute.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ export interface ResolvedRoute<T extends RouteMeta = RouteMeta>
1313
*/
1414
export const resolveRoute = <T extends RouteMeta = RouteMeta>(
1515
path: string,
16+
currentPath?: string,
1617
): ResolvedRoute<T> => {
17-
const routePath = resolveRoutePath(path)
18+
const routePath = resolveRoutePath(path, currentPath)
1819
const route = routes.value[routePath] ?? {
1920
...routes.value['/404.html'],
2021
notFound: true,

‎packages/client/src/router/resolveRoutePath.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import { redirects, routes } from '../internal/routes.js'
44
/**
55
* Resolve route path with given raw path
66
*/
7-
export const resolveRoutePath = (path: string): string => {
7+
export const resolveRoutePath = (
8+
path: string,
9+
currentPath?: string,
10+
): string => {
811
// normalized path
9-
const normalizedPath = normalizeRoutePath(path)
12+
const normalizedPath = normalizeRoutePath(path, currentPath)
1013
if (routes.value[normalizedPath]) return normalizedPath
1114

1215
// encoded path

0 commit comments

Comments
 (0)
Please sign in to comment.