-
Notifications
You must be signed in to change notification settings - Fork 922
feat(client): add AutoLink component #1546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
62cec2e
feat(shared): support relative links in normalizeRoutePath
Mister-Hope 73be4bb
feat(client): support relative link in resolveRoute and resolveRoutePath
Mister-Hope c12435a
feat(client): support relative link in RouteLink
Mister-Hope d94d9c5
chore: split function
Mister-Hope 7374e36
test: add unit tests
Mister-Hope b69d3b3
Merge branch 'main' into normalizeRoutePath
Mister-Hope ab3d6e6
Merge branch 'normalizeRoutePath' into route-link
Mister-Hope 5a4700a
feat(client): add AutoLink component
Mister-Hope 7297db5
perf: tweaks
Mister-Hope 3ffa61f
Update index.ts
meteorlxy 77b2a76
Merge branch 'main' into normalizeRoutePath
Mister-Hope f3690b8
Merge branch 'main' into route-link
Mister-Hope 6967766
Update inferRoutePath.ts
meteorlxy cddd691
test: update test
Mister-Hope 0f82b53
Merge branch 'normalizeRoutePath' into route-link
Mister-Hope 923619c
Merge branch 'route-link' into auto-link
Mister-Hope f3b3fb0
Merge branch 'main' into route-link
Mister-Hope c4eff84
Merge branch 'route-link' into auto-link
Mister-Hope 1ed8b68
Merge branch 'main' into auto-link
Mister-Hope ff460a2
feat: updates
meteorlxy d1c881e
test(e2e): fix locator usage
meteorlxy eed348d
chore: tweaks
meteorlxy 601d6f4
feat: add AutoLinkProps type
meteorlxy 11a5661
Merge remote-tracking branch 'origin/main' into auto-link
meteorlxy 3ba98e0
chore: tweaks
meteorlxy 0d4d658
chore: tweaks
meteorlxy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# AutoLink | ||
|
||
<div id="route-link"> | ||
<AutoLink v-for="item in routeLinksConfig" v-bind="item" /> | ||
</div> | ||
|
||
<div id="external-link"> | ||
<AutoLink v-for="item in externalLinksConfig" v-bind="item" /> | ||
</div> | ||
|
||
<div id="config"> | ||
<AutoLink v-bind="{ text: 'text1', link: '/', ariaLabel: 'label' }" /> | ||
<AutoLink v-bind="{ text: 'text2', link: 'https://example.com/test/' }" /> | ||
</div> | ||
|
||
<script setup lang="ts"> | ||
import { AutoLink } from 'vuepress/client' | ||
|
||
const routeLinks = [ | ||
'/', | ||
'/README.md', | ||
'/index.html', | ||
'/non-existent', | ||
'/non-existent.md', | ||
'/non-existent.html', | ||
'/routes/non-ascii-paths/中文目录名/中文文件名', | ||
'/routes/non-ascii-paths/中文目录名/中文文件名.md', | ||
'/routes/non-ascii-paths/中文目录名/中文文件名.html', | ||
'/README.md#hash', | ||
'/README.md?query', | ||
'/README.md?query#hash', | ||
'/#hash', | ||
'/?query', | ||
'/?query#hash', | ||
'#hash', | ||
'?query', | ||
'?query#hash', | ||
'route-link', | ||
'route-link.md', | ||
'route-link.html', | ||
'not-existent', | ||
'not-existent.md', | ||
'not-existent.html', | ||
'../', | ||
'../README.md', | ||
'../404.md', | ||
'../404.html', | ||
] | ||
|
||
const routeLinksConfig = routeLinks.map((link) => ({ link, text: 'text' })) | ||
|
||
const externalLinks = [ | ||
'//example.com', | ||
'http://example.com', | ||
'https://example.com', | ||
'mailto:[email protected]', | ||
'tel:+1234567890', | ||
] | ||
|
||
const externalLinksConfig = externalLinks.map((link) => ({ link, text: 'text' })) | ||
</script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { expect, test } from '@playwright/test' | ||
import { BASE } from '../../utils/env' | ||
|
||
test.beforeEach(async ({ page }) => { | ||
await page.goto('components/auto-link.html') | ||
}) | ||
|
||
test('should render route-link correctly', async ({ page }) => { | ||
for (const el of await page | ||
.locator('.e2e-theme-content #route-link a') | ||
.all()) { | ||
await expect(el).toHaveAttribute('class', /route-link/) | ||
} | ||
}) | ||
|
||
test('should render external-link correctly', async ({ page }) => { | ||
for (const el of await page | ||
.locator('.e2e-theme-content #external-link a') | ||
.all()) { | ||
await expect(el).toHaveAttribute('class', /external-link/) | ||
} | ||
}) | ||
|
||
test('should render config correctly', async ({ page }) => { | ||
const locator = page.locator('.e2e-theme-content #config a') | ||
|
||
await expect(await locator.nth(0)).toHaveText('text1') | ||
await expect(await locator.nth(0)).toHaveAttribute('href', BASE) | ||
await expect(await locator.nth(0)).toHaveAttribute('aria-label', 'label') | ||
|
||
await expect(await locator.nth(1)).toHaveText('text2') | ||
await expect(await locator.nth(1)).toHaveAttribute( | ||
'href', | ||
'https://example.com/test/', | ||
) | ||
await expect(await locator.nth(1)).toHaveAttribute('target', '_blank') | ||
await expect(await locator.nth(1)).toHaveAttribute( | ||
'rel', | ||
'noopener noreferrer', | ||
) | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
import { isLinkWithProtocol } from '@vuepress/shared' | ||
import type { SlotsType, VNode } from 'vue' | ||
import { computed, defineComponent, h } from 'vue' | ||
import { useRoute } from 'vue-router' | ||
import { useSiteData } from '../composables/index.js' | ||
import { RouteLink } from './RouteLink.js' | ||
|
||
export interface AutoLinkConfig { | ||
/** | ||
* Text of item | ||
* | ||
* 项目文字 | ||
*/ | ||
text: string | ||
|
||
/** | ||
* Aria label of item | ||
* | ||
* 项目无障碍标签 | ||
*/ | ||
ariaLabel?: string | ||
|
||
/** | ||
* Link of item | ||
* | ||
* 当前页面链接 | ||
*/ | ||
link: string | ||
|
||
/** | ||
* Rel of `<a>` tag | ||
* | ||
* `<a>` 标签的 `rel` 属性 | ||
*/ | ||
rel?: string | ||
|
||
/** | ||
* Target of `<a>` tag | ||
* | ||
* `<a>` 标签的 `target` 属性 | ||
*/ | ||
target?: string | ||
|
||
/** | ||
* Regexp mode to be active | ||
* | ||
* 匹配激活的正则表达式 | ||
*/ | ||
activeMatch?: string | ||
} | ||
|
||
export const AutoLink = defineComponent({ | ||
name: 'AutoLink', | ||
|
||
props: { | ||
/** | ||
* Text of item | ||
* | ||
* 项目文字 | ||
*/ | ||
text: { | ||
type: String, | ||
required: true, | ||
}, | ||
|
||
/** | ||
* Link of item | ||
* | ||
* 当前页面链接 | ||
*/ | ||
link: { | ||
type: String, | ||
required: true, | ||
}, | ||
|
||
/** | ||
* Aria label of item | ||
* | ||
* 项目无障碍标签 | ||
*/ | ||
ariaLabel: { | ||
type: String, | ||
default: '', | ||
}, | ||
|
||
/** | ||
* Rel of `<a>` tag | ||
* | ||
* `<a>` 标签的 `rel` 属性 | ||
*/ | ||
rel: { | ||
type: String, | ||
default: '', | ||
}, | ||
|
||
/** | ||
* Target of `<a>` tag | ||
* | ||
* `<a>` 标签的 `target` 属性 | ||
*/ | ||
target: { | ||
type: String, | ||
default: '', | ||
}, | ||
|
||
/** | ||
* Whether it's active only when exact match | ||
* | ||
* 是否当恰好匹配时激活 | ||
*/ | ||
exact: Boolean, | ||
|
||
/** | ||
* Regexp mode to be active | ||
* | ||
* @description has higher priority than exact | ||
* | ||
* 匹配激活的正则表达式 | ||
* | ||
* @description 比 exact 的优先级更高 | ||
*/ | ||
activeMatch: { | ||
type: [String, RegExp], | ||
default: '', | ||
}, | ||
}, | ||
|
||
slots: Object as SlotsType<{ | ||
default?: () => VNode[] | VNode | ||
before?: () => VNode[] | VNode | null | ||
after?: () => VNode[] | VNode | null | ||
}>, | ||
|
||
setup(props, { slots }) { | ||
const route = useRoute() | ||
const siteData = useSiteData() | ||
|
||
// If the link has non-http protocol | ||
const withProtocol = computed(() => isLinkWithProtocol(props.link)) | ||
|
||
// Resolve the `target` attr | ||
const linkTarget = computed( | ||
() => props.target || (withProtocol.value ? '_blank' : undefined), | ||
) | ||
|
||
// If the `target` attr is "_blank" | ||
const isBlankTarget = computed(() => linkTarget.value === '_blank') | ||
|
||
// Whether the link is internal | ||
const isInternal = computed( | ||
() => !withProtocol.value && !isBlankTarget.value, | ||
) | ||
|
||
// Resolve the `rel` attr | ||
const linkRel = computed( | ||
() => props.rel || (isBlankTarget.value ? 'noopener noreferrer' : null), | ||
) | ||
|
||
// Resolve the `aria-label` attr | ||
const linkAriaLabel = computed(() => props.ariaLabel ?? props.text) | ||
|
||
// Should be active when current route is a subpath of this link | ||
const shouldBeActiveInSubpath = computed(() => { | ||
// Should not be active in `exact` mode | ||
if (props.exact) return false | ||
|
||
const localePaths = Object.keys(siteData.value.locales) | ||
|
||
return localePaths.length | ||
? // Check all the locales | ||
localePaths.every((key) => key !== props.link) | ||
: // Check root | ||
props.link !== '/' | ||
}) | ||
|
||
// If this link is active | ||
const isActive = computed(() => { | ||
if (!isInternal.value) return false | ||
|
||
if (props.activeMatch) | ||
return ( | ||
props.activeMatch instanceof RegExp | ||
? props.activeMatch | ||
: new RegExp(props.activeMatch, 'u') | ||
).test(route.path) | ||
|
||
// If this link is active in subpath | ||
if (shouldBeActiveInSubpath.value) | ||
return route.path.startsWith(props.link) | ||
|
||
return route.path === props.link | ||
}) | ||
|
||
return (): VNode => { | ||
const { before, after, default: defaultSlot } = slots | ||
|
||
const content = defaultSlot?.() || [ | ||
before ? before() : null, | ||
props.text, | ||
after?.(), | ||
] | ||
|
||
return isInternal.value | ||
? h( | ||
RouteLink, | ||
{ | ||
'class': 'auto-link', | ||
'to': props.link, | ||
'active': isActive.value, | ||
'aria-label': linkAriaLabel.value, | ||
}, | ||
() => content, | ||
) | ||
: h( | ||
'a', | ||
{ | ||
'class': 'auto-link external-link', | ||
'href': props.link, | ||
'rel': linkRel.value, | ||
'target': linkTarget.value, | ||
'aria-label': linkAriaLabel.value, | ||
}, | ||
content, | ||
) | ||
} | ||
}, | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './AutoLink.js' | ||
export * from './ClientOnly.js' | ||
export * from './Content.js' | ||
export * from './RouteLink.js' |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.