@zywave/zui-slider 4.4.0-pre.5 → 4.4.0-pre.6
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 +21 -3
- package/dist/zui-slider.js +75 -37
- package/dist/zui-slider.js.map +1 -1
- package/package.json +2 -2
- package/src/zui-slider.ts +78 -37
- package/test/zui-slider.test.ts +239 -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;
|
|
@@ -366,13 +366,57 @@ suite('zui-slider', () => {
|
|
|
366
366
|
assert.equal(changeCount, 1);
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
-
test('pressing Enter in floating input commits value
|
|
369
|
+
test('pressing Enter in floating input commits value and retains focus', async () => {
|
|
370
370
|
await element.updateComplete;
|
|
371
371
|
element.value = '50';
|
|
372
372
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
373
|
+
let blurred = false;
|
|
374
|
+
floatInput.addEventListener('blur', () => (blurred = true));
|
|
373
375
|
floatInput.value = '75';
|
|
374
376
|
floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
375
377
|
assert.equal(element.value, '75');
|
|
378
|
+
assert.isFalse(blurred);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('blurring floating input commits typed value', async () => {
|
|
382
|
+
await element.updateComplete;
|
|
383
|
+
element.value = '50';
|
|
384
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
385
|
+
floatInput.value = '75';
|
|
386
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
387
|
+
floatInput.dispatchEvent(new FocusEvent('blur'));
|
|
388
|
+
assert.equal(element.value, '75');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('blurring floating input after Enter does not dispatch second change event', async () => {
|
|
392
|
+
await element.updateComplete;
|
|
393
|
+
element.value = '50';
|
|
394
|
+
let changeCount = 0;
|
|
395
|
+
element.addEventListener('change', () => changeCount++);
|
|
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 KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
400
|
+
floatInput.dispatchEvent(new FocusEvent('blur'));
|
|
401
|
+
assert.equal(element.value, '75');
|
|
402
|
+
assert.equal(changeCount, 1);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('typing after Enter in floating input re-enables blur commit', async () => {
|
|
406
|
+
await element.updateComplete;
|
|
407
|
+
element.value = '50';
|
|
408
|
+
let changeCount = 0;
|
|
409
|
+
element.addEventListener('change', () => changeCount++);
|
|
410
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
411
|
+
floatInput.value = '75';
|
|
412
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
413
|
+
floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
414
|
+
assert.equal(changeCount, 1);
|
|
415
|
+
floatInput.value = '80';
|
|
416
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
417
|
+
floatInput.dispatchEvent(new FocusEvent('blur'));
|
|
418
|
+
assert.equal(element.value, '80');
|
|
419
|
+
assert.equal(changeCount, 2);
|
|
376
420
|
});
|
|
377
421
|
|
|
378
422
|
test('focused floating input stays visible after pointerleave', async () => {
|
|
@@ -404,7 +448,7 @@ suite('zui-slider', () => {
|
|
|
404
448
|
await element.updateComplete;
|
|
405
449
|
assert.isTrue(thumbInputDiv.classList.contains('thumb-input--visible'));
|
|
406
450
|
|
|
407
|
-
// Blur without re-focusing
|
|
451
|
+
// Blur without re-focusing; #blurFloatingInput clears focused and schedules hide.
|
|
408
452
|
// #scheduleHideThumbInput uses a real 100ms setTimeout, so wait past it before asserting.
|
|
409
453
|
floatInput.dispatchEvent(new Event('blur'));
|
|
410
454
|
rangeInput.dispatchEvent(new Event('pointerleave'));
|
|
@@ -422,7 +466,7 @@ suite('zui-slider', () => {
|
|
|
422
466
|
await element.updateComplete;
|
|
423
467
|
assert.isTrue(element.shadowRoot!.querySelector('.thumb-input')!.classList.contains('thumb-input--visible'));
|
|
424
468
|
|
|
425
|
-
// Disconnect then reconnect
|
|
469
|
+
// Disconnect then reconnect; disconnectedCallback should clear state
|
|
426
470
|
document.body.removeChild(form);
|
|
427
471
|
document.body.appendChild(form);
|
|
428
472
|
await element.updateComplete;
|
|
@@ -506,7 +550,6 @@ suite('zui-slider min-max labels', () => {
|
|
|
506
550
|
});
|
|
507
551
|
|
|
508
552
|
test('min-max labels are hidden when showStepLabels is true', async () => {
|
|
509
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
510
553
|
element.showStepLabels = true;
|
|
511
554
|
await element.updateComplete;
|
|
512
555
|
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
@@ -548,7 +591,6 @@ suite('zui-slider min-max labels', () => {
|
|
|
548
591
|
});
|
|
549
592
|
|
|
550
593
|
test('show-step-labels attribute hides min-max labels', async () => {
|
|
551
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
552
594
|
element.setAttribute('show-step-labels', '');
|
|
553
595
|
await element.updateComplete;
|
|
554
596
|
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
@@ -773,61 +815,39 @@ suite('zui-slider range', () => {
|
|
|
773
815
|
document.body.removeChild(f);
|
|
774
816
|
});
|
|
775
817
|
|
|
776
|
-
test('input handler rejects start dragging past end', async () => {
|
|
818
|
+
test('input handler rejects start dragging to or past end', async () => {
|
|
777
819
|
await element.updateComplete;
|
|
778
820
|
element.valueStart = '20';
|
|
779
821
|
element.valueEnd = '60';
|
|
780
|
-
|
|
781
822
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
782
|
-
startInput.value = '70';
|
|
783
|
-
startInput.dispatchEvent(new Event('input'));
|
|
784
823
|
|
|
824
|
+
startInput.value = '70'; // past end
|
|
825
|
+
startInput.dispatchEvent(new Event('input'));
|
|
785
826
|
assert.equal(element.valueStart, '20');
|
|
786
827
|
assert.equal(element.valueEnd, '60');
|
|
787
828
|
assert.equal(startInput.value, '20');
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
test('input handler rejects start dragging to equal end', async () => {
|
|
791
|
-
await element.updateComplete;
|
|
792
|
-
element.valueStart = '20';
|
|
793
|
-
element.valueEnd = '60';
|
|
794
829
|
|
|
795
|
-
|
|
796
|
-
startInput.value = '60';
|
|
830
|
+
startInput.value = '60'; // equal to end
|
|
797
831
|
startInput.dispatchEvent(new Event('input'));
|
|
798
|
-
|
|
799
|
-
// start >= end is rejected; input snaps back and state is unchanged
|
|
800
832
|
assert.equal(element.valueStart, '20');
|
|
801
|
-
assert.equal(element.valueEnd, '60');
|
|
802
833
|
assert.equal(startInput.value, '20');
|
|
803
834
|
});
|
|
804
835
|
|
|
805
|
-
test('input handler rejects end dragging before start', async () => {
|
|
836
|
+
test('input handler rejects end dragging to or before start', async () => {
|
|
806
837
|
await element.updateComplete;
|
|
807
838
|
element.valueStart = '30';
|
|
808
839
|
element.valueEnd = '70';
|
|
809
|
-
|
|
810
840
|
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
811
|
-
endInput.value = '20';
|
|
812
|
-
endInput.dispatchEvent(new Event('input'));
|
|
813
841
|
|
|
842
|
+
endInput.value = '20'; // before start
|
|
843
|
+
endInput.dispatchEvent(new Event('input'));
|
|
814
844
|
assert.equal(element.valueEnd, '70');
|
|
815
845
|
assert.equal(element.valueStart, '30');
|
|
816
846
|
assert.equal(endInput.value, '70');
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
test('input handler rejects end dragging to equal start', async () => {
|
|
820
|
-
await element.updateComplete;
|
|
821
|
-
element.valueStart = '30';
|
|
822
|
-
element.valueEnd = '70';
|
|
823
847
|
|
|
824
|
-
|
|
825
|
-
endInput.value = '30';
|
|
848
|
+
endInput.value = '30'; // equal to start
|
|
826
849
|
endInput.dispatchEvent(new Event('input'));
|
|
827
|
-
|
|
828
|
-
// end <= start is rejected; input snaps back and state is unchanged
|
|
829
850
|
assert.equal(element.valueEnd, '70');
|
|
830
|
-
assert.equal(element.valueStart, '30');
|
|
831
851
|
assert.equal(endInput.value, '70');
|
|
832
852
|
});
|
|
833
853
|
|
|
@@ -868,63 +888,47 @@ suite('zui-slider range', () => {
|
|
|
868
888
|
assert.include(thumbInputDivs[0].style.left, '30%');
|
|
869
889
|
});
|
|
870
890
|
|
|
871
|
-
test('
|
|
891
|
+
test('native range inputs and floating inputs stay in sync with component values after drag', async () => {
|
|
872
892
|
await element.updateComplete;
|
|
873
893
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
894
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
874
895
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
875
896
|
|
|
876
897
|
startInput.value = '30';
|
|
877
898
|
startInput.dispatchEvent(new Event('input'));
|
|
899
|
+
endInput.value = '60';
|
|
900
|
+
endInput.dispatchEvent(new Event('input'));
|
|
878
901
|
await element.updateComplete;
|
|
879
902
|
|
|
880
903
|
assert.equal(element.valueStart, '30');
|
|
881
904
|
assert.equal(startInput.value, '30');
|
|
882
905
|
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
906
|
assert.equal(element.valueEnd, '60');
|
|
895
907
|
assert.equal(endInput.value, '60');
|
|
896
908
|
assert.equal(floatInputs[1].value, '60');
|
|
897
909
|
});
|
|
898
910
|
|
|
899
|
-
test('programmatic
|
|
911
|
+
test('programmatic value resets after drag keep both native range inputs in sync via live()', async () => {
|
|
900
912
|
element.valueStart = '20';
|
|
913
|
+
element.valueEnd = '80';
|
|
901
914
|
await element.updateComplete;
|
|
902
915
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
916
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
903
917
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
904
918
|
|
|
905
919
|
startInput.value = '40';
|
|
906
920
|
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');
|
|
921
|
+
element.valueStart = '20'; // Lit virtual is still '20'; live() must force the DOM write
|
|
921
922
|
|
|
922
923
|
endInput.value = '60';
|
|
923
924
|
endInput.dispatchEvent(new Event('input'));
|
|
924
|
-
element.valueEnd = '80'; //
|
|
925
|
+
element.valueEnd = '80'; // same live() invariant for end thumb
|
|
925
926
|
|
|
926
|
-
await element.updateComplete;
|
|
927
|
+
await element.updateComplete;
|
|
927
928
|
|
|
929
|
+
assert.equal(element.valueStart, '20');
|
|
930
|
+
assert.equal(startInput.value, '20');
|
|
931
|
+
assert.equal(floatInputs[0].value, '20');
|
|
928
932
|
assert.equal(element.valueEnd, '80');
|
|
929
933
|
assert.equal(endInput.value, '80');
|
|
930
934
|
assert.equal(floatInputs[1].value, '80');
|
|
@@ -1036,15 +1040,19 @@ suite('zui-slider range', () => {
|
|
|
1036
1040
|
|
|
1037
1041
|
test('range floating inputs clamp out-of-bounds values to min/max on commit', async () => {
|
|
1038
1042
|
await element.updateComplete;
|
|
1043
|
+
element.valueStart = '20';
|
|
1044
|
+
element.valueEnd = '80';
|
|
1039
1045
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1040
|
-
|
|
1046
|
+
|
|
1047
|
+
// Start thumb: type below min → clamped to min (0); 0 < endNum (80) → no nudge
|
|
1048
|
+
floatInputs[0].value = '-50';
|
|
1041
1049
|
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1042
|
-
assert.equal(element.valueStart, '
|
|
1050
|
+
assert.equal(element.valueStart, '0');
|
|
1043
1051
|
|
|
1044
|
-
|
|
1045
|
-
floatInputs[1].value = '
|
|
1052
|
+
// End thumb: type above max → clamped to max (100); 100 > startNum (0) → no nudge
|
|
1053
|
+
floatInputs[1].value = '150';
|
|
1046
1054
|
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1047
|
-
assert.equal(element.valueEnd, '
|
|
1055
|
+
assert.equal(element.valueEnd, '100');
|
|
1048
1056
|
});
|
|
1049
1057
|
|
|
1050
1058
|
test('range floating input snaps typed valueStart to nearest step on commit', async () => {
|
|
@@ -1070,13 +1078,16 @@ suite('zui-slider range', () => {
|
|
|
1070
1078
|
assert.deepEqual(detail, { valueStart: '30', valueEnd: '100' });
|
|
1071
1079
|
});
|
|
1072
1080
|
|
|
1073
|
-
test('pressing Enter in range floating inputs commits values', async () => {
|
|
1081
|
+
test('pressing Enter in range floating inputs commits values and retains focus', async () => {
|
|
1074
1082
|
await element.updateComplete;
|
|
1075
1083
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1084
|
+
let startBlurred = false;
|
|
1085
|
+
floatInputs[0].addEventListener('blur', () => (startBlurred = true));
|
|
1076
1086
|
|
|
1077
1087
|
floatInputs[0].value = '30';
|
|
1078
1088
|
floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1079
1089
|
assert.equal(element.valueStart, '30');
|
|
1090
|
+
assert.isFalse(startBlurred);
|
|
1080
1091
|
|
|
1081
1092
|
floatInputs[1].value = '70';
|
|
1082
1093
|
floatInputs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
@@ -1097,9 +1108,93 @@ suite('zui-slider range', () => {
|
|
|
1097
1108
|
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1098
1109
|
assert.equal(changeCount, 0);
|
|
1099
1110
|
});
|
|
1111
|
+
|
|
1112
|
+
test('range start floating input nudges to one step below end when typed value equals end', async () => {
|
|
1113
|
+
await element.updateComplete;
|
|
1114
|
+
element.valueStart = '20';
|
|
1115
|
+
element.valueEnd = '50';
|
|
1116
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1117
|
+
floatInputs[0].value = '50';
|
|
1118
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1119
|
+
// typed 50 = end 50; effectiveStep=1; nudge to 50-1=49 < 50 → committed as '49'
|
|
1120
|
+
assert.equal(element.valueStart, '49');
|
|
1121
|
+
assert.equal(element.valueEnd, '50');
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test('range end floating input nudges to one step above start when typed value equals start', async () => {
|
|
1125
|
+
await element.updateComplete;
|
|
1126
|
+
element.valueStart = '50';
|
|
1127
|
+
element.valueEnd = '80';
|
|
1128
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1129
|
+
floatInputs[1].value = '50';
|
|
1130
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1131
|
+
// typed 50 = start 50; effectiveStep=1; nudge to 50+1=51 > 50 → committed as '51'
|
|
1132
|
+
assert.equal(element.valueEnd, '51');
|
|
1133
|
+
assert.equal(element.valueStart, '50');
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
test('range floating input nudge respects decimal step', async () => {
|
|
1137
|
+
element.step = 0.1;
|
|
1138
|
+
await element.updateComplete;
|
|
1139
|
+
element.valueStart = '1';
|
|
1140
|
+
element.valueEnd = '5';
|
|
1141
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1142
|
+
floatInputs[1].value = '1';
|
|
1143
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1144
|
+
// typed 1 = start 1; effectiveStep=0.1; nudge to 1+0.1=1.1 > 1 → committed as '1.1'
|
|
1145
|
+
assert.equal(element.valueEnd, '1.1');
|
|
1146
|
+
assert.equal(element.valueStart, '1');
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test('blurring range floating input commits typed value', async () => {
|
|
1150
|
+
await element.updateComplete;
|
|
1151
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1152
|
+
floatInputs[0].value = '30';
|
|
1153
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1154
|
+
floatInputs[0].dispatchEvent(new FocusEvent('blur'));
|
|
1155
|
+
assert.equal(element.valueStart, '30');
|
|
1156
|
+
|
|
1157
|
+
floatInputs[1].value = '70';
|
|
1158
|
+
floatInputs[1].dispatchEvent(new Event('input'));
|
|
1159
|
+
floatInputs[1].dispatchEvent(new FocusEvent('blur'));
|
|
1160
|
+
assert.equal(element.valueEnd, '70');
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
test('blurring range floating input after Enter does not dispatch second change event', async () => {
|
|
1164
|
+
await element.updateComplete;
|
|
1165
|
+
element.valueStart = '20';
|
|
1166
|
+
element.valueEnd = '80';
|
|
1167
|
+
let changeCount = 0;
|
|
1168
|
+
element.addEventListener('change', () => changeCount++);
|
|
1169
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1170
|
+
floatInputs[0].value = '30';
|
|
1171
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1172
|
+
floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1173
|
+
floatInputs[0].dispatchEvent(new FocusEvent('blur'));
|
|
1174
|
+
assert.equal(element.valueStart, '30');
|
|
1175
|
+
assert.equal(changeCount, 1);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
test('typing after Enter in range floating input re-enables blur commit', async () => {
|
|
1179
|
+
await element.updateComplete;
|
|
1180
|
+
element.valueStart = '20';
|
|
1181
|
+
element.valueEnd = '80';
|
|
1182
|
+
let changeCount = 0;
|
|
1183
|
+
element.addEventListener('change', () => changeCount++);
|
|
1184
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1185
|
+
floatInputs[0].value = '30';
|
|
1186
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1187
|
+
floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1188
|
+
assert.equal(changeCount, 1);
|
|
1189
|
+
floatInputs[0].value = '40';
|
|
1190
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1191
|
+
floatInputs[0].dispatchEvent(new FocusEvent('blur'));
|
|
1192
|
+
assert.equal(element.valueStart, '40');
|
|
1193
|
+
assert.equal(changeCount, 2);
|
|
1194
|
+
});
|
|
1100
1195
|
});
|
|
1101
1196
|
|
|
1102
|
-
// Dispatches a click at a given 0
|
|
1197
|
+
// Dispatches a click at a given 0-1 fraction of the effective track, matching the
|
|
1103
1198
|
// coordinate math in #onTrackClick so the component computes the same fraction back.
|
|
1104
1199
|
function clickAtFraction(element: ZuiSlider, fraction: number): void {
|
|
1105
1200
|
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
@@ -1207,6 +1302,48 @@ suite('zui-slider range track click', () => {
|
|
|
1207
1302
|
assert.equal(element.valueStart, '40');
|
|
1208
1303
|
assert.equal(element.valueEnd, '50');
|
|
1209
1304
|
});
|
|
1305
|
+
|
|
1306
|
+
test('drag-end synthesized click on range-start input does not move range-end', async () => {
|
|
1307
|
+
// The browser synthesizes a click on the dragged input at mouseup; it must not reach #onTrackClick.
|
|
1308
|
+
element.valueStart = '5';
|
|
1309
|
+
element.valueEnd = '50';
|
|
1310
|
+
await element.updateComplete;
|
|
1311
|
+
|
|
1312
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1313
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1314
|
+
|
|
1315
|
+
startInput.value = '80';
|
|
1316
|
+
startInput.dispatchEvent(new Event('input'));
|
|
1317
|
+
assert.equal(element.valueStart, '5');
|
|
1318
|
+
assert.equal(element.valueEnd, '50');
|
|
1319
|
+
|
|
1320
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1321
|
+
const thumbRadius = 1.5 * parseFloat(getComputedStyle(element).getPropertyValue('--zui-slider-thumb-size'));
|
|
1322
|
+
const effectiveWidth = rect.width - 2 * thumbRadius;
|
|
1323
|
+
const clientX = rect.left + thumbRadius + 0.95 * effectiveWidth;
|
|
1324
|
+
startInput.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX }));
|
|
1325
|
+
|
|
1326
|
+
assert.equal(element.valueStart, '5');
|
|
1327
|
+
assert.equal(element.valueEnd, '50');
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
test('clicking inside floating input does not move a thumb', async () => {
|
|
1331
|
+
// The floating input is inside .range-wrapper; its click bubbles to the track handler.
|
|
1332
|
+
element.valueStart = '20';
|
|
1333
|
+
element.valueEnd = '60';
|
|
1334
|
+
await element.updateComplete;
|
|
1335
|
+
|
|
1336
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1337
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1338
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1339
|
+
const thumbRadius = 1.5 * parseFloat(getComputedStyle(element).getPropertyValue('--zui-slider-thumb-size'));
|
|
1340
|
+
const effectiveWidth = rect.width - 2 * thumbRadius;
|
|
1341
|
+
const clientX = rect.left + thumbRadius + 0.9 * effectiveWidth;
|
|
1342
|
+
floatInput.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX }));
|
|
1343
|
+
|
|
1344
|
+
assert.equal(element.valueStart, '20');
|
|
1345
|
+
assert.equal(element.valueEnd, '60');
|
|
1346
|
+
});
|
|
1210
1347
|
});
|
|
1211
1348
|
|
|
1212
1349
|
suite('zui-slider step dots', () => {
|
|
@@ -1228,13 +1365,10 @@ suite('zui-slider step dots', () => {
|
|
|
1228
1365
|
assert.equal(dots.length, 5); // 0, 25, 50, 75, 100
|
|
1229
1366
|
});
|
|
1230
1367
|
|
|
1231
|
-
test('step dots not rendered when step is 0', async () => {
|
|
1368
|
+
test('step dots not rendered when step is 0 or negative', async () => {
|
|
1232
1369
|
element.step = 0;
|
|
1233
1370
|
await element.updateComplete;
|
|
1234
1371
|
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
test('step dots not rendered when step is negative', async () => {
|
|
1238
1372
|
element.step = -5;
|
|
1239
1373
|
await element.updateComplete;
|
|
1240
1374
|
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
@@ -1302,6 +1436,7 @@ suite('zui-slider steps', () => {
|
|
|
1302
1436
|
setup(() => {
|
|
1303
1437
|
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1304
1438
|
form = buildForm({ enableSubmit: false, appendChildren: [element] });
|
|
1439
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1305
1440
|
});
|
|
1306
1441
|
|
|
1307
1442
|
teardown(() => {
|
|
@@ -1309,20 +1444,17 @@ suite('zui-slider steps', () => {
|
|
|
1309
1444
|
});
|
|
1310
1445
|
|
|
1311
1446
|
test('initializes value to first step when current value is not a step label', async () => {
|
|
1312
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1313
1447
|
await element.updateComplete;
|
|
1314
1448
|
assert.equal(element.value, 'Small');
|
|
1315
1449
|
});
|
|
1316
1450
|
|
|
1317
1451
|
test('value set to a valid step label is preserved', async () => {
|
|
1318
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1319
1452
|
element.value = 'Medium';
|
|
1320
1453
|
await element.updateComplete;
|
|
1321
1454
|
assert.equal(element.value, 'Medium');
|
|
1322
1455
|
});
|
|
1323
1456
|
|
|
1324
1457
|
test('native range input has index-based min, max, and step attributes', async () => {
|
|
1325
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1326
1458
|
await element.updateComplete;
|
|
1327
1459
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1328
1460
|
assert.equal(rangeInput.min, '0');
|
|
@@ -1331,7 +1463,6 @@ suite('zui-slider steps', () => {
|
|
|
1331
1463
|
});
|
|
1332
1464
|
|
|
1333
1465
|
test('native range input value reflects current step index', async () => {
|
|
1334
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1335
1466
|
element.value = 'Medium';
|
|
1336
1467
|
await element.updateComplete;
|
|
1337
1468
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
@@ -1339,7 +1470,6 @@ suite('zui-slider steps', () => {
|
|
|
1339
1470
|
});
|
|
1340
1471
|
|
|
1341
1472
|
test('dragging native range to index sets value to corresponding step label', async () => {
|
|
1342
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1343
1473
|
await element.updateComplete;
|
|
1344
1474
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1345
1475
|
rangeInput.value = '2';
|
|
@@ -1348,7 +1478,6 @@ suite('zui-slider steps', () => {
|
|
|
1348
1478
|
});
|
|
1349
1479
|
|
|
1350
1480
|
test('progress is computed by step index, not by numeric value', async () => {
|
|
1351
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1352
1481
|
element.value = 'Small';
|
|
1353
1482
|
await element.updateComplete;
|
|
1354
1483
|
assert.equal(element.progress, 0);
|
|
@@ -1359,14 +1488,12 @@ suite('zui-slider steps', () => {
|
|
|
1359
1488
|
});
|
|
1360
1489
|
|
|
1361
1490
|
test('floating input is type="text" in steps mode', async () => {
|
|
1362
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1363
1491
|
await element.updateComplete;
|
|
1364
1492
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1365
1493
|
assert.equal(floatInput.type, 'text');
|
|
1366
1494
|
});
|
|
1367
1495
|
|
|
1368
1496
|
test('floating input change to valid step label updates value immediately', async () => {
|
|
1369
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1370
1497
|
await element.updateComplete;
|
|
1371
1498
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1372
1499
|
floatInput.value = 'Large';
|
|
@@ -1375,7 +1502,6 @@ suite('zui-slider steps', () => {
|
|
|
1375
1502
|
});
|
|
1376
1503
|
|
|
1377
1504
|
test('steps floating input change does not dispatch change event when value is unchanged', async () => {
|
|
1378
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1379
1505
|
element.value = 'Medium';
|
|
1380
1506
|
await element.updateComplete;
|
|
1381
1507
|
let changeCount = 0;
|
|
@@ -1389,7 +1515,6 @@ suite('zui-slider steps', () => {
|
|
|
1389
1515
|
});
|
|
1390
1516
|
|
|
1391
1517
|
test('floating input change to invalid label reverts input to current value', async () => {
|
|
1392
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1393
1518
|
element.value = 'Medium';
|
|
1394
1519
|
await element.updateComplete;
|
|
1395
1520
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
@@ -1402,7 +1527,7 @@ suite('zui-slider steps', () => {
|
|
|
1402
1527
|
test('floating input resolves typed numeric alias to nearest step on commit', async () => {
|
|
1403
1528
|
element.steps = [0, 25, 50, 75, 100];
|
|
1404
1529
|
await element.updateComplete;
|
|
1405
|
-
assert.equal(element.value, '50'); // '50' is a valid label
|
|
1530
|
+
assert.equal(element.value, '50'); // '50' is a valid label; not snapped on init
|
|
1406
1531
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1407
1532
|
floatInput.value = '30';
|
|
1408
1533
|
floatInput.dispatchEvent(new Event('change'));
|
|
@@ -1423,13 +1548,11 @@ suite('zui-slider steps', () => {
|
|
|
1423
1548
|
});
|
|
1424
1549
|
|
|
1425
1550
|
test('step dots count equals steps.length', async () => {
|
|
1426
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1427
1551
|
await element.updateComplete;
|
|
1428
1552
|
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3);
|
|
1429
1553
|
});
|
|
1430
1554
|
|
|
1431
1555
|
test('min-max labels show first and last step labels', async () => {
|
|
1432
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1433
1556
|
await element.updateComplete;
|
|
1434
1557
|
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
1435
1558
|
assert.equal(labels[0].textContent!.trim(), 'Small');
|
|
@@ -1449,7 +1572,6 @@ suite('zui-slider steps', () => {
|
|
|
1449
1572
|
test('value is included in form submission as the step label', async () => {
|
|
1450
1573
|
const name = randString();
|
|
1451
1574
|
element.setAttribute('name', name);
|
|
1452
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1453
1575
|
element.value = 'Large';
|
|
1454
1576
|
await element.updateComplete;
|
|
1455
1577
|
assert.equal(new FormData(form).get(name), 'Large');
|
|
@@ -1468,7 +1590,6 @@ suite('zui-slider steps', () => {
|
|
|
1468
1590
|
});
|
|
1469
1591
|
|
|
1470
1592
|
test('showStepLabels renders a label element for each step', async () => {
|
|
1471
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1472
1593
|
element.showStepLabels = true;
|
|
1473
1594
|
await element.updateComplete;
|
|
1474
1595
|
const labels = element.shadowRoot!.querySelectorAll('.step-dot-label');
|
|
@@ -1489,13 +1610,11 @@ suite('zui-slider steps', () => {
|
|
|
1489
1610
|
});
|
|
1490
1611
|
|
|
1491
1612
|
test('showStepLabels false renders no label elements', async () => {
|
|
1492
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1493
1613
|
await element.updateComplete;
|
|
1494
1614
|
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot-label').length, 0);
|
|
1495
1615
|
});
|
|
1496
1616
|
|
|
1497
1617
|
test('pressing Enter in steps floating input commits the step label', async () => {
|
|
1498
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1499
1618
|
await element.updateComplete;
|
|
1500
1619
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1501
1620
|
floatInput.value = 'Large';
|
|
@@ -1504,7 +1623,6 @@ suite('zui-slider steps', () => {
|
|
|
1504
1623
|
});
|
|
1505
1624
|
|
|
1506
1625
|
test('committing empty steps floating input reverts display to current step label', async () => {
|
|
1507
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1508
1626
|
element.value = 'Medium';
|
|
1509
1627
|
await element.updateComplete;
|
|
1510
1628
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
@@ -1654,6 +1772,30 @@ suite('zui-slider steps range', () => {
|
|
|
1654
1772
|
assert.equal(element.valueStart, 'B');
|
|
1655
1773
|
assert.equal(element.valueEnd, 'D');
|
|
1656
1774
|
});
|
|
1775
|
+
|
|
1776
|
+
test('steps range end floating input nudges to next step when typed value matches start step', async () => {
|
|
1777
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1778
|
+
element.valueStart = 'C'; // idx=2
|
|
1779
|
+
element.valueEnd = 'E'; // idx=4
|
|
1780
|
+
await element.updateComplete;
|
|
1781
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
1782
|
+
floatInputs[1].value = 'C'; // typed = start ('C', idx=2); resolvedIdx(2) <= startIdx(2) → nudge to idx=3 ('D')
|
|
1783
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1784
|
+
assert.equal(element.valueEnd, 'D');
|
|
1785
|
+
assert.equal(element.valueStart, 'C');
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
test('steps range start floating input nudges to previous step when typed value matches end step', async () => {
|
|
1789
|
+
element.steps = ['A', 'B', 'C', 'D', 'E'];
|
|
1790
|
+
element.valueStart = 'A'; // idx=0
|
|
1791
|
+
element.valueEnd = 'C'; // idx=2
|
|
1792
|
+
await element.updateComplete;
|
|
1793
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
1794
|
+
floatInputs[0].value = 'C'; // typed = end ('C', idx=2); resolvedIdx(2) >= endIdx(2) → nudge to idx=1 ('B')
|
|
1795
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1796
|
+
assert.equal(element.valueStart, 'B');
|
|
1797
|
+
assert.equal(element.valueEnd, 'C');
|
|
1798
|
+
});
|
|
1657
1799
|
});
|
|
1658
1800
|
|
|
1659
1801
|
suite('zui-slider stepParser', () => {
|
|
@@ -1682,7 +1824,7 @@ suite('zui-slider stepParser', () => {
|
|
|
1682
1824
|
};
|
|
1683
1825
|
await element.updateComplete;
|
|
1684
1826
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1685
|
-
floatInput.value = '2k'; // exact step label
|
|
1827
|
+
floatInput.value = '2k'; // exact step label; resolved before stepParser is consulted
|
|
1686
1828
|
floatInput.dispatchEvent(new Event('change'));
|
|
1687
1829
|
assert.equal(element.value, '2k');
|
|
1688
1830
|
assert.equal(callCount, 0);
|