[MenuItem] add aria-checked for checkbox and radio menu items#48651
[MenuItem] add aria-checked for checkbox and radio menu items#48651siriwatknp wants to merge 1 commit into
aria-checked for checkbox and radio menu items#48651Conversation
Deploy previewBundle size
Check out the code infra dashboard for more information about this PR. |
8c34236 to
0b9b296
Compare
menuitemcheckbox role and aria-checked for selected itemschecked prop with menuitemcheckbox role and aria-checked
| // A boolean `checked` turns the item into a checkable control, so expose the matching ARIA | ||
| // semantics. An explicit `role` is preserved (e.g. `menuitemradio`). Independent from `selected`. | ||
| const isCheckable = typeof checked === 'boolean'; | ||
| const role = roleProp ?? (isCheckable ? 'menuitemcheckbox' : 'menuitem'); |
There was a problem hiding this comment.
Menu items should either be menuitemcheckboxs or menuitems, this logic dynamically changes an arbitrary menuitem in a group of them to a menuitemcheckbox, resulting in something like this which doesn't seem valid:
role="menu"
role="menuitem"
role="menuitem"
role="menuitemcheckbox" // "checked"
role="menuitem"
Reference: https://master--base-ui.netlify.app/react/components/menu#checkbox-items
There was a problem hiding this comment.
I decided to change to a simpler approach, to add aria-checked only if user provide explicit role as menuitemcheckbox or menuitemradio.
0b9b296 to
2627aaf
Compare
checked prop with menuitemcheckbox role and aria-checkedmenuitemcheckbox/menuitemradio with aria-checked
2627aaf to
fb6e545
Compare
| // `menuitemcheckbox`/`menuitemradio` require `aria-checked`; derive it from `selected` | ||
| // (an omitted `selected` means unchecked). Other roles keep `selected` presentational. | ||
| const isCheckableRole = role === 'menuitemcheckbox' || role === 'menuitemradio'; | ||
| const ariaChecked = isCheckableRole ? Boolean(props.selected) : undefined; | ||
|
|
There was a problem hiding this comment.
@mj12albert do you think this is a nice enhancement to auto added aria-checked?
There was a problem hiding this comment.
I think it's a necessity as nobody will bother doing it 😆
fb6e545 to
64bda4a
Compare
|
@siriwatknp Did you consider adding a |
| ref, | ||
| disabled: props.disabled, | ||
| focusableWhenDisabled: itemsFocusableWhenDisabled, | ||
| selected: props.selected, |
There was a problem hiding this comment.
For menuitemcheckbox this wouldn't work if multiple checkboxes are initially checked
There was a problem hiding this comment.
selected controlled by the consumer. What do you mean by "this wouldn't work"?
There was a problem hiding this comment.
Sorry not it's not specific to multiple initially checked checkboxes:
<Menu open>
<MenuItem variant="checkbox">1</MenuItem>
<MenuItem variant="checkbox" selected>2</MenuItem>
<MenuItem variant="checkbox">3</MenuItem>
</Menu>When it's coupled to roving focus's "selected", by making item 2 here initially checked, I think initial focus will land on 2 instead of 1, but the correct behavior should be: initial focus 1?
| // `menuitemcheckbox`/`menuitemradio` require `aria-checked`; derive it from `selected` | ||
| // (an omitted `selected` means unchecked). Other roles keep `selected` presentational. | ||
| const isCheckableRole = role === 'menuitemcheckbox' || role === 'menuitemradio'; | ||
| const ariaChecked = isCheckableRole ? Boolean(props.selected) : undefined; | ||
|
|
There was a problem hiding this comment.
I think it's a necessity as nobody will bother doing it 😆
Let me try it |
I think exposing 2 more components are overkill, how about adding <MenuItem variant="item"> // default
<MenuItem variant="checkbox">
<MenuItem variant="radio"> |
64bda4a to
1124569
Compare
menuitemcheckbox/menuitemradio with aria-checkedvariant prop for checkbox and radio menu items
1124569 to
d632bf6
Compare
Generally our variant prop indicates visual variants (e.g. primary vs secondary button) with no functional differences, but in this case both visuals and functionality are changed so I'm not sure it's the best fit? |
good point, then may be a well, if naming is too hard. I rather switch back to the |
For role=menuitemcheckbox or menuitemradio, the selected prop sets aria-checked (omitted selected means unchecked); other roles keep selected presentational. No new prop, no breakage. Add docs section with menuitemcheckbox and menuitemradio demos.
d632bf6 to
8d8b43f
Compare
variant prop for checkbox and radio menu itemsmenuitemcheckbox/menuitemradio with aria-checked
menuitemcheckbox/menuitemradio with aria-checkedaria-checked for checkbox and radio menu items
| "description": "This prop can help identify which element has keyboard focus. The class name will be applied when the element gains the focus through keyboard interaction. It's a polyfill for the <a href=\"https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo\">CSS :focus-visible selector</a>. The rationale for using this feature <a href=\"https://github.com/WICG/focus-visible/blob/HEAD/explainer.md\">is explained here</a>. A <a href=\"https://github.com/WICG/focus-visible\">polyfill can be used</a> to apply a <code>focus-visible</code> class to other components if needed." | ||
| }, | ||
| "selected": { "description": "If <code>true</code>, the component is selected." }, | ||
| "selected": { |
There was a problem hiding this comment.
is this API consistent with other such examples in the library?
if we want to keep this stateless, why not just create an example with both role and aria-checked passed directly by the users? we're not helping much with the selected prop, we just translate it for them.
the other option in my opinion is go stateful, and manage the checked state ourselves. in this case, we probably need a Context wrapper and maybe new component wrapping MenuItem: MenuItemCheckbox and MenuItemRadio or something.
what do you think? @siriwatknp @mj12albert
There was a problem hiding this comment.
Re-using the name selected for checkbox/radio items adds indirection because you typically think of them as "checked" and not "selected".
Additionally if the API was to switch components via prop (e.g. MenuItem variant="checkbox"), "selected" could influence any of these 3 things depending on the component:
- styling - whether
.Mui-selectedis added or not - the
aria-checkedstate - the initial focus target - when the
selectedprop is passed into the roving focus hook
which could get hard to reason about when it's coupled to combinations of the variant and selected props, does that make sense?
So having new checkbox/radio item components gives us a way out of some restrictions/shortcomings of the current API
Summary
Makes
MenuItemexpose correct ARIA for checkable menus, following the WAI-ARIA menu pattern (items usemenuitemcheckbox/menuitemradio+aria-checked).When
roleismenuitemcheckboxormenuitemradio, the existingselectedprop drivesaria-checked. No new prop, no breaking changes.Behavior
<MenuItem role="menuitemcheckbox" selected />aria-checked="true"<MenuItem role="menuitemcheckbox" />aria-checked="false"(unchecked item)<MenuItem role="menuitemradio" selected />aria-checked="true"<MenuItem selected />(no role)role="menuitem", presentational only, no ARIA stateSelectoptions (role="option")aria-checkedselecteddrivesaria-checked(omittedselected⇒aria-checked="false"), and noaria-selectedis emitted.selectedstays presentational for other roles, so existing menus andSelectare untouched.Docs
New Checkbox and radio menu items section in the Menus page with two demos:
CheckboxMenu— independent toggles (menuitemcheckbox).RadioMenu— single choice in a group (menuitemradio), using radio-button icons.Tests
selecteddrivesaria-checked(true/false) formenuitemcheckboxandmenuitemradio, with noaria-selected.menuitemcheckboxwithselectedomitted →aria-checked="false".option) keepselectedpresentational, noaria-checked.<MenuItem selected />keepsrole="menuitem"with no ARIA state.