Skip to content

Commit 2eb3482

Browse files
authored
feat: render slotted input type range, add keyboard support (#10958)
1 parent 8df03da commit 2eb3482

File tree

12 files changed

+727
-23
lines changed

12 files changed

+727
-23
lines changed

dev/range-slider.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@
1515
<body>
1616
<vaadin-range-slider></vaadin-range-slider>
1717

18+
<output id="output"></output>
19+
1820
<script type="module">
1921
import '@vaadin/slider/vaadin-range-slider.js';
22+
23+
const slider = document.querySelector('vaadin-range-slider');
24+
25+
slider.addEventListener('change', (e) => {
26+
document.querySelector('#output').textContent = e.target.value;
27+
});
2028
</script>
2129
</body>
2230
</html>

dev/slider.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@
1515
<body>
1616
<vaadin-slider></vaadin-slider>
1717

18+
<output id="output"></output>
19+
1820
<script type="module">
1921
import '@vaadin/slider';
22+
23+
const slider = document.querySelector('vaadin-slider');
24+
25+
slider.addEventListener('change', (e) => {
26+
document.querySelector('#output').textContent = e.target.value;
27+
});
2028
</script>
2129
</body>
2230
</html>

packages/slider/src/styles/vaadin-slider-base-styles.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,14 @@ export const sliderStyles = css`
5353
border-radius: 50%;
5454
touch-action: none;
5555
}
56+
57+
/* visually hidden */
58+
::slotted(input) {
59+
flex: 1;
60+
font: inherit;
61+
height: var(--_thumb-size);
62+
opacity: 0 !important;
63+
margin: 0 !important;
64+
pointer-events: none;
65+
}
5666
`;

packages/slider/src/vaadin-range-slider.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright (c) 2026 - 2026 Vaadin Ltd.
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
6+
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
67
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
78
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
89
import { SliderMixin } from './vaadin-slider-mixin.js';
@@ -15,7 +16,7 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
1516
* <vaadin-range-slider min="0" max="100" step="1"></vaadin-range-slider>
1617
* ```
1718
*/
18-
declare class RangeSlider extends SliderMixin(ThemableMixin(ElementMixin(HTMLElement))) {
19+
declare class RangeSlider extends SliderMixin(FocusMixin(ThemableMixin(ElementMixin(HTMLElement)))) {
1920
/**
2021
* The value of the slider.
2122
*/

packages/slider/src/vaadin-range-slider.js

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
* Copyright (c) 2026 - 2026 Vaadin Ltd.
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
6-
import { html, LitElement } from 'lit';
6+
import { css, html, LitElement, render } from 'lit';
77
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';
810
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
911
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
1012
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
13+
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
1114
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
1215
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
1316
import { sliderStyles } from './styles/vaadin-slider-base-styles.js';
@@ -24,16 +27,28 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
2427
* @customElement
2528
* @extends HTMLElement
2629
* @mixes ElementMixin
30+
* @mixes FocusMixin
2731
* @mixes SliderMixin
2832
* @mixes ThemableMixin
2933
*/
30-
class RangeSlider extends SliderMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))))) {
34+
class RangeSlider extends SliderMixin(
35+
FocusMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))))),
36+
) {
3137
static get is() {
3238
return 'vaadin-range-slider';
3339
}
3440

3541
static get styles() {
36-
return sliderStyles;
42+
return [
43+
sliderStyles,
44+
css`
45+
:host([focus-ring][start-focused]) [part~='thumb-start'],
46+
:host([focus-ring][end-focused]) [part~='thumb-end'] {
47+
outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
48+
outline-offset: 1px;
49+
}
50+
`,
51+
];
3752
}
3853

3954
static get experimental() {
@@ -73,13 +88,69 @@ class RangeSlider extends SliderMixin(ElementMixin(ThemableMixin(PolylitMixin(Lu
7388
</div>
7489
<div part="thumb thumb-start" style="${styleMap({ insetInlineStart: `${startPercent}%` })}"></div>
7590
<div part="thumb thumb-end" style="${styleMap({ insetInlineStart: `${endPercent}%` })}"></div>
91+
<slot name="input"></slot>
7692
`;
7793
}
7894

7995
constructor() {
8096
super();
8197

8298
this.__value = [...this.value];
99+
this.__inputId0 = `slider-${generateUniqueId()}`;
100+
this.__inputId1 = `slider-${generateUniqueId()}`;
101+
102+
this.addEventListener('mousedown', (e) => this._onMouseDown(e));
103+
}
104+
105+
/** @protected */
106+
firstUpdated() {
107+
super.firstUpdated();
108+
109+
const inputs = this.querySelectorAll('[slot="input"]');
110+
this._inputElements = [...inputs];
111+
}
112+
113+
/**
114+
* Override update to render slotted `<input type="range" />`
115+
* into light DOM after rendering shadow DOM.
116+
* @protected
117+
*/
118+
update(props) {
119+
super.update(props);
120+
121+
const [startValue, endValue] = this.__value;
122+
const { min, max } = this.__getConstraints();
123+
124+
render(
125+
html`
126+
<input
127+
type="range"
128+
id="${this.__inputId0}"
129+
slot="input"
130+
.min="${min}"
131+
.max="${max}"
132+
.value="${startValue}"
133+
tabindex="0"
134+
@keydown="${this.__onKeyDown}"
135+
@input="${this.__onInput}"
136+
@change="${this.__onChange}"
137+
/>
138+
<input
139+
type="range"
140+
id="${this.__inputId1}"
141+
slot="input"
142+
.min="${min}"
143+
.max="${max}"
144+
.value="${endValue}"
145+
tabindex="0"
146+
@keydown="${this.__onKeyDown}"
147+
@input="${this.__onInput}"
148+
@change="${this.__onChange}"
149+
/>
150+
`,
151+
this,
152+
{ host: this },
153+
);
83154
}
84155

85156
/** @protected */
@@ -93,6 +164,81 @@ class RangeSlider extends SliderMixin(ElementMixin(ThemableMixin(PolylitMixin(Lu
93164
});
94165
}
95166
}
167+
168+
/**
169+
* @param {FocusOptions=} options
170+
* @protected
171+
* @override
172+
*/
173+
focus(options) {
174+
if (this._inputElements) {
175+
this._inputElements[0].focus();
176+
}
177+
178+
super.focus(options);
179+
}
180+
181+
/**
182+
* Override method inherited from `FocusMixin` to set
183+
* state attributes indicating which thumb has focus.
184+
*
185+
* @param {boolean} focused
186+
* @protected
187+
* @override
188+
*/
189+
_setFocused(focused) {
190+
super._setFocused(focused);
191+
192+
this.toggleAttribute('start-focused', isElementFocused(this._inputElements[0]));
193+
this.toggleAttribute('end-focused', isElementFocused(this._inputElements[1]));
194+
}
195+
196+
/**
197+
* @param {PointerEvent} event
198+
* @protected
199+
*/
200+
_onMouseDown(event) {
201+
// Prevent blur if already focused
202+
event.preventDefault();
203+
204+
// Focus the input to allow modifying value using keyboard
205+
this.focus({ focusVisible: false });
206+
}
207+
208+
/** @private */
209+
__commitValue() {
210+
this.value = [...this.__value];
211+
}
212+
213+
/** @private */
214+
__onKeyDown(event) {
215+
this.__thumbIndex = this._inputElements.indexOf(event.target);
216+
217+
const prevKeys = ['ArrowLeft', 'ArrowDown'];
218+
const nextKeys = ['ArrowRight', 'ArrowUp'];
219+
220+
// Suppress native `input` event if start and end thumbs point to the same value,
221+
// to prevent the case where slotted range inputs would end up in broken state.
222+
if (
223+
this.__value[0] === this.__value[1] &&
224+
((this.__thumbIndex === 0 && nextKeys.includes(event.key)) ||
225+
(this.__thumbIndex === 1 && prevKeys.includes(event.key)))
226+
) {
227+
event.preventDefault();
228+
}
229+
}
230+
231+
/** @private */
232+
__onInput(event) {
233+
this.__updateValue(event.target.value);
234+
this.__commitValue();
235+
this.__detectAndDispatchChange();
236+
}
237+
238+
/** @private */
239+
__onChange(event) {
240+
event.stopPropagation();
241+
}
96242
}
97243

98244
defineCustomElement(RangeSlider);

packages/slider/src/vaadin-slider-mixin.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,18 @@ export const SliderMixin = (superClass) =>
8585
/**
8686
* @param {number} value
8787
* @return {number}
88-
* @protected
88+
* @private
8989
*/
9090
__getPercentFromValue(value) {
9191
const { min, max } = this.__getConstraints();
9292
return (100 * (value - min)) / (max - min);
9393
}
94+
95+
/** @private */
96+
__detectAndDispatchChange() {
97+
if (this.__lastCommittedValue !== this.value) {
98+
this.__lastCommittedValue = this.value;
99+
this.dispatchEvent(new Event('change', { bubbles: true }));
100+
}
101+
}
94102
};

packages/slider/src/vaadin-slider.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright (c) 2026 - 2026 Vaadin Ltd.
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
6+
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
67
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
78
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
89
import { SliderMixin } from './vaadin-slider-mixin.js';
@@ -15,7 +16,7 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
1516
* <vaadin-slider min="0" max="100" step="1"></vaadin-slider>
1617
* ```
1718
*/
18-
declare class Slider extends SliderMixin(ThemableMixin(ElementMixin(HTMLElement))) {
19+
declare class Slider extends SliderMixin(FocusMixin(ThemableMixin(ElementMixin(HTMLElement)))) {
1920
/**
2021
* The value of the slider.
2122
*/

0 commit comments

Comments
 (0)