@sondor-technology/sondor-components 1.0.6 → 1.0.8

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.
Files changed (65) hide show
  1. package/dist/components/calendar/calendar.d.ts +36 -0
  2. package/dist/components/calendar/calendar.d.ts.map +1 -0
  3. package/dist/components/calendar/calendar.js +439 -0
  4. package/dist/components/calendar/calendar.js.map +1 -0
  5. package/dist/components/cron-builder/cron-builder.d.ts +34 -0
  6. package/dist/components/cron-builder/cron-builder.d.ts.map +1 -0
  7. package/dist/components/cron-builder/cron-builder.js +456 -0
  8. package/dist/components/cron-builder/cron-builder.js.map +1 -0
  9. package/dist/components/form/form.js +2 -2
  10. package/dist/components/icon/icon-paths.d.ts +161 -86
  11. package/dist/components/icon/icon-paths.d.ts.map +1 -1
  12. package/dist/components/icon/icon-paths.js +175 -106
  13. package/dist/components/icon/icon-paths.js.map +1 -1
  14. package/dist/components/icon/icon.d.ts +1 -1
  15. package/dist/components/icon/icon.d.ts.map +1 -1
  16. package/dist/components/icon/icon.js +5 -5
  17. package/dist/components/icon/icon.js.map +1 -1
  18. package/dist/components/input/input.d.ts +1 -0
  19. package/dist/components/input/input.d.ts.map +1 -1
  20. package/dist/components/input/input.js +35 -12
  21. package/dist/components/input/input.js.map +1 -1
  22. package/dist/components/pin-input/pin-input.d.ts +33 -0
  23. package/dist/components/pin-input/pin-input.d.ts.map +1 -0
  24. package/dist/components/pin-input/pin-input.js +253 -0
  25. package/dist/components/pin-input/pin-input.js.map +1 -0
  26. package/dist/components/popover/popover.d.ts +26 -0
  27. package/dist/components/popover/popover.d.ts.map +1 -0
  28. package/dist/components/popover/popover.js +196 -0
  29. package/dist/components/popover/popover.js.map +1 -0
  30. package/dist/components/select/select.d.ts +4 -1
  31. package/dist/components/select/select.d.ts.map +1 -1
  32. package/dist/components/select/select.js +26 -4
  33. package/dist/components/select/select.js.map +1 -1
  34. package/dist/components/time-picker/time-picker.d.ts +38 -0
  35. package/dist/components/time-picker/time-picker.d.ts.map +1 -0
  36. package/dist/components/time-picker/time-picker.js +280 -0
  37. package/dist/components/time-picker/time-picker.js.map +1 -0
  38. package/dist/components/toggle-group/toggle-button.d.ts +23 -0
  39. package/dist/components/toggle-group/toggle-button.d.ts.map +1 -0
  40. package/dist/components/toggle-group/toggle-button.js +115 -0
  41. package/dist/components/toggle-group/toggle-button.js.map +1 -0
  42. package/dist/components/toggle-group/toggle-group.d.ts +32 -0
  43. package/dist/components/toggle-group/toggle-group.d.ts.map +1 -0
  44. package/dist/components/toggle-group/toggle-group.js +150 -0
  45. package/dist/components/toggle-group/toggle-group.js.map +1 -0
  46. package/dist/components/tooltip/tooltip.d.ts +33 -0
  47. package/dist/components/tooltip/tooltip.d.ts.map +1 -0
  48. package/dist/components/tooltip/tooltip.js +293 -0
  49. package/dist/components/tooltip/tooltip.js.map +1 -0
  50. package/dist/styles/index.css +1 -0
  51. package/package.json +29 -1
  52. package/src/components/calendar/calendar.ts +444 -0
  53. package/src/components/cron-builder/cron-builder.ts +452 -0
  54. package/src/components/form/form.ts +2 -2
  55. package/src/components/icon/icon-paths.ts +175 -106
  56. package/src/components/icon/icon.ts +6 -6
  57. package/src/components/input/input.ts +38 -14
  58. package/src/components/pin-input/pin-input.ts +249 -0
  59. package/src/components/popover/popover.ts +192 -0
  60. package/src/components/select/select.ts +25 -63
  61. package/src/components/time-picker/time-picker.ts +272 -0
  62. package/src/components/toggle-group/toggle-button.ts +104 -0
  63. package/src/components/toggle-group/toggle-group.ts +148 -0
  64. package/src/components/tooltip/tooltip.ts +286 -0
  65. package/src/styles/theme.css +2 -0
@@ -18,6 +18,7 @@ export class StInput extends LitElement {
18
18
  @property({type: String}) placeholder = "";
19
19
  @property({type: String}) type: InputType = "text";
20
20
  @property({attribute: "help-text"}) helpText = "";
21
+ @property({type: String}) error = "";
21
22
  @property({ type: String }) autocomplete?: string;
22
23
 
23
24
  @property({type: Boolean, reflect: true}) required = false;
@@ -34,7 +35,7 @@ export class StInput extends LitElement {
34
35
  this._internals = this.attachInternals();
35
36
 
36
37
  this.addEventListener("focusout", () => {
37
- this.invalid = !this._inputEl.validity.valid;
38
+ this.invalid = !!this.error || !this._inputEl.validity.valid;
38
39
  });
39
40
 
40
41
  this.addEventListener("invalid", () => {
@@ -115,16 +116,24 @@ export class StInput extends LitElement {
115
116
  margin: 0;
116
117
  }
117
118
 
119
+ /* 🚀 NEW: Explicit error text styling */
120
+ .error-text {
121
+ font-size: var(--st-font-size-xs);
122
+ color: var(--st-color-error, #dc2626);
123
+ margin: 0;
124
+ font-weight: 500;
125
+ }
126
+
118
127
  :host([invalid]) .input-wrapper {
119
- border-color: var(--st-color-error);
128
+ border-color: var(--st-color-error, #dc2626);
120
129
  }
121
130
 
122
131
  :host([invalid]) .input-wrapper:focus-within {
123
- box-shadow: 0 0 0 1px var(--st-color-error);
132
+ box-shadow: 0 0 0 1px var(--st-color-error, #dc2626);
124
133
  }
125
134
 
126
135
  :host([invalid]) .help-text {
127
- color: var(--st-color-error);
136
+ color: var(--st-color-error, #dc2626);
128
137
  }
129
138
 
130
139
  :host([disabled]) .input-wrapper {
@@ -149,16 +158,30 @@ export class StInput extends LitElement {
149
158
  if (changedProperties.has("value")) {
150
159
  this._internals.setFormValue(this.value);
151
160
  }
152
- this._syncValidity();
161
+
162
+ if (changedProperties.has("value") || changedProperties.has("error") || changedProperties.has("required")) {
163
+ this._syncValidity();
164
+ }
153
165
  }
154
166
 
155
167
  private _syncValidity() {
156
168
  if (!this._inputEl) return;
169
+
170
+ if (this.error) {
171
+ this._internals.setValidity({ customError: true }, this.error, this._inputEl);
172
+ this.invalid = true;
173
+ return;
174
+ }
175
+
157
176
  this._internals.setValidity(
158
177
  this._inputEl.validity,
159
178
  this._inputEl.validationMessage,
160
179
  this._inputEl,
161
180
  );
181
+
182
+ if (this.invalid && this._inputEl.validity.valid) {
183
+ this.invalid = false;
184
+ }
162
185
  }
163
186
 
164
187
  formResetCallback() {
@@ -179,7 +202,7 @@ export class StInput extends LitElement {
179
202
  const target = e.target as HTMLInputElement;
180
203
  this.value = target.value;
181
204
 
182
- if (this.invalid && target.validity.valid) {
205
+ if (this.invalid && target.validity.valid && !this.error) {
183
206
  this.invalid = false;
184
207
  }
185
208
 
@@ -190,10 +213,9 @@ export class StInput extends LitElement {
190
213
  this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
191
214
  }
192
215
 
193
- render() {
194
- const helpTextId = this.helpText ? "help-text" : undefined;
216
+ render() {
217
+ const descriptionId = this.error ? "error-text" : (this.helpText ? "help-text" : undefined);
195
218
 
196
- // Security: Prevent OS/Extensions from logging sensitive data via spellcheck
197
219
  const isSensitive = this.type === "password" || this.type === "email";
198
220
  const safeSpellcheck = isSensitive ? "false" : undefined;
199
221
  const safeAutocapitalize = isSensitive ? "none" : undefined;
@@ -221,7 +243,7 @@ render() {
221
243
  spellcheck=${ifDefined(safeSpellcheck)}
222
244
  autocapitalize=${ifDefined(safeAutocapitalize)}
223
245
  aria-invalid=${this.invalid ? "true" : "false"}
224
- aria-describedby=${ifDefined(helpTextId)}
246
+ aria-describedby=${ifDefined(descriptionId)}
225
247
  @input=${this._handleInput}
226
248
  @change=${this._handleChange}
227
249
  />
@@ -229,9 +251,11 @@ render() {
229
251
  <slot name="suffix"></slot>
230
252
  </div>
231
253
 
232
- ${this.helpText
233
- ? html`<div id="help-text" class="help-text">${this.helpText}</div>`
234
- : ""}
254
+ ${this.error
255
+ ? html`<div id="error-text" class="error-text">${this.error}</div>`
256
+ : this.helpText
257
+ ? html`<div id="help-text" class="help-text">${this.helpText}</div>`
258
+ : ""}
235
259
  </div>
236
260
  `;
237
261
  }
@@ -241,4 +265,4 @@ declare global {
241
265
  interface HTMLElementTagNameMap {
242
266
  "st-input": StInput;
243
267
  }
244
- }
268
+ }
@@ -0,0 +1,249 @@
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property, queryAll, state} from "lit/decorators.js";
3
+ import {classMap} from "lit/directives/class-map.js";
4
+ import {resetStyles} from "../../styles/reset.js";
5
+
6
+ @customElement("st-pin-input")
7
+ export class StPinInput extends LitElement {
8
+ static formAssociated = true;
9
+ private _internals: ElementInternals;
10
+
11
+ @property({type: String, reflect: true}) name = "";
12
+ @property({type: String}) label = "";
13
+ @property({type: Number}) length = 6;
14
+ @property({type: String}) value = "";
15
+
16
+ // Obscures the input for secure PINs
17
+ @property({type: Boolean}) mask = false;
18
+
19
+ @property({type: Boolean, reflect: true}) disabled = false;
20
+ @property({type: Boolean, reflect: true}) required = false;
21
+ @property({type: Boolean, reflect: true}) invalid = false;
22
+ @property({type: String}) error = "";
23
+
24
+ @queryAll(".pin-box") private _inputs!: NodeListOf<HTMLInputElement>;
25
+
26
+ @state() private _touched = false;
27
+
28
+ constructor() {
29
+ super();
30
+ this._internals = this.attachInternals();
31
+ }
32
+
33
+ static styles = [
34
+ resetStyles,
35
+ css`
36
+ :host {
37
+ display: block;
38
+ font-family: inherit;
39
+ }
40
+
41
+ label {
42
+ display: block;
43
+ font-size: var(--st-font-size-sm);
44
+ font-weight: 500;
45
+ color: var(--st-color-text-muted);
46
+ margin-bottom: var(--st-spacing-xs);
47
+ }
48
+
49
+ .pin-container {
50
+ display: flex;
51
+ gap: var(--st-spacing-sm);
52
+ }
53
+
54
+ .pin-box {
55
+ width: 48px;
56
+ height: 56px;
57
+ text-align: center;
58
+ font-size: var(--st-font-size-xl);
59
+ font-weight: var(--st-font-weight-semibold);
60
+ color: var(--st-color-text);
61
+ background: var(--st-color-surface);
62
+ border: 1px solid var(--st-color-border);
63
+ border-radius: var(--st-radius-md);
64
+ box-sizing: border-box;
65
+ transition:
66
+ border-color 0.2s,
67
+ box-shadow 0.2s,
68
+ background-color 0.2s;
69
+ /* iOS optimization for numeric pads */
70
+ caret-color: var(--st-color-primary);
71
+ }
72
+
73
+ .pin-box:focus {
74
+ outline: none;
75
+ border-color: var(--st-color-primary);
76
+ box-shadow: 0 0 0 2px var(--st-color-primary-muted, rgba(59, 130, 246, 0.25));
77
+ }
78
+
79
+ .pin-box:disabled {
80
+ background: var(--st-color-surface-muted, #f1f5f9);
81
+ color: var(--st-color-text-muted);
82
+ cursor: not-allowed;
83
+ }
84
+
85
+ .is-invalid .pin-box {
86
+ border-color: var(--st-color-danger, #dc2626);
87
+ }
88
+
89
+ .error-msg {
90
+ display: block;
91
+ margin-top: var(--st-spacing-xs);
92
+ font-size: var(--st-font-size-xs);
93
+ color: var(--st-color-danger, #dc2626);
94
+ }
95
+ `,
96
+ ];
97
+
98
+ override updated(changedProperties: PropertyValues) {
99
+ super.updated(changedProperties);
100
+ if (
101
+ changedProperties.has("value") ||
102
+ changedProperties.has("required") ||
103
+ changedProperties.has("error")
104
+ ) {
105
+ this._internals.setFormValue(this.value || null);
106
+ this._syncValidity();
107
+ }
108
+ }
109
+
110
+ private _syncValidity() {
111
+ this.invalid = false;
112
+
113
+ if (this.error) {
114
+ this._internals.setValidity({customError: true}, this.error);
115
+ this.invalid = true;
116
+ return;
117
+ }
118
+
119
+ if (this.required && (this.value.length < this.length || this.value.includes(" "))) {
120
+ this._internals.setValidity({valueMissing: true}, `Please enter all ${this.length} digits.`);
121
+ if (this._touched) this.invalid = true;
122
+ return;
123
+ }
124
+
125
+ this._internals.setValidity({});
126
+ }
127
+
128
+ public checkValidity() {
129
+ return this._internals.checkValidity();
130
+ }
131
+ public reportValidity() {
132
+ this._touched = true;
133
+ this._syncValidity();
134
+ return this._internals.reportValidity();
135
+ }
136
+
137
+ private _updateValue() {
138
+ const chars = Array.from(this._inputs).map((input) => input.value || " ");
139
+ this.value = chars.join("");
140
+
141
+ this.dispatchEvent(new Event("input", {bubbles: true, composed: true}));
142
+
143
+ // Only fire 'change' if the pin is completely filled
144
+ if (!this.value.includes(" ")) {
145
+ this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
146
+ }
147
+ }
148
+
149
+ // Handle typing and auto-advancing
150
+ private _handleInput(e: InputEvent, index: number) {
151
+ this._touched = true;
152
+ const input = e.target as HTMLInputElement;
153
+
154
+ // If the user typed a character, move to the next input
155
+ if (input.value && index < this.length - 1) {
156
+ this._inputs[index + 1].focus();
157
+ }
158
+
159
+ this._updateValue();
160
+ }
161
+
162
+ // Handle backspace rewinding and arrow keys
163
+ private _handleKeyDown(e: KeyboardEvent, index: number) {
164
+ const input = e.target as HTMLInputElement;
165
+
166
+ if (e.key === "Backspace") {
167
+ // If current box is empty, jump to previous box and clear it
168
+ if (!input.value && index > 0) {
169
+ e.preventDefault();
170
+ this._inputs[index - 1].value = "";
171
+ this._inputs[index - 1].focus();
172
+ this._updateValue();
173
+ }
174
+ } else if (e.key === "ArrowLeft" && index > 0) {
175
+ e.preventDefault();
176
+ this._inputs[index - 1].focus();
177
+ } else if (e.key === "ArrowRight" && index < this.length - 1) {
178
+ e.preventDefault();
179
+ this._inputs[index + 1].focus();
180
+ }
181
+ }
182
+
183
+ // Handle cross-field pasting (e.g. pasting "123456" into the first box)
184
+ private _handlePaste(e: ClipboardEvent, startIndex: number) {
185
+ e.preventDefault();
186
+ this._touched = true;
187
+
188
+ const pasteData = e.clipboardData?.getData("text") || "";
189
+ // Clean the string (optional: restrict to numbers if you want strict numeric PINs)
190
+ const chars = pasteData.replace(/\s/g, "").split("");
191
+
192
+ if (!chars.length) return;
193
+
194
+ let currentIndex = startIndex;
195
+ for (const char of chars) {
196
+ if (currentIndex < this.length) {
197
+ this._inputs[currentIndex].value = char;
198
+ currentIndex++;
199
+ }
200
+ }
201
+
202
+ // Focus the last filled input (or the very last input if we filled them all)
203
+ const focusIndex = Math.min(currentIndex, this.length - 1);
204
+ this._inputs[focusIndex].focus();
205
+
206
+ this._updateValue();
207
+ }
208
+
209
+ render() {
210
+ // Break the current value into an array of characters for rendering
211
+ const values = this.value.padEnd(this.length, " ").split("");
212
+
213
+ const isInvalid = this.invalid || !!this.error;
214
+
215
+ return html`
216
+ ${this.label ? html`<label>${this.label}</label>` : ""}
217
+
218
+ <div class=${classMap({"pin-container": true, "is-invalid": isInvalid})}>
219
+ ${values
220
+ .slice(0, this.length)
221
+ .map(
222
+ (char, index) => html`
223
+ <input
224
+ class="pin-box"
225
+ aria-label="Digit ${index + 1}"
226
+ type=${this.mask ? "password" : "text"}
227
+ inputmode="numeric"
228
+ autocomplete="${"one-time-code" as any}"
229
+ maxlength="1"
230
+ .value=${char.trim()}
231
+ ?disabled=${this.disabled}
232
+ @input=${(e: InputEvent) => this._handleInput(e, index)}
233
+ @keydown=${(e: KeyboardEvent) => this._handleKeyDown(e, index)}
234
+ @paste=${(e: ClipboardEvent) => this._handlePaste(e, index)}
235
+ />
236
+ `,
237
+ )}
238
+ </div>
239
+
240
+ ${isInvalid && this.error ? html`<span class="error-msg">${this.error}</span>` : ""}
241
+ `;
242
+ }
243
+ }
244
+
245
+ declare global {
246
+ interface HTMLElementTagNameMap {
247
+ "st-pin-input": StPinInput;
248
+ }
249
+ }
@@ -0,0 +1,192 @@
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property, query, state, queryAssignedElements} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset.js";
4
+
5
+ export type PopoverPosition = "top" | "bottom" | "left" | "right";
6
+
7
+ @customElement("st-popover")
8
+ export class StPopover extends LitElement {
9
+ @property({type: String}) position: PopoverPosition = "bottom";
10
+ @property({type: Number}) offset = 8;
11
+ @property({type: Boolean, reflect: true}) disabled = false;
12
+
13
+ @state() private _isOpen = false;
14
+
15
+ @query(".trigger-wrapper") private _triggerWrapper!: HTMLElement;
16
+ @query("#popover-content") private _popoverEl!: HTMLElement;
17
+
18
+ @queryAssignedElements({slot: "trigger"}) private _triggerElements!: HTMLElement[];
19
+
20
+ static styles = [
21
+ resetStyles,
22
+ css`
23
+ :host {
24
+ display: inline-block;
25
+ }
26
+
27
+ .trigger-wrapper {
28
+ display: inline-block;
29
+ cursor: pointer;
30
+ }
31
+
32
+ /* Native popover styles */
33
+ [popover] {
34
+ position: fixed;
35
+ margin: 0;
36
+ padding: 0;
37
+ border: none;
38
+ background: transparent;
39
+ overflow: visible;
40
+ /* 🚀 FIX: Erase the browser's native centering inset so our JS coordinates work */
41
+ inset: auto;
42
+ }
43
+
44
+ .popover-surface {
45
+ background: var(--st-color-surface);
46
+ border: 1px solid var(--st-color-border);
47
+ border-radius: var(--st-radius-md);
48
+ box-shadow:
49
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
50
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
51
+ padding: var(--st-spacing-md);
52
+ color: var(--st-color-text);
53
+
54
+ opacity: 0;
55
+ transform: scale(0.95);
56
+ transition:
57
+ opacity 0.15s ease,
58
+ transform 0.15s ease;
59
+ }
60
+
61
+ [popover]:popover-open .popover-surface {
62
+ opacity: 1;
63
+ transform: scale(1);
64
+ }
65
+
66
+ [popover]::backdrop {
67
+ background: transparent;
68
+ }
69
+ `,
70
+ ];
71
+
72
+ override updated(changedProperties: PropertyValues) {
73
+ super.updated(changedProperties);
74
+ if (changedProperties.has("_isOpen")) {
75
+ this._triggerElements?.forEach((el) => {
76
+ el.setAttribute("aria-expanded", String(this._isOpen));
77
+ });
78
+ }
79
+ }
80
+
81
+ private _handleTriggerSlotChange() {
82
+ this._triggerElements?.forEach((el) => {
83
+ el.setAttribute("aria-haspopup", "dialog");
84
+ el.setAttribute("aria-expanded", String(this._isOpen));
85
+ });
86
+ }
87
+
88
+ private _togglePopover() {
89
+ if (this.disabled) return;
90
+ this._isOpen ? this.hide() : this.show();
91
+ }
92
+
93
+ public show() {
94
+ if (this.disabled || this._isOpen) return;
95
+
96
+ // 🚀 FIX: Update state synchronously so the _reposition check passes instantly
97
+ this._isOpen = true;
98
+
99
+ this._popoverEl.showPopover();
100
+ this._reposition();
101
+
102
+ window.addEventListener("resize", this._reposition);
103
+ window.addEventListener("scroll", this._reposition, true);
104
+ }
105
+
106
+ public hide() {
107
+ if (!this._isOpen) return;
108
+
109
+ // 🚀 FIX: Update state synchronously
110
+ this._isOpen = false;
111
+
112
+ this._popoverEl.hidePopover();
113
+ }
114
+
115
+ private _handleNativeToggle(e: Event) {
116
+ const isOpening = (e as any).newState === "open";
117
+ this._isOpen = isOpening;
118
+
119
+ // This handles the cleanup when the user clicks outside or hits ESC
120
+ if (!isOpening) {
121
+ window.removeEventListener("resize", this._reposition);
122
+ window.removeEventListener("scroll", this._reposition, true);
123
+ }
124
+ }
125
+
126
+ private _reposition = () => {
127
+ if (!this._isOpen || !this._popoverEl || !this._triggerWrapper) return;
128
+
129
+ const triggerRect = this._triggerWrapper.getBoundingClientRect();
130
+ const popoverRect = this._popoverEl.getBoundingClientRect();
131
+
132
+ let top = 0;
133
+ let left = 0;
134
+
135
+ switch (this.position) {
136
+ case "bottom":
137
+ top = triggerRect.bottom + this.offset;
138
+ left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
139
+ break;
140
+ case "top":
141
+ top = triggerRect.top - popoverRect.height - this.offset;
142
+ left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
143
+ break;
144
+ case "right":
145
+ top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
146
+ left = triggerRect.right + this.offset;
147
+ break;
148
+ case "left":
149
+ top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
150
+ left = triggerRect.left - popoverRect.width - this.offset;
151
+ break;
152
+ }
153
+
154
+ const padding = 8;
155
+ if (left < padding) left = padding;
156
+ if (left + popoverRect.width > window.innerWidth - padding) {
157
+ left = window.innerWidth - popoverRect.width - padding;
158
+ }
159
+ if (top < padding) top = padding;
160
+ if (top + popoverRect.height > window.innerHeight - padding) {
161
+ top = window.innerHeight - popoverRect.height - padding;
162
+ }
163
+
164
+ this._popoverEl.style.top = `${top}px`;
165
+ this._popoverEl.style.left = `${left}px`;
166
+ };
167
+
168
+ render() {
169
+ return html`
170
+ <div
171
+ class="trigger-wrapper"
172
+ @click=${this._togglePopover}
173
+ @keydown=${(e: KeyboardEvent) =>
174
+ e.key === "Enter" || e.key === " " ? this._togglePopover() : null}
175
+ >
176
+ <slot name="trigger" @slotchange=${this._handleTriggerSlotChange}></slot>
177
+ </div>
178
+
179
+ <div id="popover-content" popover="auto" @toggle=${this._handleNativeToggle}>
180
+ <div class="popover-surface">
181
+ <slot name="content"></slot>
182
+ </div>
183
+ </div>
184
+ `;
185
+ }
186
+ }
187
+
188
+ declare global {
189
+ interface HTMLElementTagNameMap {
190
+ "st-popover": StPopover;
191
+ }
192
+ }
@@ -1,71 +1,14 @@
1
1
  import {LitElement, html, css, PropertyValues} from "lit";
2
2
  import {customElement, property, state, query} from "lit/decorators.js";
3
3
  import {classMap} from "lit/directives/class-map.js";
4
+ import {ifDefined} from "lit/directives/if-defined.js";
4
5
 
5
6
  export interface SelectOption {
6
7
  label: string;
7
8
  value: string;
8
9
  }
9
10
 
10
- export type SelectAutocomplete =
11
- | "on"
12
- | "off"
13
- | "name"
14
- | "honorific-prefix"
15
- | "given-name"
16
- | "additional-name"
17
- | "family-name"
18
- | "honorific-suffix"
19
- | "nickname"
20
- | "email"
21
- | "username"
22
- | "new-password"
23
- | "current-password"
24
- | "one-time-code"
25
- | "organization"
26
- | "organization-title"
27
- | "url"
28
- | "photo"
29
- | "webauthn"
30
- | "street-address"
31
- | "address-line1"
32
- | "address-line2"
33
- | "address-line3"
34
- | "address-level1"
35
- | "address-level2"
36
- | "address-level3"
37
- | "address-level4"
38
- | "country"
39
- | "country-name"
40
- | "postal-code"
41
- | "cc-name"
42
- | "cc-given-name"
43
- | "cc-additional-name"
44
- | "cc-family-name"
45
- | "cc-number"
46
- | "cc-exp"
47
- | "cc-exp-month"
48
- | "cc-exp-year"
49
- | "cc-csc"
50
- | "cc-type"
51
- | "transaction-currency"
52
- | "transaction-amount"
53
- | "language"
54
- | "bday"
55
- | "bday-day"
56
- | "bday-month"
57
- | "bday-year"
58
- | "sex"
59
- | "tel"
60
- | "tel-country-code"
61
- | "tel-national"
62
- | "tel-area-code"
63
- | "tel-local"
64
- | "tel-local-prefix"
65
- | "tel-local-suffix"
66
- | "tel-extension"
67
- | "impp"
68
- | (string & {});
11
+ export type SelectAutocomplete = "on" | "off" | "name" | "email" | (string & {});
69
12
 
70
13
  @customElement("st-select")
71
14
  export class StSelect extends LitElement {
@@ -74,6 +17,7 @@ export class StSelect extends LitElement {
74
17
 
75
18
  @property({type: String}) name = "";
76
19
  @property({type: String}) label = "";
20
+ @property({type: Boolean, attribute: "hide-label"}) hideLabel = false;
77
21
  @property({type: String}) value = "";
78
22
  @property({type: Boolean}) required = false;
79
23
  @property({type: Boolean}) multiple = false;
@@ -93,6 +37,9 @@ export class StSelect extends LitElement {
93
37
  @query(".combobox") private buttonElement!: HTMLElement;
94
38
  @query(".search-input") private searchInputEl?: HTMLInputElement;
95
39
 
40
+ private _labelId = `select-label-${Math.random().toString(36).substring(2, 9)}`;
41
+ private _listboxId = `select-listbox-${Math.random().toString(36).substring(2, 9)}`;
42
+
96
43
  static styles = css`
97
44
  :host {
98
45
  display: block;
@@ -108,6 +55,17 @@ export class StSelect extends LitElement {
108
55
  margin-bottom: var(--st-spacing-xs);
109
56
  }
110
57
 
58
+ .sr-only {
59
+ position: absolute;
60
+ width: 1px;
61
+ height: 1px;
62
+ padding: 0;
63
+ margin: -1px;
64
+ overflow: hidden;
65
+ clip: rect(0, 0, 0, 0);
66
+ border: 0;
67
+ }
68
+
111
69
  .combobox {
112
70
  display: flex;
113
71
  justify-content: space-between;
@@ -413,7 +371,11 @@ export class StSelect extends LitElement {
413
371
  : this.options;
414
372
 
415
373
  return html`
416
- ${this.label ? html`<label id="select-label">${this.label}</label>` : ""}
374
+ ${this.label
375
+ ? html`<label id=${this._labelId} class=${classMap({"sr-only": this.hideLabel})}
376
+ >${this.label}</label
377
+ >`
378
+ : ""}
417
379
 
418
380
  <button
419
381
  type="button"
@@ -426,8 +388,8 @@ export class StSelect extends LitElement {
426
388
  role="combobox"
427
389
  aria-haspopup="listbox"
428
390
  aria-expanded="${this.open}"
429
- aria-labelledby="select-label"
430
- aria-controls="select-listbox"
391
+ aria-labelledby=${ifDefined(this.label ? this._labelId : undefined)}
392
+ aria-controls=${this._listboxId}
431
393
  @click=${this.toggleOpen}
432
394
  @keydown=${this.handleButtonKeydown}
433
395
  >
@@ -470,7 +432,7 @@ export class StSelect extends LitElement {
470
432
  : ""}
471
433
 
472
434
  <ul
473
- id="select-listbox"
435
+ id=${this._listboxId}
474
436
  class="listbox"
475
437
  role="listbox"
476
438
  aria-multiselectable="${this.multiple}"