@zywave/zui-slider 4.4.0-pre.4 → 4.4.0-pre.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-elements.json +18 -35
- package/dist/zui-slider-css.js +1 -1
- package/dist/zui-slider-css.js.map +1 -1
- package/dist/zui-slider.js +95 -85
- package/dist/zui-slider.js.map +1 -1
- package/docs/demo.html +1 -1
- package/lab.html +5 -5
- package/package.json +2 -2
- package/src/zui-slider-css.js +1 -1
- package/src/zui-slider.scss +2 -0
- package/src/zui-slider.ts +93 -83
- package/test/zui-slider.test.ts +381 -301
package/test/zui-slider.test.ts
CHANGED
|
@@ -97,17 +97,14 @@ suite('zui-slider', () => {
|
|
|
97
97
|
assert.equal(element.value, '33.5');
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
test('float input position
|
|
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 () => {
|
|
@@ -127,7 +124,7 @@ suite('zui-slider', () => {
|
|
|
127
124
|
test('valueAsNumber reads the single-mode backing field regardless of range mode', () => {
|
|
128
125
|
element.value = '35';
|
|
129
126
|
element.range = true;
|
|
130
|
-
// valueAsNumber reads #value (the single-mode backing field)
|
|
127
|
+
// valueAsNumber reads #value (the single-mode backing field); range mode does not update it
|
|
131
128
|
assert.equal(element.valueAsNumber, 35);
|
|
132
129
|
});
|
|
133
130
|
|
|
@@ -196,26 +193,38 @@ suite('zui-slider', () => {
|
|
|
196
193
|
assert.equal(new FormData(form).get(name), '50');
|
|
197
194
|
});
|
|
198
195
|
|
|
199
|
-
test('
|
|
196
|
+
test('element.value, native range input, and floating input stay in sync after drag', async () => {
|
|
200
197
|
await element.updateComplete;
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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('
|
|
209
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 () => {
|
|
@@ -268,7 +277,7 @@ suite('zui-slider', () => {
|
|
|
268
277
|
});
|
|
269
278
|
|
|
270
279
|
test('connectedCallback with range property (not attribute) uses range branch', async () => {
|
|
271
|
-
// el.range = true without setAttribute
|
|
280
|
+
// el.range = true without setAttribute; connectedCallback must check this.range too.
|
|
272
281
|
// Must await so the async connectedCallback reads native inputs and calls _setFormValue.
|
|
273
282
|
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
274
283
|
el.range = true;
|
|
@@ -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,51 +322,103 @@ 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
|
|
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('
|
|
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('
|
|
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
|
|
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('
|
|
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
|
|
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('
|
|
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
|
-
|
|
407
|
-
|
|
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 and retains focus', async () => {
|
|
370
|
+
await element.updateComplete;
|
|
371
|
+
element.value = '50';
|
|
372
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
373
|
+
let blurred = false;
|
|
374
|
+
floatInput.addEventListener('blur', () => (blurred = true));
|
|
375
|
+
floatInput.value = '75';
|
|
376
|
+
floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
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'));
|
|
408
388
|
assert.equal(element.value, '75');
|
|
409
389
|
});
|
|
410
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);
|
|
420
|
+
});
|
|
421
|
+
|
|
411
422
|
test('focused floating input stays visible after pointerleave', async () => {
|
|
412
423
|
await element.updateComplete;
|
|
413
424
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
@@ -437,7 +448,7 @@ suite('zui-slider', () => {
|
|
|
437
448
|
await element.updateComplete;
|
|
438
449
|
assert.isTrue(thumbInputDiv.classList.contains('thumb-input--visible'));
|
|
439
450
|
|
|
440
|
-
// Blur without re-focusing
|
|
451
|
+
// Blur without re-focusing; #blurFloatingInput clears focused and schedules hide.
|
|
441
452
|
// #scheduleHideThumbInput uses a real 100ms setTimeout, so wait past it before asserting.
|
|
442
453
|
floatInput.dispatchEvent(new Event('blur'));
|
|
443
454
|
rangeInput.dispatchEvent(new Event('pointerleave'));
|
|
@@ -455,7 +466,7 @@ suite('zui-slider', () => {
|
|
|
455
466
|
await element.updateComplete;
|
|
456
467
|
assert.isTrue(element.shadowRoot!.querySelector('.thumb-input')!.classList.contains('thumb-input--visible'));
|
|
457
468
|
|
|
458
|
-
// Disconnect then reconnect
|
|
469
|
+
// Disconnect then reconnect; disconnectedCallback should clear state
|
|
459
470
|
document.body.removeChild(form);
|
|
460
471
|
document.body.appendChild(form);
|
|
461
472
|
await element.updateComplete;
|
|
@@ -473,8 +484,7 @@ suite('zui-slider', () => {
|
|
|
473
484
|
input.dispatchEvent(new Event('input'));
|
|
474
485
|
assert.equal(element.value, '50');
|
|
475
486
|
floatInput.value = '75';
|
|
476
|
-
floatInput.dispatchEvent(new Event('
|
|
477
|
-
await new Promise<void>((r) => setTimeout(r, 350));
|
|
487
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
478
488
|
assert.equal(element.value, '50');
|
|
479
489
|
});
|
|
480
490
|
|
|
@@ -540,7 +550,6 @@ suite('zui-slider min-max labels', () => {
|
|
|
540
550
|
});
|
|
541
551
|
|
|
542
552
|
test('min-max labels are hidden when showStepLabels is true', async () => {
|
|
543
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
544
553
|
element.showStepLabels = true;
|
|
545
554
|
await element.updateComplete;
|
|
546
555
|
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
@@ -582,7 +591,6 @@ suite('zui-slider min-max labels', () => {
|
|
|
582
591
|
});
|
|
583
592
|
|
|
584
593
|
test('show-step-labels attribute hides min-max labels', async () => {
|
|
585
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
586
594
|
element.setAttribute('show-step-labels', '');
|
|
587
595
|
await element.updateComplete;
|
|
588
596
|
const labels = element.shadowRoot!.querySelector<HTMLElement>('.min-max-labels');
|
|
@@ -807,61 +815,39 @@ suite('zui-slider range', () => {
|
|
|
807
815
|
document.body.removeChild(f);
|
|
808
816
|
});
|
|
809
817
|
|
|
810
|
-
test('input handler rejects start dragging past end', async () => {
|
|
818
|
+
test('input handler rejects start dragging to or past end', async () => {
|
|
811
819
|
await element.updateComplete;
|
|
812
820
|
element.valueStart = '20';
|
|
813
821
|
element.valueEnd = '60';
|
|
814
|
-
|
|
815
822
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
816
|
-
startInput.value = '70';
|
|
817
|
-
startInput.dispatchEvent(new Event('input'));
|
|
818
823
|
|
|
824
|
+
startInput.value = '70'; // past end
|
|
825
|
+
startInput.dispatchEvent(new Event('input'));
|
|
819
826
|
assert.equal(element.valueStart, '20');
|
|
820
827
|
assert.equal(element.valueEnd, '60');
|
|
821
828
|
assert.equal(startInput.value, '20');
|
|
822
|
-
});
|
|
823
829
|
|
|
824
|
-
|
|
825
|
-
await element.updateComplete;
|
|
826
|
-
element.valueStart = '20';
|
|
827
|
-
element.valueEnd = '60';
|
|
828
|
-
|
|
829
|
-
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
830
|
-
startInput.value = '60';
|
|
830
|
+
startInput.value = '60'; // equal to end
|
|
831
831
|
startInput.dispatchEvent(new Event('input'));
|
|
832
|
-
|
|
833
|
-
// start >= end is rejected; input snaps back and state is unchanged
|
|
834
832
|
assert.equal(element.valueStart, '20');
|
|
835
|
-
assert.equal(element.valueEnd, '60');
|
|
836
833
|
assert.equal(startInput.value, '20');
|
|
837
834
|
});
|
|
838
835
|
|
|
839
|
-
test('input handler rejects end dragging before start', async () => {
|
|
836
|
+
test('input handler rejects end dragging to or before start', async () => {
|
|
840
837
|
await element.updateComplete;
|
|
841
838
|
element.valueStart = '30';
|
|
842
839
|
element.valueEnd = '70';
|
|
843
|
-
|
|
844
840
|
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
845
|
-
endInput.value = '20';
|
|
846
|
-
endInput.dispatchEvent(new Event('input'));
|
|
847
841
|
|
|
842
|
+
endInput.value = '20'; // before start
|
|
843
|
+
endInput.dispatchEvent(new Event('input'));
|
|
848
844
|
assert.equal(element.valueEnd, '70');
|
|
849
845
|
assert.equal(element.valueStart, '30');
|
|
850
846
|
assert.equal(endInput.value, '70');
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
test('input handler rejects end dragging to equal start', async () => {
|
|
854
|
-
await element.updateComplete;
|
|
855
|
-
element.valueStart = '30';
|
|
856
|
-
element.valueEnd = '70';
|
|
857
847
|
|
|
858
|
-
|
|
859
|
-
endInput.value = '30';
|
|
848
|
+
endInput.value = '30'; // equal to start
|
|
860
849
|
endInput.dispatchEvent(new Event('input'));
|
|
861
|
-
|
|
862
|
-
// end <= start is rejected; input snaps back and state is unchanged
|
|
863
850
|
assert.equal(element.valueEnd, '70');
|
|
864
|
-
assert.equal(element.valueStart, '30');
|
|
865
851
|
assert.equal(endInput.value, '70');
|
|
866
852
|
});
|
|
867
853
|
|
|
@@ -892,20 +878,63 @@ suite('zui-slider range', () => {
|
|
|
892
878
|
assert.equal(element.valueStart, '33.5');
|
|
893
879
|
});
|
|
894
880
|
|
|
895
|
-
test('range start float input position
|
|
881
|
+
test('range start float input position updates after drag', async () => {
|
|
896
882
|
await element.updateComplete;
|
|
897
883
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
898
884
|
const thumbInputDivs = element.shadowRoot!.querySelectorAll<HTMLElement>('.thumb-input');
|
|
899
|
-
const bgBefore = startInput.style.getPropertyValue('--zui-slider-track-bg');
|
|
900
885
|
startInput.value = '30';
|
|
901
886
|
startInput.dispatchEvent(new Event('input'));
|
|
902
887
|
await element.updateComplete;
|
|
903
888
|
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
889
|
});
|
|
907
890
|
|
|
908
|
-
test('
|
|
891
|
+
test('native range inputs and floating inputs stay in sync with component values after drag', async () => {
|
|
892
|
+
await element.updateComplete;
|
|
893
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
894
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
895
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
896
|
+
|
|
897
|
+
startInput.value = '30';
|
|
898
|
+
startInput.dispatchEvent(new Event('input'));
|
|
899
|
+
endInput.value = '60';
|
|
900
|
+
endInput.dispatchEvent(new Event('input'));
|
|
901
|
+
await element.updateComplete;
|
|
902
|
+
|
|
903
|
+
assert.equal(element.valueStart, '30');
|
|
904
|
+
assert.equal(startInput.value, '30');
|
|
905
|
+
assert.equal(floatInputs[0].value, '30');
|
|
906
|
+
assert.equal(element.valueEnd, '60');
|
|
907
|
+
assert.equal(endInput.value, '60');
|
|
908
|
+
assert.equal(floatInputs[1].value, '60');
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test('programmatic value resets after drag keep both native range inputs in sync via live()', async () => {
|
|
912
|
+
element.valueStart = '20';
|
|
913
|
+
element.valueEnd = '80';
|
|
914
|
+
await element.updateComplete;
|
|
915
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
916
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
917
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input');
|
|
918
|
+
|
|
919
|
+
startInput.value = '40';
|
|
920
|
+
startInput.dispatchEvent(new Event('input'));
|
|
921
|
+
element.valueStart = '20'; // Lit virtual is still '20'; live() must force the DOM write
|
|
922
|
+
|
|
923
|
+
endInput.value = '60';
|
|
924
|
+
endInput.dispatchEvent(new Event('input'));
|
|
925
|
+
element.valueEnd = '80'; // same live() invariant for end thumb
|
|
926
|
+
|
|
927
|
+
await element.updateComplete;
|
|
928
|
+
|
|
929
|
+
assert.equal(element.valueStart, '20');
|
|
930
|
+
assert.equal(startInput.value, '20');
|
|
931
|
+
assert.equal(floatInputs[0].value, '20');
|
|
932
|
+
assert.equal(element.valueEnd, '80');
|
|
933
|
+
assert.equal(endInput.value, '80');
|
|
934
|
+
assert.equal(floatInputs[1].value, '80');
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test('change event fires with valueStart and valueEnd detail from both thumbs', async () => {
|
|
909
938
|
await element.updateComplete;
|
|
910
939
|
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
911
940
|
element.addEventListener('change', (e: Event) => {
|
|
@@ -914,11 +943,15 @@ suite('zui-slider range', () => {
|
|
|
914
943
|
|
|
915
944
|
element.valueStart = '10';
|
|
916
945
|
element.valueEnd = '40';
|
|
917
|
-
|
|
918
946
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
919
947
|
startInput.dispatchEvent(new Event('change'));
|
|
920
|
-
|
|
921
948
|
assert.deepEqual(detail, { valueStart: '10', valueEnd: '40' });
|
|
949
|
+
|
|
950
|
+
element.valueStart = '15';
|
|
951
|
+
element.valueEnd = '55';
|
|
952
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
953
|
+
endInput.dispatchEvent(new Event('change'));
|
|
954
|
+
assert.deepEqual(detail, { valueStart: '15', valueEnd: '55' });
|
|
922
955
|
});
|
|
923
956
|
|
|
924
957
|
test('range change event bubbles', async () => {
|
|
@@ -932,22 +965,6 @@ suite('zui-slider range', () => {
|
|
|
932
965
|
assert.isTrue(bubbled);
|
|
933
966
|
});
|
|
934
967
|
|
|
935
|
-
test('change event fires with valueStart and valueEnd detail on range-end change', async () => {
|
|
936
|
-
await element.updateComplete;
|
|
937
|
-
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
938
|
-
element.addEventListener('change', (e: Event) => {
|
|
939
|
-
detail = (e as CustomEvent).detail;
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
element.valueStart = '15';
|
|
943
|
-
element.valueEnd = '55';
|
|
944
|
-
|
|
945
|
-
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
946
|
-
endInput.dispatchEvent(new Event('change'));
|
|
947
|
-
|
|
948
|
-
assert.deepEqual(detail, { valueStart: '15', valueEnd: '55' });
|
|
949
|
-
});
|
|
950
|
-
|
|
951
968
|
test('floating input change fires component range change event', async () => {
|
|
952
969
|
await element.updateComplete;
|
|
953
970
|
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
@@ -962,19 +979,6 @@ suite('zui-slider range', () => {
|
|
|
962
979
|
assert.deepEqual(detail, { valueStart: '20', valueEnd: '100' });
|
|
963
980
|
});
|
|
964
981
|
|
|
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
982
|
test('form reset hides visible floating inputs in range mode', async () => {
|
|
979
983
|
await element.updateComplete;
|
|
980
984
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
@@ -988,19 +992,6 @@ suite('zui-slider range', () => {
|
|
|
988
992
|
thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
|
|
989
993
|
});
|
|
990
994
|
|
|
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
995
|
test('range floating inputs hide when disabled is set while visible', async () => {
|
|
1005
996
|
await element.updateComplete;
|
|
1006
997
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
@@ -1014,14 +1005,6 @@ suite('zui-slider range', () => {
|
|
|
1014
1005
|
thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
|
|
1015
1006
|
});
|
|
1016
1007
|
|
|
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
1008
|
test('disconnectedCallback clears timers and resets thumb input visibility in range mode', async () => {
|
|
1026
1009
|
await element.updateComplete;
|
|
1027
1010
|
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
@@ -1038,45 +1021,6 @@ suite('zui-slider range', () => {
|
|
|
1038
1021
|
thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
|
|
1039
1022
|
});
|
|
1040
1023
|
|
|
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
1024
|
test('focused range start floating input stays visible after pointerleave', async () => {
|
|
1081
1025
|
await element.updateComplete;
|
|
1082
1026
|
const startRangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
@@ -1094,32 +1038,33 @@ suite('zui-slider range', () => {
|
|
|
1094
1038
|
assert.isTrue(thumbInputDivs[0].classList.contains('thumb-input--visible'));
|
|
1095
1039
|
});
|
|
1096
1040
|
|
|
1097
|
-
test('range floating inputs clamp out-of-bounds values to min/max
|
|
1041
|
+
test('range floating inputs clamp out-of-bounds values to min/max on commit', async () => {
|
|
1098
1042
|
await element.updateComplete;
|
|
1043
|
+
element.valueStart = '20';
|
|
1044
|
+
element.valueEnd = '80';
|
|
1099
1045
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1100
|
-
floatInputs[0].value = '150';
|
|
1101
|
-
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1102
|
-
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1103
|
-
assert.equal(element.valueStart, '100');
|
|
1104
1046
|
|
|
1105
|
-
|
|
1106
|
-
floatInputs[
|
|
1107
|
-
floatInputs[
|
|
1108
|
-
|
|
1109
|
-
|
|
1047
|
+
// Start thumb: type below min → clamped to min (0); 0 < endNum (80) → no nudge
|
|
1048
|
+
floatInputs[0].value = '-50';
|
|
1049
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1050
|
+
assert.equal(element.valueStart, '0');
|
|
1051
|
+
|
|
1052
|
+
// End thumb: type above max → clamped to max (100); 100 > startNum (0) → no nudge
|
|
1053
|
+
floatInputs[1].value = '150';
|
|
1054
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1055
|
+
assert.equal(element.valueEnd, '100');
|
|
1110
1056
|
});
|
|
1111
1057
|
|
|
1112
|
-
test('range floating input snaps typed valueStart to nearest step
|
|
1058
|
+
test('range floating input snaps typed valueStart to nearest step on commit', async () => {
|
|
1113
1059
|
await element.updateComplete;
|
|
1114
1060
|
element.step = 10;
|
|
1115
1061
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1116
1062
|
floatInputs[0].value = '23';
|
|
1117
|
-
floatInputs[0].dispatchEvent(new Event('
|
|
1118
|
-
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1063
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1119
1064
|
assert.equal(element.valueStart, '20');
|
|
1120
1065
|
});
|
|
1121
1066
|
|
|
1122
|
-
test('range start floating input change
|
|
1067
|
+
test('range start floating input change dispatches correct value', async () => {
|
|
1123
1068
|
await element.updateComplete;
|
|
1124
1069
|
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
1125
1070
|
element.addEventListener('change', (e: Event) => {
|
|
@@ -1128,18 +1073,128 @@ suite('zui-slider range', () => {
|
|
|
1128
1073
|
|
|
1129
1074
|
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1130
1075
|
floatInputs[0].value = '30';
|
|
1131
|
-
floatInputs[0].dispatchEvent(new Event('
|
|
1132
|
-
assert.equal(element.valueStart, '0'); // debounce not yet fired
|
|
1133
|
-
floatInputs[0].dispatchEvent(new Event('change')); // commits immediately
|
|
1076
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1134
1077
|
assert.equal(element.valueStart, '30');
|
|
1135
1078
|
assert.deepEqual(detail, { valueStart: '30', valueEnd: '100' });
|
|
1136
|
-
|
|
1137
|
-
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test('pressing Enter in range floating inputs commits values and retains focus', async () => {
|
|
1082
|
+
await element.updateComplete;
|
|
1083
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1084
|
+
let startBlurred = false;
|
|
1085
|
+
floatInputs[0].addEventListener('blur', () => (startBlurred = true));
|
|
1086
|
+
|
|
1087
|
+
floatInputs[0].value = '30';
|
|
1088
|
+
floatInputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1089
|
+
assert.equal(element.valueStart, '30');
|
|
1090
|
+
assert.isFalse(startBlurred);
|
|
1091
|
+
|
|
1092
|
+
floatInputs[1].value = '70';
|
|
1093
|
+
floatInputs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1094
|
+
assert.equal(element.valueEnd, '70');
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
test('range floating input change does not dispatch change event when value is unchanged', async () => {
|
|
1098
|
+
await element.updateComplete;
|
|
1099
|
+
element.valueStart = '30';
|
|
1100
|
+
element.valueEnd = '70';
|
|
1101
|
+
let changeCount = 0;
|
|
1102
|
+
element.addEventListener('change', () => changeCount++);
|
|
1103
|
+
|
|
1104
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1105
|
+
floatInputs[0].value = '30';
|
|
1106
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
1107
|
+
floatInputs[1].value = '70';
|
|
1108
|
+
floatInputs[1].dispatchEvent(new Event('change'));
|
|
1109
|
+
assert.equal(changeCount, 0);
|
|
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'));
|
|
1138
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);
|
|
1139
1194
|
});
|
|
1140
1195
|
});
|
|
1141
1196
|
|
|
1142
|
-
// Dispatches a click at a given 0
|
|
1197
|
+
// Dispatches a click at a given 0-1 fraction of the effective track, matching the
|
|
1143
1198
|
// coordinate math in #onTrackClick so the component computes the same fraction back.
|
|
1144
1199
|
function clickAtFraction(element: ZuiSlider, fraction: number): void {
|
|
1145
1200
|
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
@@ -1247,6 +1302,48 @@ suite('zui-slider range track click', () => {
|
|
|
1247
1302
|
assert.equal(element.valueStart, '40');
|
|
1248
1303
|
assert.equal(element.valueEnd, '50');
|
|
1249
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
|
+
});
|
|
1250
1347
|
});
|
|
1251
1348
|
|
|
1252
1349
|
suite('zui-slider step dots', () => {
|
|
@@ -1268,13 +1365,10 @@ suite('zui-slider step dots', () => {
|
|
|
1268
1365
|
assert.equal(dots.length, 5); // 0, 25, 50, 75, 100
|
|
1269
1366
|
});
|
|
1270
1367
|
|
|
1271
|
-
test('step dots not rendered when step is 0', async () => {
|
|
1368
|
+
test('step dots not rendered when step is 0 or negative', async () => {
|
|
1272
1369
|
element.step = 0;
|
|
1273
1370
|
await element.updateComplete;
|
|
1274
1371
|
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1275
|
-
});
|
|
1276
|
-
|
|
1277
|
-
test('step dots not rendered when step is negative', async () => {
|
|
1278
1372
|
element.step = -5;
|
|
1279
1373
|
await element.updateComplete;
|
|
1280
1374
|
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
@@ -1294,15 +1388,6 @@ suite('zui-slider step dots', () => {
|
|
|
1294
1388
|
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1295
1389
|
});
|
|
1296
1390
|
|
|
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
1391
|
test('step dots toggle when step is set to 0 then back', async () => {
|
|
1307
1392
|
await element.updateComplete;
|
|
1308
1393
|
assert.exists(element.shadowRoot!.querySelector('.step-dots'));
|
|
@@ -1351,6 +1436,7 @@ suite('zui-slider steps', () => {
|
|
|
1351
1436
|
setup(() => {
|
|
1352
1437
|
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1353
1438
|
form = buildForm({ enableSubmit: false, appendChildren: [element] });
|
|
1439
|
+
element.steps = ['Small', 'Medium', 'Large'];
|
|
1354
1440
|
});
|
|
1355
1441
|
|
|
1356
1442
|
teardown(() => {
|
|
@@ -1358,20 +1444,17 @@ suite('zui-slider steps', () => {
|
|
|
1358
1444
|
});
|
|
1359
1445
|
|
|
1360
1446
|
test('initializes value to first step when current value is not a step label', async () => {
|
|
1361
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1362
1447
|
await element.updateComplete;
|
|
1363
1448
|
assert.equal(element.value, 'Small');
|
|
1364
1449
|
});
|
|
1365
1450
|
|
|
1366
1451
|
test('value set to a valid step label is preserved', async () => {
|
|
1367
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1368
1452
|
element.value = 'Medium';
|
|
1369
1453
|
await element.updateComplete;
|
|
1370
1454
|
assert.equal(element.value, 'Medium');
|
|
1371
1455
|
});
|
|
1372
1456
|
|
|
1373
1457
|
test('native range input has index-based min, max, and step attributes', async () => {
|
|
1374
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1375
1458
|
await element.updateComplete;
|
|
1376
1459
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1377
1460
|
assert.equal(rangeInput.min, '0');
|
|
@@ -1380,7 +1463,6 @@ suite('zui-slider steps', () => {
|
|
|
1380
1463
|
});
|
|
1381
1464
|
|
|
1382
1465
|
test('native range input value reflects current step index', async () => {
|
|
1383
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1384
1466
|
element.value = 'Medium';
|
|
1385
1467
|
await element.updateComplete;
|
|
1386
1468
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
@@ -1388,7 +1470,6 @@ suite('zui-slider steps', () => {
|
|
|
1388
1470
|
});
|
|
1389
1471
|
|
|
1390
1472
|
test('dragging native range to index sets value to corresponding step label', async () => {
|
|
1391
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1392
1473
|
await element.updateComplete;
|
|
1393
1474
|
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
1394
1475
|
rangeInput.value = '2';
|
|
@@ -1397,7 +1478,6 @@ suite('zui-slider steps', () => {
|
|
|
1397
1478
|
});
|
|
1398
1479
|
|
|
1399
1480
|
test('progress is computed by step index, not by numeric value', async () => {
|
|
1400
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1401
1481
|
element.value = 'Small';
|
|
1402
1482
|
await element.updateComplete;
|
|
1403
1483
|
assert.equal(element.progress, 0);
|
|
@@ -1408,14 +1488,12 @@ suite('zui-slider steps', () => {
|
|
|
1408
1488
|
});
|
|
1409
1489
|
|
|
1410
1490
|
test('floating input is type="text" in steps mode', async () => {
|
|
1411
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1412
1491
|
await element.updateComplete;
|
|
1413
1492
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1414
1493
|
assert.equal(floatInput.type, 'text');
|
|
1415
1494
|
});
|
|
1416
1495
|
|
|
1417
1496
|
test('floating input change to valid step label updates value immediately', async () => {
|
|
1418
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1419
1497
|
await element.updateComplete;
|
|
1420
1498
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1421
1499
|
floatInput.value = 'Large';
|
|
@@ -1423,8 +1501,20 @@ suite('zui-slider steps', () => {
|
|
|
1423
1501
|
assert.equal(element.value, 'Large');
|
|
1424
1502
|
});
|
|
1425
1503
|
|
|
1504
|
+
test('steps floating input change does not dispatch change event when value is unchanged', async () => {
|
|
1505
|
+
element.value = 'Medium';
|
|
1506
|
+
await element.updateComplete;
|
|
1507
|
+
let changeCount = 0;
|
|
1508
|
+
element.addEventListener('change', () => changeCount++);
|
|
1509
|
+
|
|
1510
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1511
|
+
floatInput.value = 'Medium';
|
|
1512
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1513
|
+
assert.equal(changeCount, 0);
|
|
1514
|
+
assert.equal(element.value, 'Medium');
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1426
1517
|
test('floating input change to invalid label reverts input to current value', async () => {
|
|
1427
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1428
1518
|
element.value = 'Medium';
|
|
1429
1519
|
await element.updateComplete;
|
|
1430
1520
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
@@ -1434,15 +1524,13 @@ suite('zui-slider steps', () => {
|
|
|
1434
1524
|
assert.equal(element.value, 'Medium');
|
|
1435
1525
|
});
|
|
1436
1526
|
|
|
1437
|
-
test('floating input
|
|
1527
|
+
test('floating input resolves typed numeric alias to nearest step on commit', async () => {
|
|
1438
1528
|
element.steps = [0, 25, 50, 75, 100];
|
|
1439
1529
|
await element.updateComplete;
|
|
1440
|
-
assert.equal(element.value, '50'); // '50' is a valid label
|
|
1530
|
+
assert.equal(element.value, '50'); // '50' is a valid label; not snapped on init
|
|
1441
1531
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1442
1532
|
floatInput.value = '30';
|
|
1443
|
-
floatInput.dispatchEvent(new Event('
|
|
1444
|
-
assert.equal(element.value, '50'); // debounce not fired yet
|
|
1445
|
-
await new Promise<void>((r) => setTimeout(r, 600));
|
|
1533
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
1446
1534
|
// |30-25|=5, |30-50|=20 → snaps to '25'
|
|
1447
1535
|
assert.equal(element.value, '25');
|
|
1448
1536
|
});
|
|
@@ -1460,13 +1548,11 @@ suite('zui-slider steps', () => {
|
|
|
1460
1548
|
});
|
|
1461
1549
|
|
|
1462
1550
|
test('step dots count equals steps.length', async () => {
|
|
1463
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1464
1551
|
await element.updateComplete;
|
|
1465
1552
|
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3);
|
|
1466
1553
|
});
|
|
1467
1554
|
|
|
1468
1555
|
test('min-max labels show first and last step labels', async () => {
|
|
1469
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1470
1556
|
await element.updateComplete;
|
|
1471
1557
|
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
1472
1558
|
assert.equal(labels[0].textContent!.trim(), 'Small');
|
|
@@ -1486,7 +1572,6 @@ suite('zui-slider steps', () => {
|
|
|
1486
1572
|
test('value is included in form submission as the step label', async () => {
|
|
1487
1573
|
const name = randString();
|
|
1488
1574
|
element.setAttribute('name', name);
|
|
1489
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1490
1575
|
element.value = 'Large';
|
|
1491
1576
|
await element.updateComplete;
|
|
1492
1577
|
assert.equal(new FormData(form).get(name), 'Large');
|
|
@@ -1505,7 +1590,6 @@ suite('zui-slider steps', () => {
|
|
|
1505
1590
|
});
|
|
1506
1591
|
|
|
1507
1592
|
test('showStepLabels renders a label element for each step', async () => {
|
|
1508
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1509
1593
|
element.showStepLabels = true;
|
|
1510
1594
|
await element.updateComplete;
|
|
1511
1595
|
const labels = element.shadowRoot!.querySelectorAll('.step-dot-label');
|
|
@@ -1526,30 +1610,19 @@ suite('zui-slider steps', () => {
|
|
|
1526
1610
|
});
|
|
1527
1611
|
|
|
1528
1612
|
test('showStepLabels false renders no label elements', async () => {
|
|
1529
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1530
1613
|
await element.updateComplete;
|
|
1531
1614
|
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot-label').length, 0);
|
|
1532
1615
|
});
|
|
1533
1616
|
|
|
1534
|
-
test('
|
|
1535
|
-
element.steps = [0, 25, 50, 75, 100];
|
|
1536
|
-
element.value = '50';
|
|
1617
|
+
test('pressing Enter in steps floating input commits the step label', async () => {
|
|
1537
1618
|
await element.updateComplete;
|
|
1538
1619
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1539
|
-
|
|
1540
|
-
floatInput.
|
|
1541
|
-
|
|
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');
|
|
1620
|
+
floatInput.value = 'Large';
|
|
1621
|
+
floatInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
1622
|
+
assert.equal(element.value, 'Large');
|
|
1549
1623
|
});
|
|
1550
1624
|
|
|
1551
1625
|
test('committing empty steps floating input reverts display to current step label', async () => {
|
|
1552
|
-
element.steps = ['Small', 'Medium', 'Large'];
|
|
1553
1626
|
element.value = 'Medium';
|
|
1554
1627
|
await element.updateComplete;
|
|
1555
1628
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
@@ -1699,6 +1772,30 @@ suite('zui-slider steps range', () => {
|
|
|
1699
1772
|
assert.equal(element.valueStart, 'B');
|
|
1700
1773
|
assert.equal(element.valueEnd, 'D');
|
|
1701
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
|
+
});
|
|
1702
1799
|
});
|
|
1703
1800
|
|
|
1704
1801
|
suite('zui-slider stepParser', () => {
|
|
@@ -1727,7 +1824,7 @@ suite('zui-slider stepParser', () => {
|
|
|
1727
1824
|
};
|
|
1728
1825
|
await element.updateComplete;
|
|
1729
1826
|
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input')!;
|
|
1730
|
-
floatInput.value = '2k'; // exact step label
|
|
1827
|
+
floatInput.value = '2k'; // exact step label; resolved before stepParser is consulted
|
|
1731
1828
|
floatInput.dispatchEvent(new Event('change'));
|
|
1732
1829
|
assert.equal(element.value, '2k');
|
|
1733
1830
|
assert.equal(callCount, 0);
|
|
@@ -1767,23 +1864,6 @@ suite('zui-slider stepParser', () => {
|
|
|
1767
1864
|
assert.equal(element.value, '2k');
|
|
1768
1865
|
});
|
|
1769
1866
|
|
|
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
1867
|
test('string return from stepParser is used directly as a step label', async () => {
|
|
1788
1868
|
element.steps = [
|
|
1789
1869
|
{ value: 1000, label: '1k' },
|