@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.
@@ -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;
@@ -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 without waiting for blur', async () => {
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 #blurFloatingInput clears focused and schedules hide.
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 disconnectedCallback should clear state
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
- test('input handler rejects start dragging to equal end', async () => {
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
- test('input handler rejects end dragging to equal start', async () => {
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('valueStart, range-start native input, and start floating input stay in sync after drag', async () => {
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 valueStart reset after drag keeps range-start native input in sync', async () => {
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' 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');
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'; // Lit virtual is still '80' from the last render
937
+ element.valueEnd = '80'; // same live() invariant for end thumb
925
938
 
926
- await element.updateComplete; // live() ensures '80' is written despite virtual==='80'
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
- floatInputs[0].value = '150';
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, '100');
1062
+ assert.equal(element.valueStart, '0');
1043
1063
 
1044
- element.min = 20;
1045
- floatInputs[1].value = '5';
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, '20');
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 01 fraction of the effective track, matching the
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 not snapped on init
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 resolved before stepParser is consulted
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);