Repository navigation
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Import the Vitest globals explicitly, stop shadowing the global `test`, and remove the `flushMicrotasks()` calls that directly follow `await render()`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use top-level type-only imports, replace the inline `import()` type in the store, and wait for a single condition in the drag-and-drop focus test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Master's API docs generator no longer reads enum data attributes, which dropped the List and Item data attribute tables. Use ESM constants and export the namespaces from the entry point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strip Listbox back to a core selection component. Drag-and-drop and keyboard reordering will ship separately. - Remove Listbox.DragAndDropProvider and Listbox.ItemDragHandle - Remove Alt+Arrow keyboard reordering and its Item state (dragging, dropTarget, dropTargetEdge) and data attributes - Remove the delayed highlight reporting that only existed for reorders - Drop the @atlaskit/pragmatic-drag-and-drop dependencies - Remove the drag-and-drop, drag-within-groups and custom-shortcuts demos Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strip Listbox back to a core selection component. Infinite loading will ship separately. - Remove Listbox.LoadingTrigger - Remove the loading and onLoadMore props from Listbox.Root - Remove the lazy-loading demo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace `selectionMode` with the `multiple` prop used by Select, Combobox, ToggleGroup and Accordion, and type the value the same way as Select: a single value by default, an array when `multiple` is set. `multiple` accepts `boolean | 'opt-in'`: - `true`: clicking an item (or pressing Space) toggles it and Shift+Click selects a range, following the APG listbox's recommended multiple selection model. - `'opt-in'`: the former 'explicit-multiple' behavior. Pressing an item replaces the selection, Ctrl/Cmd+Click and Ctrl+Space add or remove an item, Shift+Click selects a range, and Ctrl/Cmd+Shift+Click adds a range. Arrow keys behave as in `true`, so this doesn't yet implement the full APG alternative multiple selection model. Also removes the unprefixed `SelectionMode` type export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Match the highlight callback used by Combobox and Autocomplete:
`onItemHighlighted(itemValue, eventDetails)` with `undefined` when no
item is highlighted, and event details carrying the `reason`
('keyboard' | 'pointer' | 'none'), the native `event` and the item
`index`.
The highlighted item's DOM element is no longer passed to the callback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
commit: |
Bundle size
PerformanceTotal duration: 433.61 ms -19.27 ms(-4.3%) | Renders: 76 (+0) | Paint: 806.64 ms -18.34 ms(-2.2%)
13 tests within noise — details Metric alarms
Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
jjenzz
force-pushed
the
listbox-strip-back
branch
11 times, most recently
from
November 4, 2026 21:09
cb646b2 to
9846025
Compare
jjenzz
force-pushed
the
listbox-strip-back
branch
from
November 4, 2026 21:17
9846025 to
6652cce
Compare
jjenzz
force-pushed
the
listbox-strip-back
branch
3 times, most recently
from
November 5, 2026 17:08
bc2ae57 to
87b012a
Compare
`highlightValue(value, element)` restored the highlight after an external reorder, where the index-based highlight would otherwise stay on the old position. Reordering is no longer part of Listbox, and nothing else needs to move the highlight imperatively, so remove the action and the DOM-settle helper it relied on. A later commit, "Keep the highlight on the focused item when items are reordered", handles external reorders without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Follow the APG listbox pattern, which describes a selection widget and says nothing about form submission or validation. Remove the hidden inputs, Field and Form integration, browser autofill handling, and the `name`, `required`, `inputRef` and `itemToStringValue` props. `Listbox.Label` keeps labelling the list but no longer exposes Field validity state. Form support can be added back later without a breaking change. The usage guidelines now point to Select for selection in forms, instead of describing Listbox as "unlike Select". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
The APG listbox pattern moves focus to the next or previous option without wrapping, unlike the menu pattern, which allows it as an option. Select doesn't wrap either, and native list boxes on macOS stop at the first and last items. Remove the prop and keep focus at the ends of the list. An opt-in can be added later if needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
In Listbox, the part's only effect was to narrow the text used for typeahead, which the Item's `label` prop already overrides. Select needs its ItemText to align the selected item with the trigger, but Listbox has no trigger. Like Menu, typeahead now matches the item's rendered text unless `label` is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Root renders no element, so its `id` only prefixed the ids generated for the List and Label. Select and Combobox have a Root `id` for Field and form integration, which Listbox no longer has. Like Dialog, Popover and Menu, set the id on the part that renders the element instead: `Listbox.List` accepts an `id`. To keep a custom List id working, `Listbox.Label` now focuses the list element the Root already holds when it's clicked, instead of looking the list up by an id read when the Label rendered. That lookup broke click-to-focus when the List's `id` changed after mount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
jjenzz
force-pushed
the
listbox-strip-back
branch
4 times, most recently
from
November 6, 2026 13:13
6044128 to
8627d21
Compare
Give each selection mode its own example: - The hero shows single selection as a "Move to" list of folders, where the selection is the target of an action rather than a value submitted with a form. - "Multiple selection" lets you choose your favorite animals. - A new "Opt-in multiple selection" section and demo shows `multiple="opt-in"` with a playlist of songs and artists. The description says to use it when selecting more than one item requires a modifier key, without comparing it to a particular app. Selected tracks get a row background instead of a check icon, one grey step darker than the highlight, following ToggleGroup's hover and press greys, with a small gap between rows. The section also says that on touch devices, which have no modifier keys, a tap adds or removes an item like `multiple`. That's the planned behavior; the implementation follows separately. The hero and multiple selection demos show one line of text per item, with no groups. The lists' focus outline now uses the dark outline from the other demos (white in dark mode) instead of a blue that looked like the browser default. The docs no longer compare `multiple="opt-in"` to a file manager. File explorers can need buttons or menus inside items, which listbox options can't contain, and we may build higher-level primitives for those use cases, so the docs shouldn't point people at Listbox for them. Both selection sections are kept to a sentence or two, like the `multiple` sections in Select, Combobox, ToggleGroup and Accordion. The demos show the interaction, and the `multiple` prop's API reference describes the keys in full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Touch has no modifier keys, so with `multiple="opt-in"` every tap replaced the selection and touch users could never select more than one item. A tap from a touch pointer now adds or removes the item, the same as `multiple`, as the docs describe. Mouse clicks and keyboard presses keep the opt-in behavior. The Item records the pointer type of the last press and treats a click that follows a touch press as a toggle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
With `multiple="opt-in"`, moving focus with an arrow key and no modifier now moves the selection to the focused item, and Ctrl/Cmd plus an arrow key moves focus without changing the selection. Together with the existing Ctrl+Space toggle and Shift+arrow range behavior, this matches the required keys of the APG listbox's alternative multiple selection model. The APG doesn't say what happens when the focused item is disabled. Following its wording, moving focus unselects everything else, and a disabled item can't be selected, so moving onto one clears the selection. Home, End and typeahead, which the APG lists as optional or recommended, keep their existing behavior and don't change the selection. Plain `multiple` and single selection are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Shift+Arrow toggled the item focus moved to, so reversing direction deselected the items you came back to and left the far end selected. It now works like the APG listbox's multi-select example: the item focused when Shift is pressed is the anchor, and Shift+Arrow selects the range from the anchor to the focused item, replacing the rest of the selection, so the range grows and shrinks as focus moves. Releasing Shift and moving focus, or pressing an item with the pointer, starts a new range from wherever focus is when Shift+Arrow is next pressed. This applies to `multiple` and `multiple="opt-in"`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
The APG's Control modifier for multiple selection doesn't work as-is on a Mac: macOS reserves Ctrl+Space (switch input source), Cmd+Space (Spotlight) and Ctrl+arrows (Mission Control and Spaces), and Cmd+arrows conventionally jump to the start or end. The APG recommends swapping in macOS key assignments to avoid system conflicts, and React Aria uses Option for these keys. On macOS: - Option+Space adds or removes the focused item in multiple selection, matching on the physical Space key because Option+Space types a non-breaking space. - In `multiple="opt-in"`, Option+arrows move focus without changing the selection, so the move and the toggle use the same modifier. Cmd+arrows no longer do this. Cmd+click, Cmd+Shift+click, Cmd+Shift+Home/End and Cmd+A keep using Cmd, the Mac's usual command modifier. Other platforms are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
The highlight followed hover and keyboard events separately, so it could stay on an item after focus left the list, disappear from the focused item when the pointer left it, and hovering took focus from other controls. Following the APG, the highlighted item is now the focused item: - When focus leaves the list, nothing stays highlighted. The list still keeps the item as the tab stop. - Moving the pointer off the list no longer clears the highlight from the focused item. - Hovering only moves focus and the highlight once focus is in the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
The tab stop stayed on the last focused item, so tabbing back into the list could land on an unselected item. The APG listbox pattern says focus goes to the first selected option, or the first option when nothing is selected. When focus leaves the list, the tab stop now moves to the first selected item, or to the first item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Hovering stopped highlighting items when focus was elsewhere, since it no longer takes focus from other controls. The hovered item is now highlighted without moving focus until focus enters the list, and the highlight clears when the pointer leaves the list. Once an item has focus, hovering moves focus and the highlight together. When focus enters the list, the highlight moves to the focused item. When focus leaves, nothing stays highlighted. In both cases the item under the pointer is highlighted again once the pointer moves. In opt-in mode, hovering never moves focus, since plain arrow keys move the selection from the focused item. A key press returns the highlight from the hovered item to the focused item until the pointer moves again, so only one item is highlighted at a time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
The list container and the active item were both tab stops, and no item was one until the list had focus. Tabbing in landed on the container with nothing highlighted, so Home did nothing and the first ArrowDown skipped the first item, and Tab then stopped again on the item. Following the APG, the first selected item (or the first item) is now the only tab stop from the start, and it follows the selection while the list doesn't have focus. The container leaves the tab order but can still receive focus programmatically, such as from a Listbox.Label click, and passes focus on to that item. An empty list keeps focus on the container. The demos now show the list outline with :has(:focus-visible), since the container itself no longer holds focus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
…ered Reordering the items while one has focus left the tab stop at the focused item's old index. The highlight moved to whichever item took that index, and the next arrow key started from there, so it could seem to do nothing. The tab stop now follows the focused item to its new index. Found while checking that the Listbox composes with dnd-kit. Adds dnd-kit as a dev dependency for Chromium tests that drag an item with the pointer and move one with Alt+Arrow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Escape reported `list-navigation`, the reason for arrow-key moves. Combobox reports `escape-key` when Escape clears its selection, and the popups use it when Escape closes them, so use it here too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Ctrl/Cmd+A deselected everything when every item was already selected. That felt unexpected: Ctrl/Cmd+A should select everything, which is also what React Aria's listbox does. The APG makes the toggle optional, and Escape already clears the selection, so Ctrl/Cmd+A now always selects every item. We can think about deselecting separately if people need it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Range selections reported `item-press` or `list-navigation`, the same as selecting one item, so consumers couldn't tell them apart. A virtualized list needs to, to remove disabled items it hasn't rendered. Range selections keep the reason for what caused them, `item-press` for Shift+Click and `list-navigation` for Shift+Arrow, Ctrl/Cmd+A and Ctrl+Shift+Home/End, and add an optional `range` field, like Combobox's optional `isItemPress`: - `from` and `to` are the first and last items in the range, inclusive, whichever way the user extended it, so consumers don't need to sort them. Ctrl/Cmd+A reports `0` to the last index. - `mode` is `'replace'` when the range becomes the selection (Shift+Click, Shift+Arrow and Ctrl/Cmd+A), and `'add'` when it's added to the selection (Ctrl/Cmd+Shift+Click in opt-in mode, and Ctrl+Shift+Home/End). A string rather than a boolean leaves room for other modes, such as deselecting a range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
`useTypeahead` came from Floating UI, which builds it for popups: it
read `open` from the popup's store to stop typing keys while open, reset
the typed string when the popup opened or closed, and kept the typing
session while focus moved between the trigger and the popup. So a
component without a popup, like Listbox, couldn't use it.
- `useTypeahead` now holds only the matching: the typed string, the
reset timer, where each search starts, and cycling through items that
start with the same letter. It takes plain props and returns the
handler.
- `usePopupTypeahead` wraps it with the popup behaviour above, and
returns the same `{ reference, floating }` props as before. Select,
Menu and the Combobox trigger use it, unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
Each key searched from the item after the focused one, so a search could skip the item it should land on: typing "ba" moved past "Banana" to "Bacon". Typing the same letter twice searched for "bb" instead of cycling, and Space always selected, even mid-search. The Listbox now uses the shared `useTypeahead`, like Select and Menu: - Keys typed in quick succession build one search from where it began, and it resets after a pause. - Typing the same letter again cycles through the items that start with it. - Space continues a search in progress instead of selecting. - Typeahead lands on disabled items, the same as arrow keys, since it only moves focus here. Select skips them because typeahead there changes the value. Typeahead runs in the capture phase on the list, so a typed key doesn't reach the item it moved focus away from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
The Listbox only knew the items that were rendered and counted their
indexes from the first rendered item. With a virtualizer such as
TanStack Virtual, arrow keys and End stopped at the last rendered row,
typeahead couldn't find rows that weren't rendered, and Ctrl+A or a
Shift range only selected the rendered rows.
Pass every item to `Listbox.Root` with `items`, like Combobox, and use
each item as its `Listbox.Item` value:
- Each rendered item finds its position by looking up its value in
`items`, so positions don't depend on what's rendered.
- Arrow keys, Home and End move through every item. When the next item
isn't rendered, `onItemHighlighted` reports it so the consumer can
scroll to it. The list holds focus until the item renders, then the
item takes it. In opt-in mode, it's also selected then, so the
selection still follows focus.
- Typeahead matches the labels of every item. `itemToStringLabel` was
stored but never read; it now labels object items, with the same
`{ value, label }` default as Combobox.
- Ctrl+A and ranges select across every item, so `onValueChange={setValue}`
is enough. The listbox only knows which rendered items are disabled,
so a list with disabled items filters them out of changes with a
`range`.
- Tab reaches the selected item even when it isn't rendered. While the
item holding the tab stop isn't rendered, the list takes its place in
the tab order.
Unlike Combobox, there's no `virtualized` prop: positions come from
`items` whenever it's passed, and the Listbox has no popup scroll to
reset. There's no `index` prop on items either, since the position comes
from the item's value.
`aria-setsize` and `aria-posinset` are left to the consumer: browsers
work them out when every item is rendered, and the right values in a
grouped list depend on the groups.
Adds `@tanstack/react-virtual` as a dev dependency for the Chromium
tests, and a Virtualized demo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
`onValueChange` fired whenever a key or press could change the selection, even when it didn't: arrowing onto a disabled item in opt-in mode, or Ctrl+A on a list that's already fully selected. It now only fires when the selection changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4ErZTmoV8Xv9HpXPSHqQu
jjenzz
force-pushed
the
listbox-strip-back
branch
from
November 6, 2026 13:28
8627d21 to
95114a3
Compare
This branch has not been deployed
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.
Draft PR to get Netlify and pkg.pr.new previews of a stripped-back Listbox. Not for merging.
It's the Listbox from mui/base-ui#5115 with my DX review changes on top. The changes, and the reasoning for each, are in michaldudak/base-ui#4, so please review there. This PR only exists for the previews and will be closed once they've been shared.
Docs preview: the Listbox page under Components.
🤖 Generated with Claude Code