mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
fix: align select and autocomplete collection contracts (#41829)
This commit is contained in:
parent
08c7a2c5f3
commit
a2acf62bf1
@ -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
|
||||
|
||||
@ -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>)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>)
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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' })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user