fix: align select and autocomplete collection contracts (#41829)

This commit is contained in:
yyh 2026-09-04 12:26:26 +00:00 committed by GitHub
parent 08c7a2c5f3
commit a2acf62bf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 50 additions and 6 deletions

View File

@ -80,6 +80,11 @@ single-or-multiple union.
Prefer the Base UI `items` collection pattern so the root, value display, and item list share one
runtime source of truth. Convert values to strings only at real serialization boundaries.
Treat collection values exposed for rendering as read-only views. In multiple mode, `SelectValue`
may receive a shared frozen empty array, and `useAutocompleteFilteredItems` exposes internal
filtered state. Use non-mutating transforms such as `map`, `filter`, or `toSorted`; create an
explicit copy only when a mutable array is required.
### Combobox source items and selected values
Combobox has separate types for the selected business value and the source record rendered by the

View File

@ -19,8 +19,20 @@ import {
AutocompleteSeparator,
AutocompleteStatus,
AutocompleteTrigger,
useAutocompleteFilteredItems,
} from '../index'
function AutocompleteTypeExamples() {
const filteredItems = useAutocompleteFilteredItems<string>()
// @ts-expect-error internally filtered items are read-only
filteredItems.push('workflow')
return null
}
void AutocompleteTypeExamples
const renderWithSafeViewport = (ui: React.ReactNode) =>
render(<div style={{ minHeight: '100vh', minWidth: '100vw', padding: '240px' }}>{ui}</div>)

View File

@ -38,7 +38,8 @@ function Autocomplete(props: AutocompleteProps<unknown>): React.JSX.Element {
const AutocompleteValue = BaseAutocomplete.Value
const AutocompleteRow = BaseAutocomplete.Row
const useAutocompleteFilter = BaseAutocomplete.useFilter
const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems
const useAutocompleteFilteredItems: <Value>() => readonly Value[] =
BaseAutocomplete.useFilteredItems
type AutocompleteValueProps = BaseAutocomplete.Value.Props
type AutocompleteRowProps = BaseAutocomplete.Row.Props

View File

@ -18,6 +18,22 @@ import {
SelectValue,
} from '../index'
function SelectTypeExamples() {
return (
<Select<string, true> multiple>
<SelectValue<string, true>>
{(selectedValue) => {
// @ts-expect-error multiple value render state is read-only
selectedValue?.push('seattle')
return selectedValue?.join(', ') ?? ''
}}
</SelectValue>
</Select>
)
}
void SelectTypeExamples
const renderWithSafeViewport = (ui: React.ReactNode) =>
render(<div style={{ minHeight: '100vh', minWidth: '100vw', padding: '240px' }}>{ui}</div>)

View File

@ -31,7 +31,7 @@ function Select<Value, Multiple extends boolean | undefined = false>(
const SelectGroup = BaseSelect.Group
type SelectSelectedValue<Value, Multiple extends boolean | undefined = false> =
| (Multiple extends true ? Value[] : Value)
| (Multiple extends true ? readonly Value[] : Value)
| null
type SelectValueState<Value = unknown, Multiple extends boolean | undefined = false> = Omit<
BaseSelect.Value.State,

View File

@ -8,6 +8,7 @@ Global command palette that coordinates detached dialog triggers, typed search,
- The dialog component owns the transient search input and selected plugin installer state.
- TanStack Query owns remote search lifecycle and cache state for each generated query contract.
- Autocomplete owns option registration, highlighting, keyboard navigation, and item activation.
- ScrollArea Viewport is the only results scroll container; Autocomplete List remains the listbox.
- ScrollArea Viewport is the only results scroll container; Autocomplete List is a listbox for
search results and a named grid for command cards.
Search actions adapt application, knowledge, plugin, workflow, and RAG owners into palette results. They must not duplicate those features' authorization, navigation, or query contracts.

View File

@ -1037,7 +1037,9 @@ describe('GotoAnything', () => {
const input = screen.getByRole('combobox', { name: 'app.gotoAnything.searchTitle' })
expect(input).toHaveAttribute('aria-haspopup', 'grid')
expect(screen.getByRole('grid')).toHaveAttribute('id', input.getAttribute('aria-controls'))
expect(
screen.getByRole('grid', { name: 'app.gotoAnything.groups.commands' }),
).toHaveAttribute('id', input.getAttribute('aria-controls'))
expect(screen.getByRole('rowgroup')).toBeInTheDocument()
for (const cell of screen.getAllByRole('gridcell'))
expect(cell.parentElement).toHaveAttribute('role', 'row')

View File

@ -585,9 +585,16 @@ function GotoAnythingDialog() {
/>
)}
<AutocompleteList className="max-h-none overflow-visible p-0">
<AutocompleteList
aria-label={
isCommandsMode
? t(($) => $['gotoAnything.groups.commands'], { ns: 'app' })
: undefined
}
className="max-h-none overflow-visible p-0"
>
{!isLoading && !isError && isCommandsMode && autocompleteResultCount > 0 && (
<AutocompleteGroup items={commandOptions} role="rowgroup">
<AutocompleteGroup items={commandOptions}>
<AutocompleteGroupLabel className="px-4 pt-4 pb-2 text-left font-mono text-[11px] font-medium tracking-[0.12em] text-text-tertiary uppercase">
{isSlashMode
? t(($) => $['gotoAnything.groups.commands'], { ns: 'app' })