@@ -54,7 +54,7 @@ export interface Router {
54
54
export const RouterSymbol : InjectionKey < Router > = Symbol ( )
55
55
56
56
// 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
58
58
const fakeHost = 'http://a.com'
59
59
60
60
const getDefaultRoute = ( ) : Route => ( {
@@ -261,35 +261,57 @@ export function scrollTo(hash: string, smooth = false, scrollPosition = 0) {
261
261
return
262
262
}
263
263
264
- let target : Element | null = null
265
-
264
+ let target : HTMLElement | null = null
266
265
try {
267
266
target = document . getElementById ( decodeURIComponent ( hash ) . slice ( 1 ) )
268
267
} catch ( e ) {
269
268
console . warn ( e )
270
269
}
270
+ if ( ! target ) return
271
271
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 +
280
274
target . getBoundingClientRect ( ) . top -
281
275
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 } )
283
290
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 )
289
301
}
290
302
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 ( )
292
312
}
313
+
314
+ requestAnimationFrame ( scrollToTarget )
293
315
}
294
316
295
317
function handleHMR ( route : Route ) : void {
@@ -313,7 +335,7 @@ function shouldHotReload(payload: PageDataPayload): boolean {
313
335
function normalizeHref ( href : string ) : string {
314
336
const url = new URL ( href , fakeHost )
315
337
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
317
339
if ( siteDataRef . value . cleanUrls ) {
318
340
url . pathname = url . pathname . replace ( / \. h t m l $ / , '' )
319
341
} else if ( ! url . pathname . endsWith ( '/' ) && ! url . pathname . endsWith ( '.html' ) ) {
0 commit comments