sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.71k stars 1.94k forks source link

manage focus, and use aria-live to announce navigation #307

Closed Rich-Harris closed 3 years ago

Rich-Harris commented 3 years ago

https://github.com/sveltejs/sapper/issues/1083, basically.

There are two separate issues to consider in order to make client-side routing accessible:

1. Managing focus

In a server-rendered app, any time you navigate to a page, the <body> is focused. In Sapper and SvelteKit apps, we blur the current activeElement upon navigation, but this doesn't actually reset the focus, which is what we need — it just removes it temporarily. As soon as you press the tab key (or shift-tab), focus moves to the element after (or before) whichever element was focused prior to the navigation, assuming it still exists in the DOM. This is not desirable.

Went down a bit of a rabbit hole trying to understand current recommendations:

I'm sure there are other suggestions too. Each comes from an accessibility expert and has a solid argument, but ultimately they are mutually exclusive. Any would be better than the status quo though.

My inclination in the short term is to simply focus <body> (i.e. David Luhr's suggestion) — it's easy to implement, and matches the behaviour of server-rendered apps. In the future perhaps we could investigate alternatives, particularly ones that put power in app developers' hands rather than making one-size-fits-all decisions for them.

The short term solution ought to be fairly straightforward — we just change this...

https://github.com/sveltejs/kit/blob/e09b6be95af81077e0fb0240ad94b1c0e41c9b3a/packages/kit/src/runtime/internal/router/index.js#L180-L182

...to this:

document.body.setAttribute('tabindex', '-1');
document.body.focus();

(Better still, only set the tabindex once, when the router initialises.)

2. Announcing the page change

Per the recommendation in https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/, we should add an ARIA live region that announces new pages. That looks something like this:

<div
  id="svelte-announcer"
  class="visually-hidden"
  aria-live="assertive"
  aria-atomic="true"
>Navigated to {title}</div>

This could live inside the auto-generated root.svelte component.

It makes sense for title to simply reflect document.title, I think, rather than introducing some complicated way to customise it, since a page already needs an informational document title for SEO and accessibility purposes.

Rich-Harris commented 3 years ago

(Not pictured: a solution to i18n, when constructing the Navigated to {title} text)

Rich-Harris commented 3 years ago

closed via #309 and #311

dummdidumm commented 3 years ago

For future consideration (haven't really thought this through): Could this be a component that can be imported via $app/a11y or something? People could then use it in their layout like this:

<script>
   import Announcer from "...";
</script>

<Announcer let:title>
   Hey, I've navigated to {title}
</Announcer>
Rich-Harris commented 3 years ago

I like that solution, it's a good way to make it customisable without configitis. The trade-off is you're much likelier to omit (or duplicate!) it, would need to think about how to avoid that.

$app/a11y could also be useful for things like skip nav links

dummdidumm commented 3 years ago

On "how to avoid": I guess this is the tradeoff here. To diminish this, we could add it to the template that is bootstrapped when using create-svelte.

philrenaud commented 3 years ago

Wanted to drop a note: the main application sveltekit app I'm working with uses a full-height, full-width css grid element as its primary "canvas", and the presence of a tabindex on the body removes all keyboard-based scrolling behaviour (arrow keys, PgDn, etc.) that was previously working.

I'm using the following in our _layout.svelte to counteract this for now, but it feels pretty hacky:

  onMount(async () => {
      document.body.removeAttribute('tabindex');
dummdidumm commented 3 years ago

When using goto there's now an option to not reset focus. The same doesn't exist as a directive for a tags yet though

philrenaud commented 3 years ago

@dummdidumm I appreciate the option, but we would need this for effectively all routes throughout the app. I wonder if there's some way to do it at a global config level?

dummdidumm commented 3 years ago

There's not right now, Rich mentions this in the initial post. I think it's best if you open a new feature request, outlining the current problem in detail as well as proposing some solutions, like the config option.

Badlapje commented 2 years ago

For future consideration (haven't really thought this through): Could this be a component that can be imported via $app/a11y or something? People could then use it in their layout like this:

<script>
   import Announcer from "...";
</script>

<Announcer let:title>
   Hey, I've navigated to {title}
</Announcer>

I would like to second this. This is something which for example Drupal provides as well.

On a general note: I'm very happily surprised at the state of a11y support being provided by Svelte. Keep up the good work!

Badlapje commented 2 years ago

As an added suggestion: can we add an a11y tag to the issue tags? I'd be happy to help out where i can with anything a11y related. It's easier to search for those issues if there's a tag for it.