@vaadin/slider 25.1.0-alpha6 → 25.1.0-alpha7

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.
@@ -50,6 +50,26 @@ export const SliderMixin = (superClass) =>
50
50
  reflectToAttribute: true,
51
51
  },
52
52
 
53
+ /**
54
+ * When true, the value bubble is always visible,
55
+ * regardless of focus or hover state.
56
+ * @attr {boolean} value-always-visible
57
+ */
58
+ valueAlwaysVisible: {
59
+ type: Boolean,
60
+ value: false,
61
+ sync: true,
62
+ },
63
+
64
+ /**
65
+ * When true, displays the min and max values below the slider track.
66
+ * @attr {boolean} min-max-visible
67
+ */
68
+ minMaxVisible: {
69
+ type: Boolean,
70
+ reflectToAttribute: true,
71
+ },
72
+
53
73
  /** @private */
54
74
  __value: {
55
75
  type: Array,
@@ -83,6 +103,30 @@ export const SliderMixin = (superClass) =>
83
103
  width: var(--_thumb-width);
84
104
  height: 100%;
85
105
  }
106
+
107
+ ${tag}:not([readonly]) > input::-webkit-slider-thumb {
108
+ cursor: var(--vaadin-slider-thumb-cursor, grab);
109
+ }
110
+
111
+ ${tag}:not([readonly]) > input::-moz-range-thumb {
112
+ cursor: var(--vaadin-slider-thumb-cursor, grab);
113
+ }
114
+
115
+ ${tag}:is([active], [start-active], [end-active]) > input::-webkit-slider-thumb {
116
+ cursor: var(--vaadin-slider-thumb-cursor-active, grabbing);
117
+ }
118
+
119
+ ${tag}:is([active], [start-active], [end-active]) > input::-moz-range-thumb {
120
+ cursor: var(--vaadin-slider-thumb-cursor-active, grabbing);
121
+ }
122
+
123
+ ${tag}[disabled] > input::-webkit-slider-thumb {
124
+ cursor: var(--vaadin-disabled-cursor, not-allowed);
125
+ }
126
+
127
+ ${tag}[disabled] > input::-moz-range-thumb {
128
+ cursor: var(--vaadin-disabled-cursor, not-allowed);
129
+ }
86
130
  `,
87
131
  ];
88
132
  }
@@ -131,9 +175,9 @@ export const SliderMixin = (superClass) =>
131
175
  */
132
176
  __getConstraints() {
133
177
  return {
134
- min: this.min || 0,
135
- max: this.max || 100,
136
- step: this.step || 1,
178
+ min: this.min !== undefined ? this.min : 0,
179
+ max: this.max !== undefined ? this.max : 100,
180
+ step: this.step !== undefined ? this.step : 1,
137
181
  };
138
182
  }
139
183
 
@@ -148,6 +192,54 @@ export const SliderMixin = (superClass) =>
148
192
  return (safeValue - min) / (max - min);
149
193
  }
150
194
 
195
+ /**
196
+ * Updates bubble visibility for a thumb based on trigger state changes.
197
+ * @param {Map} props - Changed properties from willUpdate
198
+ * @param {object} config
199
+ * @param {string} config.active - Active state property name
200
+ * @param {string} config.focused - Focused state property name
201
+ * @param {string} config.hover - Hover state property name
202
+ * @param {string} config.opened - Bubble opened property name
203
+ * @param {string} [config.otherOpened] - Other thumb's opened property (range slider)
204
+ * @private
205
+ */
206
+ __updateBubbleState(props, { active, focused, hover, opened, otherOpened }) {
207
+ if (props.has(active)) {
208
+ if (this[active]) {
209
+ // When slider is activated by track pointerdown, the hover flag
210
+ // isn't set, but the thumb is actually moved, so we set it here.
211
+ this[hover] = true;
212
+ } else if (props.get(active)) {
213
+ // Close bubble when drag ends unless the thumb has hover
214
+ this[opened] = this[hover];
215
+ }
216
+ }
217
+
218
+ if (props.has(focused)) {
219
+ if (this[focused]) {
220
+ this[opened] = true;
221
+ if (otherOpened) {
222
+ this[otherOpened] = false;
223
+ }
224
+ } else if (props.get(focused)) {
225
+ // Close bubble on blur unless the thumb has hover
226
+ this[opened] = this[hover];
227
+ }
228
+ }
229
+
230
+ if (props.has(hover)) {
231
+ if (this[hover]) {
232
+ this[opened] = true;
233
+ if (otherOpened) {
234
+ this[otherOpened] = false;
235
+ }
236
+ } else if (props.get(hover)) {
237
+ // Keep bubble open during drag (active state)
238
+ this[opened] = this[active];
239
+ }
240
+ }
241
+ }
242
+
151
243
  /**
152
244
  * @param {PointerEvent} event
153
245
  * @private
@@ -72,16 +72,20 @@ export interface SliderEventMap extends HTMLElementEventMap, SliderCustomEventMa
72
72
  * `track` | The slider track
73
73
  * `track-fill` | The filled portion of the track
74
74
  * `thumb` | The slider thumb
75
+ * `marks` | Container for min/max labels
76
+ * `min` | Minimum value label
77
+ * `max` | Maximum value label
75
78
  *
76
79
  * The following state attributes are available for styling:
77
80
  *
78
- * Attribute | Description
79
- * -------------|-------------
80
- * `active` | Set when the slider is activated with mouse or touch
81
- * `disabled` | Set when the slider is disabled
82
- * `readonly` | Set when the slider is read-only
83
- * `focused` | Set when the slider has focus
84
- * `focus-ring` | Set when the slider is focused using the keyboard
81
+ * Attribute | Description
82
+ * -------------------|-------------
83
+ * `active` | Set when the slider is activated with mouse or touch
84
+ * `disabled` | Set when the slider is disabled
85
+ * `readonly` | Set when the slider is read-only
86
+ * `focused` | Set when the slider has focus
87
+ * `focus-ring` | Set when the slider is focused using the keyboard
88
+ * `min-max-visible` | Set when the min/max labels are displayed
85
89
  *
86
90
  * The following custom CSS properties are available for styling:
87
91
  *
@@ -98,13 +102,45 @@ export interface SliderEventMap extends HTMLElementEventMap, SliderCustomEventMa
98
102
  * `--vaadin-input-field-label-font-size` |
99
103
  * `--vaadin-input-field-label-font-weight` |
100
104
  * `--vaadin-input-field-required-indicator` |
105
+ * `--vaadin-slider-bubble-arrow-size` |
106
+ * `--vaadin-slider-bubble-background` |
107
+ * `--vaadin-slider-bubble-border-color` |
108
+ * `--vaadin-slider-bubble-border-radius` |
109
+ * `--vaadin-slider-bubble-border-width` |
110
+ * `--vaadin-slider-bubble-offset` |
111
+ * `--vaadin-slider-bubble-padding` |
112
+ * `--vaadin-slider-bubble-shadow` |
113
+ * `--vaadin-slider-bubble-text-color` |
114
+ * `--vaadin-slider-bubble-font-size` |
115
+ * `--vaadin-slider-bubble-font-weight` |
116
+ * `--vaadin-slider-bubble-line-height` |
101
117
  * `--vaadin-slider-fill-background` |
118
+ * `--vaadin-slider-fill-border-color` |
119
+ * `--vaadin-slider-fill-border-width` |
120
+ * `--vaadin-slider-marks-color` |
121
+ * `--vaadin-slider-marks-font-size` |
122
+ * `--vaadin-slider-marks-font-weight` |
123
+ * `--vaadin-slider-thumb-border-color` |
124
+ * `--vaadin-slider-thumb-border-radius` |
125
+ * `--vaadin-slider-thumb-border-width` |
126
+ * `--vaadin-slider-thumb-cursor` |
127
+ * `--vaadin-slider-thumb-cursor-active` |
102
128
  * `--vaadin-slider-thumb-height` |
103
129
  * `--vaadin-slider-thumb-width` |
104
130
  * `--vaadin-slider-track-background` |
131
+ * `--vaadin-slider-track-border-color` |
105
132
  * `--vaadin-slider-track-border-radius` |
133
+ * `--vaadin-slider-track-border-width` |
106
134
  * `--vaadin-slider-track-height` |
107
135
  *
136
+ * In order to style the slider bubble, use `<vaadin-slider-bubble>` shadow DOM parts:
137
+ *
138
+ * Part name | Description
139
+ * -----------------|----------------------
140
+ * `overlay` | The overlay container
141
+ * `content` | The overlay content
142
+ * `arrow` | Arrow pointing to the thumb
143
+ *
108
144
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
109
145
  *
110
146
  * @fires {Event} change - Fired when the user commits a value change.
@@ -3,7 +3,9 @@
3
3
  * Copyright (c) 2026 - 2026 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import './vaadin-slider-bubble.js';
6
7
  import { css, html, LitElement, render } from 'lit';
8
+ import { ifDefined } from 'lit/directives/if-defined.js';
7
9
  import { styleMap } from 'lit/directives/style-map.js';
8
10
  import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
9
11
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
@@ -39,16 +41,20 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
39
41
  * `track` | The slider track
40
42
  * `track-fill` | The filled portion of the track
41
43
  * `thumb` | The slider thumb
44
+ * `marks` | Container for min/max labels
45
+ * `min` | Minimum value label
46
+ * `max` | Maximum value label
42
47
  *
43
48
  * The following state attributes are available for styling:
44
49
  *
45
- * Attribute | Description
46
- * -------------|-------------
47
- * `active` | Set when the slider is activated with mouse or touch
48
- * `disabled` | Set when the slider is disabled
49
- * `readonly` | Set when the slider is read-only
50
- * `focused` | Set when the slider has focus
51
- * `focus-ring` | Set when the slider is focused using the keyboard
50
+ * Attribute | Description
51
+ * -------------------|-------------
52
+ * `active` | Set when the slider is activated with mouse or touch
53
+ * `disabled` | Set when the slider is disabled
54
+ * `readonly` | Set when the slider is read-only
55
+ * `focused` | Set when the slider has focus
56
+ * `focus-ring` | Set when the slider is focused using the keyboard
57
+ * `min-max-visible` | Set when the min/max labels are displayed
52
58
  *
53
59
  * The following custom CSS properties are available for styling:
54
60
  *
@@ -65,20 +71,52 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
65
71
  * `--vaadin-input-field-label-font-size` |
66
72
  * `--vaadin-input-field-label-font-weight` |
67
73
  * `--vaadin-input-field-required-indicator` |
74
+ * `--vaadin-slider-bubble-arrow-size` |
75
+ * `--vaadin-slider-bubble-background` |
76
+ * `--vaadin-slider-bubble-border-color` |
77
+ * `--vaadin-slider-bubble-border-radius` |
78
+ * `--vaadin-slider-bubble-border-width` |
79
+ * `--vaadin-slider-bubble-offset` |
80
+ * `--vaadin-slider-bubble-padding` |
81
+ * `--vaadin-slider-bubble-shadow` |
82
+ * `--vaadin-slider-bubble-text-color` |
83
+ * `--vaadin-slider-bubble-font-size` |
84
+ * `--vaadin-slider-bubble-font-weight` |
85
+ * `--vaadin-slider-bubble-line-height` |
68
86
  * `--vaadin-slider-fill-background` |
87
+ * `--vaadin-slider-fill-border-color` |
88
+ * `--vaadin-slider-fill-border-width` |
89
+ * `--vaadin-slider-marks-color` |
90
+ * `--vaadin-slider-marks-font-size` |
91
+ * `--vaadin-slider-marks-font-weight` |
92
+ * `--vaadin-slider-thumb-border-color` |
93
+ * `--vaadin-slider-thumb-border-radius` |
94
+ * `--vaadin-slider-thumb-border-width` |
95
+ * `--vaadin-slider-thumb-cursor` |
96
+ * `--vaadin-slider-thumb-cursor-active` |
69
97
  * `--vaadin-slider-thumb-height` |
70
98
  * `--vaadin-slider-thumb-width` |
71
99
  * `--vaadin-slider-track-background` |
100
+ * `--vaadin-slider-track-border-color` |
72
101
  * `--vaadin-slider-track-border-radius` |
102
+ * `--vaadin-slider-track-border-width` |
73
103
  * `--vaadin-slider-track-height` |
74
104
  *
105
+ * In order to style the slider bubble, use `<vaadin-slider-bubble>` shadow DOM parts:
106
+ *
107
+ * Part name | Description
108
+ * -----------------|----------------------
109
+ * `overlay` | The overlay container
110
+ * `content` | The overlay content
111
+ * `arrow` | Arrow pointing to the thumb
112
+ *
75
113
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
76
114
  *
77
115
  * @fires {Event} change - Fired when the user commits a value change.
78
116
  * @fires {Event} input - Fired when the slider value changes during user interaction.
79
117
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
80
118
  *
81
- * @customElement
119
+ * @customElement vaadin-slider
82
120
  * @extends HTMLElement
83
121
  * @mixes ElementMixin
84
122
  * @mixes FieldMixin
@@ -140,6 +178,36 @@ class Slider extends FieldMixin(
140
178
  notify: true,
141
179
  sync: true,
142
180
  },
181
+
182
+ /** @private */
183
+ __active: {
184
+ type: Boolean,
185
+ value: false,
186
+ reflectToAttribute: true,
187
+ attribute: 'active',
188
+ sync: true,
189
+ },
190
+
191
+ /** @private */
192
+ __focusInside: {
193
+ type: Boolean,
194
+ value: false,
195
+ sync: true,
196
+ },
197
+
198
+ /** @private */
199
+ __hoverInside: {
200
+ type: Boolean,
201
+ value: false,
202
+ sync: true,
203
+ },
204
+
205
+ /** @private */
206
+ __bubbleOpened: {
207
+ type: Boolean,
208
+ value: false,
209
+ sync: true,
210
+ },
143
211
  };
144
212
  }
145
213
 
@@ -147,6 +215,7 @@ class Slider extends FieldMixin(
147
215
  render() {
148
216
  const [value] = this.__value;
149
217
  const percent = this.__getPercentFromValue(value);
218
+ const { min, max } = this.__getConstraints();
150
219
 
151
220
  return html`
152
221
  <div class="vaadin-slider-container">
@@ -161,6 +230,12 @@ class Slider extends FieldMixin(
161
230
  </div>
162
231
  <div part="thumb"></div>
163
232
  <slot name="input"></slot>
233
+ <slot name="bubble"></slot>
234
+ </div>
235
+
236
+ <div part="marks" aria-hidden="true">
237
+ <span part="min">${min}</span>
238
+ <span part="max">${max}</span>
164
239
  </div>
165
240
 
166
241
  <div part="helper-text">
@@ -180,8 +255,7 @@ class Slider extends FieldMixin(
180
255
  this.__value = [this.value];
181
256
  this.__inputId = `slider-${generateUniqueId()}`;
182
257
 
183
- this.addEventListener('pointerup', (e) => this.__onPointerUp(e));
184
- this.addEventListener('pointercancel', (e) => this.__onPointerUp(e));
258
+ this.__onPointerUp = this.__onPointerUp.bind(this);
185
259
  }
186
260
 
187
261
  /** @protected */
@@ -193,22 +267,27 @@ class Slider extends FieldMixin(
193
267
  this.ariaTarget = input;
194
268
 
195
269
  this.addController(new LabelledInputController(input, this._labelController));
270
+
271
+ this.__thumbElement = this.shadowRoot.querySelector('[part="thumb"]');
272
+ this.__bubbleElement = this.querySelector('vaadin-slider-bubble');
196
273
  }
197
274
 
198
275
  /** @private */
199
276
  __onPointerDown(event) {
200
277
  super.__onPointerDown(event);
201
278
 
202
- if (event.composedPath()[0] === this._inputElement) {
279
+ if (!this.readonly && event.composedPath()[0] === this._inputElement) {
203
280
  this.setAttribute('active', '');
281
+ window.addEventListener('pointerup', this.__onPointerUp);
282
+ window.addEventListener('pointercancel', this.__onPointerUp);
204
283
  }
205
284
  }
206
285
 
207
286
  /** @private */
208
- __onPointerUp(event) {
209
- if (event.composedPath()[0] === this._inputElement) {
210
- this.removeAttribute('active');
211
- }
287
+ __onPointerUp() {
288
+ window.removeEventListener('pointerup', this.__onPointerUp);
289
+ window.removeEventListener('pointercancel', this.__onPointerUp);
290
+ this.removeAttribute('active');
212
291
  }
213
292
 
214
293
  /**
@@ -234,16 +313,39 @@ class Slider extends FieldMixin(
234
313
  .step="${step}"
235
314
  .disabled="${this.disabled}"
236
315
  tabindex="${this.disabled ? -1 : 0}"
316
+ @pointerenter="${this.__onPointerEnter}"
317
+ @pointermove="${this.__onPointerMove}"
318
+ @pointerleave="${this.__onPointerLeave}"
237
319
  @keydown="${this.__onKeyDown}"
238
320
  @input="${this.__onInput}"
239
321
  @change="${this.__onChange}"
240
322
  />
323
+ <vaadin-slider-bubble
324
+ slot="bubble"
325
+ .positionTarget="${this.__thumbElement}"
326
+ .opened="${this.valueAlwaysVisible || this.__bubbleOpened}"
327
+ theme="${ifDefined(this._theme)}"
328
+ >
329
+ ${value}
330
+ </vaadin-slider-bubble>
241
331
  `,
242
332
  this,
243
333
  { host: this },
244
334
  );
245
335
  }
246
336
 
337
+ /** @protected */
338
+ willUpdate(props) {
339
+ super.willUpdate(props);
340
+
341
+ this.__updateBubbleState(props, {
342
+ active: '__active',
343
+ focused: '__focusInside',
344
+ hover: '__hoverInside',
345
+ opened: '__bubbleOpened',
346
+ });
347
+ }
348
+
247
349
  /** @protected */
248
350
  updated(props) {
249
351
  super.updated(props);
@@ -292,17 +394,64 @@ class Slider extends FieldMixin(
292
394
  __onInput(event) {
293
395
  event.stopPropagation();
294
396
  this.__updateValue(event.target.value, 0);
397
+ this.__updateBubble();
295
398
  this.__dispatchInputEvent();
296
399
  this.__commitValue();
297
400
  }
298
401
 
299
402
  /** @private */
300
403
  __onKeyDown(event) {
301
- const arrowKeys = ['ArrowLeft', 'ArrowDown', 'ArrowRight', 'ArrowUp'];
302
- if (this.readonly && arrowKeys.includes(event.key)) {
404
+ const inputKeys = ['ArrowLeft', 'ArrowDown', 'ArrowRight', 'ArrowUp', 'PageUp', 'PageDown', 'Home', 'End'];
405
+ if (this.readonly && inputKeys.includes(event.key)) {
303
406
  event.preventDefault();
304
407
  }
305
408
  }
409
+
410
+ /**
411
+ * Override method inherited from `FocusMixin` to update bubble state.
412
+ *
413
+ * @param {boolean} focused
414
+ * @protected
415
+ * @override
416
+ */
417
+ _setFocused(focused) {
418
+ super._setFocused(focused);
419
+
420
+ this.__focusInside = focused;
421
+ }
422
+
423
+ /** @private */
424
+ __isThumbEvent(event) {
425
+ const rect = this.__thumbElement.getBoundingClientRect();
426
+ return (
427
+ event.clientX >= rect.left &&
428
+ event.clientX <= rect.right &&
429
+ event.clientY >= rect.top &&
430
+ event.clientY <= rect.bottom
431
+ );
432
+ }
433
+
434
+ /** @private */
435
+ __onPointerEnter(event) {
436
+ if (this.__isThumbEvent(event)) {
437
+ this.__hoverInside = true;
438
+ }
439
+ }
440
+
441
+ /** @private */
442
+ __onPointerMove(event) {
443
+ this.__hoverInside = this.__isThumbEvent(event);
444
+ }
445
+
446
+ /** @private */
447
+ __onPointerLeave() {
448
+ this.__hoverInside = false;
449
+ }
450
+
451
+ /** @private */
452
+ __updateBubble() {
453
+ this.__bubbleElement.$.overlay._updatePosition();
454
+ }
306
455
  }
307
456
 
308
457
  defineCustomElement(Slider);