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
Menu items with display: none
or hidden
erroneously receive focus when using arrow keys
#1107
Comments
<sl-menu-item>
s<sl-menu-item>
s
<sl-menu-item>
s<sl-menu-item inert?>
– provide a way to disable <sl-menu-item>
s from receiving focus
This change was made because, as it turns out, that allowing focus on disabled items is an accessibility best practice. If disabled items don't receive focus, here's what happens to visually impaired users:
The user is left wondering what happened to the fifth item and has no way to identify where and what the disabled item even is. Contrast this to disabled items that do receive focus:
The visually impaired user is aware of where the item is, what it is, and the fact that it's currently disabled. Here's another scenario for keyboard users who often rely on muscle memory to do things quickly. Consider the following menu when we don't skip disabled items: <sl-menu>
<sl-menu-item>New</sl-menu-item>
<sl-menu-item>Open</sl-menu-item>
<sl-menu-item>Save</sl-menu-item>
<sl-menu-item>Delete</sl-menu-item>
</sl-menu>
Now let's disable the "Save" option and try again: <sl-menu>
<sl-menu-item>New</sl-menu-item>
<sl-menu-item>Open</sl-menu-item>
<sl-menu-item disabled>Save</sl-menu-item>
<sl-menu-item>Delete</sl-menu-item>
</sl-menu>
Had the "Save" been focusable, the user would not have selected "Delete." Sure, we can argue that it's the user's fault for not being more cautious — keyboard users do tend to work quickly — or we can make the UX better by ensuring it remains consistent regardless of which options are disabled. These explanations are pretty straight-forward, but a common rebuttal is that disabled form controls and buttons don't receive focus. That's 100% true when tabbing! However, disabled form controls are still navigable with screen readers, so they'll be announced as dimmed/disabled, just not when tabbing. Here's a video showing how VoiceOver users hear disabled inputs when the virtual cursor lands on them. CleanShot.2023-01-06.at.11.09.34.mp4Note that the virtual cursor isn't moved exclusively with Tab, but also with modifier keys. In VoiceOver, for example, it's common to use Ctrl + Option + Right Arrow to move to the next element, even if it's not tabbable, as shown in the video above. By the way, I finally found the W3 source for best practices on focusing disabled controls which explains things further:
This logic doesn't apply to form controls, but certain "composite widget elements" such as options, menus, tabs, and tree items. As this appears to be the clearest, most definitive guideline available, Shoelace will continue to follow this pattern to improve the experience for as many users as possible. |
Thank you for the detailed answer. So you have no interest in providing support for |
If
How do you determine this? Do you ask your users if they have disabilities, or is “users without disabilities” a guise for personal preference? Don’t you think it’s easier to follow established best practices instead of expending all this energy reimplementing behaviors that are less accessible? I’ll agree there are inconsistencies in the wild. For example, menus in macOS allow you to focus on disabled items whereas Windows does not. But this isn’t an operating system, it’s the Web and when the W3 puts out clear guidance on how to do something accessibly, it’s a good idea to follow that advice for the benefit of everyone. I don’t get the persistence to undo this when it’s clearly documented. 🤷🏻♂️ |
o__O We cannot write this off to specs or browsers when we have Shoelace's very own if |
With Shoelace handlers explicitly disabled, <link rel=stylesheet href=//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/themes/light.css>
<script type=module>
import { SlMenu } from "//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/shoelace.js";
SlMenu.prototype.setCurrentItem = i => { console.warn('ignore', i); };
SlMenu.prototype.handleKeyDown = function(e) { console.warn(this, e.key); };
</script>
<script type=module src=//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/shoelace.js></script>
Despite the misplaced confidence, |
@claviska (@tonivj5) – I have added support for <link rel=stylesheet href=//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/themes/light.css>
<script type=module scope=sl-fix>
import { SlMenu } from "//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/shoelace.js";
const j = { // jump immediate/delayed, skip inert
i: (t, f, s = x => x?.inert) => { const x = t[f]; return s(x) ? j.i(t[f], f) : x; },
d: (t, f, s = x => x?.inert) => { const x = t; return s(x) ? j.d(t[f], f) : x; },
};
SlMenu.prototype.setCurrentItem = i => { console.warn('ignore', i); };
SlMenu.prototype.handleKeyDown = function(e) {
const t = e.target;
const a = j.d(this.firstElementChild, 'nextElementSibling');
const z = j.d(this. lastElementChild, 'previousElementSibling');
switch (e.key) {
case 'Home': a ?.focus?.(); break;
case 'End': z ?.focus?.(); break;
case 'ArrowUp': (t === a ? z : j.i(t, 'previousElementSibling'))?.focus?.(); break;
case 'ArrowDown': (t === z ? a : j.i(t, 'nextElementSibling'))?.focus?.(); break;
case 'Enter': t .click (); break;
}
};
</script>
<script type=module src=//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/shoelace.js></script> This approach does not require manually juggling around |
Now that we have left the technical aspect of this behind.
User agents can act on behalf of their users and indicate their accessibility requirements to web developers. I am not trying to make
This registers as anywhere between "uncalled for" to "borderline rude". I was very clearly not looking for reverting
I do believe in your choices in building Shoelace. That is why from the very first paragraph of this issue, I recognized the need to improve upon the state of the art and support you making the better choices. Please note that I have not asked for reverting to the old behavior this time but suggested a need for supporting In my use case, I just want to set a single The PoC in my previous comment suggests a solution that expends less
I don't get the eagerness to assume
when it's Shoelace clearly mishandling things. 🤷🏻♂️😎 |
<sl-menu-item inert?>
– provide a way to disable <sl-menu-item>
s from receiving focus<sl-menu-item inert?>
– provide a way to prevent <sl-menu-item>
s from receiving focus
I went through this issue from scratch because clearly there was a miscommunication somewhere. I overlooked the
and
The two words that made all the difference were buried below (emphasis mine):
My assumption was based on 1) the fact that this behavior changed just the other day; 2) the issue is titled “provide a way to prevent Everything in this issue except those two words led me to believe you were simply unhappy with the change and wanted an escape hatch for it. This type of request is unfortunately very common in open source, so you can understand why it might be frustrating. Anyways, I'm renaming this issue to clarify the problem and I'll address it soon. |
<sl-menu-item inert?>
– provide a way to prevent <sl-menu-item>
s from receiving focusdisplay: none
or hidden
erroneously receive focus when using arrow keys
Thank you! I am really glad we could resolve this productively. I am sorry for my part in causing a miscommunication. I will keep treating you and other developers making open-source contributions with utmost respect for their time and choices. |
Internally, extending the jump skip conditions in Original #1107 (comment). const j = { // jump immediate/delayed, skip inert
i: (t, f, s = x => x?.inert) => { const x = t[f]; return s(x) ? j.i(t[f], f) : x; },
d: (t, f, s = x => x?.inert) => { const x = t; return s(x) ? j.d(t[f], f) : x; },
}; Extended (exclude ¬ <link rel=stylesheet href=//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/themes/light.css>
<script type=module scope=sl-fix>
import { SlMenu } from "//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/shoelace.js";
const j = { // jump immediate/delayed, skip inert
i: (t, f, s = x => x?.tagName !== 'SL-MENU-ITEM' || x?.hidden || x?.inert) => { const x = t[f]; return s(x) ? j.i(t[f], f) : x; },
d: (t, f, s = x => x?.tagName !== 'SL-MENU-ITEM' || x?.hidden || x?.inert) => { const x = t; return s(x) ? j.d(t[f], f) : x; },
};
SlMenu.prototype.setCurrentItem = i => { console.warn('ignore', i); };
SlMenu.prototype.handleKeyDown = function(e) {
const t = e.target;
const a = j.d(this.firstElementChild, 'nextElementSibling');
const z = j.d(this. lastElementChild, 'previousElementSibling');
switch (e.key) {
case 'Home': a ?.focus?.(); break;
case 'End': z ?.focus?.(); break;
case 'ArrowUp': (t === a ? z : j.i(t, 'previousElementSibling'))?.focus?.(); break;
case 'ArrowDown': (t === z ? a : j.i(t, 'nextElementSibling'))?.focus?.(); break;
case 'Enter': t .click (); break;
}
};
</script>
<script type=module src=//cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.88/dist/shoelace.js></script> (haven't looked into detecting |
I don't really agree with hiding menu items as most menus shouldn't change dynamically, but I wasn't able to add all the tests I'd like due to Firefox hanging as soon as |
See previous regression from
2022-08-02
which you had resolved favorably.<sl-menu>
and<sl-menu-item>
regression in2.0.0-beta.79
caused by allowing focus on disabled items for improved accessibility #845The above link contrasts this, now repeated, regression against behaviors from various native (Windows Notepad, macOS Finder) and web apps (Visual Studio Code, Adobe Spectrum Web Components).
The reason I labeled this as a feature request rather than a bug report is because I feel your efforts for accessibility should be recognized, encouraged and earn you the license/freedom to improve upon the state of the art. That being said, we do need a simple way to restore behavior for users without disabilities matching learned experiences from all other platforms.
What issue are you having?
This causes a regression when we use
disabled
with a not-displayed<sl-menu-item>
.(shoelace.real-time.hypertext)
<sl-menu-item>
.87
focus jumps toOption 3
.88
focus is on hiddenOption 2
😅.87
to.88
Describe the solution you'd like
Provide a way to prevent
<sl-menu-item>
s from receiving focus while keeping them in DOM.<sl-menu-item inert>
does not work and probably it should be made to work (transferringinert
down to its relevant shadow-dom descendant.)Describe alternatives you've considered
Monkey-patching Shoelace, removing/adding
<sl-menu-item>
s from DOM.Both of the alternatives require excessive work for something that should be simple.
It would also be great if the focus-related Shoelace keyboard event handlers considered whether the next
<sl-menu-item>
hasCSS
display: none
without requiring a newinert
attribute.The text was updated successfully, but these errors were encountered: