fix(react-headless-components-preview): make Combobox expandIcon toggle the listbox - #36574
Open
Dmytro Kirpa (dmytrokirpa) wants to merge 4 commits into
Open
Conversation
…le the listbox The headless Combobox composes useListboxPopupState rather than the base useComboboxBase_unstable hook, so the expandIcon slot was rendered as a bare <span> with no button semantics and no way to open the listbox. Add role=button, aria-expanded and aria-disabled, the default 'Open' accessible name fallback, and toggle handlers. The listbox is a native popover, so its light-dismiss closes it on pointerup before click runs -- toggling therefore uses the open state captured on mousedown, otherwise clicking the icon to close would immediately reopen it.
|
Pull request demo site: URL |
Contributor
|
is there any way we can re-use from v9 to not duplicate the logic ? |
📊 Bundle size reportUnchanged fixtures
|
…ExpandIconSlot The expandIcon slot defaults (role="button", aria-expanded, aria-disabled) and the accessible-name fallback chain were duplicated between the v9 useComboboxBase_unstable and the headless useCombobox. Extract them into useComboboxExpandIconSlot and consume it from both. Toggle handlers stay at the call sites: v9 toggles on mousedown alongside useOnClickOutside, while the headless listbox is a native popover whose light-dismiss fires on pointerup, so it needs the mousedown/click pair.
Contributor
Author
|
Yes — done in 6bc0dd3. Extracted the shared part into |
| @@ -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 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png | 413 | Changed |
| vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png | 404 | Changed |
vr-tests-react-components/Positioning 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Positioning.Positioning end.chromium.png | 728 | Changed |
| vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png | 130 | Changed |
vr-tests-react-components/ProgressBar converged 3 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png | 91 | Changed |
| vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png | 51 | Changed |
| vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png | 26 | Changed |
vr-tests-react-components/TagPicker 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/TagPicker.disabled - Dark Mode.chromium.png | 658 | Changed |
| vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png | 635 | Changed |
There were 2 duplicate changes discarded. Check the build logs for more information.
Dmytro Kirpa (dmytrokirpa)
requested review from
Martin Hochel (Hotell) and
Victor Genaev (mainframev)
August 18, 2026 08:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
The headless
Comboboxrenders itsexpandIconas a bare<span>— no button semantics, no accessible name, and no way to open the listbox. Clicking the chevron does nothing.This happens because the headless
useComboboxcomposesuseListboxPopupState(the shared core it splits withDropdown) rather thanuseComboboxBase_unstablefrom@fluentui/react-combobox. That shared core deliberately owns only positioning, active-descendant wiring, and the listbox/root slots — trigger and icon slots are layered on by each consumer. TheexpandIconbehavior that baseComboboxprovides was simply never re-applied. Only the type was shared (ComboboxState = BaseComboboxState & {...}), which made it look like the behavior came along too.Dropdownis unaffected: itsexpandIconrenders inside the<button>trigger, so it is decorative by design —role="button"there would be invalid nested ARIA, andaria-expandedalready lives on the button.Changes
useCombobox, matchinguseComboboxBase_unstable:role="button",aria-expanded,aria-disabledon theexpandIconslotonMouseDown/onClickhandlers that toggle the listbox and return focus to the input"Open","Open [aria-label]", or anaria-labelledbychainOn the toggle implementation
The listbox is a native
popover="auto". Light-dismiss fires onpointerup, beforeclick, and cannot be suppressed withpreventDefault(). Two consequences:mousedownopens the listbox and the same pointer interaction immediately dismisses it.clickusing the currentopenstate can open but never close — light-dismiss has already setopentofalseby the timeclickruns, so!openreopens it.So
mousedownrecords the pre-interaction state andclicktoggles from that. Both failure modes are covered by the new test; the click-only variant fails it withExpected <div#fluent-listbox> not to exist in the DOM, but it was continuously found.Testing
New Cypress case
toggles on expand icon clickcovers open → focus stays on the input → close.Note for reviewers
This branch also adds
role="button"to theclearIconslot defaults, alongside the existingaria-hidden="true". Those two are in tension — an element with a button role that is hidden from assistive tech. Happy to drop it or split it out if it was not intended to ship here.