@zywave/zui-slider 4.4.0-pre.5 → 4.4.0-pre.7
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 +69 -3
- package/dist/zui-slider.js +86 -42
- package/dist/zui-slider.js.map +1 -1
- package/package.json +2 -2
- package/src/zui-slider.ts +92 -41
- package/test/zui-slider.test.ts +265 -97
package/test/zui-slider.test.ts
CHANGED
|
@@ -124,7 +124,7 @@ suite('zui-slider', () => {
|
|
|
124
124
|
test('valueAsNumber reads the single-mode backing field regardless of range mode', () => {
|
|
125
125
|
element.value = '35';
|
|
126
126
|
element.range = true;
|
|
127
|
-
// valueAsNumber reads #value (the single-mode backing field)
|
|
127
|
+
// valueAsNumber reads #value (the single-mode backing field); range mode does not update it
|
|
128
128
|
assert.equal(element.valueAsNumber, 35);
|
|
129
129
|
});
|
|
130
130
|
|
|
@@ -218,7 +218,7 @@ suite('zui-slider', () => {
|
|
|
218
218
|
input.value = '75';
|
|
219
219
|
input.dispatchEvent(new Event('input'));
|
|
220
220
|
|
|
221
|
-
// Before that render fires, programmatically revert
|
|
221
|
+
// Before that render fires, programmatically revert. This is the live() failure case.
|
|
222
222
|
element.value = '50'; // Lit virtual is still '50' from the last render
|
|
223
223
|
|
|
224
224
|
await element.updateComplete; // without live(), Lit sees virtual='50'==new='50' → skips DOM write
|
|
@@ -277,7 +277,7 @@ suite('zui-slider', () => {
|
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
test('connectedCallback with range property (not attribute) uses range branch', async () => {
|
|
280
|
-
// el.range = true without setAttribute
|
|
280
|
+
// el.range = true without setAttribute; connectedCallback must check this.range too.
|
|
281
281
|
// Must await so the async connectedCallback reads native inputs and calls _setFormValue.
|
|
282
282
|
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
283
283
|
el.range = true;
|
|
@@ -335,6 +335,18 @@ suite('zui-slider', () => {
|
|
|
335
335
|
assert.equal(element.value, '20');
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
+
test('floating input DOM value resets after commit when clamped value equals current value', async () => {
|
|
339
|
+
element.value = '100';
|
|
340
|
+
await element.updateComplete;
|
|
341
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
342
|
+
// setter no-ops when clamped result matches stored value, so live() needs a forced render to correct the DOM
|
|
343
|
+
floatInput.value = '150';
|
|
344
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
345
|
+
await element.updateComplete;
|
|
346
|
+
assert.equal(element.value, '100');
|
|
347
|
+
assert.equal(floatInput.value, '100');
|
|
348
|
+
});
|
|
349
|
+
|
|
338
350
|
test('floating input snaps typed value to nearest step on commit', async () => {
|
|
339
351
|
await element.updateComplete;
|
|
340
352
|
element.step = 10;
|
|
@@ -366,13 +378,57 @@ suite('zui-slider', () => {
|
|
|
366
378
|
assert.equal(changeCount, 1);
|
|
367
379
|
});
|
|
368
380
|
|
|
369
|
-
test('pressing Enter in floating input commits value
|
|
381
|
+
test('pressing Enter in floating input commits value and retains focus', async () => {
|
|
382
|
+
await element.updateComplete;
|
|
383
|
+
element.value = '50';
|
|
384
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
385
|
+
let blurred = false;
|
|
386
|
+
floatInput.addEventListener('blur', () => (blurred = true));
|
|
387
|
+
floatInput.value = '75';
|
|
388
|
+
floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
389
|
+
assert.equal(element.value, '75');
|
|
390
|
+
assert.isFalse(blurred);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('blurring floating input commits typed value', async () => {
|
|
394
|
+
await element.updateComplete;
|
|
395
|
+
element.value = '50';
|
|
396
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
397
|
+
floatInput.value = '75';
|
|
398
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
399
|
+
floatInput.dispatchEvent(new FocusEvent('blur'));
|
|
400
|
+
assert.equal(element.value, '75');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('blurring floating input after Enter does not dispatch second change event', async () => {
|
|
370
404
|
await element.updateComplete;
|
|
371
405
|
element.value = '50';
|
|
406
|
+
let changeCount = 0;
|
|
407
|
+
element.addEventListener('change', () => changeCount++);
|
|
372
408
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
373
409
|
floatInput.value = '75';
|
|
410
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
374
411
|
floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
412
|
+
floatInput.dispatchEvent(new FocusEvent('blur'));
|
|
375
413
|
assert.equal(element.value, '75');
|
|
414
|
+
assert.equal(changeCount, 1);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('typing after Enter in floating input re-enables blur commit', async () => {
|
|
418
|
+
await element.updateComplete;
|
|
419
|
+
element.value = '50';
|
|
420
|
+
let changeCount = 0;
|
|
421
|
+
element.addEventListener('change', () => changeCount++);
|
|
422
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
423
|
+
floatInput.value = '75';
|
|
424
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
425
|
+
floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
426
|
+
assert.equal(changeCount, 1);
|
|
427
|
+
floatInput.value = '80';
|
|
428
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
429
|
+
floatInput.dispatchEvent(new FocusEvent('blur'));
|
|
430
|
+
assert.equal(element.value, '80');
|
|
431
|
+
assert.equal(changeCount, 2);
|
|
376
432
|
});
|
|
377
433
|
|
|
378
434
|
test('focused floating input stays visible after pointerleave', async () => {
|
|
@@ -404,7 +460,7 @@ suite('zui-slider', () => {
|
|
|
404
460
|
await element.updateComplete;
|
|
405
461
|
assert.isTrue(thumbInputDiv.classList.contains('thumb-input--visible'));
|
|
406
462
|
|
|
407
|
-
// Blur without re-focusing
|
|
463
|
+
// Blur without re-focusing; #blurFloatingInput clears focused and schedules hide.
|
|
408
464
|
// #scheduleHideThumbInput uses a real 100ms setTimeout, so wait past it before asserting.
|
|
409
465
|
floatInput.dispatchEvent(new Event('blur'));
|
|
410
466
|
rangeInput.dispatchEvent(new Event('pointerleave'));
|
|
@@ -422,7 +478,7 @@ suite('zui-slider', () => {
|
|
|
422
478
|
await element.updateComplete;
|
|
423
479
|
assert.isTrue(element.shadowRoot!.querySelector('.thumb-input')!.classList.contains('thumb-input--visible'));
|
|
424
480
|
|
|
425
|
-
// Disconnect then reconnect
|
|
481
|
+
// Disconnect then reconnect; disconnectedCallback should clear state
|
|
426
482
|
document.body.removeChild(form);
|
|
427
483
|
document.body.appendChild(form);
|
|
428
484
|
await element.updateComplete;
|
|
@@ -506,7 +562,6 @@ suite('zui-slider min-max labels', () => {
|
|
|
506
562
|
});
|
|
507
563
|
|
|
508
564
|
test('min-max labels are hidden when showStepLabels is true', async () => {
|
|
509
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
510
565
|
element.showStepLabels = true;
|
|
511
566
|
await element.updateComplete;
|
|
512
567
|
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
@@ -548,7 +603,6 @@ suite('zui-slider min-max labels', () => {
|
|
|
548
603
|
});
|
|
549
604
|
|
|
550
605
|
test('show-step-labels attribute hides min-max labels', async () => {
|
|
551
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
552
606
|
element.setAttribute('show-step-labels', '');
|
|
553
607
|
await element.updateComplete;
|
|
554
608
|
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
@@ -773,61 +827,39 @@ suite('zui-slider range', () => {
|
|
|
773
827
|
document.body.removeChild(f);
|
|
774
828
|
});
|
|
775
829
|
|
|
776
|
-
test('input handler rejects start dragging past end', async () => {
|
|
830
|
+
test('input handler rejects start dragging to or past end', async () => {
|
|
777
831
|
await element.updateComplete;
|
|
778
832
|
element.valueStart = '20';
|
|
779
833
|
element.valueEnd = '60';
|
|
780
|
-
|
|
781
834
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
782
|
-
startInput.value = '70';
|
|
783
|
-
startInput.dispatchEvent(new Event('input'));
|
|
784
835
|
|
|
836
|
+
startInput.value = '70'; // past end
|
|
837
|
+
startInput.dispatchEvent(new Event('input'));
|
|
785
838
|
assert.equal(element.valueStart, '20');
|
|
786
839
|
assert.equal(element.valueEnd, '60');
|
|
787
840
|
assert.equal(startInput.value, '20');
|
|
788
|
-
});
|
|
789
841
|
|
|
790
|
-
|
|
791
|
-
await element.updateComplete;
|
|
792
|
-
element.valueStart = '20';
|
|
793
|
-
element.valueEnd = '60';
|
|
794
|
-
|
|
795
|
-
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
796
|
-
startInput.value = '60';
|
|
842
|
+
startInput.value = '60'; // equal to end
|
|
797
843
|
startInput.dispatchEvent(new Event('input'));
|
|
798
|
-
|
|
799
|
-
// start >= end is rejected; input snaps back and state is unchanged
|
|
800
844
|
assert.equal(element.valueStart, '20');
|
|
801
|
-
assert.equal(element.valueEnd, '60');
|
|
802
845
|
assert.equal(startInput.value, '20');
|
|
803
846
|
});
|
|
804
847
|
|
|
805
|
-
test('input handler rejects end dragging before start', async () => {
|
|
848
|
+
test('input handler rejects end dragging to or before start', async () => {
|
|
806
849
|
await element.updateComplete;
|
|
807
850
|
element.valueStart = '30';
|
|
808
851
|
element.valueEnd = '70';
|
|
809
|
-
|
|
810
852
|
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
811
|
-
endInput.value = '20';
|
|
812
|
-
endInput.dispatchEvent(new Event('input'));
|
|
813
853
|
|
|
854
|
+
endInput.value = '20'; // before start
|
|
855
|
+
endInput.dispatchEvent(new Event('input'));
|
|
814
856
|
assert.equal(element.valueEnd, '70');
|
|
815
857
|
assert.equal(element.valueStart, '30');
|
|
816
858
|
assert.equal(endInput.value, '70');
|
|
817
|
-
});
|
|
818
859
|
|
|
819
|
-
|
|
820
|
-
await element.updateComplete;
|
|
821
|
-
element.valueStart = '30';
|
|
822
|
-
element.valueEnd = '70';
|
|
823
|
-
|
|
824
|
-
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
825
|
-
endInput.value = '30';
|
|
860
|
+
endInput.value = '30'; // equal to start
|
|
826
861
|
endInput.dispatchEvent(new Event('input'));
|
|
827
|
-
|
|
828
|
-
// end <= start is rejected; input snaps back and state is unchanged
|
|
829
862
|
assert.equal(element.valueEnd, '70');
|
|
830
|
-
assert.equal(element.valueStart, '30');
|
|
831
863
|
assert.equal(endInput.value, '70');
|
|
832
864
|
});
|
|
833
865
|
|
|
@@ -868,63 +900,47 @@ suite('zui-slider range', () => {
|
|
|
868
900
|
assert.include(thumbInputDivs[0].style.left, '30%');
|
|
869
901
|
});
|
|
870
902
|
|
|
871
|
-
test('
|
|
903
|
+
test('native range inputs and floating inputs stay in sync with component values after drag', async () => {
|
|
872
904
|
await element.updateComplete;
|
|
873
905
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
906
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
874
907
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
875
908
|
|
|
876
909
|
startInput.value = '30';
|
|
877
910
|
startInput.dispatchEvent(new Event('input'));
|
|
911
|
+
endInput.value = '60';
|
|
912
|
+
endInput.dispatchEvent(new Event('input'));
|
|
878
913
|
await element.updateComplete;
|
|
879
914
|
|
|
880
915
|
assert.equal(element.valueStart, '30');
|
|
881
916
|
assert.equal(startInput.value, '30');
|
|
882
917
|
assert.equal(floatInputs[0].value, '30');
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
test('valueEnd, range-end native input, and end floating input stay in sync after drag', async () => {
|
|
886
|
-
await element.updateComplete;
|
|
887
|
-
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
888
|
-
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
889
|
-
|
|
890
|
-
endInput.value = '60';
|
|
891
|
-
endInput.dispatchEvent(new Event('input'));
|
|
892
|
-
await element.updateComplete;
|
|
893
|
-
|
|
894
918
|
assert.equal(element.valueEnd, '60');
|
|
895
919
|
assert.equal(endInput.value, '60');
|
|
896
920
|
assert.equal(floatInputs[1].value, '60');
|
|
897
921
|
});
|
|
898
922
|
|
|
899
|
-
test('programmatic
|
|
923
|
+
test('programmatic value resets after drag keep both native range inputs in sync via live()', async () => {
|
|
900
924
|
element.valueStart = '20';
|
|
925
|
+
element.valueEnd = '80';
|
|
901
926
|
await element.updateComplete;
|
|
902
927
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
928
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
903
929
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
904
930
|
|
|
905
931
|
startInput.value = '40';
|
|
906
932
|
startInput.dispatchEvent(new Event('input'));
|
|
907
|
-
element.valueStart = '20'; // Lit virtual is still '20'
|
|
908
|
-
|
|
909
|
-
await element.updateComplete; // live() ensures '20' is written despite virtual==='20'
|
|
910
|
-
|
|
911
|
-
assert.equal(element.valueStart, '20');
|
|
912
|
-
assert.equal(startInput.value, '20');
|
|
913
|
-
assert.equal(floatInputs[0].value, '20');
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
test('programmatic valueEnd reset after drag keeps range-end native input in sync', async () => {
|
|
917
|
-
element.valueEnd = '80';
|
|
918
|
-
await element.updateComplete;
|
|
919
|
-
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
920
|
-
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
933
|
+
element.valueStart = '20'; // Lit virtual is still '20'; live() must force the DOM write
|
|
921
934
|
|
|
922
935
|
endInput.value = '60';
|
|
923
936
|
endInput.dispatchEvent(new Event('input'));
|
|
924
|
-
element.valueEnd = '80'; //
|
|
937
|
+
element.valueEnd = '80'; // same live() invariant for end thumb
|
|
925
938
|
|
|
926
|
-
await element.updateComplete;
|
|
939
|
+
await element.updateComplete;
|
|
927
940
|
|
|
941
|
+
assert.equal(element.valueStart, '20');
|
|
942
|
+
assert.equal(startInput.value, '20');
|
|
943
|
+
assert.equal(floatInputs[0].value, '20');
|
|
928
944
|
assert.equal(element.valueEnd, '80');
|
|
929
945
|
assert.equal(endInput.value, '80');
|
|
930
946
|
assert.equal(floatInputs[1].value, '80');
|
|
@@ -1036,15 +1052,19 @@ suite('zui-slider range', () => {
|
|
|
1036
1052
|
|
|
1037
1053
|
test('range floating inputs clamp out-of-bounds values to min/max on commit', async () => {
|
|
1038
1054
|
await element.updateComplete;
|
|
1055
|
+
element.valueStart = '20';
|
|
1056
|
+
element.valueEnd = '80';
|
|
1039
1057
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1040
|
-
|
|
1058
|
+
|
|
1059
|
+
// Start thumb: type below min → clamped to min (0); 0 < endNum (80) → no nudge
|
|
1060
|
+
floatInputs[0].value = '-50';
|
|
1041
1061
|
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1042
|
-
assert.equal(element.valueStart, '
|
|
1062
|
+
assert.equal(element.valueStart, '0');
|
|
1043
1063
|
|
|
1044
|
-
|
|
1045
|
-
floatInputs[1].value = '
|
|
1064
|
+
// End thumb: type above max → clamped to max (100); 100 > startNum (0) → no nudge
|
|
1065
|
+
floatInputs[1].value = '150';
|
|
1046
1066
|
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1047
|
-
assert.equal(element.valueEnd, '
|
|
1067
|
+
assert.equal(element.valueEnd, '100');
|
|
1048
1068
|
});
|
|
1049
1069
|
|
|
1050
1070
|
test('range floating input snaps typed valueStart to nearest step on commit', async () => {
|
|
@@ -1070,13 +1090,16 @@ suite('zui-slider range', () => {
|
|
|
1070
1090
|
assert.deepEqual(detail, { valueStart: '30', valueEnd: '100' });
|
|
1071
1091
|
});
|
|
1072
1092
|
|
|
1073
|
-
test('pressing Enter in range floating inputs commits values', async () => {
|
|
1093
|
+
test('pressing Enter in range floating inputs commits values and retains focus', async () => {
|
|
1074
1094
|
await element.updateComplete;
|
|
1075
1095
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1096
|
+
let startBlurred = false;
|
|
1097
|
+
floatInputs[0].addEventListener('blur', () => (startBlurred = true));
|
|
1076
1098
|
|
|
1077
1099
|
floatInputs[0].value = '30';
|
|
1078
1100
|
floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1079
1101
|
assert.equal(element.valueStart, '30');
|
|
1102
|
+
assert.isFalse(startBlurred);
|
|
1080
1103
|
|
|
1081
1104
|
floatInputs[1].value = '70';
|
|
1082
1105
|
floatInputs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
@@ -1097,9 +1120,107 @@ suite('zui-slider range', () => {
|
|
|
1097
1120
|
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1098
1121
|
assert.equal(changeCount, 0);
|
|
1099
1122
|
});
|
|
1123
|
+
|
|
1124
|
+
test('range start floating input nudges to one step below end when typed value equals end', async () => {
|
|
1125
|
+
await element.updateComplete;
|
|
1126
|
+
element.valueStart = '20';
|
|
1127
|
+
element.valueEnd = '50';
|
|
1128
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1129
|
+
floatInputs[0].value = '50';
|
|
1130
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1131
|
+
// typed 50 = end 50; effectiveStep=1; nudge to 50-1=49 < 50 → committed as '49'
|
|
1132
|
+
assert.equal(element.valueStart, '49');
|
|
1133
|
+
assert.equal(element.valueEnd, '50');
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
test('range end floating input nudges to one step above start when typed value equals start', async () => {
|
|
1137
|
+
await element.updateComplete;
|
|
1138
|
+
element.valueStart = '50';
|
|
1139
|
+
element.valueEnd = '80';
|
|
1140
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1141
|
+
floatInputs[1].value = '50';
|
|
1142
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1143
|
+
// typed 50 = start 50; effectiveStep=1; nudge to 50+1=51 > 50 → committed as '51'
|
|
1144
|
+
assert.equal(element.valueEnd, '51');
|
|
1145
|
+
assert.equal(element.valueStart, '50');
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
test('range floating input nudge respects decimal step', async () => {
|
|
1149
|
+
element.step = 0.1;
|
|
1150
|
+
await element.updateComplete;
|
|
1151
|
+
element.valueStart = '1';
|
|
1152
|
+
element.valueEnd = '5';
|
|
1153
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1154
|
+
floatInputs[1].value = '1';
|
|
1155
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1156
|
+
// typed 1 = start 1; effectiveStep=0.1; nudge to 1+0.1=1.1 > 1 → committed as '1.1'
|
|
1157
|
+
assert.equal(element.valueEnd, '1.1');
|
|
1158
|
+
assert.equal(element.valueStart, '1');
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
test('range end floating input DOM value resets after commit when nudged value equals current valueEnd', async () => {
|
|
1162
|
+
element.step = 10;
|
|
1163
|
+
element.valueStart = '40';
|
|
1164
|
+
element.valueEnd = '50';
|
|
1165
|
+
await element.updateComplete;
|
|
1166
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1167
|
+
// setter no-ops when nudged result matches stored valueEnd, so live() needs a forced render to correct the DOM
|
|
1168
|
+
floatInputs[1].value = '40';
|
|
1169
|
+
floatInputs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1170
|
+
await element.updateComplete;
|
|
1171
|
+
assert.equal(element.valueEnd, '50');
|
|
1172
|
+
assert.equal(floatInputs[1].value, '50');
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
test('blurring range floating input commits typed value', async () => {
|
|
1176
|
+
await element.updateComplete;
|
|
1177
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1178
|
+
floatInputs[0].value = '30';
|
|
1179
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1180
|
+
floatInputs[0].dispatchEvent(new FocusEvent('blur'));
|
|
1181
|
+
assert.equal(element.valueStart, '30');
|
|
1182
|
+
|
|
1183
|
+
floatInputs[1].value = '70';
|
|
1184
|
+
floatInputs[1].dispatchEvent(new Event('input'));
|
|
1185
|
+
floatInputs[1].dispatchEvent(new FocusEvent('blur'));
|
|
1186
|
+
assert.equal(element.valueEnd, '70');
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
test('blurring range floating input after Enter does not dispatch second change event', async () => {
|
|
1190
|
+
await element.updateComplete;
|
|
1191
|
+
element.valueStart = '20';
|
|
1192
|
+
element.valueEnd = '80';
|
|
1193
|
+
let changeCount = 0;
|
|
1194
|
+
element.addEventListener('change', () => changeCount++);
|
|
1195
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1196
|
+
floatInputs[0].value = '30';
|
|
1197
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1198
|
+
floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1199
|
+
floatInputs[0].dispatchEvent(new FocusEvent('blur'));
|
|
1200
|
+
assert.equal(element.valueStart, '30');
|
|
1201
|
+
assert.equal(changeCount, 1);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
test('typing after Enter in range floating input re-enables blur commit', async () => {
|
|
1205
|
+
await element.updateComplete;
|
|
1206
|
+
element.valueStart = '20';
|
|
1207
|
+
element.valueEnd = '80';
|
|
1208
|
+
let changeCount = 0;
|
|
1209
|
+
element.addEventListener('change', () => changeCount++);
|
|
1210
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1211
|
+
floatInputs[0].value = '30';
|
|
1212
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1213
|
+
floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1214
|
+
assert.equal(changeCount, 1);
|
|
1215
|
+
floatInputs[0].value = '40';
|
|
1216
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1217
|
+
floatInputs[0].dispatchEvent(new FocusEvent('blur'));
|
|
1218
|
+
assert.equal(element.valueStart, '40');
|
|
1219
|
+
assert.equal(changeCount, 2);
|
|
1220
|
+
});
|
|
1100
1221
|
});
|
|
1101
1222
|
|
|
1102
|
-
// Dispatches a click at a given 0
|
|
1223
|
+
// Dispatches a click at a given 0-1 fraction of the effective track, matching the
|
|
1103
1224
|
// coordinate math in #onTrackClick so the component computes the same fraction back.
|
|
1104
1225
|
function clickAtFraction(element: ZuiSlider, fraction: number): void {
|
|
1105
1226
|
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
@@ -1207,6 +1328,48 @@ suite('zui-slider range track click', () => {
|
|
|
1207
1328
|
assert.equal(element.valueStart, '40');
|
|
1208
1329
|
assert.equal(element.valueEnd, '50');
|
|
1209
1330
|
});
|
|
1331
|
+
|
|
1332
|
+
test('drag-end synthesized click on range-start input does not move range-end', async () => {
|
|
1333
|
+
// The browser synthesizes a click on the dragged input at mouseup; it must not reach #onTrackClick.
|
|
1334
|
+
element.valueStart = '5';
|
|
1335
|
+
element.valueEnd = '50';
|
|
1336
|
+
await element.updateComplete;
|
|
1337
|
+
|
|
1338
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1339
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1340
|
+
|
|
1341
|
+
startInput.value = '80';
|
|
1342
|
+
startInput.dispatchEvent(new Event('input'));
|
|
1343
|
+
assert.equal(element.valueStart, '5');
|
|
1344
|
+
assert.equal(element.valueEnd, '50');
|
|
1345
|
+
|
|
1346
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1347
|
+
const thumbRadius = 1.5 * parseFloat(getComputedStyle(element).getPropertyValue('--zui-slider-thumb-size'));
|
|
1348
|
+
const effectiveWidth = rect.width - 2 * thumbRadius;
|
|
1349
|
+
const clientX = rect.left + thumbRadius + 0.95 * effectiveWidth;
|
|
1350
|
+
startInput.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX }));
|
|
1351
|
+
|
|
1352
|
+
assert.equal(element.valueStart, '5');
|
|
1353
|
+
assert.equal(element.valueEnd, '50');
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
test('clicking inside floating input does not move a thumb', async () => {
|
|
1357
|
+
// The floating input is inside .range-wrapper; its click bubbles to the track handler.
|
|
1358
|
+
element.valueStart = '20';
|
|
1359
|
+
element.valueEnd = '60';
|
|
1360
|
+
await element.updateComplete;
|
|
1361
|
+
|
|
1362
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1363
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1364
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1365
|
+
const thumbRadius = 1.5 * parseFloat(getComputedStyle(element).getPropertyValue('--zui-slider-thumb-size'));
|
|
1366
|
+
const effectiveWidth = rect.width - 2 * thumbRadius;
|
|
1367
|
+
const clientX = rect.left + thumbRadius + 0.9 * effectiveWidth;
|
|
1368
|
+
floatInput.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX }));
|
|
1369
|
+
|
|
1370
|
+
assert.equal(element.valueStart, '20');
|
|
1371
|
+
assert.equal(element.valueEnd, '60');
|
|
1372
|
+
});
|
|
1210
1373
|
});
|
|
1211
1374
|
|
|
1212
1375
|
suite('zui-slider step dots', () => {
|
|
@@ -1228,13 +1391,10 @@ suite('zui-slider step dots', () => {
|
|
|
1228
1391
|
assert.equal(dots.length, 5); // 0, 25, 50, 75, 100
|
|
1229
1392
|
});
|
|
1230
1393
|
|
|
1231
|
-
test('step dots not rendered when step is 0', async () => {
|
|
1394
|
+
test('step dots not rendered when step is 0 or negative', async () => {
|
|
1232
1395
|
element.step = 0;
|
|
1233
1396
|
await element.updateComplete;
|
|
1234
1397
|
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
test('step dots not rendered when step is negative', async () => {
|
|
1238
1398
|
element.step = -5;
|
|
1239
1399
|
await element.updateComplete;
|
|
1240
1400
|
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
@@ -1302,6 +1462,7 @@ suite('zui-slider steps', () => {
|
|
|
1302
1462
|
setup(() => {
|
|
1303
1463
|
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1304
1464
|
form = buildForm({ enableSubmit: false, appendChildren: [element] });
|
|
1465
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1305
1466
|
});
|
|
1306
1467
|
|
|
1307
1468
|
teardown(() => {
|
|
@@ -1309,20 +1470,17 @@ suite('zui-slider steps', () => {
|
|
|
1309
1470
|
});
|
|
1310
1471
|
|
|
1311
1472
|
test('initializes value to first step when current value is not a step label', async () => {
|
|
1312
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1313
1473
|
await element.updateComplete;
|
|
1314
1474
|
assert.equal(element.value, 'Small');
|
|
1315
1475
|
});
|
|
1316
1476
|
|
|
1317
1477
|
test('value set to a valid step label is preserved', async () => {
|
|
1318
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1319
1478
|
element.value = 'Medium';
|
|
1320
1479
|
await element.updateComplete;
|
|
1321
1480
|
assert.equal(element.value, 'Medium');
|
|
1322
1481
|
});
|
|
1323
1482
|
|
|
1324
1483
|
test('native range input has index-based min, max, and step attributes', async () => {
|
|
1325
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1326
1484
|
await element.updateComplete;
|
|
1327
1485
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1328
1486
|
assert.equal(rangeInput.min, '0');
|
|
@@ -1331,7 +1489,6 @@ suite('zui-slider steps', () => {
|
|
|
1331
1489
|
});
|
|
1332
1490
|
|
|
1333
1491
|
test('native range input value reflects current step index', async () => {
|
|
1334
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1335
1492
|
element.value = 'Medium';
|
|
1336
1493
|
await element.updateComplete;
|
|
1337
1494
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
@@ -1339,7 +1496,6 @@ suite('zui-slider steps', () => {
|
|
|
1339
1496
|
});
|
|
1340
1497
|
|
|
1341
1498
|
test('dragging native range to index sets value to corresponding step label', async () => {
|
|
1342
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1343
1499
|
await element.updateComplete;
|
|
1344
1500
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1345
1501
|
rangeInput.value = '2';
|
|
@@ -1348,7 +1504,6 @@ suite('zui-slider steps', () => {
|
|
|
1348
1504
|
});
|
|
1349
1505
|
|
|
1350
1506
|
test('progress is computed by step index, not by numeric value', async () => {
|
|
1351
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1352
1507
|
element.value = 'Small';
|
|
1353
1508
|
await element.updateComplete;
|
|
1354
1509
|
assert.equal(element.progress, 0);
|
|
@@ -1359,14 +1514,12 @@ suite('zui-slider steps', () => {
|
|
|
1359
1514
|
});
|
|
1360
1515
|
|
|
1361
1516
|
test('floating input is type="text" in steps mode', async () => {
|
|
1362
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1363
1517
|
await element.updateComplete;
|
|
1364
1518
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1365
1519
|
assert.equal(floatInput.type, 'text');
|
|
1366
1520
|
});
|
|
1367
1521
|
|
|
1368
1522
|
test('floating input change to valid step label updates value immediately', async () => {
|
|
1369
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1370
1523
|
await element.updateComplete;
|
|
1371
1524
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1372
1525
|
floatInput.value = 'Large';
|
|
@@ -1375,7 +1528,6 @@ suite('zui-slider steps', () => {
|
|
|
1375
1528
|
});
|
|
1376
1529
|
|
|
1377
1530
|
test('steps floating input change does not dispatch change event when value is unchanged', async () => {
|
|
1378
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1379
1531
|
element.value = 'Medium';
|
|
1380
1532
|
await element.updateComplete;
|
|
1381
1533
|
let changeCount = 0;
|
|
@@ -1389,7 +1541,6 @@ suite('zui-slider steps', () => {
|
|
|
1389
1541
|
});
|
|
1390
1542
|
|
|
1391
1543
|
test('floating input change to invalid label reverts input to current value', async () => {
|
|
1392
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1393
1544
|
element.value = 'Medium';
|
|
1394
1545
|
await element.updateComplete;
|
|
1395
1546
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
@@ -1402,7 +1553,7 @@ suite('zui-slider steps', () => {
|
|
|
1402
1553
|
test('floating input resolves typed numeric alias to nearest step on commit', async () => {
|
|
1403
1554
|
element.steps = [0, 25, 50, 75, 100];
|
|
1404
1555
|
await element.updateComplete;
|
|
1405
|
-
assert.equal(element.value, '50'); // '50' is a valid label
|
|
1556
|
+
assert.equal(element.value, '50'); // '50' is a valid label; not snapped on init
|
|
1406
1557
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1407
1558
|
floatInput.value = '30';
|
|
1408
1559
|
floatInput.dispatchEvent(new Event('change'));
|
|
@@ -1423,13 +1574,11 @@ suite('zui-slider steps', () => {
|
|
|
1423
1574
|
});
|
|
1424
1575
|
|
|
1425
1576
|
test('step dots count equals steps.length', async () => {
|
|
1426
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1427
1577
|
await element.updateComplete;
|
|
1428
1578
|
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3);
|
|
1429
1579
|
});
|
|
1430
1580
|
|
|
1431
1581
|
test('min-max labels show first and last step labels', async () => {
|
|
1432
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1433
1582
|
await element.updateComplete;
|
|
1434
1583
|
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
1435
1584
|
assert.equal(labels[0].textContent!.trim(), 'Small');
|
|
@@ -1449,7 +1598,6 @@ suite('zui-slider steps', () => {
|
|
|
1449
1598
|
test('value is included in form submission as the step label', async () => {
|
|
1450
1599
|
const name = randString();
|
|
1451
1600
|
element.setAttribute('name', name);
|
|
1452
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1453
1601
|
element.value = 'Large';
|
|
1454
1602
|
await element.updateComplete;
|
|
1455
1603
|
assert.equal(new FormData(form).get(name), 'Large');
|
|
@@ -1468,7 +1616,6 @@ suite('zui-slider steps', () => {
|
|
|
1468
1616
|
});
|
|
1469
1617
|
|
|
1470
1618
|
test('showStepLabels renders a label element for each step', async () => {
|
|
1471
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1472
1619
|
element.showStepLabels = true;
|
|
1473
1620
|
await element.updateComplete;
|
|
1474
1621
|
const labels = element.shadowRoot!.querySelectorAll('.step-dot-label');
|
|
@@ -1489,13 +1636,11 @@ suite('zui-slider steps', () => {
|
|
|
1489
1636
|
});
|
|
1490
1637
|
|
|
1491
1638
|
test('showStepLabels false renders no label elements', async () => {
|
|
1492
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1493
1639
|
await element.updateComplete;
|
|
1494
1640
|
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot-label').length, 0);
|
|
1495
1641
|
});
|
|
1496
1642
|
|
|
1497
1643
|
test('pressing Enter in steps floating input commits the step label', async () => {
|
|
1498
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1499
1644
|
await element.updateComplete;
|
|
1500
1645
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1501
1646
|
floatInput.value = 'Large';
|
|
@@ -1504,7 +1649,6 @@ suite('zui-slider steps', () => {
|
|
|
1504
1649
|
});
|
|
1505
1650
|
|
|
1506
1651
|
test('committing empty steps floating input reverts display to current step label', async () => {
|
|
1507
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1508
1652
|
element.value = 'Medium';
|
|
1509
1653
|
await element.updateComplete;
|
|
1510
1654
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
@@ -1654,6 +1798,30 @@ suite('zui-slider steps range', () => {
|
|
|
1654
1798
|
assert.equal(element.valueStart, 'B');
|
|
1655
1799
|
assert.equal(element.valueEnd, 'D');
|
|
1656
1800
|
});
|
|
1801
|
+
|
|
1802
|
+
test('steps range end floating input nudges to next step when typed value matches start step', async () => {
|
|
1803
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1804
|
+
element.valueStart = 'C'; // idx=2
|
|
1805
|
+
element.valueEnd = 'E'; // idx=4
|
|
1806
|
+
await element.updateComplete;
|
|
1807
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
1808
|
+
floatInputs[1].value = 'C'; // typed = start ('C', idx=2); resolvedIdx(2) <= startIdx(2) → nudge to idx=3 ('D')
|
|
1809
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1810
|
+
assert.equal(element.valueEnd, 'D');
|
|
1811
|
+
assert.equal(element.valueStart, 'C');
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
test('steps range start floating input nudges to previous step when typed value matches end step', async () => {
|
|
1815
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1816
|
+
element.valueStart = 'A'; // idx=0
|
|
1817
|
+
element.valueEnd = 'C'; // idx=2
|
|
1818
|
+
await element.updateComplete;
|
|
1819
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
1820
|
+
floatInputs[0].value = 'C'; // typed = end ('C', idx=2); resolvedIdx(2) >= endIdx(2) → nudge to idx=1 ('B')
|
|
1821
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1822
|
+
assert.equal(element.valueStart, 'B');
|
|
1823
|
+
assert.equal(element.valueEnd, 'C');
|
|
1824
|
+
});
|
|
1657
1825
|
});
|
|
1658
1826
|
|
|
1659
1827
|
suite('zui-slider stepParser', () => {
|
|
@@ -1682,7 +1850,7 @@ suite('zui-slider stepParser', () => {
|
|
|
1682
1850
|
};
|
|
1683
1851
|
await element.updateComplete;
|
|
1684
1852
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1685
|
-
floatInput.value = '2k'; // exact step label
|
|
1853
|
+
floatInput.value = '2k'; // exact step label; resolved before stepParser is consulted
|
|
1686
1854
|
floatInput.dispatchEvent(new Event('change'));
|
|
1687
1855
|
assert.equal(element.value, '2k');
|
|
1688
1856
|
assert.equal(callCount, 0);
|