@vaadin/slider 25.1.0-alpha5 → 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
  }
@@ -90,7 +134,7 @@ export const SliderMixin = (superClass) =>
90
134
  constructor() {
91
135
  super();
92
136
 
93
- this.addEventListener('mousedown', (e) => this.__onMouseDown(e));
137
+ this.addEventListener('pointerdown', (e) => this.__onPointerDown(e));
94
138
  }
95
139
 
96
140
  /** @protected */
@@ -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,11 +192,59 @@ 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
154
246
  */
155
- __onMouseDown(event) {
247
+ __onPointerDown(event) {
156
248
  if (!event.composedPath().includes(this.$.controls)) {
157
249
  return;
158
250
  }
@@ -72,15 +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
- * `disabled` | Set when the slider is disabled
81
- * `readonly` | Set when the slider is read-only
82
- * `focused` | Set when the slider has focus
83
- * `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
84
89
  *
85
90
  * The following custom CSS properties are available for styling:
86
91
  *
@@ -97,13 +102,45 @@ export interface SliderEventMap extends HTMLElementEventMap, SliderCustomEventMa
97
102
  * `--vaadin-input-field-label-font-size` |
98
103
  * `--vaadin-input-field-label-font-weight` |
99
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` |
100
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` |
101
128
  * `--vaadin-slider-thumb-height` |
102
129
  * `--vaadin-slider-thumb-width` |
103
130
  * `--vaadin-slider-track-background` |
131
+ * `--vaadin-slider-track-border-color` |
104
132
  * `--vaadin-slider-track-border-radius` |
133
+ * `--vaadin-slider-track-border-width` |
105
134
  * `--vaadin-slider-track-height` |
106
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
+ *
107
144
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
108
145
  *
109
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';
@@ -11,6 +13,7 @@ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
11
13
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
12
14
  import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
13
15
  import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
16
+ import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
14
17
  import { field } from '@vaadin/field-base/src/styles/field-base-styles.js';
15
18
  import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
16
19
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
@@ -38,15 +41,20 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
38
41
  * `track` | The slider track
39
42
  * `track-fill` | The filled portion of the track
40
43
  * `thumb` | The slider thumb
44
+ * `marks` | Container for min/max labels
45
+ * `min` | Minimum value label
46
+ * `max` | Maximum value label
41
47
  *
42
48
  * The following state attributes are available for styling:
43
49
  *
44
- * Attribute | Description
45
- * -------------|-------------
46
- * `disabled` | Set when the slider is disabled
47
- * `readonly` | Set when the slider is read-only
48
- * `focused` | Set when the slider has focus
49
- * `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
50
58
  *
51
59
  * The following custom CSS properties are available for styling:
52
60
  *
@@ -63,20 +71,52 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
63
71
  * `--vaadin-input-field-label-font-size` |
64
72
  * `--vaadin-input-field-label-font-weight` |
65
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` |
66
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` |
67
97
  * `--vaadin-slider-thumb-height` |
68
98
  * `--vaadin-slider-thumb-width` |
69
99
  * `--vaadin-slider-track-background` |
100
+ * `--vaadin-slider-track-border-color` |
70
101
  * `--vaadin-slider-track-border-radius` |
102
+ * `--vaadin-slider-track-border-width` |
71
103
  * `--vaadin-slider-track-height` |
72
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
+ *
73
113
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
74
114
  *
75
115
  * @fires {Event} change - Fired when the user commits a value change.
76
116
  * @fires {Event} input - Fired when the slider value changes during user interaction.
77
117
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
78
118
  *
79
- * @customElement
119
+ * @customElement vaadin-slider
80
120
  * @extends HTMLElement
81
121
  * @mixes ElementMixin
82
122
  * @mixes FieldMixin
@@ -138,6 +178,36 @@ class Slider extends FieldMixin(
138
178
  notify: true,
139
179
  sync: true,
140
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
+ },
141
211
  };
142
212
  }
143
213
 
@@ -145,12 +215,13 @@ class Slider extends FieldMixin(
145
215
  render() {
146
216
  const [value] = this.__value;
147
217
  const percent = this.__getPercentFromValue(value);
218
+ const { min, max } = this.__getConstraints();
148
219
 
149
220
  return html`
150
221
  <div class="vaadin-slider-container">
151
- <div part="label" @click="${this.focus}">
222
+ <div part="label">
152
223
  <slot name="label"></slot>
153
- <span part="required-indicator" aria-hidden="true"></span>
224
+ <span part="required-indicator" aria-hidden="true" @click="${this.focus}"></span>
154
225
  </div>
155
226
 
156
227
  <div id="controls" style="${styleMap({ '--value': percent })}">
@@ -159,6 +230,12 @@ class Slider extends FieldMixin(
159
230
  </div>
160
231
  <div part="thumb"></div>
161
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>
162
239
  </div>
163
240
 
164
241
  <div part="helper-text">
@@ -177,15 +254,40 @@ class Slider extends FieldMixin(
177
254
 
178
255
  this.__value = [this.value];
179
256
  this.__inputId = `slider-${generateUniqueId()}`;
257
+
258
+ this.__onPointerUp = this.__onPointerUp.bind(this);
180
259
  }
181
260
 
182
261
  /** @protected */
183
- firstUpdated() {
184
- super.firstUpdated();
262
+ ready() {
263
+ super.ready();
185
264
 
186
265
  const input = this.querySelector('[slot="input"]');
187
266
  this._inputElement = input;
188
267
  this.ariaTarget = input;
268
+
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');
273
+ }
274
+
275
+ /** @private */
276
+ __onPointerDown(event) {
277
+ super.__onPointerDown(event);
278
+
279
+ if (!this.readonly && event.composedPath()[0] === this._inputElement) {
280
+ this.setAttribute('active', '');
281
+ window.addEventListener('pointerup', this.__onPointerUp);
282
+ window.addEventListener('pointercancel', this.__onPointerUp);
283
+ }
284
+ }
285
+
286
+ /** @private */
287
+ __onPointerUp() {
288
+ window.removeEventListener('pointerup', this.__onPointerUp);
289
+ window.removeEventListener('pointercancel', this.__onPointerUp);
290
+ this.removeAttribute('active');
189
291
  }
190
292
 
191
293
  /**
@@ -211,16 +313,39 @@ class Slider extends FieldMixin(
211
313
  .step="${step}"
212
314
  .disabled="${this.disabled}"
213
315
  tabindex="${this.disabled ? -1 : 0}"
316
+ @pointerenter="${this.__onPointerEnter}"
317
+ @pointermove="${this.__onPointerMove}"
318
+ @pointerleave="${this.__onPointerLeave}"
214
319
  @keydown="${this.__onKeyDown}"
215
320
  @input="${this.__onInput}"
216
321
  @change="${this.__onChange}"
217
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>
218
331
  `,
219
332
  this,
220
333
  { host: this },
221
334
  );
222
335
  }
223
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
+
224
349
  /** @protected */
225
350
  updated(props) {
226
351
  super.updated(props);
@@ -269,17 +394,64 @@ class Slider extends FieldMixin(
269
394
  __onInput(event) {
270
395
  event.stopPropagation();
271
396
  this.__updateValue(event.target.value, 0);
397
+ this.__updateBubble();
272
398
  this.__dispatchInputEvent();
273
399
  this.__commitValue();
274
400
  }
275
401
 
276
402
  /** @private */
277
403
  __onKeyDown(event) {
278
- const arrowKeys = ['ArrowLeft', 'ArrowDown', 'ArrowRight', 'ArrowUp'];
279
- 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)) {
280
406
  event.preventDefault();
281
407
  }
282
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
+ }
283
455
  }
284
456
 
285
457
  defineCustomElement(Slider);