@vaadin/slider 25.1.0-alpha2
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/LICENSE +190 -0
- package/README.md +38 -0
- package/package.json +52 -0
- package/src/styles/vaadin-slider-base-styles.d.ts +8 -0
- package/src/styles/vaadin-slider-base-styles.js +84 -0
- package/src/vaadin-range-slider.d.ts +67 -0
- package/src/vaadin-range-slider.js +310 -0
- package/src/vaadin-slider-mixin.d.ts +36 -0
- package/src/vaadin-slider-mixin.js +275 -0
- package/src/vaadin-slider.d.ts +67 -0
- package/src/vaadin-slider.js +190 -0
- package/vaadin-range-slider.d.ts +1 -0
- package/vaadin-range-slider.js +2 -0
- package/vaadin-slider.d.ts +1 -0
- package/vaadin-slider.js +2 -0
- package/web-types.json +327 -0
- package/web-types.lit.json +146 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2026 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, LitElement, render } from 'lit';
|
|
7
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
|
8
|
+
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
|
|
9
|
+
import { isElementFocused } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
10
|
+
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
11
|
+
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
12
|
+
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
|
|
13
|
+
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
|
|
14
|
+
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
|
|
15
|
+
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
16
|
+
import { sliderStyles } from './styles/vaadin-slider-base-styles.js';
|
|
17
|
+
import { SliderMixin } from './vaadin-slider-mixin.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `<vaadin-range-slider>` is a web component that represents a range slider
|
|
21
|
+
* for selecting a subset of the given range.
|
|
22
|
+
*
|
|
23
|
+
* ```html
|
|
24
|
+
* <vaadin-range-slider min="0" max="100" step="1"></vaadin-range-slider>
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @fires {Event} change - Fired when the user commits a value change.
|
|
28
|
+
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
|
|
29
|
+
*
|
|
30
|
+
* @customElement
|
|
31
|
+
* @extends HTMLElement
|
|
32
|
+
* @mixes ElementMixin
|
|
33
|
+
* @mixes FocusMixin
|
|
34
|
+
* @mixes SliderMixin
|
|
35
|
+
* @mixes ThemableMixin
|
|
36
|
+
*/
|
|
37
|
+
class RangeSlider extends SliderMixin(
|
|
38
|
+
FocusMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))))),
|
|
39
|
+
) {
|
|
40
|
+
static get is() {
|
|
41
|
+
return 'vaadin-range-slider';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static get styles() {
|
|
45
|
+
return [
|
|
46
|
+
sliderStyles,
|
|
47
|
+
css`
|
|
48
|
+
:host([focus-ring][start-focused]) [part~='thumb-start'],
|
|
49
|
+
:host([focus-ring][end-focused]) [part~='thumb-end'] {
|
|
50
|
+
outline: var(--vaadin-focus-ring-width) var(--_outline-style, solid) var(--vaadin-focus-ring-color);
|
|
51
|
+
outline-offset: 1px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:host([readonly]) {
|
|
55
|
+
--_outline-style: dashed;
|
|
56
|
+
}
|
|
57
|
+
`,
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static get experimental() {
|
|
62
|
+
return 'sliderComponent';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static get properties() {
|
|
66
|
+
return {
|
|
67
|
+
/**
|
|
68
|
+
* The value of the slider.
|
|
69
|
+
*/
|
|
70
|
+
value: {
|
|
71
|
+
type: Array,
|
|
72
|
+
value: () => [0, 100],
|
|
73
|
+
notify: true,
|
|
74
|
+
sync: true,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @protected */
|
|
80
|
+
render() {
|
|
81
|
+
const [startValue, endValue] = this.__value;
|
|
82
|
+
|
|
83
|
+
const startPercent = this.__getPercentFromValue(startValue);
|
|
84
|
+
const endPercent = this.__getPercentFromValue(endValue);
|
|
85
|
+
|
|
86
|
+
return html`
|
|
87
|
+
<div part="track">
|
|
88
|
+
<div
|
|
89
|
+
part="track-fill"
|
|
90
|
+
style="${styleMap({
|
|
91
|
+
insetInlineStart: `${startPercent}%`,
|
|
92
|
+
insetInlineEnd: `${100 - endPercent}%`,
|
|
93
|
+
})}"
|
|
94
|
+
></div>
|
|
95
|
+
</div>
|
|
96
|
+
<div part="thumb thumb-start" style="${styleMap({ insetInlineStart: `${startPercent}%` })}"></div>
|
|
97
|
+
<div part="thumb thumb-end" style="${styleMap({ insetInlineStart: `${endPercent}%` })}"></div>
|
|
98
|
+
<slot name="input"></slot>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
constructor() {
|
|
103
|
+
super();
|
|
104
|
+
|
|
105
|
+
this.__value = [...this.value];
|
|
106
|
+
this.__inputId0 = `slider-${generateUniqueId()}`;
|
|
107
|
+
this.__inputId1 = `slider-${generateUniqueId()}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @protected */
|
|
111
|
+
firstUpdated() {
|
|
112
|
+
super.firstUpdated();
|
|
113
|
+
|
|
114
|
+
const inputs = this.querySelectorAll('[slot="input"]');
|
|
115
|
+
this._inputElements = [...inputs];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Override update to render slotted `<input type="range" />`
|
|
120
|
+
* into light DOM after rendering shadow DOM.
|
|
121
|
+
* @protected
|
|
122
|
+
*/
|
|
123
|
+
update(props) {
|
|
124
|
+
super.update(props);
|
|
125
|
+
|
|
126
|
+
const [startValue, endValue] = this.__value;
|
|
127
|
+
const { min, max, step } = this.__getConstraints();
|
|
128
|
+
|
|
129
|
+
render(
|
|
130
|
+
html`
|
|
131
|
+
<input
|
|
132
|
+
type="range"
|
|
133
|
+
id="${this.__inputId0}"
|
|
134
|
+
slot="input"
|
|
135
|
+
.min="${min}"
|
|
136
|
+
.max="${max}"
|
|
137
|
+
.step="${step}"
|
|
138
|
+
.value="${startValue}"
|
|
139
|
+
.disabled="${this.disabled}"
|
|
140
|
+
tabindex="${this.disabled ? -1 : 0}"
|
|
141
|
+
@keydown="${this.__onKeyDown}"
|
|
142
|
+
@input="${this.__onInput}"
|
|
143
|
+
@change="${this.__onChange}"
|
|
144
|
+
/>
|
|
145
|
+
<input
|
|
146
|
+
type="range"
|
|
147
|
+
id="${this.__inputId1}"
|
|
148
|
+
slot="input"
|
|
149
|
+
.min="${min}"
|
|
150
|
+
.max="${max}"
|
|
151
|
+
.step="${step}"
|
|
152
|
+
.value="${endValue}"
|
|
153
|
+
.disabled="${this.disabled}"
|
|
154
|
+
tabindex="${this.disabled ? -1 : 0}"
|
|
155
|
+
@keydown="${this.__onKeyDown}"
|
|
156
|
+
@input="${this.__onInput}"
|
|
157
|
+
@change="${this.__onChange}"
|
|
158
|
+
/>
|
|
159
|
+
`,
|
|
160
|
+
this,
|
|
161
|
+
{ host: this },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @protected */
|
|
166
|
+
updated(props) {
|
|
167
|
+
super.updated(props);
|
|
168
|
+
|
|
169
|
+
if (props.has('value') || props.has('min') || props.has('max')) {
|
|
170
|
+
const value = [...this.value];
|
|
171
|
+
value.forEach((v, idx) => {
|
|
172
|
+
this.__updateValue(v, idx, value);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {FocusOptions=} options
|
|
179
|
+
* @protected
|
|
180
|
+
* @override
|
|
181
|
+
*/
|
|
182
|
+
focus(options) {
|
|
183
|
+
if (this.disabled) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this._inputElements) {
|
|
188
|
+
this._inputElements[0].focus();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
super.focus(options);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Override method inherited from `FocusMixin` to set
|
|
196
|
+
* state attributes indicating which thumb has focus.
|
|
197
|
+
*
|
|
198
|
+
* @param {boolean} focused
|
|
199
|
+
* @protected
|
|
200
|
+
* @override
|
|
201
|
+
*/
|
|
202
|
+
_setFocused(focused) {
|
|
203
|
+
super._setFocused(focused);
|
|
204
|
+
|
|
205
|
+
this.toggleAttribute('start-focused', isElementFocused(this._inputElements[0]));
|
|
206
|
+
this.toggleAttribute('end-focused', isElementFocused(this._inputElements[1]));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {PointerEvent} event
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
__focusInput(event) {
|
|
214
|
+
const index = this.__getThumbIndex(event);
|
|
215
|
+
this._inputElements[index].focus();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @private */
|
|
219
|
+
__commitValue() {
|
|
220
|
+
this.value = [...this.__value];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {Event} event
|
|
225
|
+
* @return {number}
|
|
226
|
+
*/
|
|
227
|
+
__getThumbIndex(event) {
|
|
228
|
+
if (event.type === 'input') {
|
|
229
|
+
return this._inputElements.indexOf(event.target);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return this.__getClosestThumb(event);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {PointerEvent} event
|
|
237
|
+
* @return {number}
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
__getClosestThumb(event) {
|
|
241
|
+
let closestThumb;
|
|
242
|
+
|
|
243
|
+
// If both thumbs are at the start, use the second thumb,
|
|
244
|
+
// and if both are at tne end, use the first one instead.
|
|
245
|
+
if (this.__value[0] === this.__value[1]) {
|
|
246
|
+
const { min, max } = this.__getConstraints();
|
|
247
|
+
if (this.__value[0] === min) {
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.__value[0] === max) {
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const percent = this.__getEventPercent(event);
|
|
257
|
+
const value = this.__getValueFromPercent(percent);
|
|
258
|
+
|
|
259
|
+
// First thumb position from the "end"
|
|
260
|
+
const index = this.__value.findIndex((v) => value - v < 0);
|
|
261
|
+
|
|
262
|
+
// Pick the first one
|
|
263
|
+
if (index === 0) {
|
|
264
|
+
closestThumb = index;
|
|
265
|
+
} else if (index === -1) {
|
|
266
|
+
// Pick the last one (position is past all the thumbs)
|
|
267
|
+
closestThumb = this.__value.length - 1;
|
|
268
|
+
} else {
|
|
269
|
+
const lastStart = this.__value[index - 1];
|
|
270
|
+
const firstEnd = this.__value[index];
|
|
271
|
+
// Pick the first one from the "start" unless thumbs are stacked on top of each other
|
|
272
|
+
if (Math.abs(lastStart - value) < Math.abs(firstEnd - value)) {
|
|
273
|
+
closestThumb = index - 1;
|
|
274
|
+
} else {
|
|
275
|
+
// Pick the last one from the "end"
|
|
276
|
+
closestThumb = index;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return closestThumb;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** @private */
|
|
284
|
+
__onKeyDown(event) {
|
|
285
|
+
const prevKeys = ['ArrowLeft', 'ArrowDown'];
|
|
286
|
+
const nextKeys = ['ArrowRight', 'ArrowUp'];
|
|
287
|
+
|
|
288
|
+
const isNextKey = nextKeys.includes(event.key);
|
|
289
|
+
const isPrevKey = prevKeys.includes(event.key);
|
|
290
|
+
|
|
291
|
+
if (!isNextKey && !isPrevKey) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const index = this._inputElements.indexOf(event.target);
|
|
296
|
+
|
|
297
|
+
// Suppress native `input` event if start and end thumbs point to the same value,
|
|
298
|
+
// to prevent the case where slotted range inputs would end up in broken state.
|
|
299
|
+
if (
|
|
300
|
+
this.readonly ||
|
|
301
|
+
(this.__value[0] === this.__value[1] && ((index === 0 && isNextKey) || (index === 1 && isPrevKey)))
|
|
302
|
+
) {
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
defineCustomElement(RangeSlider);
|
|
309
|
+
|
|
310
|
+
export { RangeSlider };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2026 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import type { DisabledMixinClass } from '@vaadin/a11y-base/src/disabled-mixin.js';
|
|
8
|
+
|
|
9
|
+
export declare function SliderMixin<T extends Constructor<HTMLElement>>(
|
|
10
|
+
base: T,
|
|
11
|
+
): Constructor<DisabledMixinClass> & Constructor<SliderMixinClass> & T;
|
|
12
|
+
|
|
13
|
+
export declare class SliderMixinClass {
|
|
14
|
+
/**
|
|
15
|
+
* The minimum allowed value.
|
|
16
|
+
*/
|
|
17
|
+
min: number;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The maximum allowed value.
|
|
21
|
+
*/
|
|
22
|
+
max: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The stepping interval of the slider.
|
|
26
|
+
*/
|
|
27
|
+
step: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* When true, the user cannot modify the value of the slider.
|
|
31
|
+
* The difference between `disabled` and `readonly` is that the
|
|
32
|
+
* read-only slider remains focusable and is announced by screen
|
|
33
|
+
* readers.
|
|
34
|
+
*/
|
|
35
|
+
readonly: boolean;
|
|
36
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2026 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @polymerMixin
|
|
10
|
+
* @mixes DisabledMixin
|
|
11
|
+
*/
|
|
12
|
+
export const SliderMixin = (superClass) =>
|
|
13
|
+
class SliderMixinClass extends DisabledMixin(superClass) {
|
|
14
|
+
static get properties() {
|
|
15
|
+
return {
|
|
16
|
+
/**
|
|
17
|
+
* The minimum allowed value.
|
|
18
|
+
*/
|
|
19
|
+
min: {
|
|
20
|
+
type: Number,
|
|
21
|
+
sync: true,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The maximum allowed value.
|
|
26
|
+
*/
|
|
27
|
+
max: {
|
|
28
|
+
type: Number,
|
|
29
|
+
sync: true,
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The stepping interval of the slider.
|
|
34
|
+
*/
|
|
35
|
+
step: {
|
|
36
|
+
type: Number,
|
|
37
|
+
sync: true,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* When true, the user cannot modify the value of the slider.
|
|
42
|
+
* The difference between `disabled` and `readonly` is that the
|
|
43
|
+
* read-only slider remains focusable and is announced by screen
|
|
44
|
+
* readers.
|
|
45
|
+
*/
|
|
46
|
+
readonly: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
reflectToAttribute: true,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/** @private */
|
|
52
|
+
__value: {
|
|
53
|
+
type: Array,
|
|
54
|
+
sync: true,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
constructor() {
|
|
60
|
+
super();
|
|
61
|
+
|
|
62
|
+
this.__onPointerMove = this.__onPointerMove.bind(this);
|
|
63
|
+
this.__onPointerUp = this.__onPointerUp.bind(this);
|
|
64
|
+
|
|
65
|
+
// Use separate mousedown listener for focusing the input, as
|
|
66
|
+
// pointerdown fires too early and the global `keyboardActive`
|
|
67
|
+
// flag isn't updated yet, which incorrectly shows focus-ring
|
|
68
|
+
this.addEventListener('mousedown', (e) => this.__onMouseDown(e));
|
|
69
|
+
this.addEventListener('pointerdown', (e) => this.__onPointerDown(e));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @protected */
|
|
73
|
+
firstUpdated() {
|
|
74
|
+
super.firstUpdated();
|
|
75
|
+
|
|
76
|
+
this.__lastCommittedValue = this.value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {Event} event
|
|
81
|
+
* @return {number}
|
|
82
|
+
*/
|
|
83
|
+
__getThumbIndex(_event) {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {number} value
|
|
89
|
+
* @param {number} index
|
|
90
|
+
* @param {number[]} fullValue
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
__updateValue(value, index, fullValue = this.__value) {
|
|
94
|
+
const { min, max, step } = this.__getConstraints();
|
|
95
|
+
|
|
96
|
+
const minValue = fullValue[index - 1] !== undefined ? fullValue[index - 1] : min;
|
|
97
|
+
const maxValue = fullValue[index + 1] !== undefined ? fullValue[index + 1] : max;
|
|
98
|
+
|
|
99
|
+
const safeValue = Math.min(Math.max(value, minValue), maxValue);
|
|
100
|
+
|
|
101
|
+
const offset = safeValue - minValue;
|
|
102
|
+
const nearestOffset = Math.round(offset / step) * step;
|
|
103
|
+
const nearestValue = minValue + nearestOffset;
|
|
104
|
+
|
|
105
|
+
const newValue = Math.round(nearestValue);
|
|
106
|
+
|
|
107
|
+
this.__value = fullValue.with(index, newValue);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @return {{ min: number, max: number, step: number}}
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
__getConstraints() {
|
|
115
|
+
return {
|
|
116
|
+
min: this.min || 0,
|
|
117
|
+
max: this.max || 100,
|
|
118
|
+
step: this.step || 1,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {number} value
|
|
124
|
+
* @return {number}
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
__getPercentFromValue(value) {
|
|
128
|
+
const { min, max } = this.__getConstraints();
|
|
129
|
+
return (100 * (value - min)) / (max - min);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {number} percent
|
|
134
|
+
* @return {number}
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
__getValueFromPercent(percent) {
|
|
138
|
+
const { min, max } = this.__getConstraints();
|
|
139
|
+
return min + percent * (max - min);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {PointerEvent} event
|
|
144
|
+
* @return {number}
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
__getEventPercent(event) {
|
|
148
|
+
const offset = event.offsetX;
|
|
149
|
+
const size = this.offsetWidth;
|
|
150
|
+
const safeOffset = Math.min(Math.max(offset, 0), size);
|
|
151
|
+
return safeOffset / size;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {PointerEvent} event
|
|
156
|
+
* @return {number}
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
__getEventValue(event) {
|
|
160
|
+
const percent = this.__getEventPercent(event);
|
|
161
|
+
return this.__getValueFromPercent(percent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {PointerEvent} event
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
__onMouseDown(event) {
|
|
169
|
+
const part = event.composedPath()[0].getAttribute('part');
|
|
170
|
+
if (!part || (!part.startsWith('track') && !part.startsWith('thumb'))) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Prevent losing focus
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
|
|
177
|
+
this.__focusInput(event);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {PointerEvent} event
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
__onPointerDown(event) {
|
|
185
|
+
if (this.disabled || this.readonly || event.button !== 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Only handle pointerdown on the thumb, track or track-fill
|
|
190
|
+
const part = event.composedPath()[0].getAttribute('part');
|
|
191
|
+
if (!part || (!part.startsWith('track') && !part.startsWith('thumb'))) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.setPointerCapture(event.pointerId);
|
|
196
|
+
this.addEventListener('pointermove', this.__onPointerMove);
|
|
197
|
+
this.addEventListener('pointerup', this.__onPointerUp);
|
|
198
|
+
this.addEventListener('pointercancel', this.__onPointerUp);
|
|
199
|
+
|
|
200
|
+
this.__thumbIndex = this.__getThumbIndex(event);
|
|
201
|
+
|
|
202
|
+
// Update value on track click
|
|
203
|
+
if (part.startsWith('track')) {
|
|
204
|
+
const newValue = this.__getEventValue(event);
|
|
205
|
+
this.__updateValue(newValue, this.__thumbIndex);
|
|
206
|
+
this.__commitValue();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {PointerEvent} event
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
__onPointerMove(event) {
|
|
215
|
+
const newValue = this.__getEventValue(event);
|
|
216
|
+
this.__updateValue(newValue, this.__thumbIndex);
|
|
217
|
+
this.__commitValue();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {PointerEvent} event
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
__onPointerUp(event) {
|
|
225
|
+
this.__thumbIndex = null;
|
|
226
|
+
|
|
227
|
+
this.releasePointerCapture(event.pointerId);
|
|
228
|
+
this.removeEventListener('pointermove', this.__onPointerMove);
|
|
229
|
+
this.removeEventListener('pointerup', this.__onPointerUp);
|
|
230
|
+
this.removeEventListener('pointercancel', this.__onPointerUp);
|
|
231
|
+
|
|
232
|
+
this.__detectAndDispatchChange();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {Event} event
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
__focusInput(_event) {
|
|
240
|
+
this.focus({ focusVisible: false });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @private */
|
|
244
|
+
__detectAndDispatchChange() {
|
|
245
|
+
if (JSON.stringify(this.__lastCommittedValue) !== JSON.stringify(this.value)) {
|
|
246
|
+
this.__lastCommittedValue = this.value;
|
|
247
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {Event} event
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
__onInput(event) {
|
|
256
|
+
const index = this.__getThumbIndex(event);
|
|
257
|
+
this.__updateValue(event.target.value, index);
|
|
258
|
+
this.__commitValue();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {Event} event
|
|
263
|
+
* @private
|
|
264
|
+
*/
|
|
265
|
+
__onChange(event) {
|
|
266
|
+
event.stopPropagation();
|
|
267
|
+
this.__detectAndDispatchChange();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Fired when the user commits a value change.
|
|
272
|
+
*
|
|
273
|
+
* @event change
|
|
274
|
+
*/
|
|
275
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2026 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
|
|
7
|
+
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
8
|
+
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
9
|
+
import { SliderMixin } from './vaadin-slider-mixin.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fired when the user commits a value change.
|
|
13
|
+
*/
|
|
14
|
+
export type SliderChangeEvent = Event & {
|
|
15
|
+
target: Slider;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fired when the `value` property changes.
|
|
20
|
+
*/
|
|
21
|
+
export type SliderValueChangedEvent = CustomEvent<{ value: number }>;
|
|
22
|
+
|
|
23
|
+
export interface SliderCustomEventMap {
|
|
24
|
+
'value-changed': SliderValueChangedEvent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SliderEventMap extends HTMLElementEventMap, SliderCustomEventMap {
|
|
28
|
+
change: SliderChangeEvent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `<vaadin-slider>` is a web component that represents a range slider
|
|
33
|
+
* for selecting numerical values within a defined range.
|
|
34
|
+
*
|
|
35
|
+
* ```html
|
|
36
|
+
* <vaadin-slider min="0" max="100" step="1"></vaadin-slider>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @fires {Event} change - Fired when the user commits a value change.
|
|
40
|
+
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
|
|
41
|
+
*/
|
|
42
|
+
declare class Slider extends SliderMixin(FocusMixin(ThemableMixin(ElementMixin(HTMLElement)))) {
|
|
43
|
+
/**
|
|
44
|
+
* The value of the slider.
|
|
45
|
+
*/
|
|
46
|
+
value: number;
|
|
47
|
+
|
|
48
|
+
addEventListener<K extends keyof SliderEventMap>(
|
|
49
|
+
type: K,
|
|
50
|
+
listener: (this: Slider, ev: SliderEventMap[K]) => void,
|
|
51
|
+
options?: AddEventListenerOptions | boolean,
|
|
52
|
+
): void;
|
|
53
|
+
|
|
54
|
+
removeEventListener<K extends keyof SliderEventMap>(
|
|
55
|
+
type: K,
|
|
56
|
+
listener: (this: Slider, ev: SliderEventMap[K]) => void,
|
|
57
|
+
options?: EventListenerOptions | boolean,
|
|
58
|
+
): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare global {
|
|
62
|
+
interface HTMLElementTagNameMap {
|
|
63
|
+
'vaadin-slider': Slider;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Slider };
|