ListItem checkbox not spoken by screenreader (WCAG)
Duplicates
- [X] I have searched the existing issues
Latest version
- [X] I have tested the latest version
Steps to reproduce 🕹
Steps:
- install screenreader extension in chrome browser
- surf to https://v4.mui.com/components/lists/
- scroll down to List Controls - Checkbox example
- start screenreader extension
- use tab key to move up and down list. screenreader sais "Line item 1" but nothing about checkbox
- use space key to set/unset checkbox. screenreader is quiet
Current behavior 😯
screenreader sais nothing about checkbox in listitem
Expected behavior 🤔
screenreader should inform user of checkbox state
Context 🔦
WCAG compliance
Your environment 🌎
npx @mui/envinfo
Don't forget to mention which browser you used.
Output from `npx @mui/envinfo` goes here.
Can i work on this issue?
Can i work on this issue?
Can you propose what you have in mind for the fix?
So I tested with a screenreader, and, as mentioned, when using the spacebar to check and uncheck it the screen reader says nothing. However, in the list right beneath it that demonstrates checkboxes as a secondary action, the screenreader does say "Checked" and "Unchecked" in response to using the spacebar. The difference between them is that the input elements is the problematic list have the attribute "tabindex = -1", and those in the working list do not have that attribute. Is there any reason the tabindex attribute is necessary in the first list and not the second? Removing this attribute using the devtools solved the issue, so could it just be removed in the actual code as well? If it can be, I would like to make the change as part of hacktoberfest.
TL;DR: it's actually not as simple as tackling the tabindex :'D
long version:
I dug into it a little deeper, and this is what's going on:
-
In the first checkbox example ("checkbox as a primary action"):
-
The interactive element is the
<div>immediately inside<li>; the inspector reveals that it hastabindex="0"and aclickevent listener. This<div>is the element that's in focus when pressing the spacebar. -
The checkedness state is conveyed visually (by changing some stuff in nested
<span>and<svg>elements), but not programmatically; in other words, there are no attributes on that<div>element that change when its state changes, and therefore screen readers have nothing to announce.
-
-
In the second checkbox example ("checkbox as a secondary action"):
-
The interactive element is the
<input type="checkbox">itself. It does not have atabindexattribute; it doesn't need one because the element is natively interactive, and therefore natively focusable. It also does not have aclickevent listener, but it still responds to click and keyboard events according to native HTML checkbox mechanics. -
The
<Checkbox>component has anonChangeprop, but the inspector reveals that the<input type="checkbox">element has nochangeevent listener. This is because of howonChangeworks in React: it basically lets the browser run its native change mechanics on the HTML checkbox (detect suitable click or keyboard event --> toggle checkedness state), and as that happens, also runs whatever function you passed via theonChangeprop. -
Since the native HTML checkbox mechanics are able to run undisturbed, that means that the
checkedDOM property of the checkbox node toggles appropriately betweentrueandfalse. And since the reading pointer of the screen reader is currently on the HTML checkbox (remember, that's the interactive element in this case), the screen reader picks up on this change, and is able to announce the change in checkedness state.
-
Basically, we get to hear about the change in checkedness as it happens if and only if that information is made available to the screen reader in its current reading location. It's not really about whether the checkbox is focusable; in fact, in the first checkbox example, you can click on the checkbox with the mouse, which brings the reading pointer to the checkbox, which means that change in checkedness happens right where the screen reader now is, which means that we get to hear about it.
-
Now here's the kicker: by using the inspector in examples 1 and 2 and playing around a bit, I noticed an interesting behavior in both cases.
-
Basically, interacting with a checkbox toggles the value of its
checkedDOM property betweentrueandfalse, but not the presence or absence of thecheckedHTML attribute (present basically meanstrue, and absent basically meansfalse). -
This happens because the DOM property is being handled by the native checkbox mechanics (which React, by design, doesn't interfere with), while the HTML attribute is being manipulated via the
checkedprop... but said prop is being controlled by an array of numbers stored as a React state, and handled in such a way that doesn't cause React to re-render the component. -
Long story short, we end up in a situation where the "DOM checkedness" is being toggled, but the "HTML checkedness" stays at the initial value, which means they contradict each other 50% of the time, which is not supposed to happen, and can potentially lead to much worse accessibility issues in certain environments.
-
-
Also... there's a number of more typical problems. To name a couple: in the first checkbox example, we have a
<div>element that's interactive, but assistive technologies have no way to know that... it might be tempting to do something like giving it the ARIArole="button", which is basically what was done in the second checkbox example... except that's also problematic, because as per the HTML spec, buttons are not allowed to contain nested interactive content (such as checkboxes).
I'm running out of brain juice now, so I guess my overall conclusion is that there are fundamental architectural problems caused by either disregarding or going directly against the native behaviors and semantics, and I don't have any easy fixes for you at the moment, just some food for thought :D