@adia-ai/web-components 0.6.34 → 0.6.36
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/CHANGELOG.md +71 -0
- package/color/index.js +1 -1
- package/components/accordion/accordion-item.yaml +2 -2
- package/components/accordion/accordion.js +1 -1
- package/components/action-list/action-item.yaml +2 -2
- package/components/action-list/action-list.js +1 -1
- package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
- package/components/agent-artifact/agent-artifact.js +1 -1
- package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
- package/components/agent-questions/agent-questions.js +1 -1
- package/components/agent-reasoning/agent-reasoning.js +1 -1
- package/components/agent-suggestions/agent-suggestions.js +1 -1
- package/components/alert/alert.a2ui.json +64 -1
- package/components/alert/{class.js → alert.class.js} +189 -2
- package/components/alert/alert.css +78 -0
- package/components/alert/alert.d.ts +14 -0
- package/components/alert/alert.js +1 -1
- package/components/alert/alert.test.js +184 -0
- package/components/alert/alert.yaml +114 -1
- package/components/avatar/avatar-group.yaml +2 -2
- package/components/avatar/avatar.js +1 -1
- package/components/badge/badge.js +1 -1
- package/components/block/block.js +1 -1
- package/components/breadcrumb/breadcrumb.js +1 -1
- package/components/button/button.js +1 -1
- package/components/calendar-grid/calendar-grid.a2ui.json +10 -0
- package/components/calendar-grid/{class.js → calendar-grid.class.js} +30 -4
- package/components/calendar-grid/calendar-grid.css +20 -0
- package/components/calendar-grid/calendar-grid.d.ts +4 -0
- package/components/calendar-grid/calendar-grid.js +1 -1
- package/components/calendar-grid/calendar-grid.yaml +20 -0
- package/components/calendar-picker/calendar-picker.js +1 -1
- package/components/card/card.js +1 -1
- package/components/chart/chart.js +1 -1
- package/components/chart-legend/chart-legend.js +1 -1
- package/components/chat-thread/chat-input.a2ui.json +1 -1
- package/components/chat-thread/chat-input.js +6 -1
- package/components/chat-thread/chat-input.yaml +4 -1
- package/components/chat-thread/chat-thread.js +1 -1
- package/components/check/check.js +1 -1
- package/components/code/code.js +1 -1
- package/components/col/col.js +1 -1
- package/components/color-input/color-input.js +1 -1
- package/components/color-picker/color-picker.js +1 -1
- package/components/combobox/combobox.css +12 -0
- package/components/combobox/combobox.js +1 -1
- package/components/command/command.js +1 -1
- package/components/date-range-picker/{class.js → date-range-picker.class.js} +19 -3
- package/components/date-range-picker/date-range-picker.css +55 -6
- package/components/date-range-picker/date-range-picker.js +1 -1
- package/components/datetime-picker/{class.js → datetime-picker.class.js} +1 -1
- package/components/datetime-picker/datetime-picker.css +7 -1
- package/components/datetime-picker/datetime-picker.js +1 -1
- package/components/demo-toggle/demo-toggle.js +1 -1
- package/components/description-list/description-list.js +1 -1
- package/components/divider/divider.js +1 -1
- package/components/drawer/drawer.js +1 -1
- package/components/embed/embed.js +1 -1
- package/components/empty-state/empty-state.js +1 -1
- package/components/feed/feed.js +1 -1
- package/components/field/field.js +1 -1
- package/components/field/field.test.js +1 -1
- package/components/fields/fields.js +1 -1
- package/components/grid/grid.js +1 -1
- package/components/heatmap/heatmap.js +1 -1
- package/components/icon/icon.js +1 -1
- package/components/image/image.js +1 -1
- package/components/index.js +3 -0
- package/components/inline-message/inline-message.a2ui.json +143 -0
- package/components/inline-message/inline-message.class.js +169 -0
- package/components/inline-message/inline-message.css +75 -0
- package/components/inline-message/inline-message.d.ts +31 -0
- package/components/inline-message/inline-message.examples.md +19 -0
- package/components/inline-message/inline-message.js +17 -0
- package/components/inline-message/inline-message.test.js +203 -0
- package/components/inline-message/inline-message.yaml +205 -0
- package/components/input/input.css +16 -2
- package/components/input/input.js +1 -1
- package/components/input/input.test.js +40 -0
- package/components/input/input.yaml +5 -4
- package/components/inspector/inspector.js +1 -1
- package/components/integration-card/integration-card.js +1 -1
- package/components/kbd/kbd.js +1 -1
- package/components/link/link.js +1 -1
- package/components/list/list-item.yaml +2 -2
- package/components/list/list.js +1 -1
- package/components/list-window/list-window.js +1 -1
- package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
- package/components/loading-overlay/loading-overlay.class.js +203 -0
- package/components/loading-overlay/loading-overlay.css +81 -0
- package/components/loading-overlay/loading-overlay.d.ts +24 -0
- package/components/loading-overlay/loading-overlay.examples.md +50 -0
- package/components/loading-overlay/loading-overlay.js +17 -0
- package/components/loading-overlay/loading-overlay.test.js +257 -0
- package/components/loading-overlay/loading-overlay.yaml +260 -0
- package/components/menu/menu-divider.yaml +1 -1
- package/components/menu/menu-item.yaml +1 -1
- package/components/menu/menu.a2ui.json +3 -0
- package/components/menu/menu.js +1 -1
- package/components/menu/menu.yaml +7 -0
- package/components/modal/{class.js → modal.class.js} +12 -1
- package/components/modal/modal.css +11 -1
- package/components/modal/modal.js +1 -1
- package/components/nav/nav.js +1 -1
- package/components/nav-group/nav-group.js +1 -1
- package/components/nav-item/nav-item.js +1 -1
- package/components/noodles/noodles.js +1 -1
- package/components/option-card/option-card.js +1 -1
- package/components/otp-input/otp-input.js +1 -1
- package/components/page/page.js +1 -1
- package/components/pagination/pagination.js +1 -1
- package/components/pane/pane.js +1 -1
- package/components/pipeline-status/pipeline-status.js +1 -1
- package/components/popover/popover.a2ui.json +8 -1
- package/components/popover/popover.js +1 -1
- package/components/popover/popover.yaml +14 -1
- package/components/progress/progress.js +1 -1
- package/components/progress-row/progress-row.js +1 -1
- package/components/radio/radio.js +1 -1
- package/components/range/range.js +1 -1
- package/components/rating/rating.js +1 -1
- package/components/richtext/richtext.js +1 -1
- package/components/row/row.js +1 -1
- package/components/search/{class.js → search.class.js} +2 -0
- package/components/search/search.js +1 -1
- package/components/segment/segment.js +1 -1
- package/components/segmented/segmented.js +1 -1
- package/components/select/select.a2ui.json +58 -4
- package/components/select/{class.js → select.class.js} +415 -6
- package/components/select/select.css +158 -0
- package/components/select/select.d.ts +31 -1
- package/components/select/select.js +1 -1
- package/components/select/select.test.js +202 -0
- package/components/select/select.yaml +126 -5
- package/components/skeleton/skeleton.js +1 -1
- package/components/slider/slider.js +1 -1
- package/components/spinner/spinner.a2ui.json +3 -2
- package/components/spinner/{class.js → spinner.class.js} +33 -3
- package/components/spinner/spinner.css +91 -35
- package/components/spinner/spinner.d.ts +2 -2
- package/components/spinner/spinner.js +1 -1
- package/components/spinner/spinner.test.js +49 -11
- package/components/spinner/spinner.yaml +9 -1
- package/components/stack/stack.js +1 -1
- package/components/step-progress/step-progress.js +1 -1
- package/components/stepper/stepper-item.yaml +1 -1
- package/components/stepper/stepper.js +1 -1
- package/components/stream/stream.js +1 -1
- package/components/swatch/swatch.js +1 -1
- package/components/swiper/swiper.js +1 -1
- package/components/switch/switch.js +1 -1
- package/components/table/table.css +1 -1
- package/components/table/table.js +1 -1
- package/components/table-toolbar/{class.js → table-toolbar.class.js} +2 -1
- package/components/table-toolbar/table-toolbar.js +1 -1
- package/components/tabs/tab.yaml +2 -2
- package/components/tabs/tabs.js +1 -1
- package/components/tag/tag.a2ui.json +9 -0
- package/components/tag/{class.js → tag.class.js} +8 -1
- package/components/tag/tag.css +84 -20
- package/components/tag/tag.js +1 -1
- package/components/tag/tag.test.js +75 -1
- package/components/tag/tag.yaml +14 -0
- package/components/tags-input/tags-input.a2ui.json +337 -0
- package/components/tags-input/tags-input.class.js +783 -0
- package/components/tags-input/tags-input.css +210 -0
- package/components/tags-input/tags-input.d.ts +120 -0
- package/components/tags-input/tags-input.examples.md +92 -0
- package/components/tags-input/tags-input.js +17 -0
- package/components/tags-input/tags-input.test.js +368 -0
- package/components/tags-input/tags-input.yaml +367 -0
- package/components/text/text.js +1 -1
- package/components/textarea/textarea.a2ui.json +1 -1
- package/components/textarea/textarea.css +10 -1
- package/components/textarea/textarea.js +1 -1
- package/components/textarea/textarea.yaml +11 -8
- package/components/time-picker/time-picker.js +1 -1
- package/components/timeline/timeline-item.yaml +2 -2
- package/components/timeline/{class.js → timeline.class.js} +1 -1
- package/components/timeline/timeline.js +1 -1
- package/components/toast/toast.js +1 -1
- package/components/toggle-group/toggle-group.js +1 -1
- package/components/toggle-group/toggle-option.yaml +1 -1
- package/components/toggle-scheme/toggle-scheme.js +1 -1
- package/components/toolbar/toolbar-group.yaml +1 -1
- package/components/toolbar/toolbar.js +1 -1
- package/components/tooltip/tooltip.js +1 -1
- package/components/tree/tree-item.yaml +1 -1
- package/components/tree/tree.js +1 -1
- package/components/upload/upload.js +1 -1
- package/core/provider.js +19 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +112 -90
- package/package.json +3 -3
- package/styles/components.css +3 -0
- /package/components/accordion/{class.js → accordion.class.js} +0 -0
- /package/components/action-list/{class.js → action-list.class.js} +0 -0
- /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
- /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
- /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
- /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
- /package/components/avatar/{class.js → avatar.class.js} +0 -0
- /package/components/badge/{class.js → badge.class.js} +0 -0
- /package/components/block/{class.js → block.class.js} +0 -0
- /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
- /package/components/button/{class.js → button.class.js} +0 -0
- /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
- /package/components/card/{class.js → card.class.js} +0 -0
- /package/components/chart/{class.js → chart.class.js} +0 -0
- /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
- /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
- /package/components/check/{class.js → check.class.js} +0 -0
- /package/components/code/{class.js → code.class.js} +0 -0
- /package/components/col/{class.js → col.class.js} +0 -0
- /package/components/color-input/{class.js → color-input.class.js} +0 -0
- /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
- /package/components/combobox/{class.js → combobox.class.js} +0 -0
- /package/components/command/{class.js → command.class.js} +0 -0
- /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
- /package/components/description-list/{class.js → description-list.class.js} +0 -0
- /package/components/divider/{class.js → divider.class.js} +0 -0
- /package/components/drawer/{class.js → drawer.class.js} +0 -0
- /package/components/embed/{class.js → embed.class.js} +0 -0
- /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
- /package/components/feed/{class.js → feed.class.js} +0 -0
- /package/components/field/{class.js → field.class.js} +0 -0
- /package/components/fields/{class.js → fields.class.js} +0 -0
- /package/components/grid/{class.js → grid.class.js} +0 -0
- /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
- /package/components/icon/{class.js → icon.class.js} +0 -0
- /package/components/image/{class.js → image.class.js} +0 -0
- /package/components/input/{class.js → input.class.js} +0 -0
- /package/components/inspector/{class.js → inspector.class.js} +0 -0
- /package/components/integration-card/{class.js → integration-card.class.js} +0 -0
- /package/components/kbd/{class.js → kbd.class.js} +0 -0
- /package/components/link/{class.js → link.class.js} +0 -0
- /package/components/list/{class.js → list.class.js} +0 -0
- /package/components/list-window/{class.js → list-window.class.js} +0 -0
- /package/components/menu/{class.js → menu.class.js} +0 -0
- /package/components/nav/{class.js → nav.class.js} +0 -0
- /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
- /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
- /package/components/noodles/{class.js → noodles.class.js} +0 -0
- /package/components/option-card/{class.js → option-card.class.js} +0 -0
- /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
- /package/components/page/{class.js → page.class.js} +0 -0
- /package/components/pagination/{class.js → pagination.class.js} +0 -0
- /package/components/pane/{class.js → pane.class.js} +0 -0
- /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
- /package/components/popover/{class.js → popover.class.js} +0 -0
- /package/components/progress/{class.js → progress.class.js} +0 -0
- /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
- /package/components/radio/{class.js → radio.class.js} +0 -0
- /package/components/range/{class.js → range.class.js} +0 -0
- /package/components/rating/{class.js → rating.class.js} +0 -0
- /package/components/richtext/{class.js → richtext.class.js} +0 -0
- /package/components/row/{class.js → row.class.js} +0 -0
- /package/components/segment/{class.js → segment.class.js} +0 -0
- /package/components/segmented/{class.js → segmented.class.js} +0 -0
- /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
- /package/components/slider/{class.js → slider.class.js} +0 -0
- /package/components/stack/{class.js → stack.class.js} +0 -0
- /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
- /package/components/stepper/{class.js → stepper.class.js} +0 -0
- /package/components/stream/{class.js → stream.class.js} +0 -0
- /package/components/swatch/{class.js → swatch.class.js} +0 -0
- /package/components/swiper/{class.js → swiper.class.js} +0 -0
- /package/components/switch/{class.js → switch.class.js} +0 -0
- /package/components/table/{class.js → table.class.js} +0 -0
- /package/components/tabs/{class.js → tabs.class.js} +0 -0
- /package/components/text/{class.js → text.class.js} +0 -0
- /package/components/textarea/{class.js → textarea.class.js} +0 -0
- /package/components/time-picker/{class.js → time-picker.class.js} +0 -0
- /package/components/toast/{class.js → toast.class.js} +0 -0
- /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
- /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
- /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
- /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
- /package/components/tree/{class.js → tree.class.js} +0 -0
- /package/components/upload/{class.js → upload.class.js} +0 -0
|
@@ -25,7 +25,9 @@ export class UISelect extends UIFormElement {
|
|
|
25
25
|
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
26
26
|
// across all defined elements. Audited by check-required-icons.mjs
|
|
27
27
|
// (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
|
|
28
|
-
|
|
28
|
+
// SPEC-040: [multiple] mode stamps <tag-ui> chips (own `x`), checkbox
|
|
29
|
+
// indicator (`check`), and search-input prefix (`magnifying-glass`).
|
|
30
|
+
static requiredIcons = ['caret-up-down', 'check', 'x', 'magnifying-glass'];
|
|
29
31
|
|
|
30
32
|
// §225 (v0.5.9, FEEDBACK-10 §3): once-per-element console.warn dedup when
|
|
31
33
|
// consumer authors children that aren't native <option>/<optgroup>. The
|
|
@@ -49,6 +51,14 @@ export class UISelect extends UIFormElement {
|
|
|
49
51
|
// §184 (v0.5.5, FEEDBACK-08 §7): optional caption beneath the
|
|
50
52
|
// trigger, wired to aria-describedby on the host.
|
|
51
53
|
hint: { type: String, default: '', reflect: true },
|
|
54
|
+
// SPEC-040 (multi-select): chip trigger + checkbox list extensions.
|
|
55
|
+
// These are no-ops without [multiple] but reflect so [maxChips]
|
|
56
|
+
// selectors etc. work in CSS even when authored declaratively.
|
|
57
|
+
maxChips: { type: Number, default: 0, reflect: true, attribute: 'max-chips' },
|
|
58
|
+
min: { type: Number, default: 0, reflect: true },
|
|
59
|
+
max: { type: Number, default: 0, reflect: true },
|
|
60
|
+
selectAll: { type: Boolean, default: false, reflect: true, attribute: 'select-all' },
|
|
61
|
+
clearable: { type: Boolean, default: false, reflect: true },
|
|
52
62
|
};
|
|
53
63
|
|
|
54
64
|
// §184: per-instance hint id counter for aria-describedby wiring.
|
|
@@ -77,6 +87,11 @@ export class UISelect extends UIFormElement {
|
|
|
77
87
|
this.dispatchEvent(new CustomEvent('action', { bubbles: true, detail: { action: opt.action } }));
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
90
|
+
// SPEC-040 — multi-select: toggle membership without closing popover.
|
|
91
|
+
if (this.multiple) {
|
|
92
|
+
this.toggle(opt.value);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
80
95
|
this.value = opt.value;
|
|
81
96
|
this.open = false;
|
|
82
97
|
this.#query = '';
|
|
@@ -84,12 +99,237 @@ export class UISelect extends UIFormElement {
|
|
|
84
99
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
|
|
85
100
|
};
|
|
86
101
|
|
|
102
|
+
// SPEC-040 — internal helpers for [multiple] mode.
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Override syncValue to enforce min/max constraints in multi-select mode.
|
|
106
|
+
* When value.length < min, set form validity to invalid (tooShort).
|
|
107
|
+
* When value.length > max, set form validity to invalid (tooLong).
|
|
108
|
+
* Single-select path delegates straight to the base implementation.
|
|
109
|
+
*/
|
|
110
|
+
syncValue(val) {
|
|
111
|
+
if (!this.multiple) return super.syncValue(val);
|
|
112
|
+
const v = val ?? this.value ?? '';
|
|
113
|
+
this.internals.setFormValue(v);
|
|
114
|
+
const arr = String(v).split(',').map((s) => s.trim()).filter(Boolean);
|
|
115
|
+
if (this.min > 0 && arr.length < this.min) {
|
|
116
|
+
this.internals.setValidity(
|
|
117
|
+
{ tooShort: true },
|
|
118
|
+
this.getAttribute('data-msg-min') || `Please select at least ${this.min}.`,
|
|
119
|
+
this,
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (this.max > 0 && arr.length > this.max) {
|
|
124
|
+
this.internals.setValidity(
|
|
125
|
+
{ tooLong: true },
|
|
126
|
+
this.getAttribute('data-msg-max') || `Please select no more than ${this.max}.`,
|
|
127
|
+
this,
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (this.required && arr.length === 0) {
|
|
132
|
+
this.internals.setValidity(
|
|
133
|
+
{ valueMissing: true },
|
|
134
|
+
this.getAttribute('data-msg-required') || 'This field is required.',
|
|
135
|
+
this,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.internals.setValidity({});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse the current value as an array of selected ids (multi-select).
|
|
144
|
+
* Returns an empty array for empty / falsy values.
|
|
145
|
+
*/
|
|
146
|
+
#values() {
|
|
147
|
+
if (!this.value) return [];
|
|
148
|
+
return String(this.value).split(',').map((s) => s.trim()).filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Serialize an array of ids back to the canonical comma-separated form,
|
|
153
|
+
* de-duplicating in insertion order (Set semantics per SPEC-040 §3).
|
|
154
|
+
*/
|
|
155
|
+
#serialize(arr) {
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const v of arr) {
|
|
159
|
+
if (v == null || v === '') continue;
|
|
160
|
+
const k = String(v);
|
|
161
|
+
if (seen.has(k)) continue;
|
|
162
|
+
seen.add(k);
|
|
163
|
+
out.push(k);
|
|
164
|
+
}
|
|
165
|
+
return out.join(',');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Public method (per SPEC-040 §4): toggle one option's selection in
|
|
170
|
+
* multi-select mode. Single-select callers should set `value` directly.
|
|
171
|
+
*/
|
|
172
|
+
toggle(id) {
|
|
173
|
+
if (!this.multiple) {
|
|
174
|
+
this.value = id;
|
|
175
|
+
this.syncValue(id);
|
|
176
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const cur = this.#values();
|
|
180
|
+
const idx = cur.indexOf(String(id));
|
|
181
|
+
const prev = cur.slice();
|
|
182
|
+
let added = [];
|
|
183
|
+
let removed = [];
|
|
184
|
+
if (idx >= 0) {
|
|
185
|
+
cur.splice(idx, 1);
|
|
186
|
+
removed = [String(id)];
|
|
187
|
+
} else {
|
|
188
|
+
// SPEC-040 §4 max: suppress toggling past the cap.
|
|
189
|
+
if (this.max > 0 && cur.length >= this.max) {
|
|
190
|
+
this.dispatchEvent(new CustomEvent('invalid', {
|
|
191
|
+
bubbles: true,
|
|
192
|
+
detail: { value: prev, reason: 'max' },
|
|
193
|
+
}));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
cur.push(String(id));
|
|
197
|
+
added = [String(id)];
|
|
198
|
+
}
|
|
199
|
+
const next = this.#serialize(cur);
|
|
200
|
+
this.value = next;
|
|
201
|
+
this.syncValue(next);
|
|
202
|
+
// Re-render listbox aria-selected + chips
|
|
203
|
+
this.render();
|
|
204
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
205
|
+
bubbles: true,
|
|
206
|
+
detail: { value: cur, added, removed },
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Public method (SPEC-040 §4): select every non-disabled option.
|
|
212
|
+
*/
|
|
213
|
+
selectAllOptions() {
|
|
214
|
+
if (!this.multiple) return;
|
|
215
|
+
const flat = this.#options.flatMap((o) => o.options || [o]).filter((o) => !o.disabled && !o.separator && !o.header && o.value != null);
|
|
216
|
+
const next = this.#serialize(flat.map((o) => o.value));
|
|
217
|
+
if (next === this.value) return;
|
|
218
|
+
const prev = this.#values();
|
|
219
|
+
this.value = next;
|
|
220
|
+
this.syncValue(next);
|
|
221
|
+
this.render();
|
|
222
|
+
const nextArr = this.#values();
|
|
223
|
+
const added = nextArr.filter((v) => !prev.includes(v));
|
|
224
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
225
|
+
bubbles: true,
|
|
226
|
+
detail: { value: nextArr, added, removed: [] },
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Public method (SPEC-040 §4): clear all selections.
|
|
232
|
+
*/
|
|
233
|
+
clear() {
|
|
234
|
+
if (!this.multiple) {
|
|
235
|
+
this.value = '';
|
|
236
|
+
this.syncValue('');
|
|
237
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '' } }));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const prev = this.#values();
|
|
241
|
+
if (prev.length === 0) return;
|
|
242
|
+
this.value = '';
|
|
243
|
+
this.syncValue('');
|
|
244
|
+
this.render();
|
|
245
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
246
|
+
bubbles: true,
|
|
247
|
+
detail: { value: [], added: [], removed: prev },
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Internal: remove the last chip (Backspace handling).
|
|
253
|
+
*/
|
|
254
|
+
#removeLastChip() {
|
|
255
|
+
const cur = this.#values();
|
|
256
|
+
if (cur.length === 0) return;
|
|
257
|
+
const removed = cur.pop();
|
|
258
|
+
const next = this.#serialize(cur);
|
|
259
|
+
this.value = next;
|
|
260
|
+
this.syncValue(next);
|
|
261
|
+
this.render();
|
|
262
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
263
|
+
bubbles: true,
|
|
264
|
+
detail: { value: cur, added: [], removed: [removed] },
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Internal: handler for tag-ui `remove` events bubbling out of chips.
|
|
270
|
+
*/
|
|
271
|
+
#onChipRemove = (e) => {
|
|
272
|
+
if (!this.multiple) return;
|
|
273
|
+
const target = e.target;
|
|
274
|
+
if (!target || target.tagName !== 'TAG-UI') return;
|
|
275
|
+
const id = target.dataset.chipValue;
|
|
276
|
+
if (!id) return;
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
// `tag-ui` already removes itself from the DOM; rebuild value as a
|
|
279
|
+
// single source of truth and re-stamp.
|
|
280
|
+
const cur = this.#values().filter((v) => v !== id);
|
|
281
|
+
const next = this.#serialize(cur);
|
|
282
|
+
this.value = next;
|
|
283
|
+
this.syncValue(next);
|
|
284
|
+
this.render();
|
|
285
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
286
|
+
bubbles: true,
|
|
287
|
+
detail: { value: cur, added: [], removed: [id] },
|
|
288
|
+
}));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Internal: handler for the "+N more" pill click — opens the popover.
|
|
293
|
+
*/
|
|
294
|
+
#onMoreClick = (e) => {
|
|
295
|
+
e.stopPropagation();
|
|
296
|
+
if (this.disabled) return;
|
|
297
|
+
this.open = true;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Internal: handler for the clear-all `x` on the trigger.
|
|
302
|
+
*/
|
|
303
|
+
#onClearAllClick = (e) => {
|
|
304
|
+
e.stopPropagation();
|
|
305
|
+
if (this.disabled || this.readonly) return;
|
|
306
|
+
this.clear();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Internal: handler for the "Select all" / "Clear" header button.
|
|
311
|
+
*/
|
|
312
|
+
#onSelectAllClick = (e) => {
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
if (this.disabled || this.readonly) return;
|
|
315
|
+
const flat = this.#options.flatMap((o) => o.options || [o]).filter((o) => !o.disabled && !o.separator && !o.header && o.value != null);
|
|
316
|
+
const allIds = flat.map((o) => String(o.value));
|
|
317
|
+
const cur = new Set(this.#values());
|
|
318
|
+
const everyone = allIds.length > 0 && allIds.every((id) => cur.has(id));
|
|
319
|
+
if (everyone) this.clear();
|
|
320
|
+
else this.selectAllOptions();
|
|
321
|
+
};
|
|
322
|
+
|
|
87
323
|
connected() {
|
|
88
324
|
super.connected();
|
|
89
325
|
this.setAttribute('role', 'combobox');
|
|
90
326
|
this.setAttribute('tabindex', '0');
|
|
91
327
|
this.addEventListener('click', this.#onClick);
|
|
92
328
|
this.addEventListener('keydown', this.#onKey);
|
|
329
|
+
// SPEC-040: trap `remove` events from chip tag-ui children so the host
|
|
330
|
+
// value array stays the single source of truth. Bubbles, so we
|
|
331
|
+
// capture them at the host level — no per-chip wiring needed.
|
|
332
|
+
this.addEventListener('remove', this.#onChipRemove);
|
|
93
333
|
// Only parse declarative <option>/<optgroup> children if options
|
|
94
334
|
// weren't already set programmatically (e.g. via .options = [...])
|
|
95
335
|
if (this.#options.length === 0) {
|
|
@@ -98,6 +338,13 @@ export class UISelect extends UIFormElement {
|
|
|
98
338
|
}
|
|
99
339
|
|
|
100
340
|
render() {
|
|
341
|
+
// SPEC-040 — in multi-select mode the trigger needs to host chips +
|
|
342
|
+
// optional clear-x + optional "+N more" pill alongside the usual
|
|
343
|
+
// leading / display / caret. We toggle host attribute [data-multi-chips]
|
|
344
|
+
// so CSS can switch layout from single-line text to flex-wrap chip row.
|
|
345
|
+
if (this.multiple) this.setAttribute('data-multi-chips', '');
|
|
346
|
+
else this.removeAttribute('data-multi-chips');
|
|
347
|
+
|
|
101
348
|
// Stamp default trigger if none provided
|
|
102
349
|
if (!this.querySelector('[slot="trigger"]')) {
|
|
103
350
|
// Detach listbox before innerHTML wipe so it isn't destroyed
|
|
@@ -118,16 +365,31 @@ export class UISelect extends UIFormElement {
|
|
|
118
365
|
// but not the rendering until this arc).
|
|
119
366
|
const hintId = this.hint ? `select-hint-${++UISelect.#hintSeq}` : '';
|
|
120
367
|
const hintMarkup = this.hint ? `<span slot="hint" id="${hintId}">${escapeHTML(this.hint)}</span>` : '';
|
|
368
|
+
// SPEC-040 — trigger needs a [data-chips] slot we can stamp tag-ui
|
|
369
|
+
// into in multi-select mode. The chip row sits BEFORE [slot="display"]
|
|
370
|
+
// so the display (placeholder or search input) renders inline at the
|
|
371
|
+
// end of the chips, matching standard multi-select UX.
|
|
121
372
|
this.innerHTML = `
|
|
122
373
|
<span slot="trigger">
|
|
123
374
|
${leading}
|
|
375
|
+
<span data-chips></span>
|
|
124
376
|
${displayMarkup}
|
|
377
|
+
<button type="button" data-clear-all aria-label="Clear all" hidden>
|
|
378
|
+
<icon-ui name="x"></icon-ui>
|
|
379
|
+
</button>
|
|
125
380
|
<icon-ui name="caret-up-down" slot="caret"></icon-ui>
|
|
126
381
|
</span>
|
|
127
382
|
${hintMarkup}
|
|
128
383
|
`;
|
|
129
384
|
if (this.hint) this.setAttribute('aria-describedby', hintId);
|
|
130
385
|
|
|
386
|
+
// Wire clear-all click once per stamp.
|
|
387
|
+
const clearBtn = this.querySelector('[data-clear-all]');
|
|
388
|
+
if (clearBtn) {
|
|
389
|
+
clearBtn.removeEventListener('click', this.#onClearAllClick);
|
|
390
|
+
clearBtn.addEventListener('click', this.#onClearAllClick);
|
|
391
|
+
}
|
|
392
|
+
|
|
131
393
|
if (this.searchable) {
|
|
132
394
|
// Detach from previous search input if any
|
|
133
395
|
if (this.#searchInput) {
|
|
@@ -147,16 +409,38 @@ export class UISelect extends UIFormElement {
|
|
|
147
409
|
if (lb) this.appendChild(lb);
|
|
148
410
|
} else {
|
|
149
411
|
const display = this.querySelector('[slot="display"]');
|
|
150
|
-
if (display) {
|
|
412
|
+
if (display && !this.multiple) {
|
|
413
|
+
// Single-select: keep the canonical text-rendering path.
|
|
151
414
|
if (display.tagName === 'INPUT') {
|
|
152
415
|
// Only update value when not actively editing (no active query)
|
|
153
416
|
if (!this.#query) display.value = this.#displayText() === this.placeholder ? '' : this.#displayText();
|
|
154
417
|
} else {
|
|
155
418
|
display.textContent = this.#displayText();
|
|
156
419
|
}
|
|
420
|
+
} else if (display && this.multiple) {
|
|
421
|
+
// Multi-select: display element holds the placeholder ONLY when
|
|
422
|
+
// no chips are present (otherwise the chip row IS the display).
|
|
423
|
+
const hasChips = this.#values().length > 0;
|
|
424
|
+
if (display.tagName === 'INPUT') {
|
|
425
|
+
// search input — keep placeholder; never inject the value text.
|
|
426
|
+
display.placeholder = hasChips ? '' : (this.placeholder || '');
|
|
427
|
+
if (!this.#query) display.value = '';
|
|
428
|
+
} else {
|
|
429
|
+
display.textContent = hasChips ? '' : (this.placeholder || '');
|
|
430
|
+
}
|
|
157
431
|
}
|
|
158
432
|
}
|
|
159
433
|
|
|
434
|
+
// SPEC-040 — stamp / reconcile chips + "+N more" pill on every render.
|
|
435
|
+
if (this.multiple) this.#stampChips();
|
|
436
|
+
// Show clear-all only in multi-select mode when [clearable] + chips present.
|
|
437
|
+
const clearBtn = this.querySelector('[data-clear-all]');
|
|
438
|
+
if (clearBtn) {
|
|
439
|
+
const show = this.multiple && this.clearable && this.#values().length > 0 && !this.disabled && !this.readonly;
|
|
440
|
+
if (show) clearBtn.removeAttribute('hidden');
|
|
441
|
+
else clearBtn.setAttribute('hidden', '');
|
|
442
|
+
}
|
|
443
|
+
|
|
160
444
|
// Ensure listbox exists (regardless of trigger source)
|
|
161
445
|
if (!this.#listbox) {
|
|
162
446
|
this.#listbox = this.querySelector('[slot="listbox"]');
|
|
@@ -295,6 +579,24 @@ export class UISelect extends UIFormElement {
|
|
|
295
579
|
if (!this.#listbox) return;
|
|
296
580
|
this.#listbox.innerHTML = '';
|
|
297
581
|
|
|
582
|
+
// SPEC-040 — multi-select listbox is aria-multiselectable; checkbox
|
|
583
|
+
// indicator on each option row marks selection visually.
|
|
584
|
+
if (this.multiple) this.#listbox.setAttribute('aria-multiselectable', 'true');
|
|
585
|
+
else this.#listbox.removeAttribute('aria-multiselectable');
|
|
586
|
+
|
|
587
|
+
// SPEC-040 — Select-all / Clear control row at the top of the list.
|
|
588
|
+
if (this.multiple && this.selectAll) {
|
|
589
|
+
const header = document.createElement('div');
|
|
590
|
+
header.setAttribute('data-select-all', '');
|
|
591
|
+
const btn = document.createElement('button');
|
|
592
|
+
btn.type = 'button';
|
|
593
|
+
btn.dataset.selectAllBtn = '';
|
|
594
|
+
btn.textContent = this.#allSelected() ? 'Clear' : 'Select all';
|
|
595
|
+
btn.addEventListener('click', this.#onSelectAllClick);
|
|
596
|
+
header.appendChild(btn);
|
|
597
|
+
this.#listbox.appendChild(header);
|
|
598
|
+
}
|
|
599
|
+
|
|
298
600
|
const renderOpt = (opt) => {
|
|
299
601
|
if (opt.separator) {
|
|
300
602
|
const hr = document.createElement('hr');
|
|
@@ -304,13 +606,31 @@ export class UISelect extends UIFormElement {
|
|
|
304
606
|
const el = document.createElement('div');
|
|
305
607
|
el.setAttribute('role', 'option');
|
|
306
608
|
el.setAttribute('data-value', opt.value || '');
|
|
307
|
-
if (opt.icon) el.innerHTML = `<icon-ui name="${escapeHTML(opt.icon)}"></icon-ui>${escapeHTML(opt.label)}`;
|
|
308
|
-
else el.textContent = opt.label;
|
|
309
609
|
// §FB-46: [multiple] value is comma-separated — use Set membership.
|
|
310
610
|
const selSet = this.multiple
|
|
311
611
|
? new Set(this.value.split(',').map((s) => s.trim()).filter(Boolean))
|
|
312
612
|
: null;
|
|
313
|
-
|
|
613
|
+
const isSelected = selSet ? selSet.has(opt.value) : opt.value === this.value;
|
|
614
|
+
// SPEC-040 — multi-select option rows render a leading checkbox
|
|
615
|
+
// indicator (CSS-driven via [data-multi-option]); the `check` icon
|
|
616
|
+
// shows when aria-selected="true".
|
|
617
|
+
if (this.multiple) {
|
|
618
|
+
el.setAttribute('data-multi-option', '');
|
|
619
|
+
const box = document.createElement('span');
|
|
620
|
+
box.setAttribute('data-checkbox', '');
|
|
621
|
+
box.innerHTML = '<icon-ui name="check" aria-hidden="true"></icon-ui>';
|
|
622
|
+
el.appendChild(box);
|
|
623
|
+
const label = document.createElement('span');
|
|
624
|
+
label.setAttribute('data-option-label', '');
|
|
625
|
+
if (opt.icon) label.innerHTML = `<icon-ui name="${escapeHTML(opt.icon)}"></icon-ui>${escapeHTML(opt.label)}`;
|
|
626
|
+
else label.textContent = opt.label;
|
|
627
|
+
el.appendChild(label);
|
|
628
|
+
} else if (opt.icon) {
|
|
629
|
+
el.innerHTML = `<icon-ui name="${escapeHTML(opt.icon)}"></icon-ui>${escapeHTML(opt.label)}`;
|
|
630
|
+
} else {
|
|
631
|
+
el.textContent = opt.label;
|
|
632
|
+
}
|
|
633
|
+
if (isSelected) el.setAttribute('aria-selected', 'true');
|
|
314
634
|
if (opt.disabled) el.setAttribute('aria-disabled', 'true');
|
|
315
635
|
if (opt.action) el.dataset.action = opt.action;
|
|
316
636
|
el.__adiaOption = opt;
|
|
@@ -338,10 +658,78 @@ export class UISelect extends UIFormElement {
|
|
|
338
658
|
this.#listbox.appendChild(renderOpt(item));
|
|
339
659
|
}
|
|
340
660
|
}
|
|
661
|
+
|
|
662
|
+
// SPEC-040 — empty-state stamp when there are no consumer options.
|
|
663
|
+
if (this.#options.length === 0) {
|
|
664
|
+
const empty = document.createElement('div');
|
|
665
|
+
empty.setAttribute('data-empty', '');
|
|
666
|
+
empty.textContent = 'No options';
|
|
667
|
+
this.#listbox.appendChild(empty);
|
|
668
|
+
}
|
|
669
|
+
|
|
341
670
|
if (this.#query) this.#applyFilter();
|
|
342
671
|
}
|
|
343
672
|
|
|
673
|
+
/**
|
|
674
|
+
* SPEC-040 — returns true when every non-disabled option is currently
|
|
675
|
+
* selected. Used by the Select-all header to flip its label to "Clear".
|
|
676
|
+
*/
|
|
677
|
+
#allSelected() {
|
|
678
|
+
if (!this.multiple) return false;
|
|
679
|
+
const flat = this.#options.flatMap((o) => o.options || [o])
|
|
680
|
+
.filter((o) => !o.disabled && !o.separator && !o.header && o.value != null);
|
|
681
|
+
if (flat.length === 0) return false;
|
|
682
|
+
const cur = new Set(this.#values());
|
|
683
|
+
return flat.every((o) => cur.has(String(o.value)));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* SPEC-040 — reconcile the trigger chip row against the current value.
|
|
688
|
+
* Tag-ui children carry [data-chip-value] so we can identify which chip
|
|
689
|
+
* fired a `remove` event without re-parsing labels.
|
|
690
|
+
*/
|
|
691
|
+
#stampChips() {
|
|
692
|
+
const slot = this.querySelector('[slot="trigger"] [data-chips]');
|
|
693
|
+
if (!slot) return;
|
|
694
|
+
const values = this.#values();
|
|
695
|
+
const cap = Number(this.maxChips) || 0;
|
|
696
|
+
const cappedValues = cap > 0 && values.length > cap ? values.slice(0, cap) : values;
|
|
697
|
+
const overflow = cap > 0 && values.length > cap ? values.length - cap : 0;
|
|
698
|
+
// Label lookup from internal options model (handles grouped form too).
|
|
699
|
+
const flat = this.#options.flatMap((o) => o.options || [o])
|
|
700
|
+
.filter((o) => !o.separator && !o.header && o.value != null);
|
|
701
|
+
const labelOf = (v) => {
|
|
702
|
+
const found = flat.find((o) => String(o.value) === String(v));
|
|
703
|
+
return found ? found.label : String(v);
|
|
704
|
+
};
|
|
705
|
+
// Rebuild — chips are cheap and reconciliation gets us correct order
|
|
706
|
+
// + correct labels with no per-chip tracking state.
|
|
707
|
+
while (slot.firstChild) slot.removeChild(slot.firstChild);
|
|
708
|
+
for (const v of cappedValues) {
|
|
709
|
+
const tag = document.createElement('tag-ui');
|
|
710
|
+
tag.setAttribute('size', 'sm');
|
|
711
|
+
// Only show the dismiss `x` when interactive (matches [disabled]/[readonly] gating).
|
|
712
|
+
if (!this.disabled && !this.readonly) tag.setAttribute('removable', '');
|
|
713
|
+
tag.dataset.chipValue = String(v);
|
|
714
|
+
tag.setAttribute('text', labelOf(v));
|
|
715
|
+
slot.appendChild(tag);
|
|
716
|
+
}
|
|
717
|
+
if (overflow > 0) {
|
|
718
|
+
const more = document.createElement('button');
|
|
719
|
+
more.type = 'button';
|
|
720
|
+
more.dataset.more = '';
|
|
721
|
+
more.textContent = `+${overflow} more`;
|
|
722
|
+
more.addEventListener('click', this.#onMoreClick);
|
|
723
|
+
slot.appendChild(more);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
344
727
|
#displayText() {
|
|
728
|
+
if (this.multiple) {
|
|
729
|
+
// SPEC-040 — chips render the selection; display element holds the
|
|
730
|
+
// placeholder when empty and is otherwise empty (CSS sees [data-multi-chips]).
|
|
731
|
+
return this.value ? '' : this.placeholder;
|
|
732
|
+
}
|
|
345
733
|
const flat = this.#options.flatMap(o => o.options || [o]);
|
|
346
734
|
const selected = flat.find(o => o.value === this.value || (o.header && o.label === this.value));
|
|
347
735
|
return selected?.label || this.value || this.placeholder;
|
|
@@ -362,6 +750,22 @@ export class UISelect extends UIFormElement {
|
|
|
362
750
|
};
|
|
363
751
|
|
|
364
752
|
#onKey = (e) => {
|
|
753
|
+
// SPEC-040 — Backspace from the trigger removes the last chip.
|
|
754
|
+
// Two paths: (1) trigger focused, no search input — straight Backspace.
|
|
755
|
+
// (2) search input focused with empty query — Backspace removes a chip
|
|
756
|
+
// before it would start removing query characters.
|
|
757
|
+
if (e.key === 'Backspace' && this.multiple && !this.disabled && !this.readonly) {
|
|
758
|
+
const target = e.target;
|
|
759
|
+
const fromInput = target && target.tagName === 'INPUT' && target.getAttribute('slot') === 'display';
|
|
760
|
+
const queryEmpty = !this.#query || this.#query.length === 0;
|
|
761
|
+
if (!fromInput || queryEmpty) {
|
|
762
|
+
if (this.#values().length > 0) {
|
|
763
|
+
e.preventDefault();
|
|
764
|
+
this.#removeLastChip();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
365
769
|
if (e.key === 'Escape') {
|
|
366
770
|
if (this.searchable && this.#query) {
|
|
367
771
|
// First Escape: clear query
|
|
@@ -380,6 +784,10 @@ export class UISelect extends UIFormElement {
|
|
|
380
784
|
const focused = this.#listbox?.querySelector('[role="option"][data-focused]:not([data-filtered-out])');
|
|
381
785
|
if (focused) {
|
|
382
786
|
focused.click();
|
|
787
|
+
// SPEC-040 — Enter in multi-select commits + closes (matches the
|
|
788
|
+
// WAI-APG commit pattern). #onOptionClick keeps the popover open
|
|
789
|
+
// on plain clicks; Enter is the explicit commit.
|
|
790
|
+
if (this.multiple) this.open = false;
|
|
383
791
|
} else if (this.searchable && this.freeText && this.#query) {
|
|
384
792
|
// Commit free-text
|
|
385
793
|
const q = this.#query;
|
|
@@ -401,7 +809,7 @@ export class UISelect extends UIFormElement {
|
|
|
401
809
|
if (this.open) {
|
|
402
810
|
const focused = this.#listbox?.querySelector('[role="option"][data-focused]');
|
|
403
811
|
if (focused) focused.click();
|
|
404
|
-
else this.open = false;
|
|
812
|
+
else if (!this.multiple) this.open = false;
|
|
405
813
|
} else {
|
|
406
814
|
this.open = true;
|
|
407
815
|
}
|
|
@@ -470,6 +878,7 @@ export class UISelect extends UIFormElement {
|
|
|
470
878
|
super.disconnected();
|
|
471
879
|
this.removeEventListener('click', this.#onClick);
|
|
472
880
|
this.removeEventListener('keydown', this.#onKey);
|
|
881
|
+
this.removeEventListener('remove', this.#onChipRemove);
|
|
473
882
|
if (this.#rafId != null) {
|
|
474
883
|
cancelAnimationFrame(this.#rafId);
|
|
475
884
|
this.#rafId = null;
|