@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.
- package/dist/components/calendar/calendar.d.ts +36 -0
- package/dist/components/calendar/calendar.d.ts.map +1 -0
- package/dist/components/calendar/calendar.js +439 -0
- package/dist/components/calendar/calendar.js.map +1 -0
- package/dist/components/cron-builder/cron-builder.d.ts +34 -0
- package/dist/components/cron-builder/cron-builder.d.ts.map +1 -0
- package/dist/components/cron-builder/cron-builder.js +456 -0
- package/dist/components/cron-builder/cron-builder.js.map +1 -0
- package/dist/components/form/form.js +2 -2
- package/dist/components/icon/icon-paths.d.ts +161 -86
- package/dist/components/icon/icon-paths.d.ts.map +1 -1
- package/dist/components/icon/icon-paths.js +175 -106
- package/dist/components/icon/icon-paths.js.map +1 -1
- package/dist/components/icon/icon.d.ts +1 -1
- package/dist/components/icon/icon.d.ts.map +1 -1
- package/dist/components/icon/icon.js +5 -5
- package/dist/components/icon/icon.js.map +1 -1
- package/dist/components/input/input.d.ts +1 -0
- package/dist/components/input/input.d.ts.map +1 -1
- package/dist/components/input/input.js +35 -12
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/pin-input/pin-input.d.ts +33 -0
- package/dist/components/pin-input/pin-input.d.ts.map +1 -0
- package/dist/components/pin-input/pin-input.js +253 -0
- package/dist/components/pin-input/pin-input.js.map +1 -0
- package/dist/components/popover/popover.d.ts +26 -0
- package/dist/components/popover/popover.d.ts.map +1 -0
- package/dist/components/popover/popover.js +196 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +4 -1
- package/dist/components/select/select.d.ts.map +1 -1
- package/dist/components/select/select.js +26 -4
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/time-picker/time-picker.d.ts +38 -0
- package/dist/components/time-picker/time-picker.d.ts.map +1 -0
- package/dist/components/time-picker/time-picker.js +280 -0
- package/dist/components/time-picker/time-picker.js.map +1 -0
- package/dist/components/toggle-group/toggle-button.d.ts +23 -0
- package/dist/components/toggle-group/toggle-button.d.ts.map +1 -0
- package/dist/components/toggle-group/toggle-button.js +115 -0
- package/dist/components/toggle-group/toggle-button.js.map +1 -0
- package/dist/components/toggle-group/toggle-group.d.ts +32 -0
- package/dist/components/toggle-group/toggle-group.d.ts.map +1 -0
- package/dist/components/toggle-group/toggle-group.js +150 -0
- package/dist/components/toggle-group/toggle-group.js.map +1 -0
- package/dist/components/tooltip/tooltip.d.ts +33 -0
- package/dist/components/tooltip/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip/tooltip.js +293 -0
- package/dist/components/tooltip/tooltip.js.map +1 -0
- package/dist/styles/index.css +1 -0
- package/package.json +29 -1
- package/src/components/calendar/calendar.ts +444 -0
- package/src/components/cron-builder/cron-builder.ts +452 -0
- package/src/components/form/form.ts +2 -2
- package/src/components/icon/icon-paths.ts +175 -106
- package/src/components/icon/icon.ts +6 -6
- package/src/components/input/input.ts +38 -14
- package/src/components/pin-input/pin-input.ts +249 -0
- package/src/components/popover/popover.ts +192 -0
- package/src/components/select/select.ts +25 -63
- package/src/components/time-picker/time-picker.ts +272 -0
- package/src/components/toggle-group/toggle-button.ts +104 -0
- package/src/components/toggle-group/toggle-group.ts +148 -0
- package/src/components/tooltip/tooltip.ts +286 -0
- 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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
233
|
-
? html`<div id="
|
|
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
|
|
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
|
|
430
|
-
aria-controls
|
|
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
|
|
435
|
+
id=${this._listboxId}
|
|
474
436
|
class="listbox"
|
|
475
437
|
role="listbox"
|
|
476
438
|
aria-multiselectable="${this.multiple}"
|