@zywave/zui-slider 4.3.2 → 4.4.0-pre.0
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/css/zui-slider.fouc.css +1 -1
- package/dist/custom-elements.json +567 -40
- package/dist/zui-slider-css.js +1 -1
- package/dist/zui-slider-css.js.map +1 -1
- package/dist/zui-slider.d.ts +43 -17
- package/dist/zui-slider.js +510 -80
- package/dist/zui-slider.js.map +1 -1
- package/docs/demo.html +130 -9
- package/lab.html +45 -1
- package/package.json +5 -2
- package/src/css/zui-slider.fouc.scss +75 -1
- package/src/zui-slider-css.js +1 -1
- package/src/zui-slider.scss +239 -29
- package/src/zui-slider.ts +557 -75
- package/test/zui-slider.test.ts +1224 -4
package/test/zui-slider.test.ts
CHANGED
|
@@ -24,16 +24,30 @@ suite('zui-slider', () => {
|
|
|
24
24
|
document.body.removeChild(form);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
test('initializes as a ZuiSlider', () => {
|
|
28
|
-
assert.instanceOf(element, ZuiSlider);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
27
|
test('initializes defaults', () => {
|
|
32
28
|
assert.equal(element.value, '50');
|
|
33
29
|
assert.equal(element.min, 0);
|
|
34
30
|
assert.equal(element.max, 100);
|
|
35
31
|
assert.equal(element.step, 0);
|
|
36
32
|
assert.equal(element.disabled, false);
|
|
33
|
+
assert.isFalse(element.showMinMax);
|
|
34
|
+
assert.isFalse(element.range);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('valueAsNumber returns the value as a number', () => {
|
|
38
|
+
element.value = '42';
|
|
39
|
+
assert.equal(element.valueAsNumber, 42);
|
|
40
|
+
assert.typeOf(element.valueAsNumber, 'number');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('valueAsNumber returns NaN when value is empty string', () => {
|
|
44
|
+
element.value = '';
|
|
45
|
+
assert.isNaN(element.valueAsNumber);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('progress returns 0 when value is empty string', () => {
|
|
49
|
+
element.value = '';
|
|
50
|
+
assert.equal(element.progress, 0);
|
|
37
51
|
});
|
|
38
52
|
|
|
39
53
|
test('init slider associates with form properly', () => {
|
|
@@ -60,6 +74,536 @@ suite('zui-slider', () => {
|
|
|
60
74
|
assert.exists(formVal);
|
|
61
75
|
assert.equal(formVal, max.toString());
|
|
62
76
|
});
|
|
77
|
+
|
|
78
|
+
test('change event fires with the current value as detail', async () => {
|
|
79
|
+
await element.updateComplete;
|
|
80
|
+
let detail: string | undefined;
|
|
81
|
+
element.addEventListener('change', (e: Event) => {
|
|
82
|
+
detail = (e as CustomEvent<string>).detail;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
element.value = '35';
|
|
86
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
87
|
+
input.dispatchEvent(new Event('change'));
|
|
88
|
+
|
|
89
|
+
assert.equal(detail, '35');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('dragging native range input updates element value (including fractional)', async () => {
|
|
93
|
+
element.step = 0.5; // fractional step required for native input to accept non-integer values
|
|
94
|
+
await element.updateComplete;
|
|
95
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
96
|
+
input.value = '33.5';
|
|
97
|
+
input.dispatchEvent(new Event('input'));
|
|
98
|
+
assert.equal(element.value, '33.5');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('float input position and track background update after drag', async () => {
|
|
102
|
+
await element.updateComplete;
|
|
103
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
104
|
+
const thumbDiv = element.shadowRoot!.querySelector<HTMLElement>('.thumb-input')!;
|
|
105
|
+
const bgBefore = input.style.background;
|
|
106
|
+
input.value = '75';
|
|
107
|
+
input.dispatchEvent(new Event('input'));
|
|
108
|
+
await element.updateComplete;
|
|
109
|
+
assert.include(thumbDiv.style.left, '75%');
|
|
110
|
+
assert.notEqual(input.style.background, bgBefore);
|
|
111
|
+
assert.include(input.style.background, '75%');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('floating input change fires component change event', async () => {
|
|
115
|
+
await element.updateComplete;
|
|
116
|
+
let detail: string | undefined;
|
|
117
|
+
element.addEventListener('change', (e: Event) => {
|
|
118
|
+
detail = (e as CustomEvent<string>).detail;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
122
|
+
floatInput.value = '40';
|
|
123
|
+
floatInput.dispatchEvent(new Event('change'));
|
|
124
|
+
|
|
125
|
+
assert.equal(detail, '40');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('valueAsNumber reads the single-mode backing field regardless of range mode', () => {
|
|
129
|
+
element.value = '35';
|
|
130
|
+
element.range = true;
|
|
131
|
+
// valueAsNumber reads #value (the single-mode backing field) — range mode does not update it
|
|
132
|
+
assert.equal(element.valueAsNumber, 35);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('change event bubbles', async () => {
|
|
136
|
+
await element.updateComplete;
|
|
137
|
+
let bubbled = false;
|
|
138
|
+
document.body.addEventListener('change', () => (bubbled = true), { once: true });
|
|
139
|
+
|
|
140
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
141
|
+
input.dispatchEvent(new Event('change'));
|
|
142
|
+
|
|
143
|
+
assert.isTrue(bubbled);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('progress computes correct percentage including non-zero min offset', () => {
|
|
147
|
+
element.min = 0;
|
|
148
|
+
element.max = 100;
|
|
149
|
+
element.value = '25';
|
|
150
|
+
assert.equal(element.progress, 25);
|
|
151
|
+
element.value = '0';
|
|
152
|
+
assert.equal(element.progress, 0);
|
|
153
|
+
element.value = '100';
|
|
154
|
+
assert.equal(element.progress, 100);
|
|
155
|
+
// Non-zero min: (20-10)/(90-10)*100 = 12.5
|
|
156
|
+
element.min = 10;
|
|
157
|
+
element.max = 90;
|
|
158
|
+
element.value = '20';
|
|
159
|
+
assert.equal(element.progress, 12.5);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('progress returns 0 when min equals max', () => {
|
|
163
|
+
element.min = 50;
|
|
164
|
+
element.max = 50;
|
|
165
|
+
element.value = '50';
|
|
166
|
+
assert.equal(element.progress, 0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('progress updates when min or max changes', () => {
|
|
170
|
+
element.min = 0;
|
|
171
|
+
element.max = 100;
|
|
172
|
+
element.value = '50';
|
|
173
|
+
assert.equal(element.progress, 50);
|
|
174
|
+
element.max = 50;
|
|
175
|
+
assert.equal(element.progress, 100);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('value is reclamped when max decreases below current value', async () => {
|
|
179
|
+
const name = randString();
|
|
180
|
+
element.setAttribute('name', name);
|
|
181
|
+
element.value = '80';
|
|
182
|
+
element.max = 50;
|
|
183
|
+
await element.updateComplete;
|
|
184
|
+
assert.equal(element.value, '50');
|
|
185
|
+
assert.equal(element.valueAsNumber, 50);
|
|
186
|
+
assert.equal(new FormData(form).get(name), '50');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('value is reclamped when min increases above current value', async () => {
|
|
190
|
+
const name = randString();
|
|
191
|
+
element.setAttribute('name', name);
|
|
192
|
+
element.value = '20';
|
|
193
|
+
element.min = 50;
|
|
194
|
+
await element.updateComplete;
|
|
195
|
+
assert.equal(element.value, '50');
|
|
196
|
+
assert.equal(element.valueAsNumber, 50);
|
|
197
|
+
assert.equal(new FormData(form).get(name), '50');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('native inputs use step="1" when step is 0 or negative', async () => {
|
|
201
|
+
for (const s of [0, -5]) {
|
|
202
|
+
element.step = s;
|
|
203
|
+
await element.updateComplete;
|
|
204
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
205
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
206
|
+
assert.equal(input.step, '1');
|
|
207
|
+
assert.equal(floatInput.step, '1');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('native inputs have correct step attribute when step is set', async () => {
|
|
212
|
+
element.step = 25;
|
|
213
|
+
await element.updateComplete;
|
|
214
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
215
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
216
|
+
assert.equal(input.step, '25');
|
|
217
|
+
assert.equal(floatInput.step, '25');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('native inputs reflect min and max properties in DOM', async () => {
|
|
221
|
+
element.min = 10;
|
|
222
|
+
element.max = 90;
|
|
223
|
+
await element.updateComplete;
|
|
224
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
225
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
226
|
+
assert.equal(input.min, '10');
|
|
227
|
+
assert.equal(input.max, '90');
|
|
228
|
+
assert.equal(floatInput.min, '10');
|
|
229
|
+
assert.equal(floatInput.max, '90');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('single mode input has inline linear-gradient background with transparent insets', async () => {
|
|
233
|
+
await element.updateComplete;
|
|
234
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
235
|
+
const bg = input.style.background;
|
|
236
|
+
assert.include(bg, 'linear-gradient');
|
|
237
|
+
assert.include(bg, 'transparent var(--zui-slider-thumb-size)');
|
|
238
|
+
assert.include(bg, 'transparent calc(100% - var(--zui-slider-thumb-size))');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('single mode gradient uses gray when disabled and reverts to blue when re-enabled', async () => {
|
|
242
|
+
element.disabled = true;
|
|
243
|
+
await element.updateComplete;
|
|
244
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
245
|
+
assert.include(input.style.background, 'var(--zui-gray)');
|
|
246
|
+
assert.notInclude(input.style.background, 'var(--zui-blue)');
|
|
247
|
+
|
|
248
|
+
element.disabled = false;
|
|
249
|
+
await element.updateComplete;
|
|
250
|
+
assert.include(input.style.background, 'var(--zui-blue)');
|
|
251
|
+
assert.notInclude(input.style.background, 'var(--zui-gray)');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('single mode gradient progress stop shifts when value changes', async () => {
|
|
255
|
+
// At value=50 (progress=50): offset = 1.5 - (3*50)/100 = 0
|
|
256
|
+
element.value = '50';
|
|
257
|
+
await element.updateComplete;
|
|
258
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
259
|
+
assert.include(input.style.background, 'calc(50% + var(--zui-slider-thumb-size) * 0)');
|
|
260
|
+
|
|
261
|
+
// At value=75 (progress=75): offset = 1.5 - (3*75)/100 = -0.75
|
|
262
|
+
element.value = '75';
|
|
263
|
+
await element.updateComplete;
|
|
264
|
+
assert.include(input.style.background, 'calc(75% + var(--zui-slider-thumb-size) * -0.75)');
|
|
265
|
+
assert.notInclude(input.style.background, 'calc(50% + var(--zui-slider-thumb-size) * 0)');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('form reset hides visible floating input', async () => {
|
|
269
|
+
await element.updateComplete;
|
|
270
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
271
|
+
input.dispatchEvent(new Event('pointerenter'));
|
|
272
|
+
await element.updateComplete;
|
|
273
|
+
assert.isTrue(element.shadowRoot!.querySelector('.thumb-input')!.classList.contains('thumb-input--visible'));
|
|
274
|
+
|
|
275
|
+
form.reset();
|
|
276
|
+
await element.updateComplete;
|
|
277
|
+
assert.notInclude(element.shadowRoot!.querySelector('.thumb-input')!.className, 'thumb-input--visible');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('form reset restores initial single mode value', async () => {
|
|
281
|
+
// Attribute must be set BEFORE connecting so connectedCallback captures it as the default.
|
|
282
|
+
// Must await updateComplete so the async connectedCallback sets #defaultValue from the native input.
|
|
283
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
284
|
+
const name = randString();
|
|
285
|
+
el.setAttribute('name', name);
|
|
286
|
+
el.setAttribute('value', '30');
|
|
287
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
288
|
+
await el.updateComplete;
|
|
289
|
+
|
|
290
|
+
el.value = '80';
|
|
291
|
+
f.reset();
|
|
292
|
+
|
|
293
|
+
assert.equal(el.value, '30');
|
|
294
|
+
assert.equal(new FormData(f).get(name), '30');
|
|
295
|
+
document.body.removeChild(f);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('value property set before connection is captured as the form reset default', async () => {
|
|
299
|
+
// el.value = '70' (property, no setAttribute) sets #value = '70' via setter before connect.
|
|
300
|
+
// connectedCallback reads the native input value after first render and sets #defaultValue = '70'.
|
|
301
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
302
|
+
const name = randString();
|
|
303
|
+
el.setAttribute('name', name);
|
|
304
|
+
el.value = '70';
|
|
305
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
306
|
+
await el.updateComplete;
|
|
307
|
+
|
|
308
|
+
assert.equal(el.value, '70');
|
|
309
|
+
el.value = '90';
|
|
310
|
+
f.reset();
|
|
311
|
+
|
|
312
|
+
assert.equal(el.value, '70');
|
|
313
|
+
assert.equal(new FormData(f).get(name), '70');
|
|
314
|
+
document.body.removeChild(f);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('connectedCallback with range property (not attribute) uses range branch', async () => {
|
|
318
|
+
// el.range = true without setAttribute — connectedCallback must check this.range too.
|
|
319
|
+
// Must await so the async connectedCallback reads native inputs and calls _setFormValue.
|
|
320
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
321
|
+
el.range = true;
|
|
322
|
+
const name = randString();
|
|
323
|
+
el.setAttribute('name', name);
|
|
324
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
325
|
+
await el.updateComplete;
|
|
326
|
+
|
|
327
|
+
assert.equal(el.valueStart, '0');
|
|
328
|
+
assert.equal(el.valueEnd, '100');
|
|
329
|
+
assert.equal(new FormData(f).get(name), '0,100');
|
|
330
|
+
document.body.removeChild(f);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('connectedCallback clamps value to max when value attribute is set before max attribute', async () => {
|
|
334
|
+
// Native input clamps value=90 to max=80; updated() reads it back; updateComplete.then sets default.
|
|
335
|
+
// Must await so both updated() and the connectedCallback async init complete.
|
|
336
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
337
|
+
const name = randString();
|
|
338
|
+
el.setAttribute('name', name);
|
|
339
|
+
el.setAttribute('value', '90');
|
|
340
|
+
el.setAttribute('max', '80');
|
|
341
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
342
|
+
await el.updateComplete;
|
|
343
|
+
assert.equal(el.value, '80');
|
|
344
|
+
assert.equal(new FormData(f).get(name), '80');
|
|
345
|
+
// form reset should restore the clamped default, not the original '90'
|
|
346
|
+
el.value = '50';
|
|
347
|
+
f.reset();
|
|
348
|
+
assert.equal(el.value, '80');
|
|
349
|
+
assert.equal(new FormData(f).get(name), '80');
|
|
350
|
+
document.body.removeChild(f);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('floating input updates value after debounce delay but not immediately', async () => {
|
|
354
|
+
await element.updateComplete;
|
|
355
|
+
element.value = '50';
|
|
356
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
357
|
+
floatInput.value = '75';
|
|
358
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
359
|
+
// Debounce is 300ms — value must not update synchronously
|
|
360
|
+
assert.equal(element.value, '50');
|
|
361
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
362
|
+
assert.equal(element.value, '75');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('form reset cancels pending floating input debounce', async () => {
|
|
366
|
+
await element.updateComplete;
|
|
367
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
368
|
+
floatInput.value = '80';
|
|
369
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
370
|
+
// Debounce is pending — reset before it fires
|
|
371
|
+
form.reset();
|
|
372
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
373
|
+
// Debounce timer must have been cancelled by formResetCallback
|
|
374
|
+
assert.equal(element.value, '50');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('floating input empty-string input does not trigger value update', async () => {
|
|
378
|
+
await element.updateComplete;
|
|
379
|
+
element.value = '50';
|
|
380
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
381
|
+
floatInput.value = '';
|
|
382
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
383
|
+
// Empty string is an in-progress edit — value must not be updated
|
|
384
|
+
assert.equal(element.value, '50');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('floating input clamps out-of-bounds values to min/max after debounce', async () => {
|
|
388
|
+
await element.updateComplete;
|
|
389
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
390
|
+
floatInput.value = '150';
|
|
391
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
392
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
393
|
+
assert.equal(element.value, '100');
|
|
394
|
+
|
|
395
|
+
element.min = 20;
|
|
396
|
+
floatInput.value = '5';
|
|
397
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
398
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
399
|
+
assert.equal(element.value, '20');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('floating input snaps typed value to nearest step after debounce', async () => {
|
|
403
|
+
await element.updateComplete;
|
|
404
|
+
element.step = 10;
|
|
405
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
406
|
+
floatInput.value = '47';
|
|
407
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
408
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
409
|
+
assert.equal(element.value, '50');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('floating input change flushes debounce and dispatches correct value immediately', async () => {
|
|
413
|
+
await element.updateComplete;
|
|
414
|
+
let detail: string | undefined;
|
|
415
|
+
element.addEventListener('change', (e: Event) => {
|
|
416
|
+
detail = (e as CustomEvent<string>).detail;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
420
|
+
// Simulate typing then immediately committing before the 300ms debounce settles
|
|
421
|
+
floatInput.value = '75';
|
|
422
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
423
|
+
assert.equal(element.value, '50'); // debounce not yet fired
|
|
424
|
+
floatInput.dispatchEvent(new Event('change')); // commits immediately
|
|
425
|
+
assert.equal(element.value, '75');
|
|
426
|
+
assert.equal(detail, '75');
|
|
427
|
+
// Debounce timer was cleared — value must not update again after 300ms
|
|
428
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
429
|
+
assert.equal(element.value, '75');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('focused floating input stays visible after pointerleave', async () => {
|
|
433
|
+
await element.updateComplete;
|
|
434
|
+
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
435
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
436
|
+
const thumbInputDiv = element.shadowRoot!.querySelector('.thumb-input')!;
|
|
437
|
+
|
|
438
|
+
rangeInput.dispatchEvent(new Event('pointerenter'));
|
|
439
|
+
await element.updateComplete;
|
|
440
|
+
assert.isTrue(thumbInputDiv.classList.contains('thumb-input--visible'));
|
|
441
|
+
|
|
442
|
+
floatInput.dispatchEvent(new Event('focus'));
|
|
443
|
+
rangeInput.dispatchEvent(new Event('pointerleave'));
|
|
444
|
+
await element.updateComplete;
|
|
445
|
+
// Focus keeps the input visible despite pointerleave
|
|
446
|
+
assert.isTrue(thumbInputDiv.classList.contains('thumb-input--visible'));
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('floating input hides after blur when not re-focused', async () => {
|
|
450
|
+
await element.updateComplete;
|
|
451
|
+
const rangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
452
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
453
|
+
const thumbInputDiv = element.shadowRoot!.querySelector('.thumb-input')!;
|
|
454
|
+
|
|
455
|
+
rangeInput.dispatchEvent(new Event('pointerenter'));
|
|
456
|
+
await element.updateComplete;
|
|
457
|
+
floatInput.dispatchEvent(new Event('focus'));
|
|
458
|
+
await element.updateComplete;
|
|
459
|
+
assert.isTrue(thumbInputDiv.classList.contains('thumb-input--visible'));
|
|
460
|
+
|
|
461
|
+
// Blur without re-focusing — #blurFloatingInput clears focused and schedules hide.
|
|
462
|
+
// #scheduleHideThumbInput uses a real 100ms setTimeout, so wait past it before asserting.
|
|
463
|
+
floatInput.dispatchEvent(new Event('blur'));
|
|
464
|
+
rangeInput.dispatchEvent(new Event('pointerleave'));
|
|
465
|
+
await new Promise<void>((r) => setTimeout(r, 150));
|
|
466
|
+
await element.updateComplete;
|
|
467
|
+
assert.notInclude(thumbInputDiv.className, 'thumb-input--visible');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('disconnectedCallback clears timers and resets thumb input visibility', async () => {
|
|
471
|
+
await element.updateComplete;
|
|
472
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
473
|
+
|
|
474
|
+
// Show the thumb input
|
|
475
|
+
input.dispatchEvent(new Event('pointerenter'));
|
|
476
|
+
await element.updateComplete;
|
|
477
|
+
assert.isTrue(element.shadowRoot!.querySelector('.thumb-input')!.classList.contains('thumb-input--visible'));
|
|
478
|
+
|
|
479
|
+
// Disconnect then reconnect — disconnectedCallback should clear state
|
|
480
|
+
document.body.removeChild(form);
|
|
481
|
+
document.body.appendChild(form);
|
|
482
|
+
await element.updateComplete;
|
|
483
|
+
|
|
484
|
+
assert.notInclude(element.shadowRoot!.querySelector('.thumb-input')!.className, 'thumb-input--visible');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('readonly prevents value changes from dragging and floating input', async () => {
|
|
488
|
+
element.readOnly = true;
|
|
489
|
+
await element.updateComplete;
|
|
490
|
+
element.value = '50';
|
|
491
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
492
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
493
|
+
input.value = '75';
|
|
494
|
+
input.dispatchEvent(new Event('input'));
|
|
495
|
+
assert.equal(element.value, '50');
|
|
496
|
+
floatInput.value = '75';
|
|
497
|
+
floatInput.dispatchEvent(new Event('input'));
|
|
498
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
499
|
+
assert.equal(element.value, '50');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('readonly attribute is reflected on the floating number input', async () => {
|
|
503
|
+
element.readOnly = true;
|
|
504
|
+
await element.updateComplete;
|
|
505
|
+
const floatInput = element.shadowRoot!.querySelector<HTMLInputElement>('.thumb-input input[type="number"]')!;
|
|
506
|
+
assert.isTrue(floatInput.hasAttribute('readonly'));
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('disabled prevents floating thumb input from showing', async () => {
|
|
510
|
+
element.disabled = true;
|
|
511
|
+
await element.updateComplete;
|
|
512
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
513
|
+
input.dispatchEvent(new Event('pointerenter', { bubbles: true }));
|
|
514
|
+
await element.updateComplete;
|
|
515
|
+
const thumbInput = element.shadowRoot!.querySelector('.thumb-input')!;
|
|
516
|
+
assert.notInclude(thumbInput.className, 'thumb-input--visible');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('floating input hides when disabled is set while visible', async () => {
|
|
520
|
+
await element.updateComplete;
|
|
521
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
522
|
+
input.dispatchEvent(new Event('pointerenter'));
|
|
523
|
+
await element.updateComplete;
|
|
524
|
+
assert.isTrue(element.shadowRoot!.querySelector('.thumb-input')!.classList.contains('thumb-input--visible'));
|
|
525
|
+
|
|
526
|
+
element.disabled = true;
|
|
527
|
+
await element.updateComplete;
|
|
528
|
+
assert.notInclude(element.shadowRoot!.querySelector('.thumb-input')!.className, 'thumb-input--visible');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('floating input does not re-appear when disabled is toggled back off after hiding', async () => {
|
|
532
|
+
await element.updateComplete;
|
|
533
|
+
const input = element.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]')!;
|
|
534
|
+
input.dispatchEvent(new Event('pointerenter'));
|
|
535
|
+
await element.updateComplete;
|
|
536
|
+
assert.isTrue(element.shadowRoot!.querySelector('.thumb-input')!.classList.contains('thumb-input--visible'));
|
|
537
|
+
|
|
538
|
+
element.disabled = true;
|
|
539
|
+
await element.updateComplete;
|
|
540
|
+
element.disabled = false;
|
|
541
|
+
await element.updateComplete;
|
|
542
|
+
assert.notInclude(element.shadowRoot!.querySelector('.thumb-input')!.className, 'thumb-input--visible');
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
suite('zui-slider showMinMax', () => {
|
|
547
|
+
let element: ZuiSlider;
|
|
548
|
+
|
|
549
|
+
setup(() => {
|
|
550
|
+
element = document.createElement('zui-slider') as ZuiSlider;
|
|
551
|
+
document.body.appendChild(element);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
teardown(() => {
|
|
555
|
+
document.body.removeChild(element);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('min-max labels toggle with showMinMax and reflect correct values', async () => {
|
|
559
|
+
await element.updateComplete;
|
|
560
|
+
assert.notExists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
561
|
+
|
|
562
|
+
element.min = 10;
|
|
563
|
+
element.max = 90;
|
|
564
|
+
element.showMinMax = true;
|
|
565
|
+
await element.updateComplete;
|
|
566
|
+
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
567
|
+
assert.exists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
568
|
+
assert.equal(labels[0].textContent!.trim(), '10');
|
|
569
|
+
assert.equal(labels[1].textContent!.trim(), '90');
|
|
570
|
+
|
|
571
|
+
element.showMinMax = false;
|
|
572
|
+
await element.updateComplete;
|
|
573
|
+
assert.notExists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('min-max labels update when min and max change after initial render', async () => {
|
|
577
|
+
element.showMinMax = true;
|
|
578
|
+
element.min = 10;
|
|
579
|
+
element.max = 90;
|
|
580
|
+
await element.updateComplete;
|
|
581
|
+
element.min = 5;
|
|
582
|
+
element.max = 95;
|
|
583
|
+
await element.updateComplete;
|
|
584
|
+
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
585
|
+
assert.equal(labels[0].textContent!.trim(), '5');
|
|
586
|
+
assert.equal(labels[1].textContent!.trim(), '95');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('show-min-max HTML attribute enables min-max labels', async () => {
|
|
590
|
+
element.setAttribute('show-min-max', '');
|
|
591
|
+
await element.updateComplete;
|
|
592
|
+
assert.exists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('min-max labels render in range mode', async () => {
|
|
596
|
+
element.setAttribute('range', '');
|
|
597
|
+
element.min = 5;
|
|
598
|
+
element.max = 95;
|
|
599
|
+
element.showMinMax = true;
|
|
600
|
+
await element.updateComplete;
|
|
601
|
+
assert.exists(element.shadowRoot!.querySelector('.min-max-labels'));
|
|
602
|
+
const labels = element.shadowRoot!.querySelectorAll('.min-max-label');
|
|
603
|
+
assert.equal(labels.length, 2);
|
|
604
|
+
assert.equal(labels[0].textContent!.trim(), '5');
|
|
605
|
+
assert.equal(labels[1].textContent!.trim(), '95');
|
|
606
|
+
});
|
|
63
607
|
});
|
|
64
608
|
|
|
65
609
|
faceTests<ZuiSlider>('zui-slider', {
|
|
@@ -69,3 +613,679 @@ faceTests<ZuiSlider>('zui-slider', {
|
|
|
69
613
|
},
|
|
70
614
|
defaultValue: '75',
|
|
71
615
|
});
|
|
616
|
+
|
|
617
|
+
suite('zui-slider range', () => {
|
|
618
|
+
let element: ZuiSlider;
|
|
619
|
+
let form: HTMLFormElement;
|
|
620
|
+
|
|
621
|
+
setup(() => {
|
|
622
|
+
element = document.createElement('zui-slider') as ZuiSlider;
|
|
623
|
+
element.setAttribute('range', '');
|
|
624
|
+
|
|
625
|
+
form = buildForm({
|
|
626
|
+
enableSubmit: false,
|
|
627
|
+
appendChildren: [element],
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
teardown(() => {
|
|
632
|
+
document.body.removeChild(form);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test('initializes range defaults', () => {
|
|
636
|
+
assert.equal(element.valueStart, '0');
|
|
637
|
+
assert.equal(element.valueEnd, '100');
|
|
638
|
+
assert.isTrue(element.range);
|
|
639
|
+
assert.equal(element.min, 0);
|
|
640
|
+
assert.equal(element.max, 100);
|
|
641
|
+
assert.equal(element.step, 0);
|
|
642
|
+
assert.isFalse(element.disabled);
|
|
643
|
+
assert.isFalse(element.showMinMax);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('accepts value-start and value-end attributes', () => {
|
|
647
|
+
element.setAttribute('value-start', '10');
|
|
648
|
+
element.setAttribute('value-end', '90');
|
|
649
|
+
assert.equal(element.valueStart, '10');
|
|
650
|
+
assert.equal(element.valueEnd, '90');
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test('connectedCallback clamps range values to min and max', async () => {
|
|
654
|
+
// Native inputs clamp out-of-range attribute values; must await for updated() and
|
|
655
|
+
// the async connectedCallback to read back the clamped values.
|
|
656
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
657
|
+
el.setAttribute('range', '');
|
|
658
|
+
const name = randString();
|
|
659
|
+
el.setAttribute('name', name);
|
|
660
|
+
el.setAttribute('min', '20');
|
|
661
|
+
el.setAttribute('max', '80');
|
|
662
|
+
el.setAttribute('value-start', '5');
|
|
663
|
+
el.setAttribute('value-end', '95');
|
|
664
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
665
|
+
await el.updateComplete;
|
|
666
|
+
assert.equal(el.valueStart, '20');
|
|
667
|
+
assert.equal(el.valueEnd, '80');
|
|
668
|
+
assert.equal(new FormData(f).get(name), '20,80');
|
|
669
|
+
document.body.removeChild(f);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('associates range value with form as "start,end"', () => {
|
|
673
|
+
const name = randString();
|
|
674
|
+
element.setAttribute('name', name);
|
|
675
|
+
element.setAttribute('value-start', '20');
|
|
676
|
+
element.setAttribute('value-end', '80');
|
|
677
|
+
const formData = new FormData(form);
|
|
678
|
+
assert.equal(formData.get(name), '20,80');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test('setting valueStart, valueEnd, or both updates form value', () => {
|
|
682
|
+
const name = randString();
|
|
683
|
+
element.setAttribute('name', name);
|
|
684
|
+
element.valueStart = '30';
|
|
685
|
+
assert.equal(new FormData(form).get(name), '30,100');
|
|
686
|
+
element.valueEnd = '60';
|
|
687
|
+
assert.equal(new FormData(form).get(name), '30,60');
|
|
688
|
+
element.valueStart = '20';
|
|
689
|
+
element.valueEnd = '80';
|
|
690
|
+
assert.equal(new FormData(form).get(name), '20,80');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test('valueEnd is reclamped when max decreases below current valueEnd', async () => {
|
|
694
|
+
element.valueStart = '20';
|
|
695
|
+
element.valueEnd = '80';
|
|
696
|
+
element.max = 50;
|
|
697
|
+
await element.updateComplete;
|
|
698
|
+
assert.equal(element.valueEnd, '50');
|
|
699
|
+
assert.equal(element.valueStart, '20');
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test('valueStart is reclamped when min increases above current valueStart', async () => {
|
|
703
|
+
element.valueStart = '20';
|
|
704
|
+
element.valueEnd = '80';
|
|
705
|
+
element.min = 50;
|
|
706
|
+
await element.updateComplete;
|
|
707
|
+
assert.equal(element.valueStart, '50');
|
|
708
|
+
assert.equal(element.valueEnd, '80');
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test('both valueStart and valueEnd are reclamped when max decreases below both', async () => {
|
|
712
|
+
const name = randString();
|
|
713
|
+
element.setAttribute('name', name);
|
|
714
|
+
element.valueStart = '60';
|
|
715
|
+
element.valueEnd = '80';
|
|
716
|
+
element.max = 50;
|
|
717
|
+
await element.updateComplete;
|
|
718
|
+
assert.equal(element.valueStart, '50');
|
|
719
|
+
assert.equal(element.valueEnd, '50');
|
|
720
|
+
assert.equal(new FormData(form).get(name), '50,50');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test('both valueStart and valueEnd are reclamped when min increases above both', async () => {
|
|
724
|
+
const name = randString();
|
|
725
|
+
element.setAttribute('name', name);
|
|
726
|
+
element.valueStart = '20';
|
|
727
|
+
element.valueEnd = '40';
|
|
728
|
+
element.min = 60;
|
|
729
|
+
await element.updateComplete;
|
|
730
|
+
assert.equal(element.valueStart, '60');
|
|
731
|
+
assert.equal(element.valueEnd, '60');
|
|
732
|
+
assert.equal(new FormData(form).get(name), '60,60');
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test('valueStart and valueEnd properties set before connection are captured as form reset defaults', async () => {
|
|
736
|
+
// el.valueStart/el.valueEnd (properties, no setAttribute) set #valueStart/#valueEnd via setter.
|
|
737
|
+
// connectedCallback.updateComplete.then reads the native input values and captures them as defaults.
|
|
738
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
739
|
+
el.setAttribute('range', '');
|
|
740
|
+
const name = randString();
|
|
741
|
+
el.setAttribute('name', name);
|
|
742
|
+
el.valueStart = '25';
|
|
743
|
+
el.valueEnd = '75';
|
|
744
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
745
|
+
await el.updateComplete;
|
|
746
|
+
|
|
747
|
+
assert.equal(el.valueStart, '25');
|
|
748
|
+
assert.equal(el.valueEnd, '75');
|
|
749
|
+
el.valueStart = '40';
|
|
750
|
+
el.valueEnd = '60';
|
|
751
|
+
f.reset();
|
|
752
|
+
|
|
753
|
+
assert.equal(el.valueStart, '25');
|
|
754
|
+
assert.equal(el.valueEnd, '75');
|
|
755
|
+
assert.equal(new FormData(f).get(name), '25,75');
|
|
756
|
+
document.body.removeChild(f);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('progressStart and progressEnd compute correct percentages including defaults', () => {
|
|
760
|
+
assert.equal(element.progressStart, 0);
|
|
761
|
+
assert.equal(element.progressEnd, 100);
|
|
762
|
+
element.valueStart = '25';
|
|
763
|
+
element.valueEnd = '75';
|
|
764
|
+
assert.equal(element.progressStart, 25);
|
|
765
|
+
assert.equal(element.progressEnd, 75);
|
|
766
|
+
// Non-zero min: (30-10)/(90-10)*100 = 25, (70-10)/(90-10)*100 = 75
|
|
767
|
+
element.min = 10;
|
|
768
|
+
element.max = 90;
|
|
769
|
+
element.valueStart = '30';
|
|
770
|
+
element.valueEnd = '70';
|
|
771
|
+
assert.equal(element.progressStart, 25);
|
|
772
|
+
assert.equal(element.progressEnd, 75);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('progressStart and progressEnd return 0 when min equals max', () => {
|
|
776
|
+
element.min = 50;
|
|
777
|
+
element.max = 50;
|
|
778
|
+
assert.equal(element.progressStart, 0);
|
|
779
|
+
assert.equal(element.progressEnd, 0);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
test('progressStart and progressEnd return 0 when valueStart or valueEnd is empty string', () => {
|
|
783
|
+
element.valueStart = '';
|
|
784
|
+
assert.equal(element.progressStart, 0);
|
|
785
|
+
element.valueEnd = '';
|
|
786
|
+
assert.equal(element.progressEnd, 0);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test('progressStart and progressEnd update when min or max changes', async () => {
|
|
790
|
+
element.min = 0;
|
|
791
|
+
element.max = 100;
|
|
792
|
+
element.valueStart = '25';
|
|
793
|
+
element.valueEnd = '75';
|
|
794
|
+
assert.equal(element.progressStart, 25);
|
|
795
|
+
assert.equal(element.progressEnd, 75);
|
|
796
|
+
element.max = 50;
|
|
797
|
+
// updated() reads native end input (clamped to 50 by browser) and syncs #valueEnd
|
|
798
|
+
await element.updateComplete;
|
|
799
|
+
assert.equal(element.progressEnd, 100);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test('form reset restores initial range values', async () => {
|
|
803
|
+
// Attributes must be set BEFORE connecting so connectedCallback captures them as defaults.
|
|
804
|
+
// Must await so the async connectedCallback sets #defaultValueStart/#defaultValueEnd from native inputs.
|
|
805
|
+
const el = document.createElement('zui-slider') as ZuiSlider;
|
|
806
|
+
el.setAttribute('range', '');
|
|
807
|
+
const name = randString();
|
|
808
|
+
el.setAttribute('name', name);
|
|
809
|
+
el.setAttribute('value-start', '10');
|
|
810
|
+
el.setAttribute('value-end', '90');
|
|
811
|
+
const f = buildForm({ enableSubmit: false, appendChildren: [el] });
|
|
812
|
+
await el.updateComplete;
|
|
813
|
+
|
|
814
|
+
el.valueStart = '40';
|
|
815
|
+
el.valueEnd = '60';
|
|
816
|
+
|
|
817
|
+
f.reset();
|
|
818
|
+
|
|
819
|
+
assert.equal(el.valueStart, '10');
|
|
820
|
+
assert.equal(el.valueEnd, '90');
|
|
821
|
+
const formData = new FormData(f);
|
|
822
|
+
assert.equal(formData.get(name), '10,90');
|
|
823
|
+
document.body.removeChild(f);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test('input handler rejects start dragging past end', async () => {
|
|
827
|
+
await element.updateComplete;
|
|
828
|
+
element.valueStart = '20';
|
|
829
|
+
element.valueEnd = '60';
|
|
830
|
+
|
|
831
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
832
|
+
startInput.value = '70';
|
|
833
|
+
startInput.dispatchEvent(new Event('input'));
|
|
834
|
+
|
|
835
|
+
assert.equal(element.valueStart, '20');
|
|
836
|
+
assert.equal(element.valueEnd, '60');
|
|
837
|
+
assert.equal(startInput.value, '20');
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test('input handler rejects start dragging to equal end', async () => {
|
|
841
|
+
await element.updateComplete;
|
|
842
|
+
element.valueStart = '20';
|
|
843
|
+
element.valueEnd = '60';
|
|
844
|
+
|
|
845
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
846
|
+
startInput.value = '60';
|
|
847
|
+
startInput.dispatchEvent(new Event('input'));
|
|
848
|
+
|
|
849
|
+
// start >= end is rejected; input snaps back and state is unchanged
|
|
850
|
+
assert.equal(element.valueStart, '20');
|
|
851
|
+
assert.equal(element.valueEnd, '60');
|
|
852
|
+
assert.equal(startInput.value, '20');
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test('input handler rejects end dragging before start', async () => {
|
|
856
|
+
await element.updateComplete;
|
|
857
|
+
element.valueStart = '30';
|
|
858
|
+
element.valueEnd = '70';
|
|
859
|
+
|
|
860
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
861
|
+
endInput.value = '20';
|
|
862
|
+
endInput.dispatchEvent(new Event('input'));
|
|
863
|
+
|
|
864
|
+
assert.equal(element.valueEnd, '70');
|
|
865
|
+
assert.equal(element.valueStart, '30');
|
|
866
|
+
assert.equal(endInput.value, '70');
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test('input handler rejects end dragging to equal start', async () => {
|
|
870
|
+
await element.updateComplete;
|
|
871
|
+
element.valueStart = '30';
|
|
872
|
+
element.valueEnd = '70';
|
|
873
|
+
|
|
874
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
875
|
+
endInput.value = '30';
|
|
876
|
+
endInput.dispatchEvent(new Event('input'));
|
|
877
|
+
|
|
878
|
+
// end <= start is rejected; input snaps back and state is unchanged
|
|
879
|
+
assert.equal(element.valueEnd, '70');
|
|
880
|
+
assert.equal(element.valueStart, '30');
|
|
881
|
+
assert.equal(endInput.value, '70');
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
test('input handler accepts valid start and end moves', async () => {
|
|
885
|
+
await element.updateComplete;
|
|
886
|
+
element.valueStart = '20';
|
|
887
|
+
element.valueEnd = '60';
|
|
888
|
+
|
|
889
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
890
|
+
startInput.value = '40';
|
|
891
|
+
startInput.dispatchEvent(new Event('input'));
|
|
892
|
+
assert.equal(element.valueStart, '40');
|
|
893
|
+
assert.equal(element.valueEnd, '60');
|
|
894
|
+
|
|
895
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
896
|
+
endInput.value = '80';
|
|
897
|
+
endInput.dispatchEvent(new Event('input'));
|
|
898
|
+
assert.equal(element.valueEnd, '80');
|
|
899
|
+
assert.equal(element.valueStart, '40');
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test('dragging range thumb stores the exact value reported by the native range input', async () => {
|
|
903
|
+
element.step = 0.5; // fractional step required for native input to accept non-integer values
|
|
904
|
+
await element.updateComplete;
|
|
905
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
906
|
+
startInput.value = '33.5';
|
|
907
|
+
startInput.dispatchEvent(new Event('input'));
|
|
908
|
+
assert.equal(element.valueStart, '33.5');
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test('range start float input position and wrapper background update after drag', async () => {
|
|
912
|
+
await element.updateComplete;
|
|
913
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
914
|
+
const thumbInputDivs = element.shadowRoot!.querySelectorAll<HTMLElement>('.thumb-input');
|
|
915
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
916
|
+
const bgBefore = wrapper.style.background;
|
|
917
|
+
startInput.value = '30';
|
|
918
|
+
startInput.dispatchEvent(new Event('input'));
|
|
919
|
+
await element.updateComplete;
|
|
920
|
+
assert.include(thumbInputDivs[0].style.left, '30%');
|
|
921
|
+
assert.notEqual(wrapper.style.background, bgBefore);
|
|
922
|
+
assert.include(wrapper.style.background, '30%');
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test('change event fires with valueStart and valueEnd detail on range-start change', async () => {
|
|
926
|
+
await element.updateComplete;
|
|
927
|
+
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
928
|
+
element.addEventListener('change', (e: Event) => {
|
|
929
|
+
detail = (e as CustomEvent).detail;
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
element.valueStart = '10';
|
|
933
|
+
element.valueEnd = '40';
|
|
934
|
+
|
|
935
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
936
|
+
startInput.dispatchEvent(new Event('change'));
|
|
937
|
+
|
|
938
|
+
assert.deepEqual(detail, { valueStart: '10', valueEnd: '40' });
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test('range change event bubbles', async () => {
|
|
942
|
+
await element.updateComplete;
|
|
943
|
+
let bubbled = false;
|
|
944
|
+
document.body.addEventListener('change', () => (bubbled = true), { once: true });
|
|
945
|
+
|
|
946
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
947
|
+
startInput.dispatchEvent(new Event('change'));
|
|
948
|
+
|
|
949
|
+
assert.isTrue(bubbled);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test('change event fires with valueStart and valueEnd detail on range-end change', async () => {
|
|
953
|
+
await element.updateComplete;
|
|
954
|
+
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
955
|
+
element.addEventListener('change', (e: Event) => {
|
|
956
|
+
detail = (e as CustomEvent).detail;
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
element.valueStart = '15';
|
|
960
|
+
element.valueEnd = '55';
|
|
961
|
+
|
|
962
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
963
|
+
endInput.dispatchEvent(new Event('change'));
|
|
964
|
+
|
|
965
|
+
assert.deepEqual(detail, { valueStart: '15', valueEnd: '55' });
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test('floating input change fires component range change event', async () => {
|
|
969
|
+
await element.updateComplete;
|
|
970
|
+
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
971
|
+
element.addEventListener('change', (e: Event) => {
|
|
972
|
+
detail = (e as CustomEvent).detail;
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
976
|
+
floatInputs[0].value = '20';
|
|
977
|
+
floatInputs[0].dispatchEvent(new Event('change'));
|
|
978
|
+
|
|
979
|
+
assert.deepEqual(detail, { valueStart: '20', valueEnd: '100' });
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test('range inputs reflect min and max properties in DOM', async () => {
|
|
983
|
+
element.min = 10;
|
|
984
|
+
element.max = 90;
|
|
985
|
+
await element.updateComplete;
|
|
986
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
987
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
988
|
+
assert.equal(startInput.min, '10');
|
|
989
|
+
assert.equal(startInput.max, '90');
|
|
990
|
+
assert.equal(endInput.min, '10');
|
|
991
|
+
assert.equal(endInput.max, '90');
|
|
992
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
993
|
+
floatInputs.forEach((fi) => {
|
|
994
|
+
assert.equal(fi.min, '10');
|
|
995
|
+
assert.equal(fi.max, '90');
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test('all range inputs use step="1" when component step is 0', async () => {
|
|
1000
|
+
element.step = 0;
|
|
1001
|
+
await element.updateComplete;
|
|
1002
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1003
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
1004
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1005
|
+
assert.equal(startInput.step, '1');
|
|
1006
|
+
assert.equal(endInput.step, '1');
|
|
1007
|
+
floatInputs.forEach((fi) => assert.equal(fi.step, '1'));
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
test('all range inputs have correct step attribute when step is set', async () => {
|
|
1011
|
+
element.step = 10;
|
|
1012
|
+
await element.updateComplete;
|
|
1013
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1014
|
+
const endInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-end')!;
|
|
1015
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1016
|
+
assert.equal(startInput.step, '10');
|
|
1017
|
+
assert.equal(endInput.step, '10');
|
|
1018
|
+
floatInputs.forEach((fi) => assert.equal(fi.step, '10'));
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test('range-wrapper gradient uses gray when disabled and reverts to blue when re-enabled', async () => {
|
|
1022
|
+
element.disabled = true;
|
|
1023
|
+
await element.updateComplete;
|
|
1024
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1025
|
+
assert.include(wrapper.style.background, 'var(--zui-gray)');
|
|
1026
|
+
assert.notInclude(wrapper.style.background, 'var(--zui-blue)');
|
|
1027
|
+
|
|
1028
|
+
element.disabled = false;
|
|
1029
|
+
await element.updateComplete;
|
|
1030
|
+
assert.include(wrapper.style.background, 'var(--zui-blue)');
|
|
1031
|
+
assert.notInclude(wrapper.style.background, 'var(--zui-gray)');
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
test('form reset hides visible floating inputs in range mode', async () => {
|
|
1035
|
+
await element.updateComplete;
|
|
1036
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1037
|
+
startInput.dispatchEvent(new Event('pointerenter'));
|
|
1038
|
+
await element.updateComplete;
|
|
1039
|
+
const thumbInputs = element.shadowRoot!.querySelectorAll('.thumb-input');
|
|
1040
|
+
assert.isTrue(thumbInputs[0].classList.contains('thumb-input--visible'));
|
|
1041
|
+
|
|
1042
|
+
form.reset();
|
|
1043
|
+
await element.updateComplete;
|
|
1044
|
+
thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test('range floating input empty-string input does not trigger value update', async () => {
|
|
1048
|
+
await element.updateComplete;
|
|
1049
|
+
element.valueStart = '20';
|
|
1050
|
+
element.valueEnd = '80';
|
|
1051
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1052
|
+
floatInputs[0].value = '';
|
|
1053
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1054
|
+
floatInputs[1].value = '';
|
|
1055
|
+
floatInputs[1].dispatchEvent(new Event('input'));
|
|
1056
|
+
assert.equal(element.valueStart, '20');
|
|
1057
|
+
assert.equal(element.valueEnd, '80');
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test('range floating inputs hide when disabled is set while visible', async () => {
|
|
1061
|
+
await element.updateComplete;
|
|
1062
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1063
|
+
startInput.dispatchEvent(new Event('pointerenter'));
|
|
1064
|
+
await element.updateComplete;
|
|
1065
|
+
const thumbInputs = element.shadowRoot!.querySelectorAll('.thumb-input');
|
|
1066
|
+
assert.isTrue(thumbInputs[0].classList.contains('thumb-input--visible'));
|
|
1067
|
+
|
|
1068
|
+
element.disabled = true;
|
|
1069
|
+
await element.updateComplete;
|
|
1070
|
+
thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
test('range-wrapper gradient insets are transparent outside thumb-size bounds', async () => {
|
|
1074
|
+
await element.updateComplete;
|
|
1075
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1076
|
+
const bg = wrapper.style.background;
|
|
1077
|
+
assert.include(bg, 'transparent var(--zui-slider-thumb-size)');
|
|
1078
|
+
assert.include(bg, 'transparent calc(100% - var(--zui-slider-thumb-size))');
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test('disconnectedCallback clears timers and resets thumb input visibility in range mode', async () => {
|
|
1082
|
+
await element.updateComplete;
|
|
1083
|
+
const startInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1084
|
+
|
|
1085
|
+
startInput.dispatchEvent(new Event('pointerenter'));
|
|
1086
|
+
await element.updateComplete;
|
|
1087
|
+
const thumbInputs = element.shadowRoot!.querySelectorAll('.thumb-input');
|
|
1088
|
+
assert.isTrue(thumbInputs[0].classList.contains('thumb-input--visible'));
|
|
1089
|
+
|
|
1090
|
+
document.body.removeChild(form);
|
|
1091
|
+
document.body.appendChild(form);
|
|
1092
|
+
await element.updateComplete;
|
|
1093
|
+
|
|
1094
|
+
thumbInputs.forEach((ti) => assert.notInclude(ti.className, 'thumb-input--visible'));
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
test('range-wrapper gradient updates when valueStart and valueEnd change', async () => {
|
|
1098
|
+
element.valueStart = '25';
|
|
1099
|
+
element.valueEnd = '75';
|
|
1100
|
+
await element.updateComplete;
|
|
1101
|
+
const wrapper = element.shadowRoot!.querySelector<HTMLElement>('.range-wrapper')!;
|
|
1102
|
+
const bg25 = wrapper.style.background;
|
|
1103
|
+
|
|
1104
|
+
element.valueStart = '10';
|
|
1105
|
+
element.valueEnd = '90';
|
|
1106
|
+
await element.updateComplete;
|
|
1107
|
+
const bg10 = wrapper.style.background;
|
|
1108
|
+
|
|
1109
|
+
assert.notEqual(bg25, bg10);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
test('range start floating input updates valueStart after debounce delay but not immediately', async () => {
|
|
1113
|
+
await element.updateComplete;
|
|
1114
|
+
element.valueStart = '20';
|
|
1115
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1116
|
+
floatInputs[0].value = '40';
|
|
1117
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1118
|
+
// Debounce is 300ms — valueStart must not update synchronously
|
|
1119
|
+
assert.equal(element.valueStart, '20');
|
|
1120
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
1121
|
+
assert.equal(element.valueStart, '40');
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test('range end floating input updates valueEnd after debounce delay but not immediately', async () => {
|
|
1125
|
+
await element.updateComplete;
|
|
1126
|
+
element.valueEnd = '80';
|
|
1127
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1128
|
+
floatInputs[1].value = '60';
|
|
1129
|
+
floatInputs[1].dispatchEvent(new Event('input'));
|
|
1130
|
+
// Debounce is 300ms — valueEnd must not update synchronously
|
|
1131
|
+
assert.equal(element.valueEnd, '80');
|
|
1132
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
1133
|
+
assert.equal(element.valueEnd, '60');
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
test('focused range start floating input stays visible after pointerleave', async () => {
|
|
1137
|
+
await element.updateComplete;
|
|
1138
|
+
const startRangeInput = element.shadowRoot!.querySelector<HTMLInputElement>('input.range-start')!;
|
|
1139
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1140
|
+
const thumbInputDivs = element.shadowRoot!.querySelectorAll('.thumb-input');
|
|
1141
|
+
|
|
1142
|
+
startRangeInput.dispatchEvent(new Event('pointerenter'));
|
|
1143
|
+
await element.updateComplete;
|
|
1144
|
+
assert.isTrue(thumbInputDivs[0].classList.contains('thumb-input--visible'));
|
|
1145
|
+
|
|
1146
|
+
floatInputs[0].dispatchEvent(new Event('focus'));
|
|
1147
|
+
startRangeInput.dispatchEvent(new Event('pointerleave'));
|
|
1148
|
+
await element.updateComplete;
|
|
1149
|
+
// Focus keeps the start floating input visible despite pointerleave
|
|
1150
|
+
assert.isTrue(thumbInputDivs[0].classList.contains('thumb-input--visible'));
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test('range floating inputs clamp out-of-bounds values to min/max after debounce', async () => {
|
|
1154
|
+
await element.updateComplete;
|
|
1155
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1156
|
+
floatInputs[0].value = '150';
|
|
1157
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1158
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
1159
|
+
assert.equal(element.valueStart, '100');
|
|
1160
|
+
|
|
1161
|
+
element.min = 20;
|
|
1162
|
+
floatInputs[1].value = '5';
|
|
1163
|
+
floatInputs[1].dispatchEvent(new Event('input'));
|
|
1164
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
1165
|
+
assert.equal(element.valueEnd, '20');
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
test('range floating input snaps typed valueStart to nearest step after debounce', async () => {
|
|
1169
|
+
await element.updateComplete;
|
|
1170
|
+
element.step = 10;
|
|
1171
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1172
|
+
floatInputs[0].value = '23';
|
|
1173
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1174
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
1175
|
+
assert.equal(element.valueStart, '20');
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
test('range start floating input change flushes debounce and dispatches correct value immediately', async () => {
|
|
1179
|
+
await element.updateComplete;
|
|
1180
|
+
let detail: { valueStart: string; valueEnd: string } | undefined;
|
|
1181
|
+
element.addEventListener('change', (e: Event) => {
|
|
1182
|
+
detail = (e as CustomEvent).detail;
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
const floatInputs = element.shadowRoot!.querySelectorAll<HTMLInputElement>('.thumb-input input[type="number"]');
|
|
1186
|
+
floatInputs[0].value = '30';
|
|
1187
|
+
floatInputs[0].dispatchEvent(new Event('input'));
|
|
1188
|
+
assert.equal(element.valueStart, '0'); // debounce not yet fired
|
|
1189
|
+
floatInputs[0].dispatchEvent(new Event('change')); // commits immediately
|
|
1190
|
+
assert.equal(element.valueStart, '30');
|
|
1191
|
+
assert.deepEqual(detail, { valueStart: '30', valueEnd: '100' });
|
|
1192
|
+
// Debounce timer was cleared — value must not update again after 300ms
|
|
1193
|
+
await new Promise<void>((r) => setTimeout(r, 350));
|
|
1194
|
+
assert.equal(element.valueStart, '30');
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
suite('zui-slider step dots', () => {
|
|
1199
|
+
let element: ZuiSlider;
|
|
1200
|
+
|
|
1201
|
+
setup(() => {
|
|
1202
|
+
element = document.createElement('zui-slider') as ZuiSlider;
|
|
1203
|
+
element.step = 25;
|
|
1204
|
+
document.body.appendChild(element);
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
teardown(() => {
|
|
1208
|
+
document.body.removeChild(element);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
test('step dots are rendered when step is set', async () => {
|
|
1212
|
+
await element.updateComplete;
|
|
1213
|
+
const dots = element.shadowRoot!.querySelectorAll('.step-dot');
|
|
1214
|
+
assert.equal(dots.length, 5); // 0, 25, 50, 75, 100
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
test('step dots not rendered when step is 0', async () => {
|
|
1218
|
+
element.step = 0;
|
|
1219
|
+
await element.updateComplete;
|
|
1220
|
+
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test('step dots not rendered when step is negative', async () => {
|
|
1224
|
+
element.step = -5;
|
|
1225
|
+
await element.updateComplete;
|
|
1226
|
+
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
test('step dots not rendered when min equals max', async () => {
|
|
1230
|
+
// #renderStepDots guards on `this.#range === 0` independently of step
|
|
1231
|
+
element.min = 50;
|
|
1232
|
+
element.max = 50;
|
|
1233
|
+
await element.updateComplete;
|
|
1234
|
+
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
test('step dots not rendered when count exceeds 100', async () => {
|
|
1238
|
+
element.step = 0.5; // 200 steps
|
|
1239
|
+
await element.updateComplete;
|
|
1240
|
+
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
test('step dot positions are correctly offset by thumb size', async () => {
|
|
1244
|
+
await element.updateComplete;
|
|
1245
|
+
const dots = element.shadowRoot!.querySelectorAll<HTMLElement>('.step-dot');
|
|
1246
|
+
assert.equal(dots[0].style.left, 'var(--zui-slider-thumb-size)');
|
|
1247
|
+
assert.equal(dots[dots.length - 1].style.left, 'calc(100% - var(--zui-slider-thumb-size))');
|
|
1248
|
+
// pos=50: offset = 1.5 - (3*50)/100 = 0
|
|
1249
|
+
assert.equal(dots[2].style.left, 'calc(50% + var(--zui-slider-thumb-size) * 0)');
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
test('step dots toggle when step is set to 0 then back', async () => {
|
|
1253
|
+
await element.updateComplete;
|
|
1254
|
+
assert.exists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1255
|
+
|
|
1256
|
+
element.step = 0;
|
|
1257
|
+
await element.updateComplete;
|
|
1258
|
+
assert.notExists(element.shadowRoot!.querySelector('.step-dots'));
|
|
1259
|
+
|
|
1260
|
+
element.step = 25;
|
|
1261
|
+
await element.updateComplete;
|
|
1262
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 5);
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
test('step dot count reacts to step, min, and max changes', async () => {
|
|
1266
|
+
await element.updateComplete;
|
|
1267
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 5); // step=25, range=100
|
|
1268
|
+
|
|
1269
|
+
element.step = 50;
|
|
1270
|
+
await element.updateComplete;
|
|
1271
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3); // 0, 50, 100
|
|
1272
|
+
|
|
1273
|
+
element.step = 25;
|
|
1274
|
+
element.max = 50;
|
|
1275
|
+
await element.updateComplete;
|
|
1276
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3); // 0, 25, 50
|
|
1277
|
+
|
|
1278
|
+
element.max = 100;
|
|
1279
|
+
element.min = 50;
|
|
1280
|
+
await element.updateComplete;
|
|
1281
|
+
assert.equal(element.shadowRoot!.querySelectorAll('.step-dot').length, 3); // 50, 75, 100
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
test('step dots render inside range-wrapper in range mode', async () => {
|
|
1285
|
+
element.setAttribute('range', '');
|
|
1286
|
+
await element.updateComplete;
|
|
1287
|
+
const wrapper = element.shadowRoot!.querySelector('.range-wrapper')!;
|
|
1288
|
+
assert.exists(wrapper.querySelector('.step-dots'));
|
|
1289
|
+
assert.equal(wrapper.querySelectorAll('.step-dot').length, 5); // 0, 25, 50, 75, 100
|
|
1290
|
+
});
|
|
1291
|
+
});
|