@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/src/zui-slider.ts
CHANGED
|
@@ -69,21 +69,15 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
69
69
|
{
|
|
70
70
|
visible: boolean;
|
|
71
71
|
focused: boolean;
|
|
72
|
+
committed: boolean;
|
|
72
73
|
timer?: ReturnType<typeof setTimeout>;
|
|
73
|
-
debounceTimer?: ReturnType<typeof setTimeout>;
|
|
74
74
|
}
|
|
75
75
|
>([
|
|
76
|
-
['thumb', { visible: false, focused: false }],
|
|
77
|
-
['startThumb', { visible: false, focused: false }],
|
|
78
|
-
['endThumb', { visible: false, focused: false }],
|
|
76
|
+
['thumb', { visible: false, focused: false, committed: false }],
|
|
77
|
+
['startThumb', { visible: false, focused: false, committed: false }],
|
|
78
|
+
['endThumb', { visible: false, focused: false, committed: false }],
|
|
79
79
|
]);
|
|
80
80
|
|
|
81
|
-
// Pre-bound floating input handlers cached to avoid new function references on every render
|
|
82
|
-
#onThumbFloatingInput = this.#onFloatingInput('thumb', (v) => (this.value = v));
|
|
83
|
-
#onStartThumbFloatingInput = this.#onFloatingInput('startThumb', (v) => (this.valueStart = v));
|
|
84
|
-
#onEndThumbFloatingInput = this.#onFloatingInput('endThumb', (v) => (this.valueEnd = v));
|
|
85
|
-
|
|
86
|
-
// Cached floating input change handlers; flush debounce and dispatch immediately on commit (Enter/blur)
|
|
87
81
|
#onThumbFloatingChange = this.#makeFloatingChange(
|
|
88
82
|
'thumb',
|
|
89
83
|
(v) => (this.value = v),
|
|
@@ -100,30 +94,27 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
100
94
|
() => this.#onRangeChange()
|
|
101
95
|
);
|
|
102
96
|
|
|
103
|
-
// Cached
|
|
97
|
+
// Cached keydown handler: Enter commits the floating input value in both number and text modes.
|
|
98
|
+
#onFloatingInputKeydown = (e: KeyboardEvent) => {
|
|
99
|
+
if (e.key === 'Enter') {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
const input = e.target as HTMLInputElement;
|
|
102
|
+
input.dispatchEvent(new Event('change'));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
104
106
|
#onRangeStartInput = this.#onRangeInput('start');
|
|
105
107
|
#onRangeEndInput = this.#onRangeInput('end');
|
|
108
|
+
#stopClickPropagation = (e: Event) => e.stopPropagation();
|
|
106
109
|
|
|
107
110
|
// Cached pointer/focus handlers per thumb; prevents new closures on every render
|
|
108
|
-
#h: Record<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
startThumb: {
|
|
116
|
-
show: () => this.#showThumbInput('startThumb'),
|
|
117
|
-
hide: () => this.#scheduleHideThumbInput('startThumb'),
|
|
118
|
-
focus: () => this.#focusFloatingInput('startThumb'),
|
|
119
|
-
blur: () => this.#blurFloatingInput('startThumb'),
|
|
120
|
-
},
|
|
121
|
-
endThumb: {
|
|
122
|
-
show: () => this.#showThumbInput('endThumb'),
|
|
123
|
-
hide: () => this.#scheduleHideThumbInput('endThumb'),
|
|
124
|
-
focus: () => this.#focusFloatingInput('endThumb'),
|
|
125
|
-
blur: () => this.#blurFloatingInput('endThumb'),
|
|
126
|
-
},
|
|
111
|
+
#h: Record<
|
|
112
|
+
ThumbFlag,
|
|
113
|
+
{ show: () => void; hide: () => void; focus: () => void; input: () => void; blurCommit: (e: FocusEvent) => void }
|
|
114
|
+
> = {
|
|
115
|
+
thumb: this.#makeThumbHandlers('thumb'),
|
|
116
|
+
startThumb: this.#makeThumbHandlers('startThumb'),
|
|
117
|
+
endThumb: this.#makeThumbHandlers('endThumb'),
|
|
127
118
|
};
|
|
128
119
|
|
|
129
120
|
static get styles() {
|
|
@@ -243,11 +234,10 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
243
234
|
#clearAllThumbInputState() {
|
|
244
235
|
for (const entry of this.#thumbInputState.values()) {
|
|
245
236
|
clearTimeout(entry.timer);
|
|
246
|
-
clearTimeout(entry.debounceTimer);
|
|
247
237
|
entry.timer = undefined;
|
|
248
|
-
entry.debounceTimer = undefined;
|
|
249
238
|
entry.visible = false;
|
|
250
239
|
entry.focused = false;
|
|
240
|
+
entry.committed = false;
|
|
251
241
|
}
|
|
252
242
|
}
|
|
253
243
|
|
|
@@ -524,6 +514,7 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
524
514
|
#renderSingle() {
|
|
525
515
|
const progress = this.progress;
|
|
526
516
|
const { nativeMin, nativeMax, nativeStep } = this.#nativeRangeAttrs;
|
|
517
|
+
// live() required: direct DOM writes during drag don't trigger a state change, so Lit won't re-sync without it.
|
|
527
518
|
const nativeValue = this.#stepsMode ? String(Math.max(0, this.#stepsIndexOf(this.#value))) : this.#value;
|
|
528
519
|
return html`
|
|
529
520
|
<div class="single-wrapper">
|
|
@@ -534,7 +525,7 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
534
525
|
.min="${nativeMin}"
|
|
535
526
|
.max="${nativeMax}"
|
|
536
527
|
.step="${nativeStep}"
|
|
537
|
-
.value="${nativeValue}"
|
|
528
|
+
.value="${live(nativeValue)}"
|
|
538
529
|
?disabled="${this.disabled || this.readOnly}"
|
|
539
530
|
@input="${this.#onInput}"
|
|
540
531
|
@change="${this.#onChange}"
|
|
@@ -545,7 +536,6 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
545
536
|
/>
|
|
546
537
|
${this.#renderFloatingInput(
|
|
547
538
|
this.#value,
|
|
548
|
-
this.#onThumbFloatingInput,
|
|
549
539
|
this.#onThumbFloatingChange,
|
|
550
540
|
'thumb',
|
|
551
541
|
this.#isVisible('thumb'),
|
|
@@ -564,7 +554,6 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
564
554
|
${this.#renderRangeInput('start', this.#rangeTrackBackground(progressStart, progressEnd))}
|
|
565
555
|
${this.#renderFloatingInput(
|
|
566
556
|
this.#valueStart,
|
|
567
|
-
this.#onStartThumbFloatingInput,
|
|
568
557
|
this.#onStartThumbFloatingChange,
|
|
569
558
|
'startThumb',
|
|
570
559
|
this.#isVisible('startThumb'),
|
|
@@ -573,7 +562,6 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
573
562
|
${this.#renderRangeInput('end')}
|
|
574
563
|
${this.#renderFloatingInput(
|
|
575
564
|
this.#valueEnd,
|
|
576
|
-
this.#onEndThumbFloatingInput,
|
|
577
565
|
this.#onEndThumbFloatingChange,
|
|
578
566
|
'endThumb',
|
|
579
567
|
this.#isVisible('endThumb'),
|
|
@@ -603,6 +591,7 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
603
591
|
.step="${nativeStep}"
|
|
604
592
|
.value="${live(nativeValue)}"
|
|
605
593
|
?disabled="${this.disabled || this.readOnly}"
|
|
594
|
+
@click="${this.#stopClickPropagation}"
|
|
606
595
|
@input="${onInput}"
|
|
607
596
|
@change="${this.#onRangeChange}"
|
|
608
597
|
@pointerenter="${h.show}"
|
|
@@ -615,7 +604,6 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
615
604
|
|
|
616
605
|
#renderFloatingInput(
|
|
617
606
|
val: string,
|
|
618
|
-
onInput: (e: Event) => void,
|
|
619
607
|
onFloatingChange: (e: Event) => void,
|
|
620
608
|
flag: ThumbFlag,
|
|
621
609
|
visible: boolean,
|
|
@@ -623,13 +611,14 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
623
611
|
) {
|
|
624
612
|
const h = this.#h[flag];
|
|
625
613
|
// type="text" in steps mode to allow label and stepParser input.
|
|
626
|
-
// live() required:
|
|
614
|
+
// live() required: commits that snap/clamp to the current value skip reactive updates, so Lit won't re-sync without it.
|
|
627
615
|
const ariaLabel =
|
|
628
616
|
flag === 'startThumb' ? 'Range start value' : flag === 'endThumb' ? 'Range end value' : 'Slider value';
|
|
629
617
|
return html`
|
|
630
618
|
<div
|
|
631
619
|
class=${classMap({ 'thumb-input': true, 'thumb-input--visible': visible })}
|
|
632
620
|
style=${styleMap({ left: ZuiSlider.#thumbPositionCSS(progress) })}
|
|
621
|
+
@click="${this.#stopClickPropagation}"
|
|
633
622
|
@pointerenter="${h.show}"
|
|
634
623
|
@pointerleave="${h.hide}"
|
|
635
624
|
>
|
|
@@ -642,10 +631,11 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
642
631
|
.step="${this.#stepsMode ? '' : this.step > 0 ? String(this.step) : '1'}"
|
|
643
632
|
?disabled="${this.disabled}"
|
|
644
633
|
?readonly="${this.readOnly}"
|
|
645
|
-
@
|
|
634
|
+
@keydown="${this.#onFloatingInputKeydown}"
|
|
635
|
+
@input="${h.input}"
|
|
646
636
|
@change="${onFloatingChange}"
|
|
647
637
|
@focus="${h.focus}"
|
|
648
|
-
@blur="${h.
|
|
638
|
+
@blur="${h.blurCommit}"
|
|
649
639
|
/>
|
|
650
640
|
</div>
|
|
651
641
|
`;
|
|
@@ -702,7 +692,7 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
702
692
|
const normalized = this.#normalizedSteps;
|
|
703
693
|
const minLabel = this.#stepsMode ? normalized[0]?.label ?? '' : String(this.min);
|
|
704
694
|
const maxLabel = this.#stepsMode ? normalized[normalized.length - 1]?.label ?? '' : String(this.max);
|
|
705
|
-
const hidden = this.showStepLabels
|
|
695
|
+
const hidden = this.showStepLabels;
|
|
706
696
|
return html`
|
|
707
697
|
<div
|
|
708
698
|
class="min-max-labels"
|
|
@@ -750,64 +740,83 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
750
740
|
return this.#value;
|
|
751
741
|
}
|
|
752
742
|
|
|
743
|
+
#makeThumbHandlers(flag: ThumbFlag) {
|
|
744
|
+
return {
|
|
745
|
+
show: () => this.#showThumbInput(flag),
|
|
746
|
+
hide: () => this.#scheduleHideThumbInput(flag),
|
|
747
|
+
focus: () => this.#focusFloatingInput(flag),
|
|
748
|
+
input: () => {
|
|
749
|
+
this.#thumbInputState.get(flag)!.committed = false;
|
|
750
|
+
},
|
|
751
|
+
blurCommit: (e: FocusEvent) => {
|
|
752
|
+
const entry = this.#thumbInputState.get(flag)!;
|
|
753
|
+
if (!entry.committed) {
|
|
754
|
+
(e.target as HTMLInputElement).dispatchEvent(new Event('change'));
|
|
755
|
+
}
|
|
756
|
+
entry.committed = false;
|
|
757
|
+
this.#blurFloatingInput(flag);
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
753
762
|
#makeFloatingChange(flag: ThumbFlag, setter: (val: string) => void, dispatch: () => void) {
|
|
754
763
|
return (e: Event) => {
|
|
755
764
|
if (this.readOnly) {
|
|
756
765
|
return;
|
|
757
766
|
}
|
|
758
767
|
const input = e.target as HTMLInputElement;
|
|
759
|
-
const entry = this.#thumbInputState.get(flag)!;
|
|
760
|
-
clearTimeout(entry.debounceTimer);
|
|
761
|
-
entry.debounceTimer = undefined;
|
|
762
768
|
if (input.value === '') {
|
|
763
769
|
input.value = this.#currentValueForFlag(flag);
|
|
764
770
|
return;
|
|
765
771
|
}
|
|
772
|
+
const before = this.#currentValueForFlag(flag);
|
|
773
|
+
const commit = (value: string) => {
|
|
774
|
+
setter(value);
|
|
775
|
+
this.#thumbInputState.get(flag)!.committed = true;
|
|
776
|
+
if (this.#currentValueForFlag(flag) !== before) {
|
|
777
|
+
dispatch();
|
|
778
|
+
}
|
|
779
|
+
};
|
|
766
780
|
if (this.#stepsMode) {
|
|
767
781
|
const resolved = this.#resolveFloatingInput(input.value);
|
|
768
782
|
if (resolved !== null) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
783
|
+
let finalResolved = resolved;
|
|
784
|
+
if (flag !== 'thumb') {
|
|
785
|
+
const resolvedIdx = this.#stepsIndexOf(resolved);
|
|
786
|
+
const endIdx = Math.max(0, this.#stepsIndexOf(this.#valueEnd));
|
|
787
|
+
const startIdx = Math.max(0, this.#stepsIndexOf(this.#valueStart));
|
|
788
|
+
if (flag === 'startThumb' && resolvedIdx >= endIdx) {
|
|
789
|
+
const nudgedIdx = endIdx - 1;
|
|
790
|
+
finalResolved = nudgedIdx >= 0 ? this.#stepAt(nudgedIdx) : this.#currentValueForFlag(flag);
|
|
791
|
+
} else if (flag === 'endThumb' && resolvedIdx <= startIdx) {
|
|
792
|
+
const nudgedIdx = startIdx + 1;
|
|
793
|
+
finalResolved =
|
|
794
|
+
nudgedIdx < this.#normalizedSteps.length ? this.#stepAt(nudgedIdx) : this.#currentValueForFlag(flag);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
commit(finalResolved);
|
|
772
798
|
} else {
|
|
773
799
|
input.value = this.#currentValueForFlag(flag);
|
|
774
800
|
}
|
|
775
801
|
} else {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
entry.debounceTimer = undefined;
|
|
792
|
-
if (input.value === '') {
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
if (this.#stepsMode) {
|
|
796
|
-
// Capture raw string at event time so the debounce closure reads the right value
|
|
797
|
-
const raw = input.value;
|
|
798
|
-
entry.debounceTimer = setTimeout(() => {
|
|
799
|
-
const resolved = this.#resolveFloatingInput(raw);
|
|
800
|
-
if (resolved !== null) {
|
|
801
|
-
setter(resolved);
|
|
802
|
-
this.requestUpdate();
|
|
802
|
+
let processed = this.#processFloatingValue(parseFloat(input.value));
|
|
803
|
+
if (flag !== 'thumb') {
|
|
804
|
+
const effectiveStep = this.step > 0 ? this.step : 1;
|
|
805
|
+
if (flag === 'startThumb') {
|
|
806
|
+
const endNum = parseFloat(this.#valueEnd);
|
|
807
|
+
if (parseFloat(processed) >= endNum) {
|
|
808
|
+
const nudged = this.#processFloatingValue(endNum - effectiveStep);
|
|
809
|
+
processed = parseFloat(nudged) < endNum ? nudged : this.#currentValueForFlag(flag);
|
|
810
|
+
}
|
|
811
|
+
} else {
|
|
812
|
+
const startNum = parseFloat(this.#valueStart);
|
|
813
|
+
if (parseFloat(processed) <= startNum) {
|
|
814
|
+
const nudged = this.#processFloatingValue(startNum + effectiveStep);
|
|
815
|
+
processed = parseFloat(nudged) > startNum ? nudged : this.#currentValueForFlag(flag);
|
|
816
|
+
}
|
|
803
817
|
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const raw = parseFloat(input.value);
|
|
807
|
-
entry.debounceTimer = setTimeout(() => {
|
|
808
|
-
setter(this.#processFloatingValue(raw));
|
|
809
|
-
this.requestUpdate();
|
|
810
|
-
}, 500);
|
|
818
|
+
}
|
|
819
|
+
commit(processed);
|
|
811
820
|
}
|
|
812
821
|
};
|
|
813
822
|
}
|
|
@@ -866,7 +875,6 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
866
875
|
if (this.disabled || this.readOnly) {
|
|
867
876
|
return;
|
|
868
877
|
}
|
|
869
|
-
|
|
870
878
|
const wrapper = e.currentTarget as HTMLElement;
|
|
871
879
|
const rect = wrapper.getBoundingClientRect();
|
|
872
880
|
// Thumb diameter is 3× the custom property; radius is 1.5×. Track is inset by one radius each side.
|
|
@@ -961,7 +969,9 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
|
|
|
961
969
|
return;
|
|
962
970
|
}
|
|
963
971
|
this.#showThumbInput(flag);
|
|
964
|
-
this.#thumbInputState.get(flag)
|
|
972
|
+
const entry = this.#thumbInputState.get(flag)!;
|
|
973
|
+
entry.focused = true;
|
|
974
|
+
entry.committed = false;
|
|
965
975
|
}
|
|
966
976
|
|
|
967
977
|
#blurFloatingInput(flag: ThumbFlag) {
|