Merged
Conversation
duncanmcclean
added a commit
to statamic/docs
that referenced
this pull request
Feb 6, 2026
it'll 404 until statamic/cms#13843 has been merged and tagged.
This was referenced Feb 6, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calling hasOwnProperty directly through the DOMStringMap goes through the
prototype chain and is what most lint configs flag (no-prototype-builtins).
The `in` operator reads the same intent and avoids the prototype lookup;
the `?? {}` guard preserves the early-return when relatedTarget is null
(e.g., when blur happens because the window lost focus).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The manual scroll math in scrollToSelectedOption was redundant and slightly wrong: Reka's highlightSelected() already triggers virtualFocusHook, which calls virtualizer.scrollToIndex(index) on the underlying TanStack virtualizer. The manual block was running on top of that, second-guessing it with a hardcoded estimatedItemHeight of 40px. That estimate matched the virtualizer's :estimate-size, but real items vary by size variant (xs items render closer to ~28px, xl closer to ~48px) and by custom slot content, so the "centered" position drifted off-center. Worse, it raced Reka's own scroll on the next frame, so the final scrollTop depended on which rAF resolved last. Dropping the manual math means the selected option lands wherever Reka puts it (align: 'start' by default), which is the standard dropdown behavior and works correctly across all sizes and slot content. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Once the manual scroll math was removed in favor of Reka's highlightSelected(), nothing else read viewportRef. Drop the useTemplateRef declaration and the matching `ref="viewport"` on the viewport div. The data-ui-combobox-viewport hook is still there for external selectors / tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The @paste.prevent modifier always blocked the default paste, even when onPaste returned early for non-taggable comboboxes. Net effect: users couldn't paste a search term into a regular searchable combobox; the input would silently swallow the paste with no insertion. Move the preventDefault inside onPaste, after the taggable guard, so the default paste runs for non-taggable comboboxes (text lands in the search input as expected) and is only suppressed when we're handling the paste ourselves to add tags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously shouldCloseOnSelect used `||`, so an explicit :close-on-select="false" on a single-select combobox was treated as unset and fell back to "close on select". Switch to `??` so an explicit false (or true) is always respected, and only an unset prop defers to the multi-aware default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ComboboxVirtualizer was keyed on JSON.stringify(modelValue), which
forced a full destroy + remount of the virtualizer (and every rendered
row) on every selection toggle. In a long multi-select dropdown this
threw away the user's scroll position on each click and re-ran all the
row mounting work, plus the JSON.stringify itself.
The key isn't needed: row selected styling comes from
:class="itemClasses({ selected: isSelected(option) })", which is
already reactive on modelValue, so individual rows re-style themselves
without needing the virtualizer to remount. Verified that both
selecting and deselecting a row in a multi-select with 20 options
updates the blue selected styling and preserves scroll position.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reka's ListboxItem (which ComboboxItem extends) memoizes its render on [isHighlighted, isSelected] but not on disabled. So when our reactive :disabled prop changes — e.g. when removing a selection drops us below maxSelections and previously-disabled options should become enabled — the cached render output is reused and data-disabled never updates. The original code worked around this with a virtualizer-level :key="JSON.stringify(modelValue)" that destroyed and remounted every row on every selection. That was correct but threw away scroll position on each click and was a heavy fix for a per-item issue. Replace it with a per-item :key that includes the disabled state. Only items whose disabled state actually flips remount; the virtualizer and unaffected items keep their state, so scroll position is preserved through multi-select toggling. Update the TestMaxSelectionsLimit story to match: it no longer needs the trigger close-then-reopen dance that was masking the cached-render bug, and uses waitFor for the post-deselect assertion since the remount happens on the next tick. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
This pull request overhauls the Combobox component to make it faster, more accessible, and less janky.
The current implementation grew slow and messy as we (mostly I) patched edge cases during alpha/beta. After researching how other component libraries handle certain things, I picked up some ideas for improving our own - and this PR is the result.
There's a lot of changes in this PR, so I've tried to list everything I can remember:
Dropdown width
The options dropdown was previously as wide as the longest option. This meant we needed to measure the options in JS (which impacted performance) and resulted in weirdly aligned dropdowns like this:
To keep things tidy, the dropdown is now fixed to the width of the trigger. This does, however, mean that long options may be truncated.
I've adjusted the widths of comboboxes in a few places to account for this after looking through the Control Panel in German 🇩🇪.
I've also added an
adaptiveWidthprop which lets you opt-in to the old behaviour where necessary. We use this in the site selector to handle options with varying lengths.Closes #13615
Dropdown height & virtualization
Previously, the options dropdown would expand to fit the available space.
For example: if the combobox was at the top of the page, and it contained a lot of options, it'd take up a large chunk of the screen:
As recommended by Reka, the dropdown now has a max height of 300px.
Adding a max-height has meant we're able to get rid of the hacky min-heights added in #13489 to prevent squished options when resizing the viewport.
It also means that Reka's virtualizer works properly, allowing us to ditch our custom
Scrollbarcomponent and rely on the browser's scrollbar. 🎉Other improvements
searchable="false"searchable="false"#13747z-indexissue with the header site selectoralignprop to determine the alignment of the dropdown<input>, but it's read-onlyComboboxInputwouldn't actually give you a space.@keydown.openlistener on theComboboxTrigger. Fixed by preventing typing in the input from cancelling the input event.Selectcomponent so it more closely mirrors theComboboxdocs