Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Second thoughts on focus management #202

Closed
domenic opened this issue Feb 17, 2022 · 13 comments
Closed

Second thoughts on focus management #202

domenic opened this issue Feb 17, 2022 · 13 comments

Comments

@domenic
Copy link
Collaborator

domenic commented Feb 17, 2022

The current explainer draft describes a new default focus behavior that single-page apps (SPAs) can opt into, by using the new navigation API. Translating the technical details there into an overview:

By default, any [SPA nav that goes through the new navigation API] will cause focus to reset to the <body> element, or to the first element with the autofocus="" attribute set (if there is one). This focus reset will happen after [the developer indicates the navigation has finished]. However, this focus reset will not take place if the user or developer has manually changed focus [during the developer-indicated loading interval]

This default behavior is called the "after-transition" focus reset behavior.

This was motivated by this post by @marcysutton, which states

... a user’s keyboard focus point may be kept in the same place as where they clicked, which isn’t intuitive. In layouts where the page changes partially to include a deep-linked modal dialog or other view layer, a user’s focus point could be left in an entirely wrong spot on the page.

and by the desire to make the "new default" for SPA navs the same as how MPA navs behave: i.e., to reset focus. The reasoning being, resetting focus to the body (or designated element with autofocus="") would ensure none of the bad outcomes Marcy describes would occur.

However, upon testing some SPAs in the wild, I'm having second thoughts about this. Essentially, resetting the focus by default is hostile to "tab" type patterns. For example:

  • This Google search displays a carousel of the moons of Jupiter. Clicking on any of them performs an SPA nav. Focus stays on the clicked moon. Resetting focus to the body would be bad here.

  • twitter.com's logged-in view displays tabs along the sideline. Clicking on, e.g., "Profile", performs an SPA nav. And focus stays on the "Profile" link. Resetting focus to the body would be bad here.

  • Airbnb's experiences page (sample) has various filters, e.g. "Arts and culture", "Sports", "Tours". Clicking on these performs an SPA navigation (and updates the very end of the URL bar). Focus remains on the thing you clicked. Resetting focus to the body would be bad here.

I think there are many more examples.

So now I'm not sure what to do. Some ideas:

  • Stick with the current plan, and make people aware that these sorts of tab patterns will need to use the navigation API's { focusReset: "manual" } option.

  • Change the default focus behavior for navigation API SPA navs, to "manual": which means, don't touch focus. The usual focus fixup rule will apply if you remove the currently-focused element from the DOM or make it inert or hide it with display: none. But if you translate it offscreen, or hide it behind something, or use visibility: hidden, then it will stay focused, with potentially confusing consequences for users. You can opt in to the no-longer-default reset behavior by using { focusReset: "after-transition" }.

  • Slight tweak of the "manual"-as-default plan above, but with a modified version of the focus fixup rule where it also accounts for autofocus="". Essentially, have a navigation API SPA nav reset the "has autofocus happened yet?" flag, allowing easy customizability of what gets focused after whole-page-replacing SPA navs.

  • A bigger tweak of the above, where we have some sort of "super focus fixup rule" which runs after navigation API SPA navs. E.g.: if the currently-focused element is not intersecting the viewport, or is occluded in some way, then reset focus. This seems scary because getting just the right amount of magic seems difficult.

  • Have the cleverness be based on the specific case where clicking an <a> causes a navigation API SPA nav. If that particular <a> is still in the DOM and focusable after the SPA nav finishes, then don't reset focus. Otherwise, reset focus. (To be clear, this would reset focus in cases like <button onclick="navigation.navigate('foo')">. Only <a>s get special treatment; tracking navigations through arbitrary JavaScript is too much magic.)

The most important question right now is what the right default should be. We will always have a "manual" mode available. And if we think that some of these clever modes are useful and people might want them in some cases, we can always implement them later. But we need to figure out the default---whether it's "manual", "after-transition", or something more clever---before we can ship an initial version of the navigation API.

@posva
Copy link
Contributor

posva commented Feb 18, 2022

I would say there are still cases like navbars where you will like the focus to be applied to the content after the navigation. For instance, I personally find it confusing in the case of Twitter to not switch focus to the main content we are visiting (Profile, notifications, etc). You can even mix some of these behaviors in the same app, so none of them would be desirable as a default. Given the mentioned research, it also seems like different users would want different focus patterns, so maybe there should be new aria attributes to be set by the developer on the page, or reuse existing HTML semantic elements like nav and main. Depending on the user settings on the browser, the focus will be set differently.

Otherwise, focusReset: "after-transition" seems like a good default

@tbondwilkinson
Copy link
Contributor

Another possibility: is the currently focused element in the DOM? If so, preserve focus. Otherwise, reset focus. This is simpler and more straightforward than calculating visibility.

Re:navbars setting focus to their contents, I think this is something the app should do, because it knows best what new content the user probably wants to be focused to.

The cases where people move DOM off-page or hide it are often because it's expensive to re-render content, especially if that content was initially rendered on the server. But I think this makes the stronger argument for some browser-native and performant way to cache DOM, because it would make the semantics of focus easier if we knew that whatever was in the DOM was actually part of the page, and not just being cached due to browser limitations.

@domenic
Copy link
Collaborator Author

domenic commented Feb 18, 2022

Another possibility: is the currently focused element in the DOM? If so, preserve focus. Otherwise, reset focus. This is simpler and more straightforward than calculating visibility.

I think this is one of the possibilities I mentioned in the OP: Change the default focus behavior for app history SPA navs, to "manual".

@dvoytenko
Copy link

One note: currently a display:none element would drop focus, which is convenient for focus management in a SPA. However, maybe this behavior could also be applied to content-visibility:hidden. After all, does it make sense to keep focus in a content-visibility:hidden DOM subtree?

@domenic
Copy link
Collaborator Author

domenic commented Feb 23, 2022

Examples of cases where "after-transition" is correct, on twitter.com:

  • Clicking on "Tweet" does not leave focus on the Tweet button. Instead it focuses the textbox in the new tweet dialog.
  • Clicking on any of the "What's happening" links on the sidebar does not leave focus on those links. Instead it resets focus to the top of the page.

So this is an example where on the same page you want both "manual" behaviors (for the sidebar tabs) and "after-transition" behaviors (for other tabs). Hmm.

@smaug----
Copy link

The spec proposal doesn't say anything about focus handling, right? And I think that is expected. The API should expose reasonable events so that authors can then focus whatever element they want. Having some default behavior is likely being wrong in too many cases.
(Given how hard it was to have reasonable focus handling for , as an example, I'd prefer to leave any explicit focus handling out from the initial, at least, version of the navigation API.)

@domenic
Copy link
Collaborator Author

domenic commented Mar 9, 2022

The spec proposal doesn't say anything about focus handling, right?

That's not correct. As detailed in the OP of this thread, and in https://github.com/WICG/navigation-api#focus-management , the proposal attempts to provide better tools for managing focus, to address the accessibility problems with single-page apps outlined in this user research.

So we definitely need to do something. This proposal suggests that "something" is to provide a new mode, focusReset: "after-transition", which works as described in the explainer.

The question at hand in this issue is, what should be the default behavior. The Fable Tech Labs user research claims that current default behavior, of leaving focus on a potentially off-screen element, leads to bad experiences. This issue thread is questioning whether that conclusion holds or not, given counterexamples such as tab UIs.

Having some default behavior is likely being wrong in too many cases.

There's no option where we don't pick a default behavior. The question is picking the right one. The two simplest options are:

  • "manual", which does not reset focus automatically. (Leading to the problems discussed in the user research)
  • "after-transition", which resets focus automatically after transition. (Leading to problems in tab-like UIs.)

The OP also discusses some more complicated possibilities.

@MelSumner
Copy link

MelSumner commented Mar 9, 2022

This Google search displays a carousel of the moons of Jupiter. Clicking on any of them performs an SPA nav. Focus stays on the clicked moon. Resetting focus to the body would be bad here.

I think in this case, the set of images are acting more like a row of pagination buttons/links or even breadcrumbs, on a meta level. See aria-current. The selected link/image could be marked with aria-current="page" and that's the thing that gets an .active CSS class, but it would still be fine to move the focus to the content (search results).

It could be useful to consider a new option: allowing a way to customize the definition of a route change (some existing art) with a specific API that SPAs opt-into (and build into their frameworks). That way they could provide an upgrade path for their users, and help them deprecate the tab-like UIs in a backwards-compatible way.

(Edit to add: I think by adding an API for SPAs to opt-into, it could be possible to otherwise leave focus resetting the way users expect it to work.)

@LJWatson
Copy link

LJWatson commented Mar 9, 2022

There are opposing use cases and all of them are valid, so trying to identify the default behaviour that meets most user expectations most of the time, is probably the best bet.

An advantage of the after-transition focus reset is that it's what most people are likely to expect because it mimics standard page load behaviour, and keyboard users will have navigation strategies based on that expectation.

Sometimes that strategy will be to return to the point on the page where they actually wanted to be, which is where @domenic's use cases come in, and sometimes the strategy will be to navigate through the content as usual.

Setting aside developer effort for a moment, each of the proposed options will be inconvenient to users some of the time and OK for users some of the time.

So my suggestion would be to go with the option that mimics standard page load behaviour, on the basis that it's almost certainly the most common experience users will be used to, but which makes it reasonable for developers to place focus somewhere else if/when there's a valid use case - and I think that's the after-transition focus reset behaviour?

@annevk
Copy link

annevk commented Mar 10, 2022

My expectations were similar to those of @smaug----. That focus remains as-is.

I see the argument for resetting by default though.

What happens to selection given whatwg/html#7657?

@smaug----
Copy link

That's not correct. As detailed in the OP of this thread, and in https://github.com/WICG/navigation-api#focus-management , the proposal attempts to provide better tools for managing focus, to address the accessibility problems with single-page apps outlined in this user research.

ok, I'm now lost what text I should be reading. I thought https://wicg.github.io/navigation-api/ is the current proposal and the rest are just random ideas which might be added to it. But I guess this issue is about that - whether to add something to the spec proposal.

@domenic
Copy link
Collaborator Author

domenic commented Mar 10, 2022

The spec draft for focusReset and scrollRestoration is at #201 ; I guess I should merge the focusReset part (which is mostly done) to avoid any confusion, while I try to figure out the scrollRestoration parts.

@domenic
Copy link
Collaborator Author

domenic commented Apr 22, 2022

After some offline discussion, we remain unsure about the correct default, but tentatively resolved to stick with "after-transition". In part, because it better matches cross-document (MPA) navigations; and in part because among the two that choice will more likely encourage developers to think about focus management, because they'll see focus getting reset.

Another interesting idea was to force people into choosing one, by making focusReset a required member. However, that plays very poorly with multiple navigate handlers; it precludes allowing something like a first navigate handler that doesn't care about focusReset behavior and wants to delegate that decision to a second navigate handler.

So for now, we'll close this issue accepting the current behavior. We'll keep an eye out for how people feel about the behavior in the wild and see if there are things to improve. But in the end the worst-case scenario is that we guessed wrong and most router code ends up using { focusReset: "manual "}. That isn't the end of the world.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants