fix(combobox): input mirrors selected label in single mode; multi preserves filter

Two related bugs reported in the demo:

1. After selecting an item in single mode, the input stayed at the
   previously typed query (or empty), so the user couldn't tell which
   value was committed unless they peeked at the Trigger area.

2. In multi mode, selecting an item used to clear the input, which
   destroyed any active filter. cmdk-style usage (filter → pick → filter
   → pick) was impossible — the user had to retype the query for every
   selection.

Provider behaviour now:
- `selectItem()` in SINGLE mode sets `inputValue` to the selected
  label (resolved via the existing `labelRegistry`).
- `selectItem()` in MULTI mode leaves `inputValue` untouched, so the
  filter survives the click and the user can keep picking matches.
- A new $effect keeps `inputValue` in sync with `value` in SINGLE mode
  for initial mount and external `bind:value` updates.
- A second $effect clears `inputValue` whenever the picker switches
  between 'single' and 'multiple' — the two modes use the input for
  different purposes, and stale text from one would silently filter
  the other.

Also:
- Added `components.combobox.clear` to the langs dictionary so the
  ClearButton stops emitting `[langs] Translation key not found` warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 8aa6086976
commit df948aa35a

@ -15,5 +15,9 @@ export const comboboxLangs = {
toggle: {
es: 'Alternar',
en: 'Toggle'
},
clear: {
es: 'Borrar valor',
en: 'Clear value'
}
} satisfies LangNode;

@ -1,3 +1,4 @@
import { untrack } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import { createAttrs } from '$uix/morfo';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
@ -134,6 +135,38 @@ export class ComboboxProvider {
ref: this.contentRef,
onComplete: (open) => opts.onOpenChangeComplete.current(open)
});
// Single-mode auto-sync: keep `inputValue` mirroring the selected
// value's label so the input reflects external `value` changes
// (initial mount with a pre-selected value, or `bind:value` updates).
// Multi-mode intentionally leaves `inputValue` alone — that input is
// the live filter, and stamping a label onto it would hide the rest
// of the items behind a single-match query.
$effect(() => {
if (this.opts.type.current !== 'single') return;
const v = this.opts.value.current;
if (v.length === 0) return;
const label = this.labelRegistry.get(v[0]);
if (label === undefined) return;
if (this.opts.inputValue.current !== label) {
this.opts.inputValue.current = label;
}
});
// Mode switch reset: when `type` changes between 'single' and
// 'multiple', wipe `inputValue` so leftover text from the previous
// mode doesn't accidentally filter the list when the user expects a
// fresh start.
let lastType = untrack(() => this.opts.type.current);
$effect(() => {
const type = this.opts.type.current;
if (type !== lastType) {
lastType = type;
untrack(() => {
this.opts.inputValue.current = '';
});
}
});
}
isSelected(value: string): boolean {
@ -186,6 +219,17 @@ export class ComboboxProvider {
}
this.opts.value.current = next;
// Sync inputValue in SINGLE mode only — show the selected label so the
// input reflects the current value. In MULTI mode the user is likely
// still filtering for more picks (cmdk pattern: filter → pick → filter
// → pick), so we preserve the current input text and leave the
// already-selected items visible as checked items in the list.
if (this.opts.type.current === 'single') {
const label = next.length > 0 ? (this.labelRegistry.get(next[0]) ?? next[0]) : '';
this.opts.inputValue.current = label;
}
const eventTarget = target ?? this.resolveItemEl(value);
if (eventName && eventTarget) {
void this.runtime.trigger(eventName, { fallbackTarget: eventTarget });

Loading…
Cancel
Save

Powered by TurnKey Linux.