Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 41 additions & 19 deletions src/client/app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface Router {
export const RouterSymbol: InjectionKey<Router> = Symbol()

// we are just using URL to parse the pathname and hash - the base doesn't
// matter and is only passed to support same-host hrefs.
// matter and is only passed to support same-host hrefs
const fakeHost = 'http://a.com'

const getDefaultRoute = (): Route => ({
Expand Down Expand Up @@ -261,35 +261,57 @@ export function scrollTo(hash: string, smooth = false, scrollPosition = 0) {
return
}

let target: Element | null = null

let target: HTMLElement | null = null
try {
target = document.getElementById(decodeURIComponent(hash).slice(1))
} catch (e) {
console.warn(e)
}
if (!target) return

if (target) {
const targetPadding = parseInt(
window.getComputedStyle(target).paddingTop,
10
)

const targetTop =
window.scrollY +
const targetTop =
window.scrollY +
target.getBoundingClientRect().top -
getScrollOffset() +
targetPadding
Number.parseInt(window.getComputedStyle(target).paddingTop, 10) || 0

const behavior = window.matchMedia('(prefers-reduced-motion)').matches
? 'instant'
: // only smooth scroll if distance is smaller than screen height
smooth && Math.abs(targetTop - window.scrollY) <= window.innerHeight
? 'smooth'
: 'auto'

const scrollToTarget = () => {
window.scrollTo({ left: 0, top: targetTop, behavior })

// focus the target element for better accessibility
target.focus({ preventScroll: true })

function scrollToTarget() {
// only smooth scroll if distance is smaller than screen height.
if (!smooth || Math.abs(targetTop - window.scrollY) > window.innerHeight)
window.scrollTo(0, targetTop)
else window.scrollTo({ left: 0, top: targetTop, behavior: 'smooth' })
// return if focus worked
if (document.activeElement === target) return

// element has tabindex already, likely not focusable
// because of some other reason, bail out
if (target.hasAttribute('tabindex')) return

const restoreTabindex = () => {
target.removeAttribute('tabindex')
target.removeEventListener('blur', restoreTabindex)
}

requestAnimationFrame(scrollToTarget)
// temporarily make the target element focusable
target.setAttribute('tabindex', '-1')
target.addEventListener('blur', restoreTabindex)

// try to focus again
target.focus({ preventScroll: true })

// remove tabindex and event listener if focus still not worked
if (document.activeElement !== target) restoreTabindex()
}

requestAnimationFrame(scrollToTarget)
}

function handleHMR(route: Route): void {
Expand All @@ -313,7 +335,7 @@ function shouldHotReload(payload: PageDataPayload): boolean {
function normalizeHref(href: string): string {
const url = new URL(href, fakeHost)
url.pathname = url.pathname.replace(/(^|\/)index(\.html)?$/, '$1')
// ensure correct deep link so page refresh lands on correct files.
// ensure correct deep link so page refresh lands on correct files
if (siteDataRef.value.cleanUrls) {
url.pathname = url.pathname.replace(/\.html$/, '')
} else if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
Expand Down
8 changes: 1 addition & 7 deletions src/client/theme-default/components/VPDocOutlineItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,12 @@ defineProps<{
headers: DefaultTheme.OutlineItem[]
root?: boolean
}>()

function onClick({ target: el }: Event) {
const id = (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.getElementById(decodeURIComponent(id))
heading?.focus({ preventScroll: true })
}
</script>

<template>
<ul class="VPDocOutlineItem" :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick" :title>
<a class="outline-link" :href="link" :title>
{{ title }}
</a>
<template v-if="children?.length">
Expand Down
25 changes: 2 additions & 23 deletions src/client/theme-default/components/VPSkipLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,18 @@ const route = useRoute()
const backToTop = ref()

watch(() => route.path, () => backToTop.value.focus())

function focusOnTargetAnchor({ target }: Event) {
const el = document.getElementById(
decodeURIComponent((target as HTMLAnchorElement).hash).slice(1)
)

if (el) {
const removeTabIndex = () => {
el.removeAttribute('tabindex')
el.removeEventListener('blur', removeTabIndex)
}

el.setAttribute('tabindex', '-1')
el.addEventListener('blur', removeTabIndex)
el.focus()
window.scrollTo(0, 0)
}
}
</script>

<template>
<span ref="backToTop" tabindex="-1" />
<a
href="#VPContent"
class="VPSkipLink visually-hidden"
@click="focusOnTargetAnchor"
>
<a href="#VPContent" class="VPSkipLink visually-hidden">
{{ theme.skipToContentLabel || 'Skip to content' }}
</a>
</template>

<style scoped>
.VPSkipLink {
position: fixed;
top: 8px;
left: 8px;
padding: 8px 16px;
Expand Down
Loading