From b3f6772df7af7d671524a6059e4a7b7c79dd67a9 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:20:08 +0530 Subject: [PATCH] fix(a11y): improve focus handling in router --- src/client/app/router.ts | 54 +++++++++++-------- .../components/VPDocOutlineItem.vue | 8 +-- .../theme-default/components/VPSkipLink.vue | 25 +-------- 3 files changed, 35 insertions(+), 52 deletions(-) diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 09d916ec..423b9d22 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -261,35 +261,45 @@ 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) { - const targetPadding = parseInt( - window.getComputedStyle(target).paddingTop, - 10 + if (!target) return + + const targetPadding = parseInt(window.getComputedStyle(target).paddingTop, 10) + + const targetTop = + window.scrollY + + target.getBoundingClientRect().top - + getScrollOffset() + + targetPadding + + const 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' }) + + // focus the target element for better accessibility + target.focus({ preventScroll: true }) + if (document.activeElement === target) return + + // target is not focusable, make it temporarily focusable + target.setAttribute('tabindex', '-1') + target.addEventListener( + 'blur', + () => { + target.removeAttribute('tabindex') + }, + { once: true } ) - - const targetTop = - window.scrollY + - target.getBoundingClientRect().top - - getScrollOffset() + - targetPadding - - 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' }) - } - - requestAnimationFrame(scrollToTarget) + target.focus({ preventScroll: true }) } + + requestAnimationFrame(scrollToTarget) } function handleHMR(route: Route): void { diff --git a/src/client/theme-default/components/VPDocOutlineItem.vue b/src/client/theme-default/components/VPDocOutlineItem.vue index c945fc21..4cff1b38 100644 --- a/src/client/theme-default/components/VPDocOutlineItem.vue +++ b/src/client/theme-default/components/VPDocOutlineItem.vue @@ -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 }) -}