@@ -54,7 +54,7 @@ export interface Router {
5454export const RouterSymbol : InjectionKey < Router > = Symbol ( )
5555
5656// we are just using URL to parse the pathname and hash - the base doesn't
57- // matter and is only passed to support same-host hrefs.
57+ // matter and is only passed to support same-host hrefs
5858const fakeHost = 'http://a.com'
5959
6060const getDefaultRoute = ( ) : Route => ( {
@@ -261,35 +261,57 @@ export function scrollTo(hash: string, smooth = false, scrollPosition = 0) {
261261 return
262262 }
263263
264- let target : Element | null = null
265-
264+ let target : HTMLElement | null = null
266265 try {
267266 target = document . getElementById ( decodeURIComponent ( hash ) . slice ( 1 ) )
268267 } catch ( e ) {
269268 console . warn ( e )
270269 }
270+ if ( ! target ) return
271271
272- if ( target ) {
273- const targetPadding = parseInt (
274- window . getComputedStyle ( target ) . paddingTop ,
275- 10
276- )
277-
278- const targetTop =
279- window . scrollY +
272+ const targetTop =
273+ window . scrollY +
280274 target . getBoundingClientRect ( ) . top -
281275 getScrollOffset ( ) +
282- targetPadding
276+ Number . parseInt ( window . getComputedStyle ( target ) . paddingTop , 10 ) || 0
277+
278+ const behavior = window . matchMedia ( '(prefers-reduced-motion)' ) . matches
279+ ? 'instant'
280+ : // only smooth scroll if distance is smaller than screen height
281+ smooth && Math . abs ( targetTop - window . scrollY ) <= window . innerHeight
282+ ? 'smooth'
283+ : 'auto'
284+
285+ const scrollToTarget = ( ) => {
286+ window . scrollTo ( { left : 0 , top : targetTop , behavior } )
287+
288+ // focus the target element for better accessibility
289+ target . focus ( { preventScroll : true } )
283290
284- function scrollToTarget ( ) {
285- // only smooth scroll if distance is smaller than screen height.
286- if ( ! smooth || Math . abs ( targetTop - window . scrollY ) > window . innerHeight )
287- window . scrollTo ( 0 , targetTop )
288- else window . scrollTo ( { left : 0 , top : targetTop , behavior : 'smooth' } )
291+ // return if focus worked
292+ if ( document . activeElement === target ) return
293+
294+ // element has tabindex already, likely not focusable
295+ // because of some other reason, bail out
296+ if ( target . hasAttribute ( 'tabindex' ) ) return
297+
298+ const restoreTabindex = ( ) => {
299+ target . removeAttribute ( 'tabindex' )
300+ target . removeEventListener ( 'blur' , restoreTabindex )
289301 }
290302
291- requestAnimationFrame ( scrollToTarget )
303+ // temporarily make the target element focusable
304+ target . setAttribute ( 'tabindex' , '-1' )
305+ target . addEventListener ( 'blur' , restoreTabindex )
306+
307+ // try to focus again
308+ target . focus ( { preventScroll : true } )
309+
310+ // remove tabindex and event listener if focus still not worked
311+ if ( document . activeElement !== target ) restoreTabindex ( )
292312 }
313+
314+ requestAnimationFrame ( scrollToTarget )
293315}
294316
295317function handleHMR ( route : Route ) : void {
@@ -313,7 +335,7 @@ function shouldHotReload(payload: PageDataPayload): boolean {
313335function normalizeHref ( href : string ) : string {
314336 const url = new URL ( href , fakeHost )
315337 url . pathname = url . pathname . replace ( / ( ^ | \/ ) i n d e x ( \. h t m l ) ? $ / , '$1' )
316- // ensure correct deep link so page refresh lands on correct files.
338+ // ensure correct deep link so page refresh lands on correct files
317339 if ( siteDataRef . value . cleanUrls ) {
318340 url . pathname = url . pathname . replace ( / \. h t m l $ / , '' )
319341 } else if ( ! url . pathname . endsWith ( '/' ) && ! url . pathname . endsWith ( '.html' ) ) {
0 commit comments