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' ;
77import { 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' ;
810import { defineCustomElement } from '@vaadin/component-base/src/define.js' ;
911import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js' ;
1012import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js' ;
13+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js' ;
1114import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js' ;
1215import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js' ;
1316import { 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
98244defineCustomElement ( RangeSlider ) ;
0 commit comments