@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.
@@ -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 };