@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.
@@ -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) range mode does not update it
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 this is the live() failure case
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 connectedCallback must check this.range too.
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 without waiting for blur', async () => {
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 #blurFloatingInput clears focused and schedules hide.
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 disconnectedCallback should clear state
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
- const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
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
- const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
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('valueStart, range-start native input, and start floating input stay in sync after drag', async () => {
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 valueStart reset after drag keeps range-start native input in sync', async () => {
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' from the last render
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'; // Lit virtual is still '80' from the last render
925
+ element.valueEnd = '80'; // same live() invariant for end thumb
925
926
 
926
- await element.updateComplete; // live() ensures '80' is written despite virtual==='80'
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
- floatInputs[0].value = '150';
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, '100');
1050
+ assert.equal(element.valueStart, '0');
1043
1051
 
1044
- element.min = 20;
1045
- floatInputs[1].value = '5';
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, '20');
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 01 fraction of the effective track, matching the
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 not snapped on init
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 resolved before stepParser is consulted
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);