fix(react-tree): expose tree selection control to assistive technologies#36384
fix(react-tree): expose tree selection control to assistive technologies#36384PaulGMardling wants to merge 11 commits into
Conversation
📊 Bundle size reportUnchanged fixtures
|
|
Pull request demo site: URL |
a4b0786 to
7574e86
Compare
67743cc to
823bdbb
Compare
| @@ -0,0 +1,7 @@ | |||
| { | |||
There was a problem hiding this comment.
🕵🏾♀️ visual changes to review in the Visual Change Report
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png | 404 | Changed |
vr-tests-react-components/Portal 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Portal.Apply Class Names.should have green border.chromium.png | 15 | Changed |
vr-tests-react-components/Positioning 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Positioning.Positioning end.chromium.png | 878 | Changed |
| vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png | 44 | Changed |
vr-tests-react-components/ProgressBar converged 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png | 2 | Changed |
vr-tests-react-components/TagPicker 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/TagPicker.disabled - RTL.chromium.png | 635 | Changed |
There were 1 duplicate changes discarded. Check the build logs for more information.
…03.json Co-authored-by: Victor Genaev <vgenaev@gmail.com>
4d5ed51 to
e7c44a7
Compare
| if (selector) { | ||
| mainSlot.id = mainContentId; | ||
| if (!selector['aria-label'] && !selector['aria-labelledby']) { | ||
| selector['aria-labelledby'] = mainContentId; | ||
| } | ||
| } |
There was a problem hiding this comment.
Ideally this should be added as props while you construct the slot, not conditionally after as a mutation
There was a problem hiding this comment.
I've pushed an update, good catch, thank you
bsunderhus
left a comment
There was a problem hiding this comment.
Avoid mutations after the slot declaration, instead conditionally declare what you want. defaultProps should be enough for your usage here, let me know if it's not
Pass `ref` and `onBlur` to the actions slot as defaultProps during construction, rather than mutating the slot after creation. This follows Fluent UI best practices and addresses the code review feedback from @bsunderhus. Removed post-construction mutations on the actions slot and moved callback creation before slot initialization.
Pass `ref`, `onBlur`, and other props to both the `expandIcon` and `actions` slots as defaultProps during construction, rather than mutating the slots after creation. This eliminates the post-construction mutation pattern and follows Fluent UI best practices, addressing the code review feedback from @bsunderhus.
…edby Pass selector props during slot construction instead of mutating after. Preserve the conditional logic: only set aria-labelledby if the consumer didn't provide aria-label or aria-labelledby. Also set mainSlot.id unconditionally during construction. Update snapshots to reflect the id being added to the main slot.
…eeItemLayout/useTreeItemLayout.tsx Co-authored-by: Bernardo Sunderhus <bernardo.sunderhus@gmail.com>
| } as CheckboxProps | RadioProps, | ||
| elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType<CheckboxProps | RadioProps>, | ||
| }); | ||
| const mainSlot = slot.always(main, { |
There was a problem hiding this comment.
What happens if user provides a custom id, like in this example:
<FlatTree {...flatTree.getTreeProps()} aria-label="Selection">
{Array.from(flatTree.items(), flatTreeItem => {
const { content, ...treeItemProps } = flatTreeItem.getTreeItemProps();
return (
<FlatTreeItem {...treeItemProps} key={flatTreeItem.value}>
{/* what happens to the user provided id? */}
<TreeItemLayout id={`layout-${flatTreeItem.value}`}>{content}</TreeItemLayout>
</FlatTreeItem>
);
})}
</FlatTree>There was a problem hiding this comment.
We cannot just silently ignore user provided id, it should be the default. right now we're dropping it in favor of the one defined internally


Previous Behavior
Voice access users are unable to access selection tree controls.
New Behavior
The Tree selection control (a checkbox in multiselect, a radio in single-select) now participates in the accessibility tree with an accessible name derived from the item's content. Previously it was hidden from assistive technologies, leaving any tool that operates the visible control directly unable to reach it.
Although the issue was reported against Windows Voice Access, the gap was platform-agnostic and equally affected macOS Voice Control and comparable technologies. Exposing the control resolves it at the source: the selector is now discoverable and operable by assistive technologies across platforms.
Selection state remains on the tree item via aria-checked / aria-selected, so screen readers announce it once (no double-announcement), and keyboard behavior is unchanged (the selector stays tabIndex -1; Space toggles). The fix applies to both the standard tree item layout and the persona tree item layout.
Testing/Verification
Windows 11:
Voice Access — "Show numbers" now places a number on each selection control; saying that number toggles selection, and "Click (item name)" works, confirming the accessible name resolves.
Narrator — selection state is announced once (no double-announcement); the row and the checkbox are both reachable as expected.
macOS (supplementary proxy):
Voice Control number-targeting toggles selection; VoiceOver reads the state once (no doubling). The browser accessibility tree confirms the control is exposed with the correct name.
Automated:
Added unit regression tests asserting the selection control is exposed (found by role) and named; verified they fail if the fix is reverted.
Fixes #36260