@sondor-technology/sondor-components 1.0.3 → 1.0.5
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/button-group/button-group.d.ts +5 -0
- package/dist/components/button-group/button-group.d.ts.map +1 -1
- package/dist/components/checkbox/checkbox.d.ts +3 -0
- package/dist/components/checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/checkbox/checkbox.js +38 -20
- package/dist/components/code-block/code-block.d.ts +5 -0
- package/dist/components/code-block/code-block.d.ts.map +1 -1
- package/dist/components/form/form-step.d.ts +1 -0
- package/dist/components/form/form-step.d.ts.map +1 -1
- package/dist/components/form/form-step.js +3 -0
- package/dist/components/form/form.d.ts +7 -2
- package/dist/components/form/form.d.ts.map +1 -1
- package/dist/components/form/form.js +157 -59
- package/dist/components/icon/icon.d.ts +2 -1
- package/dist/components/icon/icon.d.ts.map +1 -1
- package/dist/components/input/input.d.ts +5 -0
- package/dist/components/input/input.d.ts.map +1 -1
- package/dist/components/page-layout/page-layout.js +1 -1
- package/dist/components/select/select.d.ts +43 -7
- package/dist/components/select/select.d.ts.map +1 -1
- package/dist/components/select/select.js +458 -150
- package/dist/components/tabs/tab-panel.d.ts +5 -0
- package/dist/components/tabs/tab-panel.d.ts.map +1 -1
- package/dist/components/tabs/tab.d.ts +5 -0
- package/dist/components/tabs/tab.d.ts.map +1 -1
- package/dist/components/tabs/tabs.d.ts +5 -0
- package/dist/components/tabs/tabs.d.ts.map +1 -1
- package/dist/styles/index.css +2 -0
- package/package.json +1 -1
- package/src/components/button-group/button-group.ts +6 -0
- package/src/components/checkbox/checkbox.ts +54 -26
- package/src/components/code-block/code-block.ts +6 -0
- package/src/components/form/form-step.ts +5 -1
- package/src/components/form/form.ts +182 -66
- package/src/components/icon/icon.ts +3 -1
- package/src/components/input/input.ts +6 -0
- package/src/components/page-layout/page-layout.ts +1 -1
- package/src/components/select/select.ts +502 -130
- package/src/components/tabs/tab-panel.ts +5 -0
- package/src/components/tabs/tab.ts +6 -0
- package/src/components/tabs/tabs.ts +6 -0
- package/src/styles/theme.css +2 -0
package/dist/styles/index.css
CHANGED
|
@@ -69,6 +69,8 @@
|
|
|
69
69
|
:root[data-theme=light] {
|
|
70
70
|
--st-color-bg: var(--st-white);
|
|
71
71
|
--st-color-surface: var(--st-gray-50);
|
|
72
|
+
--st-color-surface-muted: color-mix(in srgb, var(--st-color-surface), black 15%);
|
|
73
|
+
--st-color-surface-hover: color-mix(in srgb, var(--st-color-surface), white 10%);
|
|
72
74
|
--st-color-border: var(--st-gray-200);
|
|
73
75
|
--st-color-text: var(--st-gray-900);
|
|
74
76
|
--st-color-text-muted: var(--st-gray-700);
|
package/package.json
CHANGED
|
@@ -11,7 +11,6 @@ export class StCheckbox extends LitElement {
|
|
|
11
11
|
|
|
12
12
|
@property({type: String, reflect: true}) size: "sm" | "md" | "lg" = "md";
|
|
13
13
|
@property({type: String, reflect: true}) name = "";
|
|
14
|
-
// Checkboxes default to 'on' natively if no value is provided
|
|
15
14
|
@property({type: String}) value = "on";
|
|
16
15
|
|
|
17
16
|
@property({type: Boolean, reflect: true}) checked = false;
|
|
@@ -20,21 +19,20 @@ export class StCheckbox extends LitElement {
|
|
|
20
19
|
@property({type: Boolean, reflect: true}) disabled = false;
|
|
21
20
|
@property({type: Boolean, reflect: true}) invalid = false;
|
|
22
21
|
@property({type: Boolean, reflect: true}) required = false;
|
|
22
|
+
@property({type: String}) errorMessage = "";
|
|
23
23
|
|
|
24
24
|
@property({attribute: "aria-label"}) ariaLabel = "";
|
|
25
25
|
|
|
26
|
-
constructor() {
|
|
26
|
+
constructor() {
|
|
27
27
|
super();
|
|
28
28
|
this._internals = this.attachInternals();
|
|
29
29
|
|
|
30
|
-
// 1. Trigger error state if the user clicks away and the field is invalid
|
|
31
30
|
this.addEventListener('focusout', () => {
|
|
32
|
-
this.invalid = !this.
|
|
31
|
+
this.invalid = !this.checkValidity();
|
|
33
32
|
});
|
|
34
33
|
|
|
35
|
-
// 2. Trigger error state if the st-form tries to submit and fails
|
|
36
34
|
this.addEventListener('invalid', () => {
|
|
37
|
-
this.invalid =
|
|
35
|
+
this.invalid = !this.checkValidity();
|
|
38
36
|
});
|
|
39
37
|
}
|
|
40
38
|
|
|
@@ -67,7 +65,6 @@ constructor() {
|
|
|
67
65
|
font-size: var(--st-font-size-base);
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
/* Visually hide the native input but keep it in the DOM for focus/screen readers */
|
|
71
68
|
input {
|
|
72
69
|
position: absolute;
|
|
73
70
|
width: var(--checkbox-size);
|
|
@@ -80,7 +77,6 @@ constructor() {
|
|
|
80
77
|
border-width: 0;
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
/* Custom Checkbox Box */
|
|
84
80
|
.control {
|
|
85
81
|
display: flex;
|
|
86
82
|
align-items: center;
|
|
@@ -92,30 +88,25 @@ constructor() {
|
|
|
92
88
|
background-color: var(--st-color-bg);
|
|
93
89
|
transition: all 0.2s ease;
|
|
94
90
|
flex-shrink: 0;
|
|
95
|
-
/* Aligns the box nicely with standard text line-height */
|
|
96
91
|
margin-top: 0.125rem;
|
|
97
92
|
}
|
|
98
93
|
|
|
99
|
-
/* Hover State */
|
|
100
94
|
label:hover .control {
|
|
101
95
|
border-color: var(--st-color-primary-hover);
|
|
102
96
|
}
|
|
103
97
|
|
|
104
|
-
/* Focus State */
|
|
105
98
|
input:focus-visible + .control {
|
|
106
99
|
box-shadow:
|
|
107
100
|
0 0 0 2px var(--st-color-bg),
|
|
108
101
|
0 0 0 4px var(--st-color-primary);
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
/* Checked & Indeterminate States */
|
|
112
104
|
:host([checked]) .control,
|
|
113
105
|
:host([indeterminate]) .control {
|
|
114
106
|
border-color: var(--st-color-primary);
|
|
115
107
|
background-color: var(--st-color-primary);
|
|
116
108
|
}
|
|
117
109
|
|
|
118
|
-
/* SVG Icons */
|
|
119
110
|
svg {
|
|
120
111
|
width: var(--checkbox-icon-size);
|
|
121
112
|
height: var(--checkbox-icon-size);
|
|
@@ -131,14 +122,12 @@ constructor() {
|
|
|
131
122
|
opacity 0.2s ease;
|
|
132
123
|
}
|
|
133
124
|
|
|
134
|
-
/* Reveal Icons */
|
|
135
125
|
:host([checked]) svg.icon-check,
|
|
136
126
|
:host([indeterminate]) svg.icon-minus {
|
|
137
127
|
opacity: 1;
|
|
138
128
|
transform: scale(1);
|
|
139
129
|
}
|
|
140
130
|
|
|
141
|
-
/* Disabled State */
|
|
142
131
|
:host([disabled]) {
|
|
143
132
|
pointer-events: none;
|
|
144
133
|
}
|
|
@@ -147,7 +136,6 @@ constructor() {
|
|
|
147
136
|
cursor: not-allowed;
|
|
148
137
|
}
|
|
149
138
|
|
|
150
|
-
/* Invalid State */
|
|
151
139
|
:host([invalid]) .control {
|
|
152
140
|
border-color: var(--st-color-error);
|
|
153
141
|
}
|
|
@@ -162,8 +150,7 @@ constructor() {
|
|
|
162
150
|
override updated(changedProperties: PropertyValues<this>) {
|
|
163
151
|
super.updated(changedProperties);
|
|
164
152
|
|
|
165
|
-
if (changedProperties.has("checked") || changedProperties.has("value")) {
|
|
166
|
-
// Form API: If checked, submit the value. If unchecked, submit null to remove it from FormData.
|
|
153
|
+
if (changedProperties.has("checked") || changedProperties.has("value") || changedProperties.has("errorMessage")) {
|
|
167
154
|
this._internals.setFormValue(this.checked ? this.value : null);
|
|
168
155
|
this._syncValidity();
|
|
169
156
|
}
|
|
@@ -175,16 +162,54 @@ constructor() {
|
|
|
175
162
|
|
|
176
163
|
private _syncValidity() {
|
|
177
164
|
if (!this._inputEl) return;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this._inputEl.validationMessage
|
|
181
|
-
this.
|
|
182
|
-
|
|
165
|
+
|
|
166
|
+
if (this.required && !this.checked) {
|
|
167
|
+
const message = this.errorMessage || this._inputEl.validationMessage || "Please check this box if you want to proceed.";
|
|
168
|
+
this._internals.setValidity(
|
|
169
|
+
{valueMissing: true},
|
|
170
|
+
message,
|
|
171
|
+
this._inputEl,
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
this._internals.setValidity(
|
|
175
|
+
this._inputEl.validity,
|
|
176
|
+
this._inputEl.validationMessage,
|
|
177
|
+
this._inputEl,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public checkValidity(): boolean {
|
|
183
|
+
if (!this._inputEl) return true;
|
|
184
|
+
|
|
185
|
+
if (this.required && !this.checked) {
|
|
186
|
+
const message = this.errorMessage || this._inputEl.validationMessage || "Please check this box if you want to proceed.";
|
|
187
|
+
this._internals.setValidity(
|
|
188
|
+
{valueMissing: true},
|
|
189
|
+
message,
|
|
190
|
+
this._inputEl,
|
|
191
|
+
);
|
|
192
|
+
this.invalid = true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const isValid = this._inputEl.checkValidity();
|
|
197
|
+
this._syncValidity();
|
|
198
|
+
this.invalid = !isValid;
|
|
199
|
+
return isValid;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public reportValidity(): boolean {
|
|
203
|
+
this._syncValidity();
|
|
204
|
+
const isValid = this._internals.reportValidity();
|
|
205
|
+
this.invalid = !isValid;
|
|
206
|
+
return isValid;
|
|
183
207
|
}
|
|
184
208
|
|
|
185
209
|
formResetCallback() {
|
|
186
210
|
this.checked = false;
|
|
187
211
|
this.indeterminate = false;
|
|
212
|
+
this.invalid = false;
|
|
188
213
|
this._internals.setFormValue(null);
|
|
189
214
|
this.updateComplete.then(() => this._syncValidity());
|
|
190
215
|
}
|
|
@@ -194,12 +219,16 @@ constructor() {
|
|
|
194
219
|
|
|
195
220
|
if (this.disabled) return;
|
|
196
221
|
|
|
197
|
-
// Standard native behavior: clicking an indeterminate checkbox unsets the indeterminate state
|
|
198
222
|
if (this.indeterminate) {
|
|
199
223
|
this.indeterminate = false;
|
|
200
224
|
}
|
|
201
225
|
|
|
202
226
|
this.checked = this._inputEl.checked;
|
|
227
|
+
|
|
228
|
+
if (this.checkValidity()) {
|
|
229
|
+
this.invalid = false;
|
|
230
|
+
}
|
|
231
|
+
|
|
203
232
|
this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
|
|
204
233
|
}
|
|
205
234
|
|
|
@@ -218,7 +247,6 @@ constructor() {
|
|
|
218
247
|
@change=${this._handleChange}
|
|
219
248
|
/>
|
|
220
249
|
<div class="control">
|
|
221
|
-
<!-- Render both SVGs and use CSS to conditionally reveal them based on host attributes -->
|
|
222
250
|
${this.indeterminate
|
|
223
251
|
? html`<svg class="icon-minus" viewBox="0 0 24 24"><path d="M5 12h14"></path></svg>`
|
|
224
252
|
: html`<svg class="icon-check" viewBox="0 0 24 24">
|
|
@@ -235,4 +263,4 @@ declare global {
|
|
|
235
263
|
interface HTMLElementTagNameMap {
|
|
236
264
|
"st-checkbox": StCheckbox;
|
|
237
265
|
}
|
|
238
|
-
}
|
|
266
|
+
}
|
|
@@ -6,6 +6,10 @@ import {resetStyles} from "../../styles/reset.js";
|
|
|
6
6
|
export class StFormStep extends LitElement {
|
|
7
7
|
@property({type: String}) title = "";
|
|
8
8
|
@property({type: Boolean, reflect: true}) active = false;
|
|
9
|
+
|
|
10
|
+
// Declare the validate function property (attribute: false prevents reflection of functions to DOM attributes)
|
|
11
|
+
@property({attribute: false})
|
|
12
|
+
validate?: () => Promise<boolean> | boolean;
|
|
9
13
|
|
|
10
14
|
static styles = [
|
|
11
15
|
resetStyles,
|
|
@@ -29,4 +33,4 @@ declare global {
|
|
|
29
33
|
interface HTMLElementTagNameMap {
|
|
30
34
|
"st-form-step": StFormStep;
|
|
31
35
|
}
|
|
32
|
-
}
|
|
36
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {LitElement, html, css} from "lit";
|
|
2
|
-
import {customElement, property, state,
|
|
1
|
+
import {LitElement, html, css, PropertyValues} from "lit";
|
|
2
|
+
import {customElement, property, state, query} from "lit/decorators.js";
|
|
3
3
|
import {resetStyles} from "../../styles/reset.js";
|
|
4
4
|
import type {StFormStep} from "./form-step.js";
|
|
5
5
|
|
|
@@ -13,11 +13,14 @@ export class StForm extends LitElement {
|
|
|
13
13
|
|
|
14
14
|
@state() private _currentStepIndex = 0;
|
|
15
15
|
|
|
16
|
-
@queryAssignedElements({selector: "st-form-step"})
|
|
17
|
-
private _stepElements!: StFormStep[];
|
|
18
|
-
|
|
19
16
|
@query("form") private _formEl!: HTMLFormElement;
|
|
20
17
|
|
|
18
|
+
private _modalFooterEl: HTMLElement | null = null;
|
|
19
|
+
|
|
20
|
+
private get _steps(): StFormStep[] {
|
|
21
|
+
return Array.from(this.querySelectorAll("st-form-step"));
|
|
22
|
+
}
|
|
23
|
+
|
|
21
24
|
static styles = [
|
|
22
25
|
resetStyles,
|
|
23
26
|
css`
|
|
@@ -31,9 +34,6 @@ export class StForm extends LitElement {
|
|
|
31
34
|
gap: var(--st-spacing-md);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
/* ==========================================================
|
|
35
|
-
Layouts
|
|
36
|
-
========================================================== */
|
|
37
37
|
:host([layout="vertical"]) form {
|
|
38
38
|
flex-direction: column;
|
|
39
39
|
}
|
|
@@ -55,9 +55,6 @@ export class StForm extends LitElement {
|
|
|
55
55
|
flex-direction: column;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/* ==========================================================
|
|
59
|
-
Progress Navigation
|
|
60
|
-
========================================================== */
|
|
61
58
|
.progress-nav {
|
|
62
59
|
display: flex;
|
|
63
60
|
align-items: center;
|
|
@@ -121,9 +118,6 @@ export class StForm extends LitElement {
|
|
|
121
118
|
background-color: var(--st-color-success);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
/* ==========================================================
|
|
125
|
-
Actions & States
|
|
126
|
-
========================================================== */
|
|
127
121
|
.form-actions {
|
|
128
122
|
display: flex;
|
|
129
123
|
gap: var(--st-spacing-md);
|
|
@@ -162,42 +156,165 @@ export class StForm extends LitElement {
|
|
|
162
156
|
`,
|
|
163
157
|
];
|
|
164
158
|
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
override connectedCallback() {
|
|
160
|
+
super.connectedCallback();
|
|
161
|
+
this.updateComplete.then(() => {
|
|
167
162
|
this._updateStepVisibility();
|
|
163
|
+
this._updateModalFooter();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override disconnectedCallback() {
|
|
168
|
+
super.disconnectedCallback();
|
|
169
|
+
if (this._modalFooterEl) {
|
|
170
|
+
this._modalFooterEl.remove();
|
|
171
|
+
this._modalFooterEl = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
override updated(changedProperties: PropertyValues<this>) {
|
|
176
|
+
super.updated(changedProperties);
|
|
177
|
+
this._updateModalFooter();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private _updateModalFooter() {
|
|
181
|
+
const modal = this.closest("st-modal");
|
|
182
|
+
if (modal) {
|
|
183
|
+
if (!this._modalFooterEl) {
|
|
184
|
+
this._modalFooterEl = document.createElement("div");
|
|
185
|
+
this._modalFooterEl.setAttribute("slot", "footer");
|
|
186
|
+
this._modalFooterEl.style.display = "flex";
|
|
187
|
+
this._modalFooterEl.style.gap = "var(--st-spacing-md)";
|
|
188
|
+
this._modalFooterEl.style.width = "100%";
|
|
189
|
+
this._modalFooterEl.style.justifyContent = "flex-end";
|
|
190
|
+
this._modalFooterEl.style.alignItems = "center";
|
|
191
|
+
modal.appendChild(this._modalFooterEl);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const steps = this._steps;
|
|
195
|
+
const hasSteps = steps.length > 0;
|
|
196
|
+
const isLastStep = hasSteps ? this._currentStepIndex === steps.length - 1 : true;
|
|
197
|
+
|
|
198
|
+
// Imperative DOM management prevents event listener loss and re-rendering conflicts
|
|
199
|
+
this._modalFooterEl.innerHTML = "";
|
|
200
|
+
|
|
201
|
+
if (hasSteps && this._currentStepIndex > 0) {
|
|
202
|
+
const backBtn = document.createElement("st-button");
|
|
203
|
+
backBtn.setAttribute("variant", "secondary");
|
|
204
|
+
backBtn.setAttribute("type", "button");
|
|
205
|
+
if (this.loading) backBtn.setAttribute("disabled", "");
|
|
206
|
+
backBtn.textContent = "Back";
|
|
207
|
+
backBtn.addEventListener("click", (e) => {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
this._prevStep();
|
|
210
|
+
});
|
|
211
|
+
this._modalFooterEl.appendChild(backBtn);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const submitBtn = document.createElement("st-button");
|
|
215
|
+
submitBtn.setAttribute("color", "primary");
|
|
216
|
+
submitBtn.setAttribute("type", "button");
|
|
217
|
+
if (this.loading) submitBtn.setAttribute("disabled", "");
|
|
218
|
+
submitBtn.textContent = this.loading ? "Submitting..." : (isLastStep ? "Submit" : "Continue");
|
|
219
|
+
submitBtn.addEventListener("click", (e) => {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
this._triggerSubmission();
|
|
222
|
+
});
|
|
223
|
+
this._modalFooterEl.appendChild(submitBtn);
|
|
224
|
+
|
|
225
|
+
} else if (this._modalFooterEl) {
|
|
226
|
+
this._modalFooterEl.remove();
|
|
227
|
+
this._modalFooterEl = null;
|
|
168
228
|
}
|
|
169
229
|
}
|
|
170
230
|
|
|
231
|
+
private _handleSlotChange() {
|
|
232
|
+
this._updateStepVisibility();
|
|
233
|
+
this._updateModalFooter();
|
|
234
|
+
}
|
|
235
|
+
|
|
171
236
|
private _updateStepVisibility() {
|
|
172
|
-
|
|
237
|
+
const steps = this._steps;
|
|
238
|
+
steps.forEach((step, index) => {
|
|
173
239
|
step.active = index === this._currentStepIndex;
|
|
174
240
|
});
|
|
175
241
|
this.requestUpdate();
|
|
176
242
|
}
|
|
177
243
|
|
|
178
|
-
private _validateCurrentContext(): boolean {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
244
|
+
private async _validateCurrentContext(): Promise<boolean> {
|
|
245
|
+
const steps = this._steps;
|
|
246
|
+
const scope: ParentNode = steps.length > 0
|
|
247
|
+
? steps[this._currentStepIndex]
|
|
248
|
+
: this;
|
|
182
249
|
|
|
183
|
-
const currentStep = this._stepElements[this._currentStepIndex];
|
|
184
250
|
let isValid = true;
|
|
185
251
|
|
|
186
|
-
// Find all custom and native form inputs assigned to the current step
|
|
187
252
|
const inputs = Array.from(
|
|
188
|
-
|
|
189
|
-
HTMLElement & {
|
|
190
|
-
|
|
253
|
+
scope.querySelectorAll<
|
|
254
|
+
HTMLElement & {
|
|
255
|
+
checkValidity?: () => boolean;
|
|
256
|
+
reportValidity?: () => boolean;
|
|
257
|
+
required?: boolean;
|
|
258
|
+
value?: any;
|
|
259
|
+
checked?: boolean;
|
|
260
|
+
invalid?: boolean;
|
|
261
|
+
}
|
|
262
|
+
>("st-input, st-select, st-textarea, st-checkbox, st-radio, input, select, textarea"),
|
|
191
263
|
);
|
|
192
|
-
|
|
264
|
+
|
|
193
265
|
for (const input of inputs) {
|
|
266
|
+
if (input.hasAttribute("disabled") || (input as any).disabled) continue;
|
|
267
|
+
|
|
268
|
+
let fieldValid = true;
|
|
269
|
+
|
|
194
270
|
if (typeof input.checkValidity === "function") {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
271
|
+
fieldValid = input.checkValidity();
|
|
272
|
+
} else {
|
|
273
|
+
const isRequired = input.hasAttribute("required") || input.required;
|
|
274
|
+
if (isRequired) {
|
|
275
|
+
const isToggle = input.tagName === "ST-CHECKBOX" || input.tagName === "ST-RADIO";
|
|
276
|
+
const val = isToggle ? input.checked : input.value;
|
|
277
|
+
if (val === undefined || val === null || val === "" || val === false) {
|
|
278
|
+
fieldValid = false;
|
|
199
279
|
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!fieldValid) {
|
|
284
|
+
if (typeof input.reportValidity === "function") {
|
|
285
|
+
input.reportValidity();
|
|
286
|
+
} else if ("invalid" in input) {
|
|
287
|
+
input.invalid = true;
|
|
288
|
+
}
|
|
289
|
+
if (isValid && typeof (input as any).focus === "function") {
|
|
290
|
+
(input as any).focus();
|
|
291
|
+
}
|
|
292
|
+
isValid = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (steps.length === 0 && this._formEl) {
|
|
297
|
+
if (!this._formEl.checkValidity()) {
|
|
298
|
+
isValid = false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (isValid && steps.length > 0) {
|
|
303
|
+
const currentStep = steps[this._currentStepIndex] as StFormStep & {
|
|
304
|
+
validate?: () => Promise<boolean> | boolean;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (typeof currentStep.validate === "function") {
|
|
308
|
+
try {
|
|
309
|
+
this.loading = true;
|
|
310
|
+
const stepPassed = await currentStep.validate();
|
|
311
|
+
if (!stepPassed) {
|
|
312
|
+
isValid = false;
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
200
315
|
isValid = false;
|
|
316
|
+
} finally {
|
|
317
|
+
this.loading = false;
|
|
201
318
|
}
|
|
202
319
|
}
|
|
203
320
|
}
|
|
@@ -206,7 +323,6 @@ export class StForm extends LitElement {
|
|
|
206
323
|
}
|
|
207
324
|
|
|
208
325
|
private _triggerSubmission() {
|
|
209
|
-
// HTML5 method that triggers form validation and fires the native submit event
|
|
210
326
|
this._formEl.requestSubmit();
|
|
211
327
|
}
|
|
212
328
|
|
|
@@ -220,13 +336,11 @@ export class StForm extends LitElement {
|
|
|
220
336
|
private _extractFormData(): FormData {
|
|
221
337
|
const formData = new FormData();
|
|
222
338
|
|
|
223
|
-
// Query all light-DOM children that have a 'name' attribute
|
|
224
339
|
const fields = this.querySelectorAll<
|
|
225
340
|
HTMLElement & {name: string; value: any; checked?: boolean; files?: FileList}
|
|
226
341
|
>("[name]");
|
|
227
342
|
|
|
228
343
|
fields.forEach((field) => {
|
|
229
|
-
// Standard HTML form behavior: ignore disabled fields
|
|
230
344
|
if (field.hasAttribute("disabled")) return;
|
|
231
345
|
|
|
232
346
|
const isToggle =
|
|
@@ -236,7 +350,6 @@ export class StForm extends LitElement {
|
|
|
236
350
|
field.getAttribute("type") === "checkbox";
|
|
237
351
|
|
|
238
352
|
if (isToggle) {
|
|
239
|
-
// Only append radios/checkboxes if they are actively checked
|
|
240
353
|
if (field.checked) {
|
|
241
354
|
formData.append(field.name, field.value || "on");
|
|
242
355
|
}
|
|
@@ -245,10 +358,8 @@ export class StForm extends LitElement {
|
|
|
245
358
|
field.getAttribute("type") === "file" &&
|
|
246
359
|
field.files
|
|
247
360
|
) {
|
|
248
|
-
// Handle native file uploads
|
|
249
361
|
Array.from(field.files).forEach((file) => formData.append(field.name, file));
|
|
250
362
|
} else if (field.value !== undefined) {
|
|
251
|
-
// Handle st-input, st-select, st-textarea, and standard inputs
|
|
252
363
|
formData.append(field.name, field.value);
|
|
253
364
|
}
|
|
254
365
|
});
|
|
@@ -256,14 +367,16 @@ export class StForm extends LitElement {
|
|
|
256
367
|
return formData;
|
|
257
368
|
}
|
|
258
369
|
|
|
259
|
-
private _handleSubmit(e: Event) {
|
|
370
|
+
private async _handleSubmit(e: Event) {
|
|
260
371
|
e.preventDefault();
|
|
261
372
|
e.stopPropagation();
|
|
262
373
|
|
|
263
|
-
|
|
374
|
+
const isValid = await this._validateCurrentContext();
|
|
375
|
+
if (!isValid) return;
|
|
264
376
|
|
|
265
|
-
const
|
|
266
|
-
const
|
|
377
|
+
const steps = this._steps;
|
|
378
|
+
const hasSteps = steps.length > 0;
|
|
379
|
+
const isLastStep = hasSteps ? this._currentStepIndex === steps.length - 1 : true;
|
|
267
380
|
|
|
268
381
|
if (hasSteps && !isLastStep) {
|
|
269
382
|
this._currentStepIndex++;
|
|
@@ -271,7 +384,6 @@ export class StForm extends LitElement {
|
|
|
271
384
|
return;
|
|
272
385
|
}
|
|
273
386
|
|
|
274
|
-
// Use our custom extractor to pierce the Shadow DOM boundary
|
|
275
387
|
const formData = this._extractFormData();
|
|
276
388
|
|
|
277
389
|
this.dispatchEvent(
|
|
@@ -285,8 +397,9 @@ export class StForm extends LitElement {
|
|
|
285
397
|
}
|
|
286
398
|
|
|
287
399
|
render() {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
400
|
+
const steps = this._steps;
|
|
401
|
+
const hasSteps = steps.length > 0;
|
|
402
|
+
const isInModal = !!this.closest("st-modal");
|
|
290
403
|
|
|
291
404
|
return html`
|
|
292
405
|
<div class="${hasSteps ? "has-steps" : ""}" aria-busy=${this.loading}>
|
|
@@ -300,7 +413,7 @@ export class StForm extends LitElement {
|
|
|
300
413
|
${hasSteps
|
|
301
414
|
? html`
|
|
302
415
|
<div class="progress-nav" role="tablist">
|
|
303
|
-
${
|
|
416
|
+
${steps.map((step, index) => {
|
|
304
417
|
const isActive = index === this._currentStepIndex;
|
|
305
418
|
const isCompleted = index < this._currentStepIndex;
|
|
306
419
|
|
|
@@ -319,7 +432,7 @@ export class StForm extends LitElement {
|
|
|
319
432
|
</div>
|
|
320
433
|
<span>${step.title || `Step ${index + 1}`}</span>
|
|
321
434
|
</div>
|
|
322
|
-
${index <
|
|
435
|
+
${index < steps.length - 1
|
|
323
436
|
? html` <div class="step-connector ${isCompleted ? "completed" : ""}"></div> `
|
|
324
437
|
: ""}
|
|
325
438
|
`;
|
|
@@ -339,30 +452,33 @@ export class StForm extends LitElement {
|
|
|
339
452
|
<form @submit=${this._handleSubmit}>
|
|
340
453
|
<slot @slotchange=${this._handleSlotChange}></slot>
|
|
341
454
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
455
|
+
${!isInModal
|
|
456
|
+
? html`
|
|
457
|
+
<div class="form-actions">
|
|
458
|
+
${hasSteps && this._currentStepIndex > 0
|
|
459
|
+
? html`
|
|
460
|
+
<st-button
|
|
461
|
+
variant="secondary"
|
|
462
|
+
type="button"
|
|
463
|
+
@click=${this._prevStep}
|
|
464
|
+
?disabled=${this.loading}
|
|
465
|
+
>
|
|
466
|
+
Back
|
|
467
|
+
</st-button>
|
|
468
|
+
`
|
|
469
|
+
: ""}
|
|
470
|
+
|
|
345
471
|
<st-button
|
|
346
|
-
|
|
472
|
+
color="primary"
|
|
347
473
|
type="button"
|
|
348
|
-
@click=${this.
|
|
474
|
+
@click=${this._triggerSubmission}
|
|
349
475
|
?disabled=${this.loading}
|
|
350
476
|
>
|
|
351
|
-
|
|
477
|
+
${this.loading ? "Submitting..." : hasSteps && this._currentStepIndex === steps.length - 1 ? "Submit" : hasSteps ? "Continue" : "Submit"}
|
|
352
478
|
</st-button>
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<!-- Type is "button", but click triggers requestSubmit() on the internal form -->
|
|
357
|
-
<st-button
|
|
358
|
-
color="primary"
|
|
359
|
-
type="button"
|
|
360
|
-
@click=${this._triggerSubmission}
|
|
361
|
-
?disabled=${this.loading}
|
|
362
|
-
>
|
|
363
|
-
${this.loading ? "Submitting..." : isLastStep ? "Submit" : "Continue"}
|
|
364
|
-
</st-button>
|
|
365
|
-
</div>
|
|
479
|
+
</div>
|
|
480
|
+
`
|
|
481
|
+
: ""}
|
|
366
482
|
</form>
|
|
367
483
|
</div>
|
|
368
484
|
`;
|
|
@@ -373,4 +489,4 @@ declare global {
|
|
|
373
489
|
interface HTMLElementTagNameMap {
|
|
374
490
|
"st-form": StForm;
|
|
375
491
|
}
|
|
376
|
-
}
|
|
492
|
+
}
|
|
@@ -3,10 +3,12 @@ import { customElement, property } from 'lit/decorators.js';
|
|
|
3
3
|
import { iconPaths, IconName } from './icon-paths.js';
|
|
4
4
|
import { resetStyles } from '../../styles/reset.js';
|
|
5
5
|
|
|
6
|
+
export type IconVariant = 'outline' | 'solid';
|
|
7
|
+
|
|
6
8
|
@customElement('st-icon')
|
|
7
9
|
export class StIcon extends LitElement {
|
|
8
10
|
@property({ type: String }) name: IconName | string = '';
|
|
9
|
-
@property({ type: String }) variant:
|
|
11
|
+
@property({ type: String }) variant: IconVariant = 'outline';
|
|
10
12
|
|
|
11
13
|
// Semantic Colors
|
|
12
14
|
@property({ type: String, reflect: true }) color: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' = 'default';
|
|
@@ -153,7 +153,7 @@ export class StPageLayout extends LitElement {
|
|
|
153
153
|
--layout-surface-color: var(--st-color-surface, #f8fafc);
|
|
154
154
|
--layout-border-color: var(--st-color-border, #e2e8f0);
|
|
155
155
|
--layout-sidebar-width: 16rem;
|
|
156
|
-
--layout-right-sidebar-width:
|
|
156
|
+
--layout-right-sidebar-width: 16rem;
|
|
157
157
|
--layout-main-max-width: none;
|
|
158
158
|
--layout-nav-height: 4rem;
|
|
159
159
|
}
|