@threadlabs/looma 0.14.6 → 0.14.7
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.
- package/components/ui-combobox/ui-combobox.html +48 -11
- package/components/ui-combobox/ui-combobox.js +23 -11
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-combobox.css +18 -0
- package/vanilla/UiCombobox.d.ts +7 -7
- package/vanilla/UiCombobox.js +43 -27
- package/vue/UiCombobox.d.ts +78 -3
- package/vue/UiCombobox.vue +277 -8
- package/vue/chunks/UiCombobox.js +63 -25
- package/vue/components.css +67 -50
|
@@ -14,7 +14,9 @@
|
|
|
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
16
|
this value.</prop>
|
|
17
|
-
<prop
|
|
17
|
+
<prop
|
|
18
|
+
name="items"
|
|
19
|
+
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 }) }))"
|
|
18
20
|
>The selected items in multiple mode, as JSON; a named combobox submits each one's value. A form reset returns to these items, or to none when unset.</prop>
|
|
19
21
|
<prop name="selectedValues" type="list(string)"
|
|
20
22
|
>The selected option values in multiple mode, when the consumer controls the selection. Once set, the consumer owns it: each value shows as a badge with its
|
|
@@ -60,14 +62,18 @@
|
|
|
60
62
|
<prop name="help" type="string" default=""
|
|
61
63
|
>Help text for the field. When set, a help button appears beside the field and opens a tooltip with this text; while it is open, the input is described by
|
|
62
64
|
it.</prop>
|
|
63
|
-
<state
|
|
65
|
+
<state
|
|
66
|
+
name="internalItems"
|
|
67
|
+
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 }) }))"
|
|
68
|
+
:value="[]"
|
|
69
|
+
></state>
|
|
64
70
|
<state name="raw" :value="''"></state>
|
|
65
71
|
<state name="display" :value="''"></state>
|
|
66
72
|
<state name="selected" type="string | null" :value="null"></state>
|
|
67
73
|
<state name="expanded" :value="false"></state>
|
|
68
74
|
<state
|
|
69
75
|
name="rows"
|
|
70
|
-
type="list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean }))"
|
|
76
|
+
type="list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger }) }))"
|
|
71
77
|
:value="[]"
|
|
72
78
|
></state>
|
|
73
79
|
<state name="active" :value="-1"></state>
|
|
@@ -83,7 +89,7 @@
|
|
|
83
89
|
<state name="submitted" :value="''"></state>
|
|
84
90
|
<state
|
|
85
91
|
name="groups"
|
|
86
|
-
type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean, index: integer })) }))"
|
|
92
|
+
type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger }), index: integer, labelledBy: string | null, detail: string | null })) }))"
|
|
87
93
|
:value="[]"
|
|
88
94
|
></state>
|
|
89
95
|
<state name="creatable" :value="false"></state>
|
|
@@ -94,28 +100,31 @@
|
|
|
94
100
|
<event name="query-change" type="object({ query: string, display: string, trigger: keyboard | pointer | programmatic })"></event>
|
|
95
101
|
<event
|
|
96
102
|
name="value-change"
|
|
97
|
-
type="object({ value: string | null, query: string, option: object({ id: string, value: string, label: string, group?: string, disabled?: boolean }) | null, kind: selection | clear | free-entry | create, trigger: keyboard | pointer | programmatic }) | list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))"
|
|
103
|
+
type="object({ value: string | null, query: string, option: object({ id: string, value: string, label: string, group?: string, disabled?: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger }) }) | null, kind: selection | clear | free-entry | create, trigger: keyboard | pointer | programmatic }) | 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 }) }))"
|
|
98
104
|
></event>
|
|
99
105
|
<event
|
|
100
106
|
name="free-entry"
|
|
101
|
-
type="object({ value: string | null, query: string, option: object({ id: string, value: string, label: string, group?: string, disabled?: boolean }) | null, kind: selection | clear | free-entry | create, trigger: keyboard | pointer | programmatic })"
|
|
107
|
+
type="object({ value: string | null, query: string, option: object({ id: string, value: string, label: string, group?: string, disabled?: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger }) }) | null, kind: selection | clear | free-entry | create, trigger: keyboard | pointer | programmatic })"
|
|
102
108
|
></event>
|
|
103
109
|
<event
|
|
104
110
|
name="create-entry"
|
|
105
|
-
type="object({ value: string | null, query: string, option: object({ id: string, value: string, label: string, group?: string, disabled?: boolean }) | null, kind: selection | clear | free-entry | create, trigger: keyboard | pointer | programmatic })"
|
|
111
|
+
type="object({ value: string | null, query: string, option: object({ id: string, value: string, label: string, group?: string, disabled?: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger }) }) | null, kind: selection | clear | free-entry | create, trigger: keyboard | pointer | programmatic })"
|
|
106
112
|
></event>
|
|
107
113
|
<event
|
|
108
114
|
name="validation-change"
|
|
109
115
|
type="object({ status: pristine | pending | valid | warning | error, touched: boolean, dirty: boolean, issues: list(object({ message: string, path?: list(unknown), severity?: error | warning })), output?: unknown })"
|
|
110
116
|
></event>
|
|
111
|
-
<event
|
|
117
|
+
<event
|
|
118
|
+
name="options-change"
|
|
119
|
+
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 }) }))"
|
|
120
|
+
></event>
|
|
112
121
|
<event
|
|
113
122
|
name="add-item"
|
|
114
|
-
type="object({ item: object({ id: string, value: string, label: string, group?: string, disabled?: boolean }), index: integer, trigger: keyboard | pointer | programmatic })"
|
|
123
|
+
type="object({ item: object({ id: string, value: string, label: string, group?: string, disabled?: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger }) }), index: integer, trigger: keyboard | pointer | programmatic })"
|
|
115
124
|
></event>
|
|
116
125
|
<event
|
|
117
126
|
name="remove-item"
|
|
118
|
-
type="object({ item: object({ id: string, value: string, label: string, group?: string, disabled?: boolean }), index: integer, trigger: keyboard | pointer | programmatic })"
|
|
127
|
+
type="object({ item: object({ id: string, value: string, label: string, group?: string, disabled?: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger }) }), index: integer, trigger: keyboard | pointer | programmatic })"
|
|
119
128
|
></event>
|
|
120
129
|
<event name="create-item" type="object({ query: string, trigger: keyboard | pointer | programmatic })"></event>
|
|
121
130
|
<event name="selected-values-change" type="object({ selectedValues: list(string), trigger: keyboard | pointer | programmatic })"></event>
|
|
@@ -222,8 +231,19 @@
|
|
|
222
231
|
:aria-selected="row.selected"
|
|
223
232
|
:aria-disabled="row.disabled"
|
|
224
233
|
:data-index="row.index"
|
|
234
|
+
:aria-labelledby="row.labelledBy"
|
|
235
|
+
:aria-describedby="row.detail"
|
|
225
236
|
>
|
|
226
|
-
<slot :name="format('option-%s', row.id)"
|
|
237
|
+
<slot :name="format('option-%s', row.id)">
|
|
238
|
+
<span $if="row.detail" class="detail">
|
|
239
|
+
<span class="line">
|
|
240
|
+
<span class="primary" :id="format('%s-option-%s-label', uid, row.index)" $value="row.label"></span>
|
|
241
|
+
<ui-badge $if="row.tag" :tone="row.tag.tone"><span :id="format('%s-option-%s-tag', uid, row.index)" $value="row.tag.label"></span></ui-badge>
|
|
242
|
+
</span>
|
|
243
|
+
<span $if="row.description" class="description" :id="format('%s-option-%s-description', uid, row.index)" $value="row.description"></span>
|
|
244
|
+
</span>
|
|
245
|
+
<span $if="not row.detail" class="primary" $value="row.label"></span>
|
|
246
|
+
</slot>
|
|
227
247
|
</div>
|
|
228
248
|
</div>
|
|
229
249
|
<div
|
|
@@ -486,6 +506,23 @@
|
|
|
486
506
|
color: var(--_ui-combobox-option-text, var(--ui-text-primary));
|
|
487
507
|
font-weight: 500;
|
|
488
508
|
}
|
|
509
|
+
/* A row with a tag or a description: the label and its tag on one line, the description under them. */
|
|
510
|
+
.detail {
|
|
511
|
+
display: grid;
|
|
512
|
+
gap: var(--ui-space-0-5);
|
|
513
|
+
min-inline-size: 0;
|
|
514
|
+
}
|
|
515
|
+
.line {
|
|
516
|
+
display: flex;
|
|
517
|
+
flex-wrap: wrap;
|
|
518
|
+
align-items: center;
|
|
519
|
+
gap: var(--ui-space-2);
|
|
520
|
+
}
|
|
521
|
+
.description {
|
|
522
|
+
color: var(--ui-text-muted);
|
|
523
|
+
font-size: var(--ui-font-size-sm);
|
|
524
|
+
line-height: var(--ui-line-height-md);
|
|
525
|
+
}
|
|
489
526
|
.message, .group {
|
|
490
527
|
color: var(--_ui-combobox-message-text, var(--ui-text-secondary));
|
|
491
528
|
font-weight: 400;
|
|
@@ -5,14 +5,21 @@ const instances = new WeakMap();
|
|
|
5
5
|
|
|
6
6
|
let comboboxes = 0;
|
|
7
7
|
|
|
8
|
+
// An option's secondary line and tag ride on data-* attributes: <option> holds only text, so markup inside
|
|
9
|
+
// it would run into its label before JavaScript. A row without them keeps the declared shape exactly.
|
|
8
10
|
function authoredOptions(container) {
|
|
9
|
-
return Array.from(container.querySelectorAll("option")).map((option, index) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
return Array.from(container.querySelectorAll("option")).map((option, index) => {
|
|
12
|
+
const { description, tag, tagTone } = option.dataset;
|
|
13
|
+
return {
|
|
14
|
+
id: option.id || option.value || `option-${index}`,
|
|
15
|
+
value: option.value,
|
|
16
|
+
label: option.label || option.textContent?.trim() || option.value,
|
|
17
|
+
group: option.closest("optgroup")?.label || undefined,
|
|
18
|
+
disabled: option.disabled,
|
|
19
|
+
...(description ? { description } : {}),
|
|
20
|
+
...(tag ? { tag: tagTone ? { label: tag, tone: tagTone } : { label: tag } } : {}),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export async function validate(host) {
|
|
@@ -218,12 +225,14 @@ export default function controller(host) {
|
|
|
218
225
|
host.dispatch("query-change", { query, display: query, trigger });
|
|
219
226
|
};
|
|
220
227
|
// A row carries a view-only `selected` flag; an item is the option itself, in its declared shape.
|
|
221
|
-
const asItem = ({ id, value, label, group, disabled }) => ({
|
|
228
|
+
const asItem = ({ id, value, label, group, disabled, description, tag }) => ({
|
|
222
229
|
id,
|
|
223
230
|
value,
|
|
224
231
|
label,
|
|
225
232
|
...(group === undefined ? {} : { group }),
|
|
226
|
-
...(disabled === undefined ? {} : { disabled })
|
|
233
|
+
...(disabled === undefined ? {} : { disabled }),
|
|
234
|
+
...(description === undefined ? {} : { description }),
|
|
235
|
+
...(tag === undefined ? {} : { tag })
|
|
227
236
|
});
|
|
228
237
|
const addSelectedItem = (row, trigger) => {
|
|
229
238
|
const option = asItem(row);
|
|
@@ -363,7 +372,10 @@ export default function controller(host) {
|
|
|
363
372
|
group = { name, role: name ? "group" : "presentation", label: name || null, rows: [] };
|
|
364
373
|
groups.push(group);
|
|
365
374
|
}
|
|
366
|
-
|
|
375
|
+
// The label names the option and the tag and description describe it, as a listbox option's
|
|
376
|
+
// label and description parts do; a row without either keeps its name from its content.
|
|
377
|
+
const detail = [row.tag ? `${uid}-option-${index}-tag` : "", row.description ? `${uid}-option-${index}-description` : ""].filter(Boolean).join(" ");
|
|
378
|
+
group.rows.push({ ...row, index, labelledBy: detail ? `${uid}-option-${index}-label` : null, detail: detail || null });
|
|
367
379
|
});
|
|
368
380
|
host.state.groups = groups;
|
|
369
381
|
host.state.creatable = canCreate();
|
|
@@ -491,7 +503,7 @@ export default function controller(host) {
|
|
|
491
503
|
});
|
|
492
504
|
// Options can change while the list is open, as when a consumer adds the option it just created;
|
|
493
505
|
// the open list shows them at once rather than at the next keystroke.
|
|
494
|
-
const optionsKey = () => JSON.stringify((config().options ?? []).map((row) => [row.value, row.label, row.disabled]));
|
|
506
|
+
const optionsKey = () => JSON.stringify((config().options ?? []).map((row) => [row.value, row.label, row.disabled, row.description, row.tag]));
|
|
495
507
|
let lastOptionsKey = optionsKey();
|
|
496
508
|
const observer = new MutationObserver(() => {
|
|
497
509
|
relabel();
|