@threadlabs/looma 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,8 @@
13
13
  when none is chosen. A disabled combobox sends nothing.</prop>
14
14
  <prop name="value" type="string | null"
15
15
  >The selected value in single mode, which a named combobox submits with its form. The field shows the matching option's label, and a form reset returns to
16
- this value. If omitted, the first authored option with selected sets the initial value.</prop>
16
+ this value. In a strict combobox, typing a search does not change the selection; leaving with no match restores its label. If omitted, the first authored
17
+ option with selected sets the initial value.</prop>
17
18
  <prop
18
19
  name="items"
19
20
  type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger, color?: string }) }))"
@@ -186,8 +186,8 @@ export default function controller(host) {
186
186
  openOverlay({ id: overlayId, element: popup, relatedElements: [element], modal: false, requestClose: close });
187
187
  search(reason);
188
188
  };
189
- // The last option the user committed. Typing clears the selection while searching; leaving a strict
190
- // combobox with unmatched text restores this rather than keeping the text.
189
+ // The last option the user committed. A strict combobox keeps that selection while searching;
190
+ // leaving with unmatched text restores its label rather than keeping the search draft.
191
191
  let lastSelection = null;
192
192
  const commit = (value, query, option, kind, trigger) => {
193
193
  awaitingLabel = null;
@@ -271,14 +271,22 @@ export default function controller(host) {
271
271
  };
272
272
  // A strict combobox (single, no free text, no create) behaves like a select: leaving it resolves the
273
273
  // typed text to a valid option (the highlighted one, an exact label, the first label it begins, or the
274
- // only remaining option); with no match it reverts to the previous selection, or clears. With filter
274
+ // only remaining option); with no match it reverts to the previous selection, or empties the draft. With filter
275
275
  // "none" the remaining options are all of them, whatever their labels, so the only one listed counts.
276
276
  const resolveTyped = (trigger) => {
277
277
  const current = config();
278
278
  if (host.state.multiple || current.allowFreeText || current.allowCreate) return;
279
279
  const typed = String(host.state.raw ?? "");
280
- const selectedOption = (current.options ?? []).find((option) => option.value === host.state.selected) ?? lastSelection;
280
+ const selectedOption = (current.options ?? []).find((option) => option.value === host.state.selected)
281
+ ?? knownOptions.get(host.state.selected) ?? lastSelection;
281
282
  if (selectedOption && selectedOption.label === typed) return;
283
+ const restoreDraft = (label) => {
284
+ if (host.state.query !== undefined || label === host.state.raw) return;
285
+ host.state.raw = label;
286
+ host.state.display = label;
287
+ resetValidation();
288
+ host.dispatch("query-change", { query: label, display: label, trigger });
289
+ };
282
290
  const query = typed.trim().toLocaleLowerCase();
283
291
  // Match against every option, not only the filtered list: filtering is asynchronous, and a fast
284
292
  // Tab can arrive before it settles. A highlighted row still wins.
@@ -293,9 +301,10 @@ export default function controller(host) {
293
301
  const containing = current.filterByLabel ? enabled.filter((option) => option.label.toLocaleLowerCase().includes(query)) : enabled;
294
302
  return containing.length === 1 ? containing[0] : undefined;
295
303
  })();
296
- if (match) commit(match.value, match.label, match, "selection", trigger);
297
- else if (selectedOption && query !== "") commit(selectedOption.value, selectedOption.label, selectedOption, "selection", trigger);
298
- else if (host.state.selected !== null || typed !== "") { lastSelection = null; commit(null, "", null, "clear", trigger); }
304
+ if (match && match.value !== host.state.selected) commit(match.value, match.label, match, "selection", trigger);
305
+ else if (match) restoreDraft(match.label);
306
+ else if (selectedOption) restoreDraft(selectedOption.label);
307
+ else restoreDraft("");
299
308
  };
300
309
  const commitQuery = (trigger) => {
301
310
  const query = String(host.state.raw).trim();
@@ -406,7 +415,11 @@ export default function controller(host) {
406
415
  host.state.display = input.value;
407
416
  resetValidation();
408
417
  host.dispatch("query-change", { query: host.state.raw, display: host.state.display, trigger: "keyboard" });
409
- if (host.state.selected !== null) commit(null, host.state.raw, null, "clear", "keyboard");
418
+ // A strict combobox keeps its committed selection while the input holds a search draft.
419
+ // Clearing a controlled value here makes the consumer restore its label on every keystroke.
420
+ if (host.state.selected !== null && (host.state.allowFreeText || host.state.allowCreate)) {
421
+ commit(null, host.state.raw, null, "clear", "keyboard");
422
+ }
410
423
  open();
411
424
  queueMicrotask(() => { if (host.state.query !== undefined && host.state.query !== host.state.raw) syncQuery(); });
412
425
  };