@threadlabs/looma 0.18.3 → 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.
|
|
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.
|
|
190
|
-
//
|
|
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
|
|
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)
|
|
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 (
|
|
298
|
-
else if (
|
|
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
|
-
|
|
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
|
};
|