-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Programmatically setting focus navigation start point? #5326
Comments
cc @whatwg/a11y |
Can you describe the negative impacts of setting focus to a non-interactive element? I kind of was under the impression that's why almost everything is programmatically focusable, is so as to accomplish the use cases you describe here. But I guess something is not good enough about the existing solution? I worry about having a separate active element and focus navigation starting point, which seems like it would be confusing. So I think it's worth getting a sense of what's wrong with the current coupling of the two so we can evaluate the tradeoff. |
for clarity, what is the focus navigation start point? the equivalent of the reading position if you were using a screen reader? is it something internal to the browser? |
Most things are not programmatically focusable, unless you add This would work on any element, without requiring an opt-in on the element.
It can trigger confusing focus styling/indication - both from Focus indication is only helpful (IMO) when the focused element has some kind of keyboard interactivity.
Apologies, I was unclear about my intent there: my intent was that it would still blur the active element, so |
Right, I meant if you add that. So we're comparing this new primitive vs. using the existing focus primitive on a
In the case of skip links (for example), it seems like the keyboard interactivity is "you can use the keyboard to navigate focus within the main content". E.g. pressing Tab takes you to the first focusable piece of main content. Similarly for side menus. Perhaps I'm not understanding what you mean by keyboard interactivity?
I see. This does seem to reduce the potential confusion by making it similar to how browsers seem to behave in the existing case, e.g. of clicking on a non-click-focusable element. So, this complexity does already exist in the platform. The question is whether it's worth exposing this complexity to JavaScript, and thus letting authors trigger it instead of users. I can see how when used for good, it probably aligns with user expectations. But it feels a bit like we're solving the problem of "people are using Ultimately what I'm not quite grasping is why it's better for users to have the body focused than to have the container element focused, in these cases. This is likely just a matter of me not having enough experience in these areas, so please take all of this in the spirit of me trying to strenghten your case, and not just being skeptical or resistant to change. |
As in an interactive control that can be/is operated via keyboard... |
Setting aside whether this functionality is useful or necessary, I think a way to set focus navigation start point without actually focusing anything should be a new method, not a new parameter to |
The reason I proposed overloading I take your point about overloading focus beyond its current meaning, but in practice Adding a new method is definitely a possibility, although it risks being stuck forever in a naming bikeshed, for what I see as debatable value.
I agree with @bkardell's framing. Meaningful keyboard interactivity in this case is, approximately, "handles keyboard events" (modulo event delegation). |
@othermaciej Apologies for not addressing this in my comment above:
This is a good point. The fallback, I believe, would default to either focusing the element (if it is focusable), or a no-op (if it is not). Since the latter is what we want to avoid, we would need to seriously consider how to address that if we didn't add a new method. |
@alice I find I am confused about the situation. What is the current effect of (Or is the distinction that some elements are focusable but non-interactive? And focusing interactive elements has an undesirable side effect? In which case, I think a new method is still a better design.) |
@othermaciej Sorry for the confusion, let me clarify the comments which probably read that way:
That looks like this: <!-- non-interactive element with tabindex -->
<div id="container" style="display: none" tabindex="-1">
<!-- interactive content goes in here -->
</div> function openModal() {
container.style.display = "block";
// focus the container to allow moving focus into interactive content
container.focus();
} So
The former case looks like: <button id="button">Focusable element</button> // if the setActiveElement option isn't supported, this will focus button as usual
button.focus({ setActiveElement: false }); The latter looks like: <div id="container"> <!-- no tabindex -->
<!-- interactive content goes here -->
</div> // if the setActiveElement option isn't supported, this is a no-op:
// focus stays on the previous activeElement
container.focus({ setActiveElement: false }); Hope that clarifies things! |
OK. So part of this is avoiding the need to carefully prepare a non-interactive focusable element, such by setting |
I address that in earlier comments:
|
wondering naively if all that would be required (but of course, throughout all user agents) is changing the behavior of i.e. i can't currently think of a situation where i'd need to move focus navigation start point to a focusable element without wanting to also set focus to it (unless i wanted to park the start point "just before" it, but then i'd generally want to target something preceding) |
Rob and I thought it might be an issue if existing code expects |
Setting If JavaScript is not available and you want to set a different "start point", a new tag or attribute would be needed and processed by the browser. For instance, let's say that I have a static site with several deep-linking pages. User starts on the homepage and gets the normal experience (start point is document.body). Then uses a link to navigate to a subpage. With "start point" set, while the referrer is the same origin and location is not the baseUrl, focus begins at the "start point" (I imagine it to be What this looks like in code could be |
but then the thing has focus, which is distinct from it being where the focus start point is. i'd have to test, so i may be talking rubbish here, but from memory when a container with |
It's true that this works today, but we would argue that it's kind of a hack that developers have to be taught because there is no standard way of setting the focus start point. Internally, browsers have the ability to move the focus start point and it would be useful to expose this to developers. Otherwise, on a large app, there might be many instances where they need to sprinkle |
As someone who has found himself setting focus to Example cases I can think of: modal window, expand/collapse functionality and tabs (if not following ARIA Authoring Practices for focus management).
|
I agree that this would be a very helpful API to have. Setting Using the current pattern, developers have to consider the following steps:
Making this pattern re-usable is very difficult for a large scale website. In these steps, I'd say step 1 alone is already a big task and quite a burden to maintain. And for step 2 to 5, the developer will have to consider how to handle the conditional cases if the start point is already an interactive element v.s. not. This is further complicated by how "interactive element" isn't always "focusable" (#4464). For example, I've been working on adding focus management to the file list on GitHub's repository page. Currently when user navigates through the file directories with a keyboard, each navigation drops user's focus back to the top of the page. The most basic solution would be to set the file table as the start point. However, with the current recommended pattern, we have to go through that 5 steps above. With this new API, it'll be:
This one line solution would immediately make the experience 10 times better. If we want to further enhance the experience, we can write more code to track which directory user came from, and is going to, and move focus to the links themselves, but that's immediately a much more complex solution, and requires developers to know exactly what goes into this piece of UI, therefore won't be shareable to other parts of the site. Whereas the proposed solution can be easily applied to other places with/without interactive content in them. I think this convenience is much needed considering that it is very rare for businesses to put development resource into designing a keyboard/screen reader specific experience. With regards to using For clarity sake, I think it'd be the most ideal to separate their responsibilities, especially considering |
@muan Thank you so much for this detailed motivating example! Would you mind if I copied parts of it up to the issue description (with a link to your comment)? Regarding introducing a new method, good point about focus events, I hadn't considered that. What might a good method name be for the new method? This also prompts me to think we might want an API to get the focus navigation start point (just like we can get the active element), as well; that might play into the design. |
Not at all. I’m glad it’s helpful.
Big question 😬. To start with, I hope that it’ll communicate not just setting the start point, but also the fact that focus will be taken away from the current active element. I’ll read through the spec to see if I can think of something that makes sense. |
I like the direction this issue is heading, but wanted to call out that is seems like not all browsers have separate state to track the location of a focus navigation starting point. The canonical example given in the HTML spec of how a user can set a focus navigation starting point is by clicking somewhere. Clicking somewhere also moves the selection, and in Firefox it looks like the location of selection is what represents the concept of the focus navigation starting point (either that or moving selection also moves the focus navigation starting point such that the two locations cannot be distinguished). So one candidate for the API you want could be Selection.collapse - seems to work today in Firefox. Some other benefits of using selection to represent the focus navigation start point:
So some questions to consider:
Here's a test page if you want to try out the interaction between selection and the focus navigation starting point. |
See also #7657 about the selection vs. focus distinction. |
At the risk of making a '+1' comment, I've also encountered this need in two separate contexts. The first is SPA navigation where the link is to The second is the case where a Of course, in order to set the start point in that case, we also need to know what it was before the dialog was opened. So if we're talking about adding a new method for setting the start point, should we also consider a counterpart method for getting the current start point? const element = document.getFocusStartPoint();
// later
document.setFocusStartPoint(element); For anyone who stumbled upon this thread looking for existing solutions, this is what I'm currently doing, until someone tells me they're harmful: function getFocusStartPoint() {
return window.getSelection()?.focusNode.parentElement;
}
function setFocusStartPoint(element) {
const tabindex = element.getAttribute('tabindex');
element.setAttribute('tabindex', '-1');
element.focus();
element.blur();
if (tabindex) {
element.setAttribute('tabindex', tabindex);
} else {
element.removeAttribute('tabindex');
}
} |
Why remove the "tabindex" when it is not there? it has two conditions, |
element.setAttribute('tabindex', '-1'); |
unless browsers error-correct here (and some seem to do), this undoes all the good you're doing before it and loses/resets focus/reading position back to the start of the document (certainly in combinations like Chrome/JAWS) as an example, see this zenorocha/clipboard.js#805 (which has since been fixed in clipboard.js via zenorocha/clipboard.js#807) |
Notes for TPAC, slide for context. how does the current experience differs from the new experience and is it ideal?
In Focus management still matters by @smhigley, she mentions, and I paraphrase here: having good SFNSP is a general accessibility win, but support is poor (post includes a table of AT support), therefore the old hacky script is very much still required. The magic and benefits to set SFNSP is currently reserved to the browser, and the point of this proposal is exposing this to developers. why not actually focus on something?
Regardless, I will attempt to write a polyfill to get AT user feedback (still need to investigate if it's possible to replicate the API experience, if not, I'd hope that we could try something behind flag in one of the browsers to allow AT users to test it). Todo:
|
To answer "how does the current experience differs from the new experience and is it ideal?" more, cc @zcorpan, when focus is set on a container, everything in the container is read by AT, but when a virtual cursor (SFNSP) is set on an element, the element's name is read, instead of the full content, which is much preferable. cc @patrickhlauke for more feedback on AT behaviors. I also just talked to ARIA WG about this and realized (probably should've earlier) that similar patterns with regards to current virtual cursor behavior already exist in a similar manner. For example fragment linking sets SFNSP, and moves virtual cursor to the element. If there was an I am not seeing this in AAM spec but I will dig deeper. cc @hidde / @spectranaut if you know where I should look. I will also try to track down engine engineers to see if they know how this is happening internally. |
This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60
Relevant WebKit code since it had the best support (from 2020): SFNSP on
cc @cookiecrook, would love to have your view on this as well. |
This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60
This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhangg@chromium.org> Commit-Queue: Joey Arhar <jarhar@chromium.org> Cr-Commit-Position: refs/heads/main@{#1197956}
This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhangg@chromium.org> Commit-Queue: Joey Arhar <jarhar@chromium.org> Cr-Commit-Position: refs/heads/main@{#1197956}
This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhangg@chromium.org> Commit-Queue: Joey Arhar <jarhar@chromium.org> Cr-Commit-Position: refs/heads/main@{#1197956}
…t, a=testonly Automatic update from web-platform-tests Implement setSequentialFocusStartingPoint This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhangg@chromium.org> Commit-Queue: Joey Arhar <jarhar@chromium.org> Cr-Commit-Position: refs/heads/main@{#1197956} -- wpt-commits: ac2aa5a07011181a9437a071786ded31431b6b73 wpt-pr: 42006
…t, a=testonly Automatic update from web-platform-tests Implement setSequentialFocusStartingPoint This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhanggchromium.org> Commit-Queue: Joey Arhar <jarharchromium.org> Cr-Commit-Position: refs/heads/main{#1197956} -- wpt-commits: ac2aa5a07011181a9437a071786ded31431b6b73 wpt-pr: 42006 UltraBlame original commit: e31a9e86898a1b6b91b3acb6c3b792ba4d67166f
…t, a=testonly Automatic update from web-platform-tests Implement setSequentialFocusStartingPoint This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhanggchromium.org> Commit-Queue: Joey Arhar <jarharchromium.org> Cr-Commit-Position: refs/heads/main{#1197956} -- wpt-commits: ac2aa5a07011181a9437a071786ded31431b6b73 wpt-pr: 42006 UltraBlame original commit: e31a9e86898a1b6b91b3acb6c3b792ba4d67166f
…t, a=testonly Automatic update from web-platform-tests Implement setSequentialFocusStartingPoint This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhangg@chromium.org> Commit-Queue: Joey Arhar <jarhar@chromium.org> Cr-Commit-Position: refs/heads/main@{#1197956} -- wpt-commits: ac2aa5a07011181a9437a071786ded31431b6b73 wpt-pr: 42006
…t, a=testonly Automatic update from web-platform-tests Implement setSequentialFocusStartingPoint This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhanggchromium.org> Commit-Queue: Joey Arhar <jarharchromium.org> Cr-Commit-Position: refs/heads/main{#1197956} -- wpt-commits: ac2aa5a07011181a9437a071786ded31431b6b73 wpt-pr: 42006 UltraBlame original commit: e31a9e86898a1b6b91b3acb6c3b792ba4d67166f
This is proposed here: whatwg/html#5326 Change-Id: I2c0c0ac578180ce5e1cf327fb65c6450a7d49f60 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4866726 Reviewed-by: Di Zhang <dizhangg@chromium.org> Commit-Queue: Joey Arhar <jarhar@chromium.org> Cr-Commit-Position: refs/heads/main@{#1197956}
The snippet above by @Rich-Harris works great in Chrome & Firefox. Safari apparently still requires some handholding. The snippet below, though verbose, seems to work well across browsers. It works by creating an invisible empty node as the first child of the target element, focussing it, then removing it from the document. This will remove focus, but most modern browsers will remember where focus was lost and restore focus when initiating keyboard navigation again. function setFocusStartPoint(element) {
const focusElement = createInvisibleFocusElement();
element.prepend(focusElement);
focusElement.focus({ preventScroll: true });
focusElement.remove();
}
function createInvisibleFocusElement() {
const element = document.createElement('div');
element.setAttribute('tabindex', '-1');
element.style.position = 'absolute';
element.style.width = '1px';
element.style.height = '1px';
element.style.overflow = 'hidden';
element.style.clip = 'rect(1px, 1px, 1px, 1px)';
element.style.clipPath = 'inset(50%)';
element.style.outline = 'none';
return element;
} |
note that this leads to focus resetting to the very start of the page when you're running the JAWS screen reader (just tested this with a throwaway codepen in Chrome/JAWS) |
@patrickhlauke Good to know! I assume this would apply equally to the original suggestion of using |
as noted here #5326 (comment) and yes still happens despite browser error correction |
@patrickhlauke Interesting! We'll keep tightening the screws on our solution and report back 🔩 |
For a while, @robdodson and I have been noting use cases where setting the focus navigation start point, as opposed to the active element, would be preferable.
One motivating example is skip links - currently the technique may require setting focus to a non-interactive element purely to effectively set the focus navigation start point.
Another example is focus management for transient UI - for example, a side menu which appears as a result of a keyboard shortcut. It may not be appropriate to focus any specific element in the newly visible UI, but the user should be able to easily move focus within that UI, even if the UI is not modal.
Strawman API proposal:
This would unfocus the current active element, and set the focus navigation start point to
el
.Additional naming proposals from @othermaciej in #5326 (comment):
More name suggestions from @muan in #5326 (comment)
Another example from @muan in #5326 (comment):
I've been working on adding focus management to the file list on GitHub's repository page. Currently when user navigates through the file directories with a keyboard, each navigation drops user's focus back to the top of the page.
The most basic solution would be to set the file table as the start point. However, with the current recommended pattern, we have to go through 5 steps:
tabindex="-1"
on the file tableCall focus()
on the file tableWith this new API, it'll be:
table.focus({ setActiveElement: false })
This one line solution would immediately make the experience 10 times better.
The text was updated successfully, but these errors were encountered: