@zywave/zui-slider 4.4.0-pre.4 → 4.4.0-pre.5

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.
@@ -97,17 +97,14 @@ suite('zui-slider', () => {
97
97
  assert.equal(element.value, '33.5');
98
98
  });
99
99
 
100
- test('float input position and track background update after drag', async () => {
100
+ test('float input position updates after drag', async () => {
101
101
  await element.updateComplete;
102
102
  const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
103
103
  const thumbDiv = element.shadowRoot!.querySelector<HTMLElement>('.thumb-input')!;
104
- const bgBefore = input.style.getPropertyValue('--zui-slider-track-bg');
105
104
  input.value = '75';
106
105
  input.dispatchEvent(new Event('input'));
107
106
  await element.updateComplete;
108
107
  assert.include(thumbDiv.style.left, '75%');
109
- assert.notEqual(input.style.getPropertyValue('--zui-slider-track-bg'), bgBefore);
110
- assert.include(input.style.getPropertyValue('--zui-slider-track-bg'), '75%');
111
108
  });
112
109
 
113
110
  test('floating input change fires component change event', async () => {
@@ -196,26 +193,38 @@ suite('zui-slider', () => {
196
193
  assert.equal(new FormData(form).get(name), '50');
197
194
  });
198
195
 
199
- test('single mode input has inline linear-gradient background with transparent insets', async () => {
196
+ test('element.value, native range input, and floating input stay in sync after drag', async () => {
200
197
  await element.updateComplete;
201
- const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
202
- const bg = input.style.getPropertyValue('--zui-slider-track-bg');
203
- assert.include(bg, 'linear-gradient');
204
- assert.include(bg, 'transparent var(--zui-slider-thumb-size)');
205
- assert.include(bg, 'transparent calc(100% - var(--zui-slider-thumb-size))');
198
+ const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
199
+ const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
200
+
201
+ rangeInput.value = '75';
202
+ rangeInput.dispatchEvent(new Event('input'));
203
+ await element.updateComplete;
204
+
205
+ assert.equal(element.value, '75');
206
+ assert.equal(rangeInput.value, '75');
207
+ assert.equal(floatInput.value, '75');
206
208
  });
207
209
 
208
- test('single mode gradient uses gray when disabled and reverts to blue when re-enabled', async () => {
209
- element.disabled = true;
210
+ test('programmatic value reset after drag keeps native range input in sync', async () => {
211
+ // Reproduces a live() omission: user drags while a programmatic set is pending,
212
+ // causing Lit to skip the DOM write because its stored virtual still matches.
213
+ element.value = '50';
210
214
  await element.updateComplete;
211
215
  const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
212
- assert.include(input.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-gray)');
213
- assert.notInclude(input.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-blue)');
214
216
 
215
- element.disabled = false;
216
- await element.updateComplete;
217
- assert.include(input.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-blue)');
218
- assert.notInclude(input.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-gray)');
217
+ // Simulate drag: browser writes directly to DOM, then fires input event
218
+ input.value = '75';
219
+ input.dispatchEvent(new Event('input'));
220
+
221
+ // Before that render fires, programmatically revert — this is the live() failure case
222
+ element.value = '50'; // Lit virtual is still '50' from the last render
223
+
224
+ await element.updateComplete; // without live(), Lit sees virtual='50'==new='50' → skips DOM write
225
+
226
+ assert.equal(input.value, '50');
227
+ assert.equal(element.value, '50');
219
228
  });
220
229
 
221
230
  test('form reset hides visible floating input', async () => {
@@ -303,56 +312,6 @@ suite('zui-slider', () => {
303
312
  document.body.removeChild(f);
304
313
  });
305
314
 
306
- test('floating input updates value after debounce delay but not immediately', async () => {
307
- await element.updateComplete;
308
- element.value = '50';
309
- const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
310
- floatInput.value = '75';
311
- floatInput.dispatchEvent(new Event('input'));
312
- // Debounce is 500ms — value must not update synchronously
313
- assert.equal(element.value, '50');
314
- await new Promise<void>((r) => setTimeout(r, 600));
315
- assert.equal(element.value, '75');
316
- });
317
-
318
- test('form reset cancels pending floating input debounce', async () => {
319
- await element.updateComplete;
320
- const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
321
- floatInput.value = '80';
322
- floatInput.dispatchEvent(new Event('input'));
323
- // Debounce is pending — reset before it fires
324
- form.reset();
325
- await new Promise<void>((r) => setTimeout(r, 600));
326
- // Debounce timer must have been cancelled by formResetCallback
327
- assert.equal(element.value, '50');
328
- });
329
-
330
- test('floating input empty-string input does not trigger value update', async () => {
331
- await element.updateComplete;
332
- element.value = '50';
333
- const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
334
- floatInput.value = '';
335
- floatInput.dispatchEvent(new Event('input'));
336
- // Empty string is an in-progress edit — value must not be updated
337
- assert.equal(element.value, '50');
338
- });
339
-
340
- test('clearing floating input cancels pending debounce so stale value is not committed', async () => {
341
- await element.updateComplete;
342
- element.value = '50';
343
- const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
344
- // Start a debounce for '75'
345
- floatInput.value = '75';
346
- floatInput.dispatchEvent(new Event('input'));
347
- assert.equal(element.value, '50'); // debounce not yet fired
348
- // Then clear the field — should cancel the '75' debounce
349
- floatInput.value = '';
350
- floatInput.dispatchEvent(new Event('input'));
351
- await new Promise<void>((r) => setTimeout(r, 350));
352
- // The pending debounce was cancelled; value stays at '50'
353
- assert.equal(element.value, '50');
354
- });
355
-
356
315
  test('committing empty floating input reverts display to current value', async () => {
357
316
  await element.updateComplete;
358
317
  element.value = '75';
@@ -363,48 +322,56 @@ suite('zui-slider', () => {
363
322
  assert.equal(floatInput.value, '75');
364
323
  });
365
324
 
366
- test('floating input clamps out-of-bounds values to min/max after debounce', async () => {
325
+ test('floating input clamps out-of-bounds values to min/max on commit', async () => {
367
326
  await element.updateComplete;
368
327
  const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
369
328
  floatInput.value = '150';
370
- floatInput.dispatchEvent(new Event('input'));
371
- await new Promise<void>((r) => setTimeout(r, 600));
329
+ floatInput.dispatchEvent(new Event('change'));
372
330
  assert.equal(element.value, '100');
373
331
 
374
332
  element.min = 20;
375
333
  floatInput.value = '5';
376
- floatInput.dispatchEvent(new Event('input'));
377
- await new Promise<void>((r) => setTimeout(r, 600));
334
+ floatInput.dispatchEvent(new Event('change'));
378
335
  assert.equal(element.value, '20');
379
336
  });
380
337
 
381
- test('floating input snaps typed value to nearest step after debounce', async () => {
338
+ test('floating input snaps typed value to nearest step on commit', async () => {
382
339
  await element.updateComplete;
383
340
  element.step = 10;
384
341
  const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
385
342
  floatInput.value = '47';
386
- floatInput.dispatchEvent(new Event('input'));
387
- await new Promise<void>((r) => setTimeout(r, 600));
343
+ floatInput.dispatchEvent(new Event('change'));
388
344
  assert.equal(element.value, '50');
389
345
  });
390
346
 
391
- test('floating input change flushes debounce and dispatches correct value immediately', async () => {
347
+ test('floating input change dispatches correct value and does not re-fire if value is unchanged', async () => {
392
348
  await element.updateComplete;
349
+ let changeCount = 0;
393
350
  let detail: string | undefined;
394
351
  element.addEventListener('change', (e: Event) => {
352
+ changeCount++;
395
353
  detail = (e as CustomEvent<string>).detail;
396
354
  });
397
355
 
398
356
  const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
399
- // Simulate typing then immediately committing before the 300ms debounce settles
400
357
  floatInput.value = '75';
401
- floatInput.dispatchEvent(new Event('input'));
402
- assert.equal(element.value, '50'); // debounce not yet fired
403
- floatInput.dispatchEvent(new Event('change')); // commits immediately
358
+ floatInput.dispatchEvent(new Event('change'));
404
359
  assert.equal(element.value, '75');
405
360
  assert.equal(detail, '75');
406
- // Debounce timer was cleared — value must not update again after 500ms
407
- await new Promise<void>((r) => setTimeout(r, 600));
361
+ assert.equal(changeCount, 1);
362
+
363
+ // Committing the same value again must not fire another change event
364
+ floatInput.value = '75';
365
+ floatInput.dispatchEvent(new Event('change'));
366
+ assert.equal(changeCount, 1);
367
+ });
368
+
369
+ test('pressing Enter in floating input commits value without waiting for blur', async () => {
370
+ await element.updateComplete;
371
+ element.value = '50';
372
+ const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
373
+ floatInput.value = '75';
374
+ floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
408
375
  assert.equal(element.value, '75');
409
376
  });
410
377
 
@@ -473,8 +440,7 @@ suite('zui-slider', () => {
473
440
  input.dispatchEvent(new Event('input'));
474
441
  assert.equal(element.value, '50');
475
442
  floatInput.value = '75';
476
- floatInput.dispatchEvent(new Event('input'));
477
- await new Promise<void>((r) => setTimeout(r, 350));
443
+ floatInput.dispatchEvent(new Event('change'));
478
444
  assert.equal(element.value, '50');
479
445
  });
480
446
 
@@ -892,62 +858,109 @@ suite('zui-slider range', () => {
892
858
  assert.equal(element.valueStart, '33.5');
893
859
  });
894
860
 
895
- test('range start float input position and wrapper background update after drag', async () => {
861
+ test('range start float input position updates after drag', async () => {
896
862
  await element.updateComplete;
897
863
  const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
898
864
  const thumbInputDivs = element.shadowRoot!.querySelectorAll<HTMLElement>('.thumb-input');
899
- const bgBefore = startInput.style.getPropertyValue('--zui-slider-track-bg');
900
865
  startInput.value = '30';
901
866
  startInput.dispatchEvent(new Event('input'));
902
867
  await element.updateComplete;
903
868
  assert.include(thumbInputDivs[0].style.left, '30%');
904
- assert.notEqual(startInput.style.getPropertyValue('--zui-slider-track-bg'), bgBefore);
905
- assert.include(startInput.style.getPropertyValue('--zui-slider-track-bg'), '30%');
906
869
  });
907
870
 
908
- test('change event fires with valueStart and valueEnd detail on range-start change', async () => {
871
+ test('valueStart, range-start native input, and start floating input stay in sync after drag', async () => {
909
872
  await element.updateComplete;
910
- let detail: { valueStart: string; valueEnd: string } | undefined;
911
- element.addEventListener('change', (e: Event) => {
912
- detail = (e as CustomEvent).detail;
913
- });
873
+ const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
874
+ const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
914
875
 
915
- element.valueStart = '10';
916
- element.valueEnd = '40';
876
+ startInput.value = '30';
877
+ startInput.dispatchEvent(new Event('input'));
878
+ await element.updateComplete;
879
+
880
+ assert.equal(element.valueStart, '30');
881
+ assert.equal(startInput.value, '30');
882
+ 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
+ assert.equal(element.valueEnd, '60');
895
+ assert.equal(endInput.value, '60');
896
+ assert.equal(floatInputs[1].value, '60');
897
+ });
917
898
 
899
+ test('programmatic valueStart reset after drag keeps range-start native input in sync', async () => {
900
+ element.valueStart = '20';
901
+ await element.updateComplete;
918
902
  const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
919
- startInput.dispatchEvent(new Event('change'));
903
+ const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
920
904
 
921
- assert.deepEqual(detail, { valueStart: '10', valueEnd: '40' });
905
+ startInput.value = '40';
906
+ 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');
922
914
  });
923
915
 
924
- test('range change event bubbles', async () => {
916
+ test('programmatic valueEnd reset after drag keeps range-end native input in sync', async () => {
917
+ element.valueEnd = '80';
925
918
  await element.updateComplete;
926
- let bubbled = false;
927
- document.body.addEventListener('change', () => (bubbled = true), { once: true });
919
+ const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
920
+ const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
928
921
 
929
- const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
930
- startInput.dispatchEvent(new Event('change'));
922
+ endInput.value = '60';
923
+ endInput.dispatchEvent(new Event('input'));
924
+ element.valueEnd = '80'; // Lit virtual is still '80' from the last render
931
925
 
932
- assert.isTrue(bubbled);
926
+ await element.updateComplete; // live() ensures '80' is written despite virtual==='80'
927
+
928
+ assert.equal(element.valueEnd, '80');
929
+ assert.equal(endInput.value, '80');
930
+ assert.equal(floatInputs[1].value, '80');
933
931
  });
934
932
 
935
- test('change event fires with valueStart and valueEnd detail on range-end change', async () => {
933
+ test('change event fires with valueStart and valueEnd detail from both thumbs', async () => {
936
934
  await element.updateComplete;
937
935
  let detail: { valueStart: string; valueEnd: string } | undefined;
938
936
  element.addEventListener('change', (e: Event) => {
939
937
  detail = (e as CustomEvent).detail;
940
938
  });
941
939
 
940
+ element.valueStart = '10';
941
+ element.valueEnd = '40';
942
+ const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
943
+ startInput.dispatchEvent(new Event('change'));
944
+ assert.deepEqual(detail, { valueStart: '10', valueEnd: '40' });
945
+
942
946
  element.valueStart = '15';
943
947
  element.valueEnd = '55';
944
-
945
948
  const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
946
949
  endInput.dispatchEvent(new Event('change'));
947
-
948
950
  assert.deepEqual(detail, { valueStart: '15', valueEnd: '55' });
949
951
  });
950
952
 
953
+ test('range change event bubbles', async () => {
954
+ await element.updateComplete;
955
+ let bubbled = false;
956
+ document.body.addEventListener('change', () => (bubbled = true), { once: true });
957
+
958
+ const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
959
+ startInput.dispatchEvent(new Event('change'));
960
+
961
+ assert.isTrue(bubbled);
962
+ });
963
+
951
964
  test('floating input change fires component range change event', async () => {
952
965
  await element.updateComplete;
953
966
  let detail: { valueStart: string; valueEnd: string } | undefined;
@@ -962,19 +975,6 @@ suite('zui-slider range', () => {
962
975
  assert.deepEqual(detail, { valueStart: '20', valueEnd: '100' });
963
976
  });
964
977
 
965
- test('range-wrapper gradient uses gray when disabled and reverts to blue when re-enabled', async () => {
966
- element.disabled = true;
967
- await element.updateComplete;
968
- const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
969
- assert.include(startInput.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-gray)');
970
- assert.notInclude(startInput.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-blue)');
971
-
972
- element.disabled = false;
973
- await element.updateComplete;
974
- assert.include(startInput.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-blue)');
975
- assert.notInclude(startInput.style.getPropertyValue('--zui-slider-track-bg'), 'var(--zui-gray)');
976
- });
977
-
978
978
  test('form reset hides visible floating inputs in range mode', async () => {
979
979
  await element.updateComplete;
980
980
  const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
@@ -988,19 +988,6 @@ suite('zui-slider range', () => {
988
988
  thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
989
989
  });
990
990
 
991
- test('range floating input empty-string input does not trigger value update', async () => {
992
- await element.updateComplete;
993
- element.valueStart = '20';
994
- element.valueEnd = '80';
995
- const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
996
- floatInputs[0].value = '';
997
- floatInputs[0].dispatchEvent(new Event('input'));
998
- floatInputs[1].value = '';
999
- floatInputs[1].dispatchEvent(new Event('input'));
1000
- assert.equal(element.valueStart, '20');
1001
- assert.equal(element.valueEnd, '80');
1002
- });
1003
-
1004
991
  test('range floating inputs hide when disabled is set while visible', async () => {
1005
992
  await element.updateComplete;
1006
993
  const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
@@ -1014,14 +1001,6 @@ suite('zui-slider range', () => {
1014
1001
  thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
1015
1002
  });
1016
1003
 
1017
- test('range-wrapper gradient insets are transparent outside thumb-size bounds', async () => {
1018
- await element.updateComplete;
1019
- const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
1020
- const bg = startInput.style.getPropertyValue('--zui-slider-track-bg');
1021
- assert.include(bg, 'transparent var(--zui-slider-thumb-size)');
1022
- assert.include(bg, 'transparent calc(100% - var(--zui-slider-thumb-size))');
1023
- });
1024
-
1025
1004
  test('disconnectedCallback clears timers and resets thumb input visibility in range mode', async () => {
1026
1005
  await element.updateComplete;
1027
1006
  const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
@@ -1038,45 +1017,6 @@ suite('zui-slider range', () => {
1038
1017
  thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
1039
1018
  });
1040
1019
 
1041
- test('range-wrapper gradient updates when valueStart and valueEnd change', async () => {
1042
- element.valueStart = '25';
1043
- element.valueEnd = '75';
1044
- await element.updateComplete;
1045
- const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
1046
- const bg25 = startInput.style.getPropertyValue('--zui-slider-track-bg');
1047
-
1048
- element.valueStart = '10';
1049
- element.valueEnd = '90';
1050
- await element.updateComplete;
1051
- const bg10 = startInput.style.getPropertyValue('--zui-slider-track-bg');
1052
-
1053
- assert.notEqual(bg25, bg10);
1054
- });
1055
-
1056
- test('range start floating input updates valueStart after debounce delay but not immediately', async () => {
1057
- await element.updateComplete;
1058
- element.valueStart = '20';
1059
- const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
1060
- floatInputs[0].value = '40';
1061
- floatInputs[0].dispatchEvent(new Event('input'));
1062
- // Debounce is 500ms — valueStart must not update synchronously
1063
- assert.equal(element.valueStart, '20');
1064
- await new Promise<void>((r) => setTimeout(r, 600));
1065
- assert.equal(element.valueStart, '40');
1066
- });
1067
-
1068
- test('range end floating input updates valueEnd after debounce delay but not immediately', async () => {
1069
- await element.updateComplete;
1070
- element.valueEnd = '80';
1071
- const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
1072
- floatInputs[1].value = '60';
1073
- floatInputs[1].dispatchEvent(new Event('input'));
1074
- // Debounce is 500ms — valueEnd must not update synchronously
1075
- assert.equal(element.valueEnd, '80');
1076
- await new Promise<void>((r) => setTimeout(r, 600));
1077
- assert.equal(element.valueEnd, '60');
1078
- });
1079
-
1080
1020
  test('focused range start floating input stays visible after pointerleave', async () => {
1081
1021
  await element.updateComplete;
1082
1022
  const startRangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
@@ -1094,32 +1034,29 @@ suite('zui-slider range', () => {
1094
1034
  assert.isTrue(thumbInputDivs[0].classList.contains('thumb-input--visible'));
1095
1035
  });
1096
1036
 
1097
- test('range floating inputs clamp out-of-bounds values to min/max after debounce', async () => {
1037
+ test('range floating inputs clamp out-of-bounds values to min/max on commit', async () => {
1098
1038
  await element.updateComplete;
1099
1039
  const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
1100
1040
  floatInputs[0].value = '150';
1101
- floatInputs[0].dispatchEvent(new Event('input'));
1102
- await new Promise<void>((r) => setTimeout(r, 600));
1041
+ floatInputs[0].dispatchEvent(new Event('change'));
1103
1042
  assert.equal(element.valueStart, '100');
1104
1043
 
1105
1044
  element.min = 20;
1106
1045
  floatInputs[1].value = '5';
1107
- floatInputs[1].dispatchEvent(new Event('input'));
1108
- await new Promise<void>((r) => setTimeout(r, 600));
1046
+ floatInputs[1].dispatchEvent(new Event('change'));
1109
1047
  assert.equal(element.valueEnd, '20');
1110
1048
  });
1111
1049
 
1112
- test('range floating input snaps typed valueStart to nearest step after debounce', async () => {
1050
+ test('range floating input snaps typed valueStart to nearest step on commit', async () => {
1113
1051
  await element.updateComplete;
1114
1052
  element.step = 10;
1115
1053
  const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
1116
1054
  floatInputs[0].value = '23';
1117
- floatInputs[0].dispatchEvent(new Event('input'));
1118
- await new Promise<void>((r) => setTimeout(r, 600));
1055
+ floatInputs[0].dispatchEvent(new Event('change'));
1119
1056
  assert.equal(element.valueStart, '20');
1120
1057
  });
1121
1058
 
1122
- test('range start floating input change flushes debounce and dispatches correct value immediately', async () => {
1059
+ test('range start floating input change dispatches correct value', async () => {
1123
1060
  await element.updateComplete;
1124
1061
  let detail: { valueStart: string; valueEnd: string } | undefined;
1125
1062
  element.addEventListener('change', (e: Event) => {
@@ -1128,14 +1065,37 @@ suite('zui-slider range', () => {
1128
1065
 
1129
1066
  const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
1130
1067
  floatInputs[0].value = '30';
1131
- floatInputs[0].dispatchEvent(new Event('input'));
1132
- assert.equal(element.valueStart, '0'); // debounce not yet fired
1133
- floatInputs[0].dispatchEvent(new Event('change')); // commits immediately
1068
+ floatInputs[0].dispatchEvent(new Event('change'));
1134
1069
  assert.equal(element.valueStart, '30');
1135
1070
  assert.deepEqual(detail, { valueStart: '30', valueEnd: '100' });
1136
- // Debounce timer was cleared — value must not update again after 500ms
1137
- await new Promise<void>((r) => setTimeout(r, 600));
1071
+ });
1072
+
1073
+ test('pressing Enter in range floating inputs commits values', async () => {
1074
+ await element.updateComplete;
1075
+ const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
1076
+
1077
+ floatInputs[0].value = '30';
1078
+ floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
1138
1079
  assert.equal(element.valueStart, '30');
1080
+
1081
+ floatInputs[1].value = '70';
1082
+ floatInputs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
1083
+ assert.equal(element.valueEnd, '70');
1084
+ });
1085
+
1086
+ test('range floating input change does not dispatch change event when value is unchanged', async () => {
1087
+ await element.updateComplete;
1088
+ element.valueStart = '30';
1089
+ element.valueEnd = '70';
1090
+ let changeCount = 0;
1091
+ element.addEventListener('change', () => changeCount++);
1092
+
1093
+ const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
1094
+ floatInputs[0].value = '30';
1095
+ floatInputs[0].dispatchEvent(new Event('change'));
1096
+ floatInputs[1].value = '70';
1097
+ floatInputs[1].dispatchEvent(new Event('change'));
1098
+ assert.equal(changeCount, 0);
1139
1099
  });
1140
1100
  });
1141
1101
 
@@ -1294,15 +1254,6 @@ suite('zui-slider step dots', () => {
1294
1254
  assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
1295
1255
  });
1296
1256
 
1297
- test('step dot positions are correctly offset by thumb size', async () => {
1298
- await element.updateComplete;
1299
- const dots = element.shadowRoot!.querySelectorAll<HTMLElement>('.step-dot');
1300
- assert.equal(dots[0].style.left, 'var(--zui-slider-thumb-size)');
1301
- assert.equal(dots[dots.length - 1].style.left, 'calc(100% - var(--zui-slider-thumb-size))');
1302
- // pos=50: offset = 1.5 - (3*50)/100 = 0
1303
- assert.equal(dots[2].style.left, 'calc(50% + var(--zui-slider-thumb-size) * 0)');
1304
- });
1305
-
1306
1257
  test('step dots toggle when step is set to 0 then back', async () => {
1307
1258
  await element.updateComplete;
1308
1259
  assert.exists(element.shadowRoot!.querySelector('.step-dots'));
@@ -1423,6 +1374,20 @@ suite('zui-slider steps', () => {
1423
1374
  assert.equal(element.value, 'Large');
1424
1375
  });
1425
1376
 
1377
+ test('steps floating input change does not dispatch change event when value is unchanged', async () => {
1378
+ element.steps = ['Small', 'Medium', 'Large'];
1379
+ element.value = 'Medium';
1380
+ await element.updateComplete;
1381
+ let changeCount = 0;
1382
+ element.addEventListener('change', () => changeCount++);
1383
+
1384
+ const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
1385
+ floatInput.value = 'Medium';
1386
+ floatInput.dispatchEvent(new Event('change'));
1387
+ assert.equal(changeCount, 0);
1388
+ assert.equal(element.value, 'Medium');
1389
+ });
1390
+
1426
1391
  test('floating input change to invalid label reverts input to current value', async () => {
1427
1392
  element.steps = ['Small', 'Medium', 'Large'];
1428
1393
  element.value = 'Medium';
@@ -1434,15 +1399,13 @@ suite('zui-slider steps', () => {
1434
1399
  assert.equal(element.value, 'Medium');
1435
1400
  });
1436
1401
 
1437
- test('floating input debounce resolves typed numeric alias to nearest step', async () => {
1402
+ test('floating input resolves typed numeric alias to nearest step on commit', async () => {
1438
1403
  element.steps = [0, 25, 50, 75, 100];
1439
1404
  await element.updateComplete;
1440
1405
  assert.equal(element.value, '50'); // '50' is a valid label — not snapped on init
1441
1406
  const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
1442
1407
  floatInput.value = '30';
1443
- floatInput.dispatchEvent(new Event('input'));
1444
- assert.equal(element.value, '50'); // debounce not fired yet
1445
- await new Promise<void>((r) => setTimeout(r, 600));
1408
+ floatInput.dispatchEvent(new Event('change'));
1446
1409
  // |30-25|=5, |30-50|=20 → snaps to '25'
1447
1410
  assert.equal(element.value, '25');
1448
1411
  });
@@ -1531,21 +1494,13 @@ suite('zui-slider steps', () => {
1531
1494
  assert.equal(element.shadowRoot!.querySelectorAll('.step-dot-label').length, 0);
1532
1495
  });
1533
1496
 
1534
- test('clearing steps floating input cancels pending debounce so stale step is not committed', async () => {
1535
- element.steps = [0, 25, 50, 75, 100];
1536
- element.value = '50';
1497
+ test('pressing Enter in steps floating input commits the step label', async () => {
1498
+ element.steps = ['Small', 'Medium', 'Large'];
1537
1499
  await element.updateComplete;
1538
1500
  const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
1539
- // Start a debounce for '80' (would snap to '75')
1540
- floatInput.value = '80';
1541
- floatInput.dispatchEvent(new Event('input'));
1542
- assert.equal(element.value, '50'); // debounce not yet fired
1543
- // Clear the field before debounce fires
1544
- floatInput.value = '';
1545
- floatInput.dispatchEvent(new Event('input'));
1546
- await new Promise<void>((r) => setTimeout(r, 600));
1547
- // The debounce was cancelled; value must stay at '50'
1548
- assert.equal(element.value, '50');
1501
+ floatInput.value = 'Large';
1502
+ floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
1503
+ assert.equal(element.value, 'Large');
1549
1504
  });
1550
1505
 
1551
1506
  test('committing empty steps floating input reverts display to current step label', async () => {
@@ -1767,23 +1722,6 @@ suite('zui-slider stepParser', () => {
1767
1722
  assert.equal(element.value, '2k');
1768
1723
  });
1769
1724
 
1770
- test('null return from debounced stepParser does not update value', async () => {
1771
- element.steps = [
1772
- { value: 1000, label: '1k' },
1773
- { value: 2000, label: '2k' },
1774
- { value: 3000, label: '3k' },
1775
- ];
1776
- element.stepParser = () => null;
1777
- await element.updateComplete;
1778
- // Default '50' snapped to '1k' on init
1779
- const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
1780
- floatInput.value = 'anything';
1781
- floatInput.dispatchEvent(new Event('input'));
1782
- await new Promise<void>((r) => setTimeout(r, 600));
1783
- // null → debounce does not call the setter → value stays at '1k'
1784
- assert.equal(element.value, '1k');
1785
- });
1786
-
1787
1725
  test('string return from stepParser is used directly as a step label', async () => {
1788
1726
  element.steps = [
1789
1727
  { value: 1000, label: '1k' },