@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.
@@ -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 name="items" type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))"
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 name="internalItems" type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))" :value="[]"></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 name="options-change" type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))"></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)"><span class="primary" $value="row.label"></span></slot>
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
- id: option.id || option.value || `option-${index}`,
11
- value: option.value,
12
- label: option.label || option.textContent?.trim() || option.value,
13
- group: option.closest("optgroup")?.label || undefined,
14
- disabled: option.disabled,
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
- group.rows.push({ ...row, index });
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();