@zywave/zui-slider 4.4.0-pre.2 → 4.4.0-pre.4
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/custom-elements.json +201 -7
- package/dist/zui-slider-css.js +1 -1
- package/dist/zui-slider-css.js.map +1 -1
- package/dist/zui-slider.d.ts +16 -21
- package/dist/zui-slider.js +408 -90
- package/dist/zui-slider.js.map +1 -1
- package/docs/demo.html +99 -78
- package/lab.html +415 -15
- package/package.json +2 -2
- package/src/zui-slider-css.js +1 -1
- package/src/zui-slider.scss +38 -7
- package/src/zui-slider.ts +420 -88
- package/test/zui-slider.test.ts +671 -131
package/test/zui-slider.test.ts
CHANGED
|
@@ -30,7 +30,6 @@ suite('zui-slider', () => {
|
|
|
30
30
|
assert.equal(element.max, 100);
|
|
31
31
|
assert.equal(element.step, 0);
|
|
32
32
|
assert.equal(element.disabled, false);
|
|
33
|
-
assert.isFalse(element.showMinMax);
|
|
34
33
|
assert.isFalse(element.range);
|
|
35
34
|
});
|
|
36
35
|
|
|
@@ -197,38 +196,6 @@ suite('zui-slider', () => {
|
|
|
197
196
|
assert.equal(new FormData(form).get(name), '50');
|
|
198
197
|
});
|
|
199
198
|
|
|
200
|
-
test('native inputs use step="1" when step is 0 or negative', async () => {
|
|
201
|
-
for (const s of [0, -5]) {
|
|
202
|
-
element.step = s;
|
|
203
|
-
await element.updateComplete;
|
|
204
|
-
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
205
|
-
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
206
|
-
assert.equal(input.step, '1');
|
|
207
|
-
assert.equal(floatInput.step, '1');
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('native inputs have correct step attribute when step is set', async () => {
|
|
212
|
-
element.step = 25;
|
|
213
|
-
await element.updateComplete;
|
|
214
|
-
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
215
|
-
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
216
|
-
assert.equal(input.step, '25');
|
|
217
|
-
assert.equal(floatInput.step, '25');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('native inputs reflect min and max properties in DOM', async () => {
|
|
221
|
-
element.min = 10;
|
|
222
|
-
element.max = 90;
|
|
223
|
-
await element.updateComplete;
|
|
224
|
-
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
225
|
-
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
226
|
-
assert.equal(input.min, '10');
|
|
227
|
-
assert.equal(input.max, '90');
|
|
228
|
-
assert.equal(floatInput.min, '10');
|
|
229
|
-
assert.equal(floatInput.max, '90');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
199
|
test('single mode input has inline linear-gradient background with transparent insets', async () => {
|
|
233
200
|
await element.updateComplete;
|
|
234
201
|
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
@@ -251,29 +218,6 @@ suite('zui-slider', () => {
|
|
|
251
218
|
assert.notInclude(input.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-gray)');
|
|
252
219
|
});
|
|
253
220
|
|
|
254
|
-
test('single mode gradient progress stop shifts when value changes', async () => {
|
|
255
|
-
// At value=50 (progress=50): offset = 1.5 - (3*50)/100 = 0
|
|
256
|
-
element.value = '50';
|
|
257
|
-
await element.updateComplete;
|
|
258
|
-
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
259
|
-
assert.include(
|
|
260
|
-
input.style.getPropertyValue('--zui-slider-track-bg'),
|
|
261
|
-
'calc(50% + var(--zui-slider-thumb-size) * 0)'
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
// At value=75 (progress=75): offset = 1.5 - (3*75)/100 = -0.75
|
|
265
|
-
element.value = '75';
|
|
266
|
-
await element.updateComplete;
|
|
267
|
-
assert.include(
|
|
268
|
-
input.style.getPropertyValue('--zui-slider-track-bg'),
|
|
269
|
-
'calc(75% + var(--zui-slider-thumb-size) * -0.75)'
|
|
270
|
-
);
|
|
271
|
-
assert.notInclude(
|
|
272
|
-
input.style.getPropertyValue('--zui-slider-track-bg'),
|
|
273
|
-
'calc(50% + var(--zui-slider-thumb-size) * 0)'
|
|
274
|
-
);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
221
|
test('form reset hides visible floating input', async () => {
|
|
278
222
|
await element.updateComplete;
|
|
279
223
|
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
@@ -365,9 +309,9 @@ suite('zui-slider', () => {
|
|
|
365
309
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
366
310
|
floatInput.value = '75';
|
|
367
311
|
floatInput.dispatchEvent(new Event('input'));
|
|
368
|
-
// Debounce is
|
|
312
|
+
// Debounce is 500ms — value must not update synchronously
|
|
369
313
|
assert.equal(element.value, '50');
|
|
370
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
314
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
371
315
|
assert.equal(element.value, '75');
|
|
372
316
|
});
|
|
373
317
|
|
|
@@ -378,7 +322,7 @@ suite('zui-slider', () => {
|
|
|
378
322
|
floatInput.dispatchEvent(new Event('input'));
|
|
379
323
|
// Debounce is pending — reset before it fires
|
|
380
324
|
form.reset();
|
|
381
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
325
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
382
326
|
// Debounce timer must have been cancelled by formResetCallback
|
|
383
327
|
assert.equal(element.value, '50');
|
|
384
328
|
});
|
|
@@ -393,18 +337,44 @@ suite('zui-slider', () => {
|
|
|
393
337
|
assert.equal(element.value, '50');
|
|
394
338
|
});
|
|
395
339
|
|
|
340
|
+
test('clearing floating input cancels pending debounce so stale value is not committed', async () => {
|
|
341
|
+
await element.updateComplete;
|
|
342
|
+
element.value = '50';
|
|
343
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
344
|
+
// Start a debounce for '75'
|
|
345
|
+
floatInput.value = '75';
|
|
346
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
347
|
+
assert.equal(element.value, '50'); // debounce not yet fired
|
|
348
|
+
// Then clear the field — should cancel the '75' debounce
|
|
349
|
+
floatInput.value = '';
|
|
350
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
351
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
352
|
+
// The pending debounce was cancelled; value stays at '50'
|
|
353
|
+
assert.equal(element.value, '50');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('committing empty floating input reverts display to current value', async () => {
|
|
357
|
+
await element.updateComplete;
|
|
358
|
+
element.value = '75';
|
|
359
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
360
|
+
floatInput.value = '';
|
|
361
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
362
|
+
assert.equal(element.value, '75');
|
|
363
|
+
assert.equal(floatInput.value, '75');
|
|
364
|
+
});
|
|
365
|
+
|
|
396
366
|
test('floating input clamps out-of-bounds values to min/max after debounce', async () => {
|
|
397
367
|
await element.updateComplete;
|
|
398
368
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
399
369
|
floatInput.value = '150';
|
|
400
370
|
floatInput.dispatchEvent(new Event('input'));
|
|
401
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
371
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
402
372
|
assert.equal(element.value, '100');
|
|
403
373
|
|
|
404
374
|
element.min = 20;
|
|
405
375
|
floatInput.value = '5';
|
|
406
376
|
floatInput.dispatchEvent(new Event('input'));
|
|
407
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
377
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
408
378
|
assert.equal(element.value, '20');
|
|
409
379
|
});
|
|
410
380
|
|
|
@@ -414,7 +384,7 @@ suite('zui-slider', () => {
|
|
|
414
384
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
415
385
|
floatInput.value = '47';
|
|
416
386
|
floatInput.dispatchEvent(new Event('input'));
|
|
417
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
387
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
418
388
|
assert.equal(element.value, '50');
|
|
419
389
|
});
|
|
420
390
|
|
|
@@ -433,8 +403,8 @@ suite('zui-slider', () => {
|
|
|
433
403
|
floatInput.dispatchEvent(new Event('change')); // commits immediately
|
|
434
404
|
assert.equal(element.value, '75');
|
|
435
405
|
assert.equal(detail, '75');
|
|
436
|
-
// Debounce timer was cleared — value must not update again after
|
|
437
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
406
|
+
// Debounce timer was cleared — value must not update again after 500ms
|
|
407
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
438
408
|
assert.equal(element.value, '75');
|
|
439
409
|
});
|
|
440
410
|
|
|
@@ -552,7 +522,7 @@ suite('zui-slider', () => {
|
|
|
552
522
|
});
|
|
553
523
|
});
|
|
554
524
|
|
|
555
|
-
suite('zui-slider
|
|
525
|
+
suite('zui-slider min-max labels', () => {
|
|
556
526
|
let element: ZuiSlider;
|
|
557
527
|
|
|
558
528
|
setup(() => {
|
|
@@ -564,26 +534,30 @@ suite('zui-slider showMinMax', () => {
|
|
|
564
534
|
document.body.removeChild(element);
|
|
565
535
|
});
|
|
566
536
|
|
|
567
|
-
test('min-max labels
|
|
537
|
+
test('min-max labels are always rendered when showStepLabels is false', async () => {
|
|
538
|
+
await element.updateComplete;
|
|
539
|
+
assert.exists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('min-max labels are hidden when showStepLabels is true', async () => {
|
|
543
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
544
|
+
element.showStepLabels = true;
|
|
568
545
|
await element.updateComplete;
|
|
569
|
-
|
|
546
|
+
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
547
|
+
assert.exists(labels);
|
|
548
|
+
assert.equal(labels!.style.visibility, 'hidden');
|
|
549
|
+
});
|
|
570
550
|
|
|
551
|
+
test('min-max labels reflect min and max values', async () => {
|
|
571
552
|
element.min = 10;
|
|
572
553
|
element.max = 90;
|
|
573
|
-
element.showMinMax = true;
|
|
574
554
|
await element.updateComplete;
|
|
575
555
|
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
576
|
-
assert.exists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
577
556
|
assert.equal(labels[0].textContent!.trim(), '10');
|
|
578
557
|
assert.equal(labels[1].textContent!.trim(), '90');
|
|
579
|
-
|
|
580
|
-
element.showMinMax = false;
|
|
581
|
-
await element.updateComplete;
|
|
582
|
-
assert.notExists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
583
558
|
});
|
|
584
559
|
|
|
585
|
-
test('min-max labels update when min and max change
|
|
586
|
-
element.showMinMax = true;
|
|
560
|
+
test('min-max labels update when min and max change', async () => {
|
|
587
561
|
element.min = 10;
|
|
588
562
|
element.max = 90;
|
|
589
563
|
await element.updateComplete;
|
|
@@ -595,17 +569,10 @@ suite('zui-slider showMinMax', () => {
|
|
|
595
569
|
assert.equal(labels[1].textContent!.trim(), '95');
|
|
596
570
|
});
|
|
597
571
|
|
|
598
|
-
test('show-min-max HTML attribute enables min-max labels', async () => {
|
|
599
|
-
element.setAttribute('show-min-max', '');
|
|
600
|
-
await element.updateComplete;
|
|
601
|
-
assert.exists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
602
|
-
});
|
|
603
|
-
|
|
604
572
|
test('min-max labels render in range mode', async () => {
|
|
605
573
|
element.setAttribute('range', '');
|
|
606
574
|
element.min = 5;
|
|
607
575
|
element.max = 95;
|
|
608
|
-
element.showMinMax = true;
|
|
609
576
|
await element.updateComplete;
|
|
610
577
|
assert.exists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
611
578
|
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
@@ -613,6 +580,15 @@ suite('zui-slider showMinMax', () => {
|
|
|
613
580
|
assert.equal(labels[0].textContent!.trim(), '5');
|
|
614
581
|
assert.equal(labels[1].textContent!.trim(), '95');
|
|
615
582
|
});
|
|
583
|
+
|
|
584
|
+
test('show-step-labels attribute hides min-max labels', async () => {
|
|
585
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
586
|
+
element.setAttribute('show-step-labels', '');
|
|
587
|
+
await element.updateComplete;
|
|
588
|
+
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
589
|
+
assert.exists(labels);
|
|
590
|
+
assert.equal(labels!.style.visibility, 'hidden');
|
|
591
|
+
});
|
|
616
592
|
});
|
|
617
593
|
|
|
618
594
|
faceTests<ZuiSlider>('zui-slider', {
|
|
@@ -649,7 +625,6 @@ suite('zui-slider range', () => {
|
|
|
649
625
|
assert.equal(element.max, 100);
|
|
650
626
|
assert.equal(element.step, 0);
|
|
651
627
|
assert.isFalse(element.disabled);
|
|
652
|
-
assert.isFalse(element.showMinMax);
|
|
653
628
|
});
|
|
654
629
|
|
|
655
630
|
test('accepts value-start and value-end attributes', () => {
|
|
@@ -987,45 +962,6 @@ suite('zui-slider range', () => {
|
|
|
987
962
|
assert.deepEqual(detail, { valueStart: '20', valueEnd: '100' });
|
|
988
963
|
});
|
|
989
964
|
|
|
990
|
-
test('range inputs reflect min and max properties in DOM', async () => {
|
|
991
|
-
element.min = 10;
|
|
992
|
-
element.max = 90;
|
|
993
|
-
await element.updateComplete;
|
|
994
|
-
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
995
|
-
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
996
|
-
assert.equal(startInput.min, '10');
|
|
997
|
-
assert.equal(startInput.max, '90');
|
|
998
|
-
assert.equal(endInput.min, '10');
|
|
999
|
-
assert.equal(endInput.max, '90');
|
|
1000
|
-
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1001
|
-
floatInputs.forEach((fi) => {
|
|
1002
|
-
assert.equal(fi.min, '10');
|
|
1003
|
-
assert.equal(fi.max, '90');
|
|
1004
|
-
});
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
test('all range inputs use step="1" when component step is 0', async () => {
|
|
1008
|
-
element.step = 0;
|
|
1009
|
-
await element.updateComplete;
|
|
1010
|
-
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1011
|
-
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
1012
|
-
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1013
|
-
assert.equal(startInput.step, '1');
|
|
1014
|
-
assert.equal(endInput.step, '1');
|
|
1015
|
-
floatInputs.forEach((fi) => assert.equal(fi.step, '1'));
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
test('all range inputs have correct step attribute when step is set', async () => {
|
|
1019
|
-
element.step = 10;
|
|
1020
|
-
await element.updateComplete;
|
|
1021
|
-
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1022
|
-
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
1023
|
-
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1024
|
-
assert.equal(startInput.step, '10');
|
|
1025
|
-
assert.equal(endInput.step, '10');
|
|
1026
|
-
floatInputs.forEach((fi) => assert.equal(fi.step, '10'));
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
965
|
test('range-wrapper gradient uses gray when disabled and reverts to blue when re-enabled', async () => {
|
|
1030
966
|
element.disabled = true;
|
|
1031
967
|
await element.updateComplete;
|
|
@@ -1123,9 +1059,9 @@ suite('zui-slider range', () => {
|
|
|
1123
1059
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1124
1060
|
floatInputs[0].value = '40';
|
|
1125
1061
|
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1126
|
-
// Debounce is
|
|
1062
|
+
// Debounce is 500ms — valueStart must not update synchronously
|
|
1127
1063
|
assert.equal(element.valueStart, '20');
|
|
1128
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
1064
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1129
1065
|
assert.equal(element.valueStart, '40');
|
|
1130
1066
|
});
|
|
1131
1067
|
|
|
@@ -1135,9 +1071,9 @@ suite('zui-slider range', () => {
|
|
|
1135
1071
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1136
1072
|
floatInputs[1].value = '60';
|
|
1137
1073
|
floatInputs[1].dispatchEvent(new Event('input'));
|
|
1138
|
-
// Debounce is
|
|
1074
|
+
// Debounce is 500ms — valueEnd must not update synchronously
|
|
1139
1075
|
assert.equal(element.valueEnd, '80');
|
|
1140
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
1076
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1141
1077
|
assert.equal(element.valueEnd, '60');
|
|
1142
1078
|
});
|
|
1143
1079
|
|
|
@@ -1163,13 +1099,13 @@ suite('zui-slider range', () => {
|
|
|
1163
1099
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1164
1100
|
floatInputs[0].value = '150';
|
|
1165
1101
|
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1166
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
1102
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1167
1103
|
assert.equal(element.valueStart, '100');
|
|
1168
1104
|
|
|
1169
1105
|
element.min = 20;
|
|
1170
1106
|
floatInputs[1].value = '5';
|
|
1171
1107
|
floatInputs[1].dispatchEvent(new Event('input'));
|
|
1172
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
1108
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1173
1109
|
assert.equal(element.valueEnd, '20');
|
|
1174
1110
|
});
|
|
1175
1111
|
|
|
@@ -1179,7 +1115,7 @@ suite('zui-slider range', () => {
|
|
|
1179
1115
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1180
1116
|
floatInputs[0].value = '23';
|
|
1181
1117
|
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1182
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
1118
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1183
1119
|
assert.equal(element.valueStart, '20');
|
|
1184
1120
|
});
|
|
1185
1121
|
|
|
@@ -1197,12 +1133,122 @@ suite('zui-slider range', () => {
|
|
|
1197
1133
|
floatInputs[0].dispatchEvent(new Event('change')); // commits immediately
|
|
1198
1134
|
assert.equal(element.valueStart, '30');
|
|
1199
1135
|
assert.deepEqual(detail, { valueStart: '30', valueEnd: '100' });
|
|
1200
|
-
// Debounce timer was cleared — value must not update again after
|
|
1201
|
-
await new Promise<void>((r) => setTimeout(r,
|
|
1136
|
+
// Debounce timer was cleared — value must not update again after 500ms
|
|
1137
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1202
1138
|
assert.equal(element.valueStart, '30');
|
|
1203
1139
|
});
|
|
1204
1140
|
});
|
|
1205
1141
|
|
|
1142
|
+
// Dispatches a click at a given 0–1 fraction of the effective track, matching the
|
|
1143
|
+
// coordinate math in #onTrackClick so the component computes the same fraction back.
|
|
1144
|
+
function clickAtFraction(element: ZuiSlider, fraction: number): void {
|
|
1145
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1146
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1147
|
+
const thumbRadius = 1.5 * parseFloat(getComputedStyle(element).getPropertyValue('--zui-slider-thumb-size'));
|
|
1148
|
+
const effectiveWidth = rect.width - 2 * thumbRadius;
|
|
1149
|
+
const clientX = rect.left + thumbRadius + fraction * effectiveWidth;
|
|
1150
|
+
wrapper.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX }));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
suite('zui-slider range track click', () => {
|
|
1154
|
+
let element: ZuiSlider;
|
|
1155
|
+
let form: HTMLFormElement;
|
|
1156
|
+
|
|
1157
|
+
setup(() => {
|
|
1158
|
+
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1159
|
+
element.setAttribute('range', '');
|
|
1160
|
+
form = buildForm({ enableSubmit: false, appendChildren: [element] });
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
teardown(() => {
|
|
1164
|
+
document.body.removeChild(form);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
test('moves start thumb when clicking left of both thumbs', async () => {
|
|
1168
|
+
await element.updateComplete;
|
|
1169
|
+
clickAtFraction(element, 0.25);
|
|
1170
|
+
assert.equal(element.valueStart, '25');
|
|
1171
|
+
assert.equal(element.valueEnd, '100');
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test('moves end thumb when clicking right of both thumbs', async () => {
|
|
1175
|
+
await element.updateComplete;
|
|
1176
|
+
clickAtFraction(element, 0.75);
|
|
1177
|
+
assert.equal(element.valueStart, '0');
|
|
1178
|
+
assert.equal(element.valueEnd, '75');
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
test('moves start thumb when closer to start than end', async () => {
|
|
1182
|
+
element.valueStart = '20';
|
|
1183
|
+
element.valueEnd = '60';
|
|
1184
|
+
await element.updateComplete;
|
|
1185
|
+
clickAtFraction(element, 0.3);
|
|
1186
|
+
assert.equal(element.valueStart, '30');
|
|
1187
|
+
assert.equal(element.valueEnd, '60');
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test('moves end thumb when closer to end than start', async () => {
|
|
1191
|
+
element.valueStart = '20';
|
|
1192
|
+
element.valueEnd = '60';
|
|
1193
|
+
await element.updateComplete;
|
|
1194
|
+
clickAtFraction(element, 0.5);
|
|
1195
|
+
assert.equal(element.valueStart, '20');
|
|
1196
|
+
assert.equal(element.valueEnd, '50');
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test('prefers start thumb when equidistant from both', async () => {
|
|
1200
|
+
await element.updateComplete;
|
|
1201
|
+
clickAtFraction(element, 0.5);
|
|
1202
|
+
assert.equal(element.valueStart, '50');
|
|
1203
|
+
assert.equal(element.valueEnd, '100');
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test('does nothing when disabled', async () => {
|
|
1207
|
+
element.disabled = true;
|
|
1208
|
+
await element.updateComplete;
|
|
1209
|
+
clickAtFraction(element, 0.25);
|
|
1210
|
+
assert.equal(element.valueStart, '0');
|
|
1211
|
+
assert.equal(element.valueEnd, '100');
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
test('does nothing when readonly', async () => {
|
|
1215
|
+
element.setAttribute('readonly', '');
|
|
1216
|
+
await element.updateComplete;
|
|
1217
|
+
clickAtFraction(element, 0.25);
|
|
1218
|
+
assert.equal(element.valueStart, '0');
|
|
1219
|
+
assert.equal(element.valueEnd, '100');
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
test('fires change event with valueStart and valueEnd detail', async () => {
|
|
1223
|
+
await element.updateComplete;
|
|
1224
|
+
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
1225
|
+
element.addEventListener('change', (e: Event) => {
|
|
1226
|
+
detail = (e as CustomEvent).detail;
|
|
1227
|
+
});
|
|
1228
|
+
clickAtFraction(element, 0.25);
|
|
1229
|
+
assert.deepEqual(detail, { valueStart: '25', valueEnd: '100' });
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
test('snaps clicked value to nearest step', async () => {
|
|
1233
|
+
element.step = 10;
|
|
1234
|
+
await element.updateComplete;
|
|
1235
|
+
clickAtFraction(element, 0.23);
|
|
1236
|
+
assert.equal(element.valueStart, '20');
|
|
1237
|
+
assert.equal(element.valueEnd, '100');
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
test('does not move start thumb when snapped value would equal end', async () => {
|
|
1241
|
+
element.valueStart = '40';
|
|
1242
|
+
element.valueEnd = '50';
|
|
1243
|
+
element.step = 10;
|
|
1244
|
+
await element.updateComplete;
|
|
1245
|
+
// 45% is equidistant (tie → start preferred), but snaps to 50 which is not < endNum=50 → no move
|
|
1246
|
+
clickAtFraction(element, 0.45);
|
|
1247
|
+
assert.equal(element.valueStart, '40');
|
|
1248
|
+
assert.equal(element.valueEnd, '50');
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1206
1252
|
suite('zui-slider step dots', () => {
|
|
1207
1253
|
let element: ZuiSlider;
|
|
1208
1254
|
|
|
@@ -1297,3 +1343,497 @@ suite('zui-slider step dots', () => {
|
|
|
1297
1343
|
assert.equal(wrapper.querySelectorAll('.step-dot').length, 5); // 0, 25, 50, 75, 100
|
|
1298
1344
|
});
|
|
1299
1345
|
});
|
|
1346
|
+
|
|
1347
|
+
suite('zui-slider steps', () => {
|
|
1348
|
+
let element: ZuiSlider;
|
|
1349
|
+
let form: HTMLFormElement;
|
|
1350
|
+
|
|
1351
|
+
setup(() => {
|
|
1352
|
+
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1353
|
+
form = buildForm({ enableSubmit: false, appendChildren: [element] });
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
teardown(() => {
|
|
1357
|
+
document.body.removeChild(form);
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
test('initializes value to first step when current value is not a step label', async () => {
|
|
1361
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1362
|
+
await element.updateComplete;
|
|
1363
|
+
assert.equal(element.value, 'Small');
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
test('value set to a valid step label is preserved', async () => {
|
|
1367
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1368
|
+
element.value = 'Medium';
|
|
1369
|
+
await element.updateComplete;
|
|
1370
|
+
assert.equal(element.value, 'Medium');
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
test('native range input has index-based min, max, and step attributes', async () => {
|
|
1374
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1375
|
+
await element.updateComplete;
|
|
1376
|
+
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1377
|
+
assert.equal(rangeInput.min, '0');
|
|
1378
|
+
assert.equal(rangeInput.max, '2');
|
|
1379
|
+
assert.equal(rangeInput.step, '1');
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
test('native range input value reflects current step index', async () => {
|
|
1383
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1384
|
+
element.value = 'Medium';
|
|
1385
|
+
await element.updateComplete;
|
|
1386
|
+
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1387
|
+
assert.equal(rangeInput.value, '1');
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
test('dragging native range to index sets value to corresponding step label', async () => {
|
|
1391
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1392
|
+
await element.updateComplete;
|
|
1393
|
+
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1394
|
+
rangeInput.value = '2';
|
|
1395
|
+
rangeInput.dispatchEvent(new Event('input'));
|
|
1396
|
+
assert.equal(element.value, 'Large');
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
test('progress is computed by step index, not by numeric value', async () => {
|
|
1400
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1401
|
+
element.value = 'Small';
|
|
1402
|
+
await element.updateComplete;
|
|
1403
|
+
assert.equal(element.progress, 0);
|
|
1404
|
+
element.value = 'Medium';
|
|
1405
|
+
assert.equal(element.progress, 50);
|
|
1406
|
+
element.value = 'Large';
|
|
1407
|
+
assert.equal(element.progress, 100);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
test('floating input is type="text" in steps mode', async () => {
|
|
1411
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1412
|
+
await element.updateComplete;
|
|
1413
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1414
|
+
assert.equal(floatInput.type, 'text');
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
test('floating input change to valid step label updates value immediately', async () => {
|
|
1418
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1419
|
+
await element.updateComplete;
|
|
1420
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1421
|
+
floatInput.value = 'Large';
|
|
1422
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1423
|
+
assert.equal(element.value, 'Large');
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test('floating input change to invalid label reverts input to current value', async () => {
|
|
1427
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1428
|
+
element.value = 'Medium';
|
|
1429
|
+
await element.updateComplete;
|
|
1430
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1431
|
+
floatInput.value = 'ExtraLarge';
|
|
1432
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1433
|
+
assert.equal(floatInput.value, 'Medium');
|
|
1434
|
+
assert.equal(element.value, 'Medium');
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
test('floating input debounce resolves typed numeric alias to nearest step', async () => {
|
|
1438
|
+
element.steps = [0, 25, 50, 75, 100];
|
|
1439
|
+
await element.updateComplete;
|
|
1440
|
+
assert.equal(element.value, '50'); // '50' is a valid label — not snapped on init
|
|
1441
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1442
|
+
floatInput.value = '30';
|
|
1443
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
1444
|
+
assert.equal(element.value, '50'); // debounce not fired yet
|
|
1445
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1446
|
+
// |30-25|=5, |30-50|=20 → snaps to '25'
|
|
1447
|
+
assert.equal(element.value, '25');
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
test('overflow step is preferred when value exceeds last finite step', async () => {
|
|
1451
|
+
element.steps = [
|
|
1452
|
+
{ value: 0, label: '0' },
|
|
1453
|
+
{ value: 100, label: '100' },
|
|
1454
|
+
{ value: Infinity, label: '100+' },
|
|
1455
|
+
];
|
|
1456
|
+
await element.updateComplete;
|
|
1457
|
+
// 150 > lastFiniteValue=100 → overflow step wins
|
|
1458
|
+
element.value = '150';
|
|
1459
|
+
assert.equal(element.value, '100+');
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
test('step dots count equals steps.length', async () => {
|
|
1463
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1464
|
+
await element.updateComplete;
|
|
1465
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3);
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
test('min-max labels show first and last step labels', async () => {
|
|
1469
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1470
|
+
await element.updateComplete;
|
|
1471
|
+
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
1472
|
+
assert.equal(labels[0].textContent!.trim(), 'Small');
|
|
1473
|
+
assert.equal(labels[1].textContent!.trim(), 'Large');
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
test('updating steps snaps current value to first step when no longer a valid label', async () => {
|
|
1477
|
+
element.steps = ['A', 'B', 'C'];
|
|
1478
|
+
element.value = 'B';
|
|
1479
|
+
await element.updateComplete;
|
|
1480
|
+
element.steps = ['X', 'Y', 'Z'];
|
|
1481
|
+
await element.updateComplete;
|
|
1482
|
+
// 'B' not in new steps → snaps to first step
|
|
1483
|
+
assert.equal(element.value, 'X');
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
test('value is included in form submission as the step label', async () => {
|
|
1487
|
+
const name = randString();
|
|
1488
|
+
element.setAttribute('name', name);
|
|
1489
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1490
|
+
element.value = 'Large';
|
|
1491
|
+
await element.updateComplete;
|
|
1492
|
+
assert.equal(new FormData(form).get(name), 'Large');
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
test('object form steps expose label as public value and use numeric value for snapping', async () => {
|
|
1496
|
+
element.steps = [
|
|
1497
|
+
{ value: 0, label: 'None' },
|
|
1498
|
+
{ value: 500, label: 'Half' },
|
|
1499
|
+
{ value: 1000, label: 'Full' },
|
|
1500
|
+
];
|
|
1501
|
+
// |400-0|=400, |400-500|=100, |400-1000|=600 → snaps to 'Half'
|
|
1502
|
+
element.value = '400';
|
|
1503
|
+
await element.updateComplete;
|
|
1504
|
+
assert.equal(element.value, 'Half');
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
test('showStepLabels renders a label element for each step', async () => {
|
|
1508
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1509
|
+
element.showStepLabels = true;
|
|
1510
|
+
await element.updateComplete;
|
|
1511
|
+
const labels = element.shadowRoot!.querySelectorAll('.step-dot-label');
|
|
1512
|
+
assert.equal(labels.length, 3);
|
|
1513
|
+
assert.equal(labels[0].textContent!.trim(), 'Small');
|
|
1514
|
+
assert.equal(labels[1].textContent!.trim(), 'Medium');
|
|
1515
|
+
assert.equal(labels[2].textContent!.trim(), 'Large');
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
test('showStepLabels uses label when provided, value otherwise', async () => {
|
|
1519
|
+
element.steps = [{ value: 0, label: 'None' }, { value: 500 }, { value: 1000, label: 'Full' }];
|
|
1520
|
+
element.showStepLabels = true;
|
|
1521
|
+
await element.updateComplete;
|
|
1522
|
+
const labels = element.shadowRoot!.querySelectorAll('.step-dot-label');
|
|
1523
|
+
assert.equal(labels[0].textContent!.trim(), 'None');
|
|
1524
|
+
assert.equal(labels[1].textContent!.trim(), '500');
|
|
1525
|
+
assert.equal(labels[2].textContent!.trim(), 'Full');
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
test('showStepLabels false renders no label elements', async () => {
|
|
1529
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1530
|
+
await element.updateComplete;
|
|
1531
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot-label').length, 0);
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
test('clearing steps floating input cancels pending debounce so stale step is not committed', async () => {
|
|
1535
|
+
element.steps = [0, 25, 50, 75, 100];
|
|
1536
|
+
element.value = '50';
|
|
1537
|
+
await element.updateComplete;
|
|
1538
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1539
|
+
// Start a debounce for '80' (would snap to '75')
|
|
1540
|
+
floatInput.value = '80';
|
|
1541
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
1542
|
+
assert.equal(element.value, '50'); // debounce not yet fired
|
|
1543
|
+
// Clear the field before debounce fires
|
|
1544
|
+
floatInput.value = '';
|
|
1545
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
1546
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1547
|
+
// The debounce was cancelled; value must stay at '50'
|
|
1548
|
+
assert.equal(element.value, '50');
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
test('committing empty steps floating input reverts display to current step label', async () => {
|
|
1552
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1553
|
+
element.value = 'Medium';
|
|
1554
|
+
await element.updateComplete;
|
|
1555
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1556
|
+
floatInput.value = '';
|
|
1557
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1558
|
+
assert.equal(element.value, 'Medium');
|
|
1559
|
+
assert.equal(floatInput.value, 'Medium');
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
test('steps attribute is parsed from comma-separated string', async () => {
|
|
1563
|
+
element.setAttribute('steps', 'Small,Medium,Large');
|
|
1564
|
+
await element.updateComplete;
|
|
1565
|
+
assert.equal(element.value, 'Small');
|
|
1566
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3);
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
test('form reset restores initial step label', async () => {
|
|
1570
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
1571
|
+
const name = randString();
|
|
1572
|
+
el.setAttribute('name', name);
|
|
1573
|
+
el.steps = ['Small', 'Medium', 'Large'];
|
|
1574
|
+
el.value = 'Medium';
|
|
1575
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
1576
|
+
await el.updateComplete;
|
|
1577
|
+
|
|
1578
|
+
el.value = 'Large';
|
|
1579
|
+
f.reset();
|
|
1580
|
+
|
|
1581
|
+
assert.equal(el.value, 'Medium');
|
|
1582
|
+
assert.equal(new FormData(f).get(name), 'Medium');
|
|
1583
|
+
document.body.removeChild(f);
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
suite('zui-slider steps range', () => {
|
|
1588
|
+
let element: ZuiSlider;
|
|
1589
|
+
let form: HTMLFormElement;
|
|
1590
|
+
|
|
1591
|
+
setup(() => {
|
|
1592
|
+
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1593
|
+
element.setAttribute('range', '');
|
|
1594
|
+
form = buildForm({ enableSubmit: false, appendChildren: [element] });
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
teardown(() => {
|
|
1598
|
+
document.body.removeChild(form);
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
test('invalid default valueStart and valueEnd snap to first and last step', async () => {
|
|
1602
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1603
|
+
await element.updateComplete;
|
|
1604
|
+
assert.equal(element.valueStart, 'A');
|
|
1605
|
+
assert.equal(element.valueEnd, 'E');
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
test('valid step labels for valueStart and valueEnd are preserved', async () => {
|
|
1609
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1610
|
+
element.valueStart = 'B';
|
|
1611
|
+
element.valueEnd = 'D';
|
|
1612
|
+
await element.updateComplete;
|
|
1613
|
+
assert.equal(element.valueStart, 'B');
|
|
1614
|
+
assert.equal(element.valueEnd, 'D');
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
test('dragging start thumb to index equal to endIdx is rejected', async () => {
|
|
1618
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1619
|
+
await element.updateComplete;
|
|
1620
|
+
// valueStart='A'(idx=0), valueEnd='E'(idx=4)
|
|
1621
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1622
|
+
startInput.value = '4'; // equal to endIdx → rejected
|
|
1623
|
+
startInput.dispatchEvent(new Event('input'));
|
|
1624
|
+
assert.equal(element.valueStart, 'A');
|
|
1625
|
+
assert.equal(startInput.value, '0');
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
test('dragging start thumb to valid index advances valueStart', async () => {
|
|
1629
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1630
|
+
await element.updateComplete;
|
|
1631
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1632
|
+
startInput.value = '2'; // index of 'C', less than endIdx=4 → accepted
|
|
1633
|
+
startInput.dispatchEvent(new Event('input'));
|
|
1634
|
+
assert.equal(element.valueStart, 'C');
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
test('progressStart and progressEnd are computed by step index', async () => {
|
|
1638
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1639
|
+
await element.updateComplete;
|
|
1640
|
+
assert.equal(element.progressStart, 0); // 'A' at idx 0/4 = 0%
|
|
1641
|
+
assert.equal(element.progressEnd, 100); // 'E' at idx 4/4 = 100%
|
|
1642
|
+
element.valueStart = 'B'; // idx 1/4 = 25%
|
|
1643
|
+
element.valueEnd = 'D'; // idx 3/4 = 75%
|
|
1644
|
+
assert.equal(element.progressStart, 25);
|
|
1645
|
+
assert.equal(element.progressEnd, 75);
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
test('form value is step labels joined as "start,end"', async () => {
|
|
1649
|
+
const name = randString();
|
|
1650
|
+
element.setAttribute('name', name);
|
|
1651
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1652
|
+
await element.updateComplete;
|
|
1653
|
+
assert.equal(new FormData(form).get(name), 'A,E');
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
test('dynamic steps change snaps both invalid values to first and last step', async () => {
|
|
1657
|
+
element.steps = ['A', 'B', 'C'];
|
|
1658
|
+
element.valueStart = 'A';
|
|
1659
|
+
element.valueEnd = 'C';
|
|
1660
|
+
await element.updateComplete;
|
|
1661
|
+
element.steps = ['X', 'Y', 'Z'];
|
|
1662
|
+
await element.updateComplete;
|
|
1663
|
+
// 'A' and 'C' not in new steps → snap to first and last
|
|
1664
|
+
assert.equal(element.valueStart, 'X');
|
|
1665
|
+
assert.equal(element.valueEnd, 'Z');
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
test('form reset restores initial step labels in range mode', async () => {
|
|
1669
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
1670
|
+
el.setAttribute('range', '');
|
|
1671
|
+
const name = randString();
|
|
1672
|
+
el.setAttribute('name', name);
|
|
1673
|
+
el.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1674
|
+
el.valueStart = 'B';
|
|
1675
|
+
el.valueEnd = 'D';
|
|
1676
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
1677
|
+
await el.updateComplete;
|
|
1678
|
+
|
|
1679
|
+
el.valueStart = 'A';
|
|
1680
|
+
el.valueEnd = 'E';
|
|
1681
|
+
f.reset();
|
|
1682
|
+
|
|
1683
|
+
assert.equal(el.valueStart, 'B');
|
|
1684
|
+
assert.equal(el.valueEnd, 'D');
|
|
1685
|
+
assert.equal(new FormData(f).get(name), 'B,D');
|
|
1686
|
+
document.body.removeChild(f);
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
test('track click moves nearer thumb in steps mode', async () => {
|
|
1690
|
+
// 5 steps → indices 0-4 (total=4); fraction 0.25 → idx 1 ('B'), fraction 0.75 → idx 3 ('D')
|
|
1691
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1692
|
+
await element.updateComplete;
|
|
1693
|
+
|
|
1694
|
+
clickAtFraction(element, 0.25); // closer to start (idx=0 'A') than end (idx=4 'E') → moves start
|
|
1695
|
+
assert.equal(element.valueStart, 'B');
|
|
1696
|
+
assert.equal(element.valueEnd, 'E');
|
|
1697
|
+
|
|
1698
|
+
clickAtFraction(element, 0.75); // closer to end (idx=4 'E') than start (idx=1 'B') → moves end
|
|
1699
|
+
assert.equal(element.valueStart, 'B');
|
|
1700
|
+
assert.equal(element.valueEnd, 'D');
|
|
1701
|
+
});
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
suite('zui-slider stepParser', () => {
|
|
1705
|
+
let element: ZuiSlider;
|
|
1706
|
+
let form: HTMLFormElement;
|
|
1707
|
+
|
|
1708
|
+
setup(() => {
|
|
1709
|
+
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1710
|
+
form = buildForm({ enableSubmit: false, appendChildren: [element] });
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
teardown(() => {
|
|
1714
|
+
document.body.removeChild(form);
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
test('exact step label match does not invoke stepParser', async () => {
|
|
1718
|
+
let callCount = 0;
|
|
1719
|
+
element.steps = [
|
|
1720
|
+
{ value: 1000, label: '1k' },
|
|
1721
|
+
{ value: 2000, label: '2k' },
|
|
1722
|
+
{ value: 3000, label: '3k' },
|
|
1723
|
+
];
|
|
1724
|
+
element.stepParser = (input) => {
|
|
1725
|
+
callCount++;
|
|
1726
|
+
return parseFloat(input) * 1000;
|
|
1727
|
+
};
|
|
1728
|
+
await element.updateComplete;
|
|
1729
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1730
|
+
floatInput.value = '2k'; // exact step label — resolved before stepParser is consulted
|
|
1731
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1732
|
+
assert.equal(element.value, '2k');
|
|
1733
|
+
assert.equal(callCount, 0);
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
test('number return from stepParser snaps to nearest step by numeric value', async () => {
|
|
1737
|
+
element.steps = [
|
|
1738
|
+
{ value: 1000, label: '1k' },
|
|
1739
|
+
{ value: 2000, label: '2k' },
|
|
1740
|
+
{ value: 3000, label: '3k' },
|
|
1741
|
+
];
|
|
1742
|
+
element.stepParser = (input) => {
|
|
1743
|
+
const m = input.match(/^([\d.]+)k$/);
|
|
1744
|
+
return m ? parseFloat(m[1]) * 1000 : null;
|
|
1745
|
+
};
|
|
1746
|
+
await element.updateComplete;
|
|
1747
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1748
|
+
floatInput.value = '1.5k'; // stepParser returns 1500; |1500-1000|=|1500-2000|=500 → tie → first ('1k') wins
|
|
1749
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1750
|
+
assert.equal(element.value, '1k');
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
test('null return from stepParser reverts floating input to current value', async () => {
|
|
1754
|
+
element.steps = [
|
|
1755
|
+
{ value: 1000, label: '1k' },
|
|
1756
|
+
{ value: 2000, label: '2k' },
|
|
1757
|
+
{ value: 3000, label: '3k' },
|
|
1758
|
+
];
|
|
1759
|
+
element.stepParser = (input) => (input === 'bad' ? null : parseFloat(input) * 1000);
|
|
1760
|
+
element.value = '2k';
|
|
1761
|
+
await element.updateComplete;
|
|
1762
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1763
|
+
floatInput.value = 'bad';
|
|
1764
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1765
|
+
// null → rejected; floating input reverts to the current step value
|
|
1766
|
+
assert.equal(floatInput.value, '2k');
|
|
1767
|
+
assert.equal(element.value, '2k');
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
test('null return from debounced stepParser does not update value', async () => {
|
|
1771
|
+
element.steps = [
|
|
1772
|
+
{ value: 1000, label: '1k' },
|
|
1773
|
+
{ value: 2000, label: '2k' },
|
|
1774
|
+
{ value: 3000, label: '3k' },
|
|
1775
|
+
];
|
|
1776
|
+
element.stepParser = () => null;
|
|
1777
|
+
await element.updateComplete;
|
|
1778
|
+
// Default '50' snapped to '1k' on init
|
|
1779
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1780
|
+
floatInput.value = 'anything';
|
|
1781
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
1782
|
+
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1783
|
+
// null → debounce does not call the setter → value stays at '1k'
|
|
1784
|
+
assert.equal(element.value, '1k');
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
test('string return from stepParser is used directly as a step label', async () => {
|
|
1788
|
+
element.steps = [
|
|
1789
|
+
{ value: 1000, label: '1k' },
|
|
1790
|
+
{ value: 2000, label: '2k' },
|
|
1791
|
+
{ value: 3000, label: '3k' },
|
|
1792
|
+
];
|
|
1793
|
+
element.stepParser = (input) => (input.startsWith('3') ? '3k' : null);
|
|
1794
|
+
await element.updateComplete;
|
|
1795
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1796
|
+
floatInput.value = '3000'; // stepParser returns '3k', an exact step label
|
|
1797
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1798
|
+
assert.equal(element.value, '3k');
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
test('programmatic value set uses stepParser for non-label strings via #clampToRange', async () => {
|
|
1802
|
+
element.steps = [
|
|
1803
|
+
{ value: 1000, label: '1k' },
|
|
1804
|
+
{ value: 2000, label: '2k' },
|
|
1805
|
+
{ value: 3000, label: '3k' },
|
|
1806
|
+
];
|
|
1807
|
+
element.stepParser = (input) => {
|
|
1808
|
+
const m = input.match(/^([\d.]+)k$/);
|
|
1809
|
+
return m ? parseFloat(m[1]) * 1000 : null;
|
|
1810
|
+
};
|
|
1811
|
+
await element.updateComplete;
|
|
1812
|
+
// '2.5k' → stepParser returns 2500; |2500-2000|=|2500-3000|=500 → tie → '2k' wins
|
|
1813
|
+
element.value = '2.5k';
|
|
1814
|
+
assert.equal(element.value, '2k');
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
test('stepParser resolves typed aliases in range mode floating inputs', async () => {
|
|
1818
|
+
element.setAttribute('range', '');
|
|
1819
|
+
element.steps = [
|
|
1820
|
+
{ value: 1000, label: '1k' },
|
|
1821
|
+
{ value: 2000, label: '2k' },
|
|
1822
|
+
{ value: 3000, label: '3k' },
|
|
1823
|
+
];
|
|
1824
|
+
element.stepParser = (input) => {
|
|
1825
|
+
const m = input.match(/^([\d.]+)k$/);
|
|
1826
|
+
return m ? parseFloat(m[1]) * 1000 : null;
|
|
1827
|
+
};
|
|
1828
|
+
await element.updateComplete;
|
|
1829
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
1830
|
+
// '1.5k' → stepParser returns 1500; tie at 500 from both '1k' and '2k' → first ('1k') wins
|
|
1831
|
+
floatInputs[0].value = '1.5k';
|
|
1832
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1833
|
+
assert.equal(element.valueStart, '1k');
|
|
1834
|
+
// '2.5k' → stepParser returns 2500; tie → first ('2k') wins
|
|
1835
|
+
floatInputs[1].value = '2.5k';
|
|
1836
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1837
|
+
assert.equal(element.valueEnd, '2k');
|
|
1838
|
+
});
|
|
1839
|
+
});
|