@design.estate/dees-catalog 9.5.0 → 9.6.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/dist_bundle/bundle.js +1307 -1448
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.d.ts +21 -2
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.d.ts +4 -0
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.js +218 -193
- package/dist_ts_web/elements/00group-layout/dees-stepper/dees-stepper.js +288 -403
- package/dist_ts_web/elements/00group-layout/dees-stepper/styles.d.ts +1 -0
- package/dist_ts_web/elements/00group-layout/dees-stepper/styles.js +41 -0
- package/package.json +1 -1
- package/readme.md +31 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts +142 -200
- package/ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts +216 -400
- package/ts_web/elements/00group-layout/dees-stepper/styles.ts +41 -0
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
DeesElement,
|
|
5
5
|
customElement,
|
|
6
6
|
html,
|
|
7
|
-
css,
|
|
8
7
|
cssManager,
|
|
9
8
|
property,
|
|
9
|
+
state,
|
|
10
10
|
type TemplateResult,
|
|
11
11
|
} from '@design.estate/dees-element';
|
|
12
12
|
|
|
@@ -14,11 +14,13 @@ import * as domtools from '@design.estate/dees-domtools';
|
|
|
14
14
|
import { stepperDemo } from './dees-stepper.demo.js';
|
|
15
15
|
import { themeDefaultStyles } from '../../00theme.js';
|
|
16
16
|
import { zIndexRegistry } from '../../00zindex.js';
|
|
17
|
-
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
|
18
17
|
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
|
19
18
|
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
|
|
20
19
|
import '../../00group-feedback/dees-progressbar/dees-progressbar.js';
|
|
21
20
|
import '../dees-tile/dees-tile.js';
|
|
21
|
+
import '../../00group-button/dees-button/dees-button.js';
|
|
22
|
+
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
23
|
+
import { stepperStyles } from './styles.js';
|
|
22
24
|
|
|
23
25
|
export interface IStepProgressState {
|
|
24
26
|
label?: string;
|
|
@@ -36,6 +38,7 @@ export interface IStepProgress extends IStepProgressState {
|
|
|
36
38
|
|
|
37
39
|
export interface IStep {
|
|
38
40
|
title: string;
|
|
41
|
+
description?: string;
|
|
39
42
|
content: TemplateResult;
|
|
40
43
|
allowBack?: boolean;
|
|
41
44
|
progressStep?: IStepProgress;
|
|
@@ -70,14 +73,12 @@ export class DeesStepper extends DeesElement {
|
|
|
70
73
|
if (optionsArg.cancelable !== undefined) {
|
|
71
74
|
stepper.cancelable = optionsArg.cancelable;
|
|
72
75
|
}
|
|
73
|
-
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
|
|
74
|
-
body.append(stepper.windowLayer);
|
|
75
76
|
body.append(stepper);
|
|
76
77
|
|
|
77
|
-
//
|
|
78
|
+
// Share overlay ordering with catalog controls opened from this dialog.
|
|
78
79
|
stepper.stepperZIndex = zIndexRegistry.getNextZIndex();
|
|
79
80
|
zIndexRegistry.register(stepper, stepper.stepperZIndex);
|
|
80
|
-
|
|
81
|
+
await stepper.updateComplete;
|
|
81
82
|
return stepper;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -121,290 +122,46 @@ export class DeesStepper extends DeesElement {
|
|
|
121
122
|
|
|
122
123
|
private activeFormSubscription?: { unsubscribe: () => void };
|
|
123
124
|
|
|
124
|
-
private
|
|
125
|
+
@state() private accessor stepError = '';
|
|
126
|
+
@state() private accessor progressRunning = false;
|
|
127
|
+
@state() private accessor actionPending = false;
|
|
128
|
+
private resizeObserver?: ResizeObserver;
|
|
129
|
+
private activeValidationStep?: IStep;
|
|
130
|
+
private actionStep?: IStep;
|
|
131
|
+
private destroyPromise?: Promise<void>;
|
|
132
|
+
private scrollTarget?: number;
|
|
133
|
+
private scrollInFlight = false;
|
|
134
|
+
private cancelRequestPending = false;
|
|
125
135
|
|
|
126
136
|
constructor() {
|
|
127
137
|
super();
|
|
138
|
+
this.registerStartupFunction(async (signal) => {
|
|
139
|
+
await this.updateComplete;
|
|
140
|
+
if (signal.aborted) return;
|
|
141
|
+
this.destroyPromise = undefined;
|
|
142
|
+
const dialog = this.shadowRoot?.querySelector('dialog');
|
|
143
|
+
if (dialog && !dialog.open) dialog.showModal();
|
|
144
|
+
this.resizeObserver = new ResizeObserver(() => this.positionSelectedStep(false));
|
|
145
|
+
this.resizeObserver.observe(this.shadowRoot!.querySelector('.stepperContainer')!);
|
|
146
|
+
for (const tile of this.shadowRoot!.querySelectorAll('.step')) this.resizeObserver.observe(tile);
|
|
147
|
+
await this.setScrollStatus();
|
|
148
|
+
});
|
|
128
149
|
}
|
|
129
150
|
|
|
130
|
-
public static styles = [
|
|
131
|
-
themeDefaultStyles,
|
|
132
|
-
cssManager.defaultStyles,
|
|
133
|
-
css`
|
|
134
|
-
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
|
135
|
-
:host {
|
|
136
|
-
position: absolute;
|
|
137
|
-
width: 100%;
|
|
138
|
-
height: 100%;
|
|
139
|
-
font-family: var(--dees-font-family);
|
|
140
|
-
color: var(--dees-color-text-primary);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/*
|
|
144
|
-
* In overlay mode the host is "transparent" to layout — the inner
|
|
145
|
-
* .stepperContainer.overlay is what pins to the viewport and carries the
|
|
146
|
-
* z-index. Keeping :host unpositioned avoids nesting the stacking context
|
|
147
|
-
* under an auto-z-index parent (which was trapping .stepperContainer
|
|
148
|
-
* below DeesWindowLayer's sibling layers). This mirrors how dees-modal
|
|
149
|
-
* keeps its own :host unpositioned and lets .modalContainer drive layout.
|
|
150
|
-
*/
|
|
151
|
-
:host([overlay]) {
|
|
152
|
-
position: static;
|
|
153
|
-
width: 0;
|
|
154
|
-
height: 0;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.stepperContainer {
|
|
158
|
-
position: absolute;
|
|
159
|
-
width: 100%;
|
|
160
|
-
height: 100%;
|
|
161
|
-
overflow: hidden;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.stepperContainer.overlay {
|
|
165
|
-
position: fixed;
|
|
166
|
-
top: 0;
|
|
167
|
-
left: 0;
|
|
168
|
-
width: 100vw;
|
|
169
|
-
height: 100vh;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/* Exit animation — reverse of the entry. Tiles slide DOWN + fade out,
|
|
173
|
-
mirroring the .entrance slide-up. The transition override is needed
|
|
174
|
-
because dees-tile.step has its own 0.7s transition for step selection
|
|
175
|
-
which would otherwise make the exit sluggish. */
|
|
176
|
-
.stepperContainer.predestroy dees-tile.step {
|
|
177
|
-
transform: translateY(16px);
|
|
178
|
-
filter: opacity(0);
|
|
179
|
-
transition: transform 0.25s, filter 0.2s;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
dees-tile.step {
|
|
183
|
-
position: relative;
|
|
184
|
-
pointer-events: none;
|
|
185
|
-
box-sizing: border-box;
|
|
186
|
-
width: min(500px, calc(100vw - 32px));
|
|
187
|
-
max-width: 500px;
|
|
188
|
-
max-height: calc(100vh - 64px);
|
|
189
|
-
min-height: min(300px, calc(100vh - 64px));
|
|
190
|
-
margin: auto;
|
|
191
|
-
margin-bottom: 20px;
|
|
192
|
-
filter: opacity(0.55) saturate(0.85);
|
|
193
|
-
transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1);
|
|
194
|
-
user-select: none;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
dees-tile.step.selected {
|
|
198
|
-
pointer-events: all;
|
|
199
|
-
filter: opacity(1) saturate(1);
|
|
200
|
-
user-select: auto;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
dees-tile.step.hiddenStep {
|
|
204
|
-
filter: opacity(0);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
dees-tile.step.entrance {
|
|
208
|
-
transition: transform 0.35s ease, filter 0.35s ease;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
dees-tile.step.entrance.hiddenStep {
|
|
212
|
-
transform: translateY(16px);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
dees-tile.step:last-child {
|
|
216
|
-
margin-bottom: 100vh;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.stepperContainer.overlay dees-tile.step::part(outer) {
|
|
220
|
-
box-shadow:
|
|
221
|
-
0 0 0 1px color-mix(in srgb, var(--dees-color-text-primary) 3%, transparent),
|
|
222
|
-
var(--dees-shadow-lg);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
.stepperContainer:not(.overlay) dees-tile.step {
|
|
226
|
-
max-height: calc(100% - 64px);
|
|
227
|
-
min-height: min(300px, calc(100% - 64px));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
.step-header {
|
|
231
|
-
height: 36px;
|
|
232
|
-
display: flex;
|
|
233
|
-
align-items: center;
|
|
234
|
-
justify-content: space-between;
|
|
235
|
-
padding: 0 8px 0 12px;
|
|
236
|
-
gap: 12px;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.goBack-spacer {
|
|
240
|
-
width: 1px;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
.step-header .goBack {
|
|
244
|
-
display: inline-flex;
|
|
245
|
-
align-items: center;
|
|
246
|
-
gap: 6px;
|
|
247
|
-
height: 24px;
|
|
248
|
-
padding: 0 8px;
|
|
249
|
-
font-size: 12px;
|
|
250
|
-
font-weight: 500;
|
|
251
|
-
line-height: 1;
|
|
252
|
-
border: none;
|
|
253
|
-
background: transparent;
|
|
254
|
-
color: var(--dees-color-text-muted);
|
|
255
|
-
border-radius: var(--dees-radius-sm);
|
|
256
|
-
cursor: pointer;
|
|
257
|
-
transition:
|
|
258
|
-
background-color var(--dees-transition-fast) var(--dees-ease-standard),
|
|
259
|
-
color var(--dees-transition-fast) var(--dees-ease-standard),
|
|
260
|
-
transform var(--dees-transition-default) var(--dees-ease-standard);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
.step-header .goBack:hover {
|
|
264
|
-
background: var(--dees-color-hover);
|
|
265
|
-
color: var(--dees-color-text-secondary);
|
|
266
|
-
transform: translateX(-2px);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.step-header .goBack:active {
|
|
270
|
-
background: var(--dees-color-border-default);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.step-header .goBack span {
|
|
274
|
-
transition: transform var(--dees-transition-default) var(--dees-ease-standard);
|
|
275
|
-
display: inline-block;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
.step-header .goBack:hover span {
|
|
279
|
-
transform: translateX(-2px);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
.step-header .stepCounter {
|
|
283
|
-
color: var(--dees-color-text-muted);
|
|
284
|
-
font-size: 12px;
|
|
285
|
-
font-weight: 500;
|
|
286
|
-
letter-spacing: -0.01em;
|
|
287
|
-
padding: 0 8px;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
.step-body .title {
|
|
291
|
-
text-align: center;
|
|
292
|
-
padding-top: 32px;
|
|
293
|
-
font-family: var(--dees-font-family);
|
|
294
|
-
font-size: 24px;
|
|
295
|
-
font-weight: 600;
|
|
296
|
-
letter-spacing: -0.01em;
|
|
297
|
-
color: inherit;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
.step-body .content {
|
|
301
|
-
padding: 32px;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
.step-body .content.withProgressStep {
|
|
305
|
-
padding-top: 20px;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
.progressStep {
|
|
309
|
-
padding: 0 24px;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/* --- Footer: modal-style bottom buttons --- */
|
|
313
|
-
.bottomButtons {
|
|
314
|
-
display: flex;
|
|
315
|
-
flex-direction: row;
|
|
316
|
-
justify-content: flex-end;
|
|
317
|
-
align-items: center;
|
|
318
|
-
gap: 0;
|
|
319
|
-
height: 36px;
|
|
320
|
-
width: 100%;
|
|
321
|
-
box-sizing: border-box;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
.bottomButtons .bottomButton {
|
|
325
|
-
padding: 0 16px;
|
|
326
|
-
height: 100%;
|
|
327
|
-
text-align: center;
|
|
328
|
-
font-size: 12px;
|
|
329
|
-
font-weight: 500;
|
|
330
|
-
cursor: pointer;
|
|
331
|
-
user-select: none;
|
|
332
|
-
transition:
|
|
333
|
-
background-color var(--dees-transition-fast) var(--dees-ease-standard),
|
|
334
|
-
color var(--dees-transition-fast) var(--dees-ease-standard);
|
|
335
|
-
background: transparent;
|
|
336
|
-
border: none;
|
|
337
|
-
color: var(--dees-color-text-muted);
|
|
338
|
-
white-space: nowrap;
|
|
339
|
-
display: flex;
|
|
340
|
-
align-items: center;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/* Border-left separator on every button EXCEPT the first one.
|
|
344
|
-
Uses general sibling so the stepHint (if rendered on the left) does
|
|
345
|
-
not shift which button counts as "first" and create a phantom border. */
|
|
346
|
-
.bottomButtons .bottomButton ~ .bottomButton {
|
|
347
|
-
border-left: 1px solid var(--dees-color-border-subtle);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
.bottomButtons .bottomButton:hover {
|
|
351
|
-
background: var(--dees-color-hover);
|
|
352
|
-
color: var(--dees-color-text-primary);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.bottomButtons .bottomButton:active {
|
|
356
|
-
background: var(--dees-color-border-subtle);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
.bottomButtons .bottomButton.primary {
|
|
360
|
-
color: var(--dees-color-link);
|
|
361
|
-
font-weight: 600;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.bottomButtons .bottomButton.primary:hover {
|
|
365
|
-
background: color-mix(in srgb, var(--dees-color-link) 8%, transparent);
|
|
366
|
-
color: color-mix(in srgb, var(--dees-color-link) 80%, var(--dees-color-text-primary));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
.bottomButtons .bottomButton.primary:active {
|
|
370
|
-
background: color-mix(in srgb, var(--dees-color-link) 12%, transparent);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.bottomButtons .bottomButton.disabled {
|
|
374
|
-
pointer-events: none;
|
|
375
|
-
opacity: 0.4;
|
|
376
|
-
cursor: not-allowed;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
.bottomButtons .bottomButton.disabled:hover {
|
|
380
|
-
background: transparent;
|
|
381
|
-
color: var(--dees-color-text-muted);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/* Hint shown on the left of the footer when the active step's form has
|
|
385
|
-
unfilled required fields. Uses margin-right: auto to push right-aligned
|
|
386
|
-
buttons to the right while keeping the hint flush-left. */
|
|
387
|
-
.bottomButtons .stepHint {
|
|
388
|
-
margin-right: auto;
|
|
389
|
-
padding: 0 16px;
|
|
390
|
-
font-size: 11px;
|
|
391
|
-
line-height: 1;
|
|
392
|
-
letter-spacing: -0.01em;
|
|
393
|
-
color: var(--dees-color-text-muted);
|
|
394
|
-
display: flex;
|
|
395
|
-
align-items: center;
|
|
396
|
-
user-select: none;
|
|
397
|
-
}
|
|
398
|
-
`,
|
|
399
|
-
];
|
|
151
|
+
public static styles = [themeDefaultStyles, cssManager.defaultStyles, stepperStyles];
|
|
400
152
|
|
|
401
153
|
public render() {
|
|
154
|
+
const label = this.selectedStep?.title ?? 'Setup';
|
|
155
|
+
return this.overlay
|
|
156
|
+
? html`<dialog class="stepperContainer overlay" aria-label=${label} aria-modal="true"
|
|
157
|
+
style=${`z-index: ${this.stepperZIndex};`} @cancel=${this.handleDialogCancel}
|
|
158
|
+
@click=${this.handleOutsideClick}>${this.renderSteps()}</dialog>`
|
|
159
|
+
: html`<div class="stepperContainer" role="region" aria-label=${label}>${this.renderSteps()}</div>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private renderSteps() {
|
|
163
|
+
if (!this.steps.length) return html`<div class="empty">No steps to show.</div>`;
|
|
402
164
|
return html`
|
|
403
|
-
<div
|
|
404
|
-
class="stepperContainer ${this.overlay ? 'overlay' : ''}"
|
|
405
|
-
style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
|
|
406
|
-
@click=${this.handleOutsideClick}
|
|
407
|
-
>
|
|
408
165
|
${this.steps.map((stepArg, stepIndex) => {
|
|
409
166
|
const isSelected = stepArg === this.selectedStep;
|
|
410
167
|
const isHidden =
|
|
@@ -413,21 +170,24 @@ export class DeesStepper extends DeesElement {
|
|
|
413
170
|
const showBackButton = !isFirst && stepArg.allowBack !== false;
|
|
414
171
|
const progressStepState = stepArg.progressStep ? this.getProgressStepState(stepArg) : null;
|
|
415
172
|
return html`<dees-tile
|
|
416
|
-
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''}
|
|
417
|
-
overscroll="contain"
|
|
173
|
+
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''}"
|
|
174
|
+
overscroll="contain" ?inert=${!isSelected} aria-hidden=${!isSelected}
|
|
418
175
|
>
|
|
419
176
|
<div slot="header" class="step-header">
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
<
|
|
426
|
-
Step ${stepIndex + 1} of ${this.steps.length}
|
|
427
|
-
</div>
|
|
177
|
+
<ol class="step-rail" aria-label="Progress">${this.steps.map((step, index) => html`
|
|
178
|
+
<li class="step-marker ${index < stepIndex ? 'complete' : index === stepIndex ? 'current' : ''}"
|
|
179
|
+
aria-label=${`${index + 1}. ${step.title}${index < stepIndex ? ', completed' : ''}`}
|
|
180
|
+
aria-current=${index === stepIndex ? 'step' : 'false'} title=${step.title}></li>
|
|
181
|
+
`)}</ol>
|
|
182
|
+
<span class="step-counter">${stepIndex + 1} of ${this.steps.length}</span>
|
|
428
183
|
</div>
|
|
429
184
|
<div class="step-body">
|
|
430
|
-
<div class="
|
|
185
|
+
<div class="heading-row">
|
|
186
|
+
<h2 class="title" tabindex="-1">${stepArg.title}</h2>
|
|
187
|
+
<span class="step-hint" aria-hidden=${!isSelected || this.activeForm === null || this.activeFormValid}
|
|
188
|
+
title="Fill in the required fields to continue.">Complete required fields</span>
|
|
189
|
+
</div>
|
|
190
|
+
${stepArg.description ? html`<p class="description">${stepArg.description}</p>` : ''}
|
|
431
191
|
${stepArg.progressStep && progressStepState ? html`
|
|
432
192
|
<div class="progressStep">
|
|
433
193
|
<dees-progressbar
|
|
@@ -442,31 +202,32 @@ export class DeesStepper extends DeesElement {
|
|
|
442
202
|
</div>
|
|
443
203
|
` : ''}
|
|
444
204
|
<div class="content ${stepArg.progressStep ? 'withProgressStep' : ''}">${stepArg.content}</div>
|
|
205
|
+
${isSelected && this.stepError ? html`<div class="step-error" role="alert">
|
|
206
|
+
<dees-icon icon="lucide:CircleAlert" iconSize="16"></dees-icon><span>${this.stepError}</span>
|
|
207
|
+
</div>` : ''}
|
|
445
208
|
</div>
|
|
446
|
-
<div slot="footer" class="
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
:
|
|
209
|
+
<div slot="footer" class="step-footer">
|
|
210
|
+
<div class="bottomButtons">
|
|
211
|
+
${showBackButton ? html`<dees-button class="back-action" type="ghost" text="Back"
|
|
212
|
+
icon="lucide:ChevronLeft" .disabled=${this.actionPending} @clicked=${this.goBack}></dees-button>`
|
|
213
|
+
: html`<span class="footer-spacer"></span>`}
|
|
450
214
|
${this.cancelable
|
|
451
|
-
? html`<
|
|
452
|
-
class="bottomButton"
|
|
453
|
-
@click=${() => this.handleCancelRequest()}
|
|
454
|
-
>Cancel</div>`
|
|
215
|
+
? html`<dees-button type="ghost" text="Cancel" @clicked=${this.handleCancelRequest}></dees-button>`
|
|
455
216
|
: ''}
|
|
217
|
+
${isSelected && this.stepError && stepArg.validationFunc
|
|
218
|
+
? html`<dees-button type="accent" text="Try again" icon="lucide:RotateCw" @clicked=${this.retryStep}></dees-button>` : ''}
|
|
456
219
|
${stepArg.menuOptions?.map((actionArg, actionIndex) => {
|
|
457
220
|
const isPrimary = actionIndex === stepArg.menuOptions!.length - 1;
|
|
458
|
-
const isDisabled = isPrimary && this.
|
|
459
|
-
return html
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
>${actionArg.name}</div>
|
|
464
|
-
`;
|
|
221
|
+
const isDisabled = this.actionPending || (isPrimary && (!this.activeFormValid || this.progressRunning || (!!this.stepError && !!stepArg.progressStep)));
|
|
222
|
+
return html`<dees-button class="step-action" .text=${actionArg.name}
|
|
223
|
+
.icon=${actionArg.iconName ?? ''} .type=${isPrimary ? 'accent' : 'secondary'}
|
|
224
|
+
.disabled=${isDisabled} .status=${isPrimary && this.actionPending ? 'pending' : 'normal'}
|
|
225
|
+
@clicked=${() => this.handleMenuOptionClick(actionArg, isPrimary)}></dees-button>`;
|
|
465
226
|
}) ?? ''}
|
|
227
|
+
</div>
|
|
466
228
|
</div>
|
|
467
229
|
</dees-tile>`;
|
|
468
230
|
})}
|
|
469
|
-
</div>
|
|
470
231
|
`;
|
|
471
232
|
}
|
|
472
233
|
|
|
@@ -474,74 +235,88 @@ export class DeesStepper extends DeesElement {
|
|
|
474
235
|
return this.steps.findIndex((stepArg2) => stepArg === stepArg2);
|
|
475
236
|
};
|
|
476
237
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
238
|
+
protected willUpdate(changed: Map<PropertyKey, unknown>) {
|
|
239
|
+
if (changed.has('steps') && !this.steps.includes(this.selectedStep)) this.selectedStep = this.steps[0];
|
|
240
|
+
if (changed.has('selectedStep') || changed.has('steps')) {
|
|
241
|
+
if (this.activeValidationStep !== this.selectedStep) {
|
|
242
|
+
this.activeValidationStep?.abortController?.abort();
|
|
243
|
+
if (this.activeValidationStep) this.activeValidationStep.validationFuncCalled = false;
|
|
244
|
+
this.activeValidationStep = this.selectedStep;
|
|
245
|
+
this.stepError = '';
|
|
246
|
+
this.progressRunning = false;
|
|
247
|
+
this.actionPending = false;
|
|
248
|
+
this.actionStep = undefined;
|
|
249
|
+
this.prepareStepForActivation(this.selectedStep);
|
|
250
|
+
}
|
|
482
251
|
}
|
|
483
|
-
this.prepareStepForActivation(this.steps[0]);
|
|
484
|
-
this.selectedStep = this.steps[0];
|
|
485
|
-
await this.updateComplete;
|
|
486
|
-
await this.setScrollStatus();
|
|
487
|
-
// Remove entrance class after initial animation completes
|
|
488
|
-
await this.domtools.convenience.smartdelay.delayFor(350);
|
|
489
|
-
this.shadowRoot!.querySelector('.step.entrance')?.classList.remove('entrance');
|
|
490
252
|
}
|
|
491
253
|
|
|
492
254
|
public async updated(changedProperties: Map<string | number | symbol, unknown>) {
|
|
493
|
-
if (
|
|
494
|
-
|
|
255
|
+
if (changedProperties.has('overlay')) {
|
|
256
|
+
this.scroller?.destroy();
|
|
257
|
+
this.scroller = undefined!;
|
|
258
|
+
this.scrollTarget = undefined;
|
|
259
|
+
this.scrollInFlight = false;
|
|
495
260
|
}
|
|
496
|
-
|
|
261
|
+
const dialog = this.shadowRoot?.querySelector('dialog');
|
|
262
|
+
if (dialog && !dialog.open && this.isConnected) dialog.showModal();
|
|
263
|
+
if (!changedProperties.has('selectedStep') && !changedProperties.has('steps') && !changedProperties.has('overlay')) return;
|
|
497
264
|
await this.setScrollStatus();
|
|
265
|
+
if (!this.isConnected) return;
|
|
266
|
+
if (changedProperties.has('steps') || changedProperties.has('overlay')) {
|
|
267
|
+
this.resizeObserver?.disconnect();
|
|
268
|
+
this.resizeObserver?.observe(this.shadowRoot!.querySelector('.stepperContainer')!);
|
|
269
|
+
for (const tile of this.shadowRoot!.querySelectorAll('.step')) this.resizeObserver?.observe(tile);
|
|
270
|
+
}
|
|
271
|
+
if (this.overlay || this.shadowRoot?.activeElement) {
|
|
272
|
+
this.shadowRoot?.querySelector<HTMLElement>('.selected .title')?.focus({ preventScroll: true });
|
|
273
|
+
}
|
|
498
274
|
}
|
|
499
275
|
|
|
500
276
|
public scroller!: typeof domtools.plugins.SweetScroll.prototype;
|
|
501
277
|
|
|
502
278
|
public async setScrollStatus() {
|
|
503
|
-
const
|
|
504
|
-
const selectedStepElement = this.shadowRoot
|
|
505
|
-
if (!selectedStepElement)
|
|
506
|
-
|
|
507
|
-
|
|
279
|
+
const step = this.selectedStep;
|
|
280
|
+
const selectedStepElement = this.shadowRoot?.querySelector<HTMLElement>('.step.selected');
|
|
281
|
+
if (!selectedStepElement || !this.isConnected) return;
|
|
282
|
+
const form = selectedStepElement.querySelector('dees-form');
|
|
283
|
+
await form?.updateComplete;
|
|
284
|
+
if (!this.isConnected || step !== this.selectedStep) return;
|
|
508
285
|
this.scanActiveForm(selectedStepElement);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
286
|
+
this.positionSelectedStep(true);
|
|
287
|
+
if (!step.validationFuncCalled && step.validationFunc) {
|
|
288
|
+
step.abortController = new AbortController();
|
|
289
|
+
step.validationFuncCalled = true;
|
|
290
|
+
void this.runStepValidation(step, selectedStepElement, step.abortController.signal);
|
|
513
291
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private positionSelectedStep(animate: boolean) {
|
|
295
|
+
const container = this.shadowRoot?.querySelector<HTMLElement>('.stepperContainer');
|
|
296
|
+
const selected = this.shadowRoot?.querySelector<HTMLElement>('.step.selected');
|
|
297
|
+
const first = this.shadowRoot?.querySelector<HTMLElement>('.step');
|
|
298
|
+
if (!container || !selected || !first || !this.isConnected) return;
|
|
299
|
+
container.style.paddingTop = `${Math.max(24, (container.clientHeight - first.offsetHeight) / 2)}px`;
|
|
300
|
+
const position = Math.max(0, selected.offsetTop - (container.clientHeight - selected.offsetHeight) / 2);
|
|
519
301
|
if (!this.scroller) {
|
|
520
302
|
this.scroller = new domtools.plugins.SweetScroll(
|
|
521
|
-
{
|
|
522
|
-
|
|
523
|
-
horizontal: false,
|
|
524
|
-
easing: 'easeInOutExpo',
|
|
525
|
-
duration: 700,
|
|
526
|
-
},
|
|
527
|
-
stepperContainer
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
if (!this.selectedStep.validationFuncCalled && this.selectedStep.validationFunc) {
|
|
531
|
-
this.selectedStep.abortController = new AbortController();
|
|
532
|
-
this.selectedStep.validationFuncCalled = true;
|
|
533
|
-
void this.runStepValidation(
|
|
534
|
-
this.selectedStep,
|
|
535
|
-
selectedStepElement,
|
|
536
|
-
this.selectedStep.abortController.signal,
|
|
303
|
+
{ vertical: true, horizontal: false, easing: 'easeOutCubic', duration: 320, cancellable: false },
|
|
304
|
+
container
|
|
537
305
|
);
|
|
306
|
+
animate = false;
|
|
538
307
|
}
|
|
539
|
-
|
|
308
|
+
// Form validity can resize the footer after selection. Retarget the active
|
|
309
|
+
// scroll instead of letting ResizeObserver snap it to the destination.
|
|
310
|
+
if (this.scrollTarget !== undefined && Math.abs(this.scrollTarget - position) < 1) return;
|
|
311
|
+
const duration = (animate || this.scrollInFlight) && !matchMedia('(prefers-reduced-motion: reduce)').matches ? 320 : 0;
|
|
312
|
+
this.scrollTarget = position;
|
|
313
|
+
this.scroller.to(position, { duration, complete: () => { this.scrollInFlight = false; } });
|
|
314
|
+
this.scrollInFlight = duration > 0;
|
|
540
315
|
}
|
|
541
316
|
|
|
542
317
|
public async goBack() {
|
|
543
318
|
const currentIndex = this.steps.findIndex((stepArg) => stepArg === this.selectedStep);
|
|
544
|
-
if (currentIndex <= 0) {
|
|
319
|
+
if (currentIndex <= 0 || this.selectedStep.allowBack === false) {
|
|
545
320
|
return;
|
|
546
321
|
}
|
|
547
322
|
// Abort any active listeners on current step
|
|
@@ -554,9 +329,11 @@ export class DeesStepper extends DeesElement {
|
|
|
554
329
|
previousStep.validationFuncCalled = false;
|
|
555
330
|
this.prepareStepForActivation(previousStep);
|
|
556
331
|
this.selectedStep = previousStep;
|
|
557
|
-
await this.
|
|
558
|
-
|
|
559
|
-
|
|
332
|
+
await this.updateComplete;
|
|
333
|
+
if (this.isConnected && this.selectedStep === previousStep) {
|
|
334
|
+
try { await previousStep.onReturnToStepFunc?.(this, this.shadowRoot!.querySelector('.selected') as HTMLElement); }
|
|
335
|
+
catch (error) { if (this.selectedStep === previousStep) this.stepError = error instanceof Error ? error.message : 'Could not return to this step.'; }
|
|
336
|
+
}
|
|
560
337
|
}
|
|
561
338
|
|
|
562
339
|
public goNext() {
|
|
@@ -659,9 +436,11 @@ export class DeesStepper extends DeesElement {
|
|
|
659
436
|
return;
|
|
660
437
|
}
|
|
661
438
|
const fields = form.getFormElements();
|
|
662
|
-
this.activeFormValid = fields.every(
|
|
663
|
-
(
|
|
664
|
-
|
|
439
|
+
this.activeFormValid = fields.every(field => {
|
|
440
|
+
if (!field.required || field.disabled) return true;
|
|
441
|
+
const value = 'getValue' in field && field.getValue ? field.getValue() : field.value;
|
|
442
|
+
return value !== null && value !== undefined && value !== '' && value !== false && (!Array.isArray(value) || value.length > 0);
|
|
443
|
+
});
|
|
665
444
|
}
|
|
666
445
|
|
|
667
446
|
/**
|
|
@@ -675,15 +454,23 @@ export class DeesStepper extends DeesElement {
|
|
|
675
454
|
optionArg: plugins.tsclass.website.IMenuItem<DeesStepper>,
|
|
676
455
|
isPrimary: boolean,
|
|
677
456
|
) {
|
|
457
|
+
if (this.actionPending || (isPrimary && this.progressRunning)) return;
|
|
458
|
+
this.recomputeFormValid();
|
|
678
459
|
const form = this.activeForm;
|
|
679
|
-
if (isPrimary &&
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
460
|
+
if (isPrimary && !this.activeFormValid) return;
|
|
461
|
+
const step = this.selectedStep;
|
|
462
|
+
this.actionPending = true;
|
|
463
|
+
this.actionStep = step;
|
|
464
|
+
this.stepError = '';
|
|
465
|
+
try {
|
|
466
|
+
if (isPrimary && form) await form.gatherAndDispatch();
|
|
467
|
+
if (!this.isConnected || step !== this.selectedStep) return;
|
|
468
|
+
await optionArg.action(this);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (this.isConnected && step === this.selectedStep) this.stepError = error instanceof Error ? error.message : 'The action could not be completed. Try again.';
|
|
471
|
+
} finally {
|
|
472
|
+
if (this.actionStep === step) { this.actionPending = false; this.actionStep = undefined; }
|
|
685
473
|
}
|
|
686
|
-
await optionArg.action(this);
|
|
687
474
|
}
|
|
688
475
|
|
|
689
476
|
private getProgressStepState(stepArg: IStep): IStepProgressState {
|
|
@@ -691,11 +478,7 @@ export class DeesStepper extends DeesElement {
|
|
|
691
478
|
return {};
|
|
692
479
|
}
|
|
693
480
|
|
|
694
|
-
|
|
695
|
-
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return stepArg.progressStepState;
|
|
481
|
+
return stepArg.progressStepState ?? this.createInitialProgressStepState(stepArg);
|
|
699
482
|
}
|
|
700
483
|
|
|
701
484
|
private createInitialProgressStepState(stepArg: IStep): IStepProgressState {
|
|
@@ -723,10 +506,11 @@ export class DeesStepper extends DeesElement {
|
|
|
723
506
|
selectedStepElement: HTMLElement,
|
|
724
507
|
signal: AbortSignal,
|
|
725
508
|
): Promise<void> {
|
|
509
|
+
this.progressRunning = !!stepArg.progressStep;
|
|
726
510
|
try {
|
|
727
511
|
await stepArg.validationFunc?.(this, selectedStepElement, signal);
|
|
728
512
|
|
|
729
|
-
if (signal.aborted) {
|
|
513
|
+
if (signal.aborted || !this.isConnected || this.selectedStep !== stepArg) {
|
|
730
514
|
return;
|
|
731
515
|
}
|
|
732
516
|
|
|
@@ -734,12 +518,13 @@ export class DeesStepper extends DeesElement {
|
|
|
734
518
|
this.goNext();
|
|
735
519
|
}
|
|
736
520
|
} catch (error) {
|
|
737
|
-
if (signal.aborted) {
|
|
521
|
+
if (signal.aborted || !this.isConnected || this.selectedStep !== stepArg) {
|
|
738
522
|
return;
|
|
739
523
|
}
|
|
740
524
|
|
|
525
|
+
const errorText = error instanceof Error ? error.message : 'This step could not be completed. Try again.';
|
|
526
|
+
this.stepError = errorText;
|
|
741
527
|
if (stepArg.progressStep) {
|
|
742
|
-
const errorText = error instanceof Error ? error.message : 'Unexpected error';
|
|
743
528
|
this.appendProgressStepLine(`Error: ${errorText}`, stepArg);
|
|
744
529
|
this.updateProgressStep(
|
|
745
530
|
{
|
|
@@ -750,10 +535,27 @@ export class DeesStepper extends DeesElement {
|
|
|
750
535
|
);
|
|
751
536
|
}
|
|
752
537
|
|
|
753
|
-
|
|
538
|
+
} finally {
|
|
539
|
+
if (!signal.aborted && this.selectedStep === stepArg) this.progressRunning = false;
|
|
754
540
|
}
|
|
755
541
|
}
|
|
756
542
|
|
|
543
|
+
/** Rerun the current step after a recoverable validation/provisioning failure. */
|
|
544
|
+
public async retryStep() {
|
|
545
|
+
const step = this.selectedStep;
|
|
546
|
+
if (!step?.validationFunc || this.progressRunning || this.actionPending) return;
|
|
547
|
+
step.abortController?.abort();
|
|
548
|
+
step.validationFuncCalled = false;
|
|
549
|
+
this.stepError = '';
|
|
550
|
+
this.prepareStepForActivation(step);
|
|
551
|
+
await this.setScrollStatus();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private handleDialogCancel(event: Event) {
|
|
555
|
+
event.preventDefault();
|
|
556
|
+
void this.handleCancelRequest();
|
|
557
|
+
}
|
|
558
|
+
|
|
757
559
|
/**
|
|
758
560
|
* Currently-open confirmation modal (if any). Prevents double-stacking when
|
|
759
561
|
* the user clicks the backdrop or the Cancel button while a confirm modal
|
|
@@ -793,10 +595,11 @@ export class DeesStepper extends DeesElement {
|
|
|
793
595
|
* fallback check.
|
|
794
596
|
*/
|
|
795
597
|
public async handleCancelRequest() {
|
|
796
|
-
if (!this.cancelable) return;
|
|
598
|
+
if (!this.cancelable || this.cancelRequestPending || !this.isConnected) return;
|
|
797
599
|
if (this.cancelConfirmModal && this.cancelConfirmModal.isConnected) return;
|
|
798
600
|
this.cancelConfirmModal = undefined;
|
|
799
|
-
this.
|
|
601
|
+
this.cancelRequestPending = true;
|
|
602
|
+
try { this.cancelConfirmModal = await DeesModal.createAndShow({
|
|
800
603
|
heading: 'Cancel setup?',
|
|
801
604
|
width: 'small',
|
|
802
605
|
content: html`
|
|
@@ -816,37 +619,50 @@ export class DeesStepper extends DeesElement {
|
|
|
816
619
|
name: 'Yes, cancel',
|
|
817
620
|
action: async (modal) => {
|
|
818
621
|
this.cancelConfirmModal = undefined;
|
|
819
|
-
modal!.destroy();
|
|
820
|
-
const domtools = await this.domtoolsPromise;
|
|
821
|
-
await domtools.convenience.smartdelay.delayFor(100);
|
|
622
|
+
await modal!.destroy();
|
|
822
623
|
await this.destroy();
|
|
823
624
|
},
|
|
824
625
|
},
|
|
825
626
|
],
|
|
826
627
|
});
|
|
628
|
+
if (!this.isConnected) await this.cancelConfirmModal.destroy();
|
|
629
|
+
} finally { this.cancelRequestPending = false; }
|
|
827
630
|
}
|
|
828
631
|
|
|
829
|
-
public
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (this.selectedStep?.abortController) {
|
|
833
|
-
this.selectedStep.abortController.abort();
|
|
834
|
-
}
|
|
835
|
-
container?.classList.add('predestroy');
|
|
836
|
-
await domtools.convenience.smartdelay.delayFor(250);
|
|
837
|
-
if (this.parentElement) {
|
|
838
|
-
this.parentElement.removeChild(this);
|
|
839
|
-
}
|
|
840
|
-
if (this.windowLayer) {
|
|
841
|
-
await this.windowLayer.destroy();
|
|
842
|
-
}
|
|
632
|
+
public destroy(): Promise<void> {
|
|
633
|
+
return this.destroyPromise ??= this.closeAndRemove();
|
|
634
|
+
}
|
|
843
635
|
|
|
844
|
-
|
|
636
|
+
private async closeAndRemove() {
|
|
637
|
+
this.selectedStep?.abortController?.abort();
|
|
638
|
+
// Close nested dialogs first so the browser can restore the original opener.
|
|
639
|
+
if (this.cancelConfirmModal?.isConnected) await this.cancelConfirmModal.destroy();
|
|
640
|
+
this.shadowRoot?.querySelector('.stepperContainer')?.classList.add('predestroy');
|
|
641
|
+
const tile = this.shadowRoot?.querySelector('.step.selected');
|
|
642
|
+
if (tile) await Promise.allSettled(tile.getAnimations().map(animation => animation.finished));
|
|
643
|
+
this.shadowRoot?.querySelector('dialog')?.close();
|
|
644
|
+
this.remove();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
public async disconnectedCallback() {
|
|
648
|
+
this.selectedStep?.abortController?.abort();
|
|
649
|
+
if (this.selectedStep) this.selectedStep.validationFuncCalled = false;
|
|
650
|
+
this.resizeObserver?.disconnect();
|
|
651
|
+
this.resizeObserver = undefined;
|
|
652
|
+
this.scroller?.destroy();
|
|
653
|
+
this.scroller = undefined!;
|
|
654
|
+
this.scrollTarget = undefined;
|
|
655
|
+
this.scrollInFlight = false;
|
|
656
|
+
this.shadowRoot?.querySelector('dialog')?.close();
|
|
657
|
+
if (this.cancelConfirmModal?.isConnected) void this.cancelConfirmModal.destroy();
|
|
845
658
|
this.activeFormSubscription?.unsubscribe();
|
|
846
659
|
this.activeFormSubscription = undefined;
|
|
847
660
|
this.activeForm = null;
|
|
848
|
-
|
|
849
|
-
|
|
661
|
+
this.actionPending = false;
|
|
662
|
+
this.actionStep = undefined;
|
|
663
|
+
this.progressRunning = false;
|
|
850
664
|
zIndexRegistry.unregister(this);
|
|
665
|
+
this.dispatchEvent(new CustomEvent('stepper-close', { bubbles: true, composed: true }));
|
|
666
|
+
await super.disconnectedCallback();
|
|
851
667
|
}
|
|
852
668
|
}
|