@aceshooting/lyra-ui 1.0.1 → 1.1.0
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/README.md +8 -9
- package/custom-elements.json +12479 -6535
- package/dist/components/chart/chart.d.ts +46 -1
- package/dist/components/chart/chart.js +148 -10
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +74 -0
- package/dist/components/chart/lite-chart.js +384 -0
- package/dist/components/chart/lite-chart.stories.d.ts +12 -0
- package/dist/components/chart/lite-chart.stories.js +98 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +75 -12
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +48 -3
- package/dist/components/flag/flag.js +65 -29
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +137 -12
- package/dist/components/heatmap/heatmap.js +427 -27
- package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
- package/dist/components/heatmap/heatmap.stories.js +130 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +122 -0
- package/dist/components/select/select.js +516 -0
- package/dist/components/select/select.stories.d.ts +19 -0
- package/dist/components/select/select.stories.js +75 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +188 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +27 -1
- package/dist/components/stat/stat.js +56 -3
- package/dist/components/stat/stat.stories.d.ts +3 -0
- package/dist/components/stat/stat.stories.js +49 -1
- package/dist/components/stat/stat.styles.js +44 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +15 -1
- package/dist/components/widget/widget.js +140 -5
- package/dist/components/widget/widget.stories.d.ts +1 -0
- package/dist/components/widget/widget.stories.js +10 -0
- package/dist/components/widget/widget.styles.js +11 -1
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +54 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +892 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { html } from 'lit';
|
|
8
|
+
import { property, state } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { place } from '../../internal/positioner.js';
|
|
12
|
+
import { nextId } from '../../internal/a11y.js';
|
|
13
|
+
import { chevronIcon } from '../../internal/icons.js';
|
|
14
|
+
import { styles } from './select.styles.js';
|
|
15
|
+
import { LyraOption } from '../combobox/option.js';
|
|
16
|
+
import '../combobox/option.js';
|
|
17
|
+
/**
|
|
18
|
+
* `<lyra-select>` — a plain closed-list dropdown: a direct `<lyra-*>`
|
|
19
|
+
* counterpart to `<wa-select>`/`<wa-option>`. Trigger is a button (not a text
|
|
20
|
+
* input) -- click/Enter/Space/ArrowDown opens it, there's no typing-to-filter.
|
|
21
|
+
* A printable keypress instead jumps (or, while closed, directly selects) the
|
|
22
|
+
* next option whose label starts with what's been typed, like a native
|
|
23
|
+
* `<select>`'s type-ahead.
|
|
24
|
+
*
|
|
25
|
+
* Options are `<lyra-option value>` children, the same element `<lyra-combobox>`
|
|
26
|
+
* uses. Unlike `lyra-combobox` this is single-select only, with no filter/
|
|
27
|
+
* source/with-clear/max-options-visible/empty-text/max-render/multiple surface
|
|
28
|
+
* -- see `<lyra-combobox>` for the filterable/multi-select case.
|
|
29
|
+
*
|
|
30
|
+
* Reuses `lyra-combobox`'s popup positioning (`internal/positioner.js`) and
|
|
31
|
+
* click-outside/Escape/Home/End/Arrow-key listbox navigation patterns,
|
|
32
|
+
* adapted to a trigger button that keeps DOM focus throughout (the listbox's
|
|
33
|
+
* "active" row is conveyed via `aria-activedescendant`, never actual focus),
|
|
34
|
+
* matching the WAI-ARIA "select-only combobox" pattern.
|
|
35
|
+
*
|
|
36
|
+
* @customElement lyra-select
|
|
37
|
+
* @slot - `<lyra-option>` elements.
|
|
38
|
+
* @slot label - Custom label content.
|
|
39
|
+
* @slot hint - Custom hint content.
|
|
40
|
+
* @slot error - Custom error content.
|
|
41
|
+
* @event change - The selection changed.
|
|
42
|
+
* @event input - Fired alongside `change` on every selection change (native
|
|
43
|
+
* `<select>` doesn't meaningfully distinguish the two either).
|
|
44
|
+
* @event lyra-show - The listbox opened.
|
|
45
|
+
* @event lyra-hide - The listbox closed.
|
|
46
|
+
* @csspart form-control - The outer wrapper around label, trigger, listbox, error and hint.
|
|
47
|
+
* @csspart form-control-label - The `<label>` element.
|
|
48
|
+
* @csspart trigger - The trigger button (positioning anchor).
|
|
49
|
+
* @csspart listbox - The options popover.
|
|
50
|
+
* @csspart option - An option row.
|
|
51
|
+
* @csspart option-dot - An option row's leading status dot (when `dot-color` is set).
|
|
52
|
+
* @csspart option-label - An option row's label/sub wrapper.
|
|
53
|
+
* @csspart option-sub - An option row's secondary line (when `sub` is set).
|
|
54
|
+
* @csspart expand-icon - The dropdown indicator.
|
|
55
|
+
* @csspart error - The error message.
|
|
56
|
+
* @csspart hint - The hint message.
|
|
57
|
+
*/
|
|
58
|
+
export class LyraSelect extends LyraElement {
|
|
59
|
+
static { this.formAssociated = true; }
|
|
60
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
61
|
+
static { this.properties = {
|
|
62
|
+
value: { noAccessor: true },
|
|
63
|
+
}; }
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
this.placeholder = '';
|
|
67
|
+
this.disabled = false;
|
|
68
|
+
this.required = false;
|
|
69
|
+
this.name = '';
|
|
70
|
+
this.label = '';
|
|
71
|
+
this.hint = '';
|
|
72
|
+
this.errorText = '';
|
|
73
|
+
this.open = false;
|
|
74
|
+
this.activeIndex = -1;
|
|
75
|
+
this.options = [];
|
|
76
|
+
// Set on the trigger button's first `blur`; gates the `data-invalid`
|
|
77
|
+
// reflection below so validity styling never flashes on first render.
|
|
78
|
+
this.touched = false;
|
|
79
|
+
// `[part]:empty` never matches -- the part always contains a literal
|
|
80
|
+
// `<slot>` child element regardless of assigned content -- so real
|
|
81
|
+
// emptiness is tracked in JS instead (same fix as lyra-combobox's
|
|
82
|
+
// hasHintSlot/hasErrorSlot/hasLabelSlot) and reflected via `hidden`.
|
|
83
|
+
this.hasHintSlot = false;
|
|
84
|
+
this.hasErrorSlot = false;
|
|
85
|
+
this.hasLabelSlot = false;
|
|
86
|
+
this.listId = nextId('select-list');
|
|
87
|
+
this.triggerId = nextId('select-trigger');
|
|
88
|
+
this._isFirstUpdate = true;
|
|
89
|
+
this._selected = '';
|
|
90
|
+
// What `form.reset()` restores to. Captured exactly once, from whatever
|
|
91
|
+
// `<lyra-option selected>` markup was present the first time slotted
|
|
92
|
+
// options are collected (mirrors native `<select><option selected>`) --
|
|
93
|
+
// never from the `value` setter, so a user picking an option (even the
|
|
94
|
+
// very first pick on an initially-unselected select) can't itself become
|
|
95
|
+
// the reset default. See lyra-combobox's identical `_defaultSelected`.
|
|
96
|
+
this._defaultSelected = '';
|
|
97
|
+
this._defaultCaptured = false;
|
|
98
|
+
// Standard listbox type-ahead: printable keystrokes accumulate into this
|
|
99
|
+
// buffer and reset ~500ms after the last one, so "b" then "a" narrows to
|
|
100
|
+
// "ba" instead of restarting the search on every keystroke.
|
|
101
|
+
this.typeAheadBuffer = '';
|
|
102
|
+
this.collectOptions = (e) => {
|
|
103
|
+
const slot = e.target;
|
|
104
|
+
const previous = new Set(this.options);
|
|
105
|
+
this.options = slot
|
|
106
|
+
.assignedElements({ flatten: true })
|
|
107
|
+
.filter((el) => el instanceof LyraOption);
|
|
108
|
+
if (!this._defaultCaptured) {
|
|
109
|
+
this._defaultCaptured = true;
|
|
110
|
+
// Seed the initial selection -- and the reset default -- from
|
|
111
|
+
// declarative `<lyra-option selected>` markup, mirroring native
|
|
112
|
+
// `<select><option selected>`. Only the *first* declared-selected
|
|
113
|
+
// option matters when several declare it, mirroring lyra-combobox's
|
|
114
|
+
// single-mode behavior. This is the only place `_defaultSelected` is
|
|
115
|
+
// set; picking an option later (the `value` setter) never redefines
|
|
116
|
+
// the reset default.
|
|
117
|
+
const declared = this.options.filter((o) => o.selected).map((o) => o.value);
|
|
118
|
+
this._defaultSelected = declared[0] ?? '';
|
|
119
|
+
if (declared.length) {
|
|
120
|
+
this.value = declared[0];
|
|
121
|
+
return; // `value=`'s setter already called reflectSelected()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Options slotted in after the first pass (e.g. a lazily-populated
|
|
126
|
+
// list appended post-connect) still declare selection the same way a
|
|
127
|
+
// native `<select><option selected>` would -- seed the newest one
|
|
128
|
+
// into the live selection instead of letting reflectSelected() below
|
|
129
|
+
// strip its `selected` attribute back off.
|
|
130
|
+
const newlySelected = this.options.filter((o) => !previous.has(o) && o.selected).map((o) => o.value);
|
|
131
|
+
if (newlySelected.length) {
|
|
132
|
+
this.value = newlySelected[newlySelected.length - 1];
|
|
133
|
+
return; // `value=`'s setter already called reflectSelected()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.reflectSelected();
|
|
137
|
+
};
|
|
138
|
+
this.onDocPointer = (e) => {
|
|
139
|
+
if (!e.composedPath().includes(this))
|
|
140
|
+
this.hide();
|
|
141
|
+
};
|
|
142
|
+
this.onTriggerClick = () => {
|
|
143
|
+
if (this.disabled)
|
|
144
|
+
return;
|
|
145
|
+
this.open ? this.hide() : this.show();
|
|
146
|
+
};
|
|
147
|
+
this.onTriggerBlur = () => {
|
|
148
|
+
this.touched = true;
|
|
149
|
+
// A mouse click outside the element is already handled by
|
|
150
|
+
// onDocPointer/hide(), but that leaves keyboard users with no way to
|
|
151
|
+
// dismiss the listbox short of Escape -- tabbing focus away from the
|
|
152
|
+
// trigger should close it too, the same as lyra-combobox's input blur.
|
|
153
|
+
this.hide();
|
|
154
|
+
};
|
|
155
|
+
this.onHintSlotChange = (e) => {
|
|
156
|
+
this.hasHintSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
157
|
+
};
|
|
158
|
+
this.onErrorSlotChange = (e) => {
|
|
159
|
+
this.hasErrorSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
160
|
+
};
|
|
161
|
+
this.onLabelSlotChange = (e) => {
|
|
162
|
+
this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
163
|
+
};
|
|
164
|
+
this.onKeyDown = (e) => {
|
|
165
|
+
const navigable = this.options.filter((o) => !o.disabled);
|
|
166
|
+
switch (e.key) {
|
|
167
|
+
case 'ArrowDown':
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
if (!this.open)
|
|
170
|
+
return this.show();
|
|
171
|
+
this.activeIndex = Math.min(navigable.length - 1, this.activeIndex + 1);
|
|
172
|
+
break;
|
|
173
|
+
case 'ArrowUp':
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
if (!this.open)
|
|
176
|
+
return this.show();
|
|
177
|
+
this.activeIndex = Math.max(0, this.activeIndex - 1);
|
|
178
|
+
break;
|
|
179
|
+
case 'Enter':
|
|
180
|
+
case ' ':
|
|
181
|
+
// When closed, let the button's native Enter/Space activation fire
|
|
182
|
+
// its own `click` handler (onTriggerClick) to open -- only intercept
|
|
183
|
+
// here to commit/dismiss while already open, so that synthesized
|
|
184
|
+
// click doesn't also re-toggle it shut.
|
|
185
|
+
if (this.open) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
if (this.activeIndex >= 0 && navigable[this.activeIndex]) {
|
|
188
|
+
this.selectOption(navigable[this.activeIndex]);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.hide();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case 'Escape':
|
|
196
|
+
if (this.open) {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
this.hide();
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'Home':
|
|
202
|
+
if (this.open) {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
this.activeIndex = 0;
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
case 'End':
|
|
208
|
+
if (this.open) {
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
this.activeIndex = navigable.length - 1;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
if (e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey) {
|
|
215
|
+
this.typeAhead(e.key);
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
// Delegated onto [part="listbox"] (see render()) rather than one closure
|
|
221
|
+
// pair allocated per option per render -- resolves the target row via
|
|
222
|
+
// closest('[part="option"]') + a data-value lookup, mirroring lyra-combobox.
|
|
223
|
+
this.onListboxMouseDown = (e) => {
|
|
224
|
+
if (e.target.closest('[part="option"]'))
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
};
|
|
227
|
+
this.onListboxClick = (e) => {
|
|
228
|
+
const optionEl = e.target.closest('[part="option"]');
|
|
229
|
+
const value = optionEl?.dataset.value;
|
|
230
|
+
if (value === undefined)
|
|
231
|
+
return;
|
|
232
|
+
const option = this.options.find((o) => o.value === value);
|
|
233
|
+
if (option)
|
|
234
|
+
this.selectOption(option);
|
|
235
|
+
};
|
|
236
|
+
this.internals = this.attachInternals();
|
|
237
|
+
}
|
|
238
|
+
connectedCallback() {
|
|
239
|
+
super.connectedCallback();
|
|
240
|
+
this.updateValidity();
|
|
241
|
+
}
|
|
242
|
+
willUpdate() {
|
|
243
|
+
// `hasUpdated` flips to `true` before `updated()` even sees its first
|
|
244
|
+
// call, so it can't distinguish "just mounted" from "just changed" there
|
|
245
|
+
// -- capture that distinction here, while it's still reliable, for
|
|
246
|
+
// `updated()`'s `open`-handling below to consult.
|
|
247
|
+
this._isFirstUpdate = !this.hasUpdated;
|
|
248
|
+
if (!this.hasUpdated) {
|
|
249
|
+
this.hasHintSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'hint');
|
|
250
|
+
this.hasErrorSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'error');
|
|
251
|
+
this.hasLabelSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'label');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** The selected value: a single string (empty when nothing is selected). */
|
|
255
|
+
get value() {
|
|
256
|
+
return this._selected;
|
|
257
|
+
}
|
|
258
|
+
set value(next) {
|
|
259
|
+
const old = this._selected;
|
|
260
|
+
this._selected = next ?? '';
|
|
261
|
+
this.syncFormValue();
|
|
262
|
+
this.reflectSelected();
|
|
263
|
+
this.updateValidity();
|
|
264
|
+
this.requestUpdate('value', old);
|
|
265
|
+
}
|
|
266
|
+
updateValidity() {
|
|
267
|
+
if (this.required && !this._selected) {
|
|
268
|
+
this.internals.setValidity({ valueMissing: true }, 'Please select an option.');
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
this.internals.setValidity({});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
syncFormValue() {
|
|
275
|
+
this.internals.setFormValue(this._selected);
|
|
276
|
+
}
|
|
277
|
+
formResetCallback() {
|
|
278
|
+
this.value = this._defaultSelected;
|
|
279
|
+
}
|
|
280
|
+
formDisabledCallback(disabled) {
|
|
281
|
+
this.disabled = disabled;
|
|
282
|
+
}
|
|
283
|
+
checkValidity() {
|
|
284
|
+
return this.internals.checkValidity();
|
|
285
|
+
}
|
|
286
|
+
reportValidity() {
|
|
287
|
+
return this.internals.reportValidity();
|
|
288
|
+
}
|
|
289
|
+
disconnectedCallback() {
|
|
290
|
+
super.disconnectedCallback();
|
|
291
|
+
this.cleanup?.();
|
|
292
|
+
clearTimeout(this.typeAheadTimer);
|
|
293
|
+
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
294
|
+
}
|
|
295
|
+
reflectSelected() {
|
|
296
|
+
for (const o of this.options)
|
|
297
|
+
o.selected = o.value === this._selected;
|
|
298
|
+
}
|
|
299
|
+
show() {
|
|
300
|
+
if (this.open || this.disabled)
|
|
301
|
+
return;
|
|
302
|
+
this.open = true;
|
|
303
|
+
}
|
|
304
|
+
hide() {
|
|
305
|
+
if (!this.open)
|
|
306
|
+
return;
|
|
307
|
+
this.open = false;
|
|
308
|
+
this.activeIndex = -1;
|
|
309
|
+
}
|
|
310
|
+
updated(changed) {
|
|
311
|
+
if (changed.has('open')) {
|
|
312
|
+
this.cleanup?.();
|
|
313
|
+
this.cleanup = undefined;
|
|
314
|
+
// All `open`-driven side effects (positioning, the click-outside
|
|
315
|
+
// listener, and the lyra-show/lyra-hide events) live here rather than
|
|
316
|
+
// in show()/hide() so they fire however `open` became true -- via
|
|
317
|
+
// show()/hide()'s own user-interaction paths, or a consumer/test
|
|
318
|
+
// setting `el.open` directly, which bypasses both entirely.
|
|
319
|
+
if (this.open) {
|
|
320
|
+
document.addEventListener('pointerdown', this.onDocPointer);
|
|
321
|
+
// Don't announce a "show" transition for markup that's simply
|
|
322
|
+
// rendering open for the first time (e.g. `<lyra-select open>`) --
|
|
323
|
+
// only for an actual closed-to-open transition.
|
|
324
|
+
if (!this._isFirstUpdate)
|
|
325
|
+
this.emit('lyra-show');
|
|
326
|
+
const anchor = this.renderRoot.querySelector('[part="trigger"]');
|
|
327
|
+
const listbox = this.renderRoot.querySelector('[part="listbox"]');
|
|
328
|
+
if (anchor && listbox)
|
|
329
|
+
this.cleanup = place(anchor, listbox);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
333
|
+
if (!this._isFirstUpdate)
|
|
334
|
+
this.emit('lyra-hide');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (changed.has('required'))
|
|
338
|
+
this.updateValidity();
|
|
339
|
+
if (changed.has('touched') || changed.has('required') || changed.has('value')) {
|
|
340
|
+
this.toggleAttribute('data-invalid', this.touched && !this.internals.validity.valid);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
selectOption(option) {
|
|
344
|
+
if (option.disabled)
|
|
345
|
+
return;
|
|
346
|
+
this.value = option.value;
|
|
347
|
+
this.hide();
|
|
348
|
+
this.emit('input');
|
|
349
|
+
this.emit('change');
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Standard listbox type-ahead: moves to the next non-disabled option whose
|
|
353
|
+
* label starts with the accumulated buffer, cycling from just after the
|
|
354
|
+
* "current" option (the active row while open, the selected value while
|
|
355
|
+
* closed). While open this only moves `activeIndex` (a highlight, matching
|
|
356
|
+
* Arrow-key nav -- Enter/click still commits it); while closed there's no
|
|
357
|
+
* highlight to show, so it commits immediately, matching a native
|
|
358
|
+
* `<select>`'s closed-state type-ahead.
|
|
359
|
+
*/
|
|
360
|
+
typeAhead(char) {
|
|
361
|
+
clearTimeout(this.typeAheadTimer);
|
|
362
|
+
this.typeAheadBuffer += char.toLowerCase();
|
|
363
|
+
this.typeAheadTimer = setTimeout(() => {
|
|
364
|
+
this.typeAheadBuffer = '';
|
|
365
|
+
}, 500);
|
|
366
|
+
const navigable = this.options.filter((o) => !o.disabled);
|
|
367
|
+
if (!navigable.length)
|
|
368
|
+
return;
|
|
369
|
+
const currentValue = this.open ? navigable[this.activeIndex]?.value : this._selected;
|
|
370
|
+
const currentIndex = navigable.findIndex((o) => o.value === currentValue);
|
|
371
|
+
const n = navigable.length;
|
|
372
|
+
for (let step = 1; step <= n; step++) {
|
|
373
|
+
const idx = (currentIndex + step + n) % n;
|
|
374
|
+
const candidate = navigable[idx];
|
|
375
|
+
if (candidate.label.toLowerCase().startsWith(this.typeAheadBuffer)) {
|
|
376
|
+
if (this.open) {
|
|
377
|
+
this.activeIndex = idx;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
this.selectOption(candidate);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
renderRows(options, activeId) {
|
|
387
|
+
const out = [];
|
|
388
|
+
let currentGroup;
|
|
389
|
+
options.forEach((o, i) => {
|
|
390
|
+
if (o.group !== currentGroup) {
|
|
391
|
+
currentGroup = o.group;
|
|
392
|
+
if (currentGroup)
|
|
393
|
+
out.push(html `<div class="group-label">${currentGroup}</div>`);
|
|
394
|
+
}
|
|
395
|
+
const id = `${this.listId}-opt-${i}`;
|
|
396
|
+
const selected = o.value === this._selected;
|
|
397
|
+
out.push(html `<div
|
|
398
|
+
part="option"
|
|
399
|
+
id=${id}
|
|
400
|
+
role="option"
|
|
401
|
+
data-value=${o.value}
|
|
402
|
+
aria-selected=${selected ? 'true' : 'false'}
|
|
403
|
+
aria-disabled=${o.disabled ? 'true' : 'false'}
|
|
404
|
+
?data-active=${id === activeId}
|
|
405
|
+
>
|
|
406
|
+
${o.dotColor ? html `<span part="option-dot" style=${`background:${o.dotColor}`}></span>` : ''}
|
|
407
|
+
<span part="option-label">
|
|
408
|
+
<span>${o.label}</span>
|
|
409
|
+
${o.sub ? html `<span part="option-sub">${o.sub}</span>` : ''}
|
|
410
|
+
</span>
|
|
411
|
+
</div>`);
|
|
412
|
+
});
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
render() {
|
|
416
|
+
const options = this.options;
|
|
417
|
+
const navigable = options.filter((o) => !o.disabled);
|
|
418
|
+
const active = this.activeIndex >= 0 ? navigable[this.activeIndex] : undefined;
|
|
419
|
+
const activeId = active ? `${this.listId}-opt-${options.indexOf(active)}` : '';
|
|
420
|
+
const selectedLabel = this._selected
|
|
421
|
+
? (options.find((o) => o.value === this._selected)?.label ?? this._selected)
|
|
422
|
+
: '';
|
|
423
|
+
const hasValue = this._selected.length > 0;
|
|
424
|
+
const hasHint = this.hasHintSlot || this.hint.length > 0;
|
|
425
|
+
const hasError = this.hasErrorSlot || this.errorText.length > 0;
|
|
426
|
+
const hasLabel = this.hasLabelSlot || this.label.length > 0;
|
|
427
|
+
return html `
|
|
428
|
+
<div part="form-control">
|
|
429
|
+
<label part="form-control-label" for=${this.triggerId} ?hidden=${!hasLabel}>
|
|
430
|
+
${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
|
|
431
|
+
</label>
|
|
432
|
+
<button
|
|
433
|
+
id=${this.triggerId}
|
|
434
|
+
part="trigger"
|
|
435
|
+
type="button"
|
|
436
|
+
role="combobox"
|
|
437
|
+
aria-haspopup="listbox"
|
|
438
|
+
aria-expanded=${this.open ? 'true' : 'false'}
|
|
439
|
+
aria-controls=${this.listId}
|
|
440
|
+
aria-activedescendant=${activeId}
|
|
441
|
+
aria-label=${this.label || this.placeholder || 'Select'}
|
|
442
|
+
aria-required=${this.required ? 'true' : 'false'}
|
|
443
|
+
aria-invalid=${this.touched && !this.internals.validity.valid ? 'true' : 'false'}
|
|
444
|
+
?disabled=${this.disabled}
|
|
445
|
+
@click=${this.onTriggerClick}
|
|
446
|
+
@keydown=${this.onKeyDown}
|
|
447
|
+
@blur=${this.onTriggerBlur}
|
|
448
|
+
>
|
|
449
|
+
<span class="trigger-label" ?data-placeholder=${!hasValue}
|
|
450
|
+
>${hasValue ? selectedLabel : this.placeholder}</span
|
|
451
|
+
>
|
|
452
|
+
<span part="expand-icon" aria-hidden="true">${chevronIcon()}</span>
|
|
453
|
+
</button>
|
|
454
|
+
<div
|
|
455
|
+
part="listbox"
|
|
456
|
+
id=${this.listId}
|
|
457
|
+
role="listbox"
|
|
458
|
+
@mousedown=${this.onListboxMouseDown}
|
|
459
|
+
@click=${this.onListboxClick}
|
|
460
|
+
>
|
|
461
|
+
${this.renderRows(options, activeId)}
|
|
462
|
+
</div>
|
|
463
|
+
<div part="error" ?hidden=${!hasError}>
|
|
464
|
+
${this.errorText}<slot name="error" @slotchange=${this.onErrorSlotChange}></slot>
|
|
465
|
+
</div>
|
|
466
|
+
<div part="hint" ?hidden=${!hasHint}>
|
|
467
|
+
${this.hint}<slot name="hint" @slotchange=${this.onHintSlotChange}></slot>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
<slot @slotchange=${this.collectOptions} hidden></slot>
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
__decorate([
|
|
475
|
+
property()
|
|
476
|
+
], LyraSelect.prototype, "placeholder", void 0);
|
|
477
|
+
__decorate([
|
|
478
|
+
property({ type: Boolean, reflect: true })
|
|
479
|
+
], LyraSelect.prototype, "disabled", void 0);
|
|
480
|
+
__decorate([
|
|
481
|
+
property({ type: Boolean, reflect: true })
|
|
482
|
+
], LyraSelect.prototype, "required", void 0);
|
|
483
|
+
__decorate([
|
|
484
|
+
property()
|
|
485
|
+
], LyraSelect.prototype, "name", void 0);
|
|
486
|
+
__decorate([
|
|
487
|
+
property()
|
|
488
|
+
], LyraSelect.prototype, "label", void 0);
|
|
489
|
+
__decorate([
|
|
490
|
+
property()
|
|
491
|
+
], LyraSelect.prototype, "hint", void 0);
|
|
492
|
+
__decorate([
|
|
493
|
+
property({ attribute: 'error-text' })
|
|
494
|
+
], LyraSelect.prototype, "errorText", void 0);
|
|
495
|
+
__decorate([
|
|
496
|
+
property({ type: Boolean, reflect: true })
|
|
497
|
+
], LyraSelect.prototype, "open", void 0);
|
|
498
|
+
__decorate([
|
|
499
|
+
state()
|
|
500
|
+
], LyraSelect.prototype, "activeIndex", void 0);
|
|
501
|
+
__decorate([
|
|
502
|
+
state()
|
|
503
|
+
], LyraSelect.prototype, "options", void 0);
|
|
504
|
+
__decorate([
|
|
505
|
+
state()
|
|
506
|
+
], LyraSelect.prototype, "touched", void 0);
|
|
507
|
+
__decorate([
|
|
508
|
+
state()
|
|
509
|
+
], LyraSelect.prototype, "hasHintSlot", void 0);
|
|
510
|
+
__decorate([
|
|
511
|
+
state()
|
|
512
|
+
], LyraSelect.prototype, "hasErrorSlot", void 0);
|
|
513
|
+
__decorate([
|
|
514
|
+
state()
|
|
515
|
+
], LyraSelect.prototype, "hasLabelSlot", void 0);
|
|
516
|
+
defineElement('select', LyraSelect);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/web-components-vite';
|
|
2
|
+
declare const meta: Meta;
|
|
3
|
+
export default meta;
|
|
4
|
+
type Story = StoryObj;
|
|
5
|
+
export declare const Default: Story;
|
|
6
|
+
/** `<lyra-option selected>` seeds the initial value, mirroring `<option selected>`. */
|
|
7
|
+
export declare const PreSelectedValue: Story;
|
|
8
|
+
/**
|
|
9
|
+
* Rows come from `<lyra-option>` children plus `group` (section headers),
|
|
10
|
+
* `sub` (a secondary line), and `dot-color` (a leading status dot) — same
|
|
11
|
+
* rich-row support as `<lyra-combobox>`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RichRows: Story;
|
|
14
|
+
export declare const Disabled: Story;
|
|
15
|
+
/**
|
|
16
|
+
* `required` blocks form submission while empty; `error-text` renders a
|
|
17
|
+
* validation message alongside the shared `hint`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RequiredWithValidation: Story;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
const meta = {
|
|
3
|
+
title: 'Select',
|
|
4
|
+
component: 'lyra-select',
|
|
5
|
+
tags: ['autodocs'],
|
|
6
|
+
};
|
|
7
|
+
export default meta;
|
|
8
|
+
export const Default = {
|
|
9
|
+
render: () => html `
|
|
10
|
+
<lyra-select label="Fruit" placeholder="Pick one…" style="max-width: 20rem">
|
|
11
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
12
|
+
<lyra-option value="b">Banana</lyra-option>
|
|
13
|
+
<lyra-option value="c">Cherry</lyra-option>
|
|
14
|
+
<lyra-option value="d">Date</lyra-option>
|
|
15
|
+
</lyra-select>
|
|
16
|
+
`,
|
|
17
|
+
};
|
|
18
|
+
/** `<lyra-option selected>` seeds the initial value, mirroring `<option selected>`. */
|
|
19
|
+
export const PreSelectedValue = {
|
|
20
|
+
render: () => html `
|
|
21
|
+
<lyra-select label="Fruit" style="max-width: 20rem">
|
|
22
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
23
|
+
<lyra-option value="b" selected>Banana</lyra-option>
|
|
24
|
+
<lyra-option value="c">Cherry</lyra-option>
|
|
25
|
+
<lyra-option value="d">Date</lyra-option>
|
|
26
|
+
</lyra-select>
|
|
27
|
+
`,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Rows come from `<lyra-option>` children plus `group` (section headers),
|
|
31
|
+
* `sub` (a secondary line), and `dot-color` (a leading status dot) — same
|
|
32
|
+
* rich-row support as `<lyra-combobox>`.
|
|
33
|
+
*/
|
|
34
|
+
export const RichRows = {
|
|
35
|
+
render: () => html `
|
|
36
|
+
<lyra-select label="Inverter" placeholder="Pick one…" style="max-width: 22rem">
|
|
37
|
+
<lyra-option value="inv-1" group="Building A" sub="Running" dot-color="var(--lyra-color-success)">
|
|
38
|
+
Inverter 1
|
|
39
|
+
</lyra-option>
|
|
40
|
+
<lyra-option value="inv-2" group="Building A" sub="Idle" dot-color="var(--lyra-color-text-quiet)">
|
|
41
|
+
Inverter 2
|
|
42
|
+
</lyra-option>
|
|
43
|
+
<lyra-option value="inv-3" group="Building B" sub="Fault" dot-color="var(--lyra-color-danger)">
|
|
44
|
+
Inverter 3
|
|
45
|
+
</lyra-option>
|
|
46
|
+
</lyra-select>
|
|
47
|
+
`,
|
|
48
|
+
};
|
|
49
|
+
export const Disabled = {
|
|
50
|
+
render: () => html `
|
|
51
|
+
<lyra-select label="Disabled" disabled placeholder="Can't touch this" style="max-width: 20rem">
|
|
52
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
53
|
+
<lyra-option value="b">Banana</lyra-option>
|
|
54
|
+
</lyra-select>
|
|
55
|
+
`,
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* `required` blocks form submission while empty; `error-text` renders a
|
|
59
|
+
* validation message alongside the shared `hint`.
|
|
60
|
+
*/
|
|
61
|
+
export const RequiredWithValidation = {
|
|
62
|
+
render: () => html `
|
|
63
|
+
<form style="display: flex; flex-direction: column; gap: 1.5rem; max-width: 22rem">
|
|
64
|
+
<lyra-select label="Required" required hint="Pick your favorite">
|
|
65
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
66
|
+
<lyra-option value="b">Banana</lyra-option>
|
|
67
|
+
</lyra-select>
|
|
68
|
+
<lyra-select label="Invalid" required error-text="Selection required">
|
|
69
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
70
|
+
<lyra-option value="b">Banana</lyra-option>
|
|
71
|
+
</lyra-select>
|
|
72
|
+
<button type="submit">Submit</button>
|
|
73
|
+
</form>
|
|
74
|
+
`,
|
|
75
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|