-
Notifications
You must be signed in to change notification settings - Fork 28
Description
This is spinning off from #25. It was also briefly mentioned in #162.
We have consistent feedback that focus management during SPA navs is an accessibility concern. This writeup goes into good detail.
I think we should make sure that by default, same-document navigations that are controlled by app history (i.e., using navigateEvent.transitionWhile()
) give a good focus experience. The starting point here is to get parity with cross-document navs (MPA navs). This means:
- For "push", "replace", and "reload" navigations, resetting focus to the
<body>
element, or the firstautofocus=""
element if there is one. - For "traverse" navigations, it's complicated.
https://boom-bath.glitch.me/focus-navigation-test.html is a useful test page for this.
For traverse navigations, if the page is stored in the bfcache, then the current plan per whatwg/html#5878 / whatwg/html#6696 is to restore focus. I.e., since the entire Document
is kept around, we just make sure not to clear its focus state when transitioning into or out of bfcache.
But for traverse navigations where the result is not coming from bfcache, there is no attempt at focus restoration in current browsers. I.e. there is nothing like scroll restoration (cf. #187) which tries to somehow find the right element to focus in this newly-created from-network-or-cache Document
. It just does the usual <body>
-or-autofocus=""
dance (and autofocus=""
doesn't seem to work in Chrome...).
So what should we do for app history and traverse navigations?
- On the one hand, SPA navs are supposed to be snappy and kinda like bfcache navs. So trying to make sure the focus is on the same element when you go back would be ideal.
- On the other hand, in the general case we won't be able to identify "the same element"!
- In most SPA architectures, off-screen content (i.e. from a previous history entry) will have been at least removed from the DOM, and probably destroyed via garbage collection; when you traverse back, the page will reconstruct new DOM elements for the content. We could keep a pointer to the element around, but that'd be a potential memory leak, and most likely that specific element won't reenter the DOM anyway, so we wouldn't get anything out of it in most cases. The only case where this would work really well would be when an SPA is implemented by just hiding and showing things, which might be the case for some toy apps but doesn't seem likely in general.
- We could try to do some heuristic, from something simple like "if the element has an ID, use that" to something complicated like "store the fact that we are on the 3rd
<input>
element inside the#nearest-ancestor-with-id
container and on restore try to focus based on that pointer". This seems really brittle and unpredictable.
My inclination then is to treat traverse navigations the same as push/replace/reload, and reset focus to the body or autofocus=""
element by default when using app history. Anything more complicated pretty much needs to be done via manual focus()
calls, IMO.
OK, so what does this all mean for the API? I think it means the following:
- If you do
navigateEvent.transitionWhile(promise, { focusReset: "after-transition" })
, this waits forpromise
to settle and then resets focus to the<body>
element or the firstautofocus=""
element.- Maybe, if the user or the developer has moved focus before the promise settles, we do nothing instead?
- If you do
navigateEvent.transitionWhile(promise, { focusReset: "manual" })
, app history does nothing with focus, i.e. the probably-now-offscreen element stays focused, or if it gets removed from the DOM/madedisplay: none
then focus resets to<body>
whenever that removal happens. (Note: this "focus fixup" ignoresautofocus=""
elements.) - If you do
navigateEvent.transitionWhile(promise, { focusReset: "immediate" })
, this immediately resets focus to the<body>
element or the firstautofocus=""
element.
Which of these should be the default? I think "after-transition"
is probably best, specifically because of the autofocus=""
interaction. We want to do this focus reset whenever all the elements, including the potential autofocus target, are ready. This does lead to a potentially-problematic situation where the focus stays on an obstructed element (e.g. behind a loading spinner), but I think more likely focus would get reset to <body>
due to element removal or hiding at some point, and then either stay there or get moved to the autofocus=""
control once things are loaded.
Overall this proposal is OK, but it does require some manual work by developers: mostly, putting autofocus=""
on appropriate places. In particular per the research cited above, screen reader users preferred resetting focus to a heading or to a wrapper element, instead of to the <body>
. To achieve that, we need help from developers to tell us what element focus should go to, and that's what autofocus=""
provides.