Skip to content

Commit 5921415

Browse files
authored
feat: implement slider and range-slider pointer events support (#10966)
1 parent de3f664 commit 5921415

File tree

5 files changed

+602
-46
lines changed

5 files changed

+602
-46
lines changed

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

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ class RangeSlider extends SliderMixin(
101101
this.__value = [...this.value];
102102
this.__inputId0 = `slider-${generateUniqueId()}`;
103103
this.__inputId1 = `slider-${generateUniqueId()}`;
104-
105-
this.addEventListener('mousedown', (e) => this._onMouseDown(e));
106104
}
107105

108106
/** @protected */
@@ -163,9 +161,9 @@ class RangeSlider extends SliderMixin(
163161
super.updated(props);
164162

165163
if (props.has('value') || props.has('min') || props.has('max')) {
166-
const value = Array.isArray(this.value) ? this.value : [];
167-
value.forEach((value, idx) => {
168-
this.__updateValue(value, idx);
164+
const value = [...this.value];
165+
value.forEach((v, idx) => {
166+
this.__updateValue(v, idx, value);
169167
});
170168
}
171169
}
@@ -200,24 +198,81 @@ class RangeSlider extends SliderMixin(
200198

201199
/**
202200
* @param {PointerEvent} event
203-
* @protected
201+
* @private
204202
*/
205-
_onMouseDown(event) {
206-
// Prevent blur if already focused
207-
event.preventDefault();
208-
209-
// Focus the input to allow modifying value using keyboard
210-
this.focus({ focusVisible: false });
203+
__focusInput(event) {
204+
const index = this.__getThumbIndex(event);
205+
this._inputElements[index].focus();
211206
}
212207

213208
/** @private */
214209
__commitValue() {
215210
this.value = [...this.__value];
216211
}
217212

213+
/**
214+
* @param {Event} event
215+
* @return {number}
216+
*/
217+
__getThumbIndex(event) {
218+
if (event.type === 'input') {
219+
return this._inputElements.indexOf(event.target);
220+
}
221+
222+
return this.__getClosestThumb(event);
223+
}
224+
225+
/**
226+
* @param {PointerEvent} event
227+
* @return {number}
228+
* @private
229+
*/
230+
__getClosestThumb(event) {
231+
let closestThumb;
232+
233+
// If both thumbs are at the start, use the second thumb,
234+
// and if both are at tne end, use the first one instead.
235+
if (this.__value[0] === this.__value[1]) {
236+
const { min, max } = this.__getConstraints();
237+
if (this.__value[0] === min) {
238+
return 1;
239+
}
240+
241+
if (this.__value[0] === max) {
242+
return 0;
243+
}
244+
}
245+
246+
const percent = this.__getEventPercent(event);
247+
const value = this.__getValueFromPercent(percent);
248+
249+
// First thumb position from the "end"
250+
const index = this.__value.findIndex((v) => value - v < 0);
251+
252+
// Pick the first one
253+
if (index === 0) {
254+
closestThumb = index;
255+
} else if (index === -1) {
256+
// Pick the last one (position is past all the thumbs)
257+
closestThumb = this.__value.length - 1;
258+
} else {
259+
const lastStart = this.__value[index - 1];
260+
const firstEnd = this.__value[index];
261+
// Pick the first one from the "start" unless thumbs are stacked on top of each other
262+
if (Math.abs(lastStart - value) < Math.abs(firstEnd - value)) {
263+
closestThumb = index - 1;
264+
} else {
265+
// Pick the last one from the "end"
266+
closestThumb = index;
267+
}
268+
}
269+
270+
return closestThumb;
271+
}
272+
218273
/** @private */
219274
__onKeyDown(event) {
220-
this.__thumbIndex = this._inputElements.indexOf(event.target);
275+
const index = this._inputElements.indexOf(event.target);
221276

222277
const prevKeys = ['ArrowLeft', 'ArrowDown'];
223278
const nextKeys = ['ArrowRight', 'ArrowUp'];
@@ -226,8 +281,7 @@ class RangeSlider extends SliderMixin(
226281
// to prevent the case where slotted range inputs would end up in broken state.
227282
if (
228283
this.__value[0] === this.__value[1] &&
229-
((this.__thumbIndex === 0 && nextKeys.includes(event.key)) ||
230-
(this.__thumbIndex === 1 && prevKeys.includes(event.key)))
284+
((index === 0 && nextKeys.includes(event.key)) || (index === 1 && prevKeys.includes(event.key)))
231285
) {
232286
event.preventDefault();
233287
}

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

Lines changed: 153 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,33 +46,56 @@ export const SliderMixin = (superClass) =>
4646
constructor() {
4747
super();
4848

49-
this.__thumbIndex = 0;
49+
this.__onPointerMove = this.__onPointerMove.bind(this);
50+
this.__onPointerUp = this.__onPointerUp.bind(this);
51+
52+
// Use separate mousedown listener for focusing the input, as
53+
// pointerdown fires too early and the global `keyboardActive`
54+
// flag isn't updated yet, which incorrectly shows focus-ring
55+
this.addEventListener('mousedown', (e) => this.__onMouseDown(e));
56+
this.addEventListener('pointerdown', (e) => this.__onPointerDown(e));
57+
}
58+
59+
/** @protected */
60+
firstUpdated() {
61+
super.firstUpdated();
62+
63+
this.__lastCommittedValue = this.value;
64+
}
65+
66+
/**
67+
* @param {Event} event
68+
* @return {number}
69+
*/
70+
__getThumbIndex(_event) {
71+
return 0;
5072
}
5173

5274
/**
5375
* @param {number} value
5476
* @param {number} index
77+
* @param {number[]} fullValue
5578
* @private
5679
*/
57-
__updateValue(value, index = this.__thumbIndex) {
80+
__updateValue(value, index, fullValue = this.__value) {
5881
const { min, max, step } = this.__getConstraints();
5982

60-
const minValue = this.__value[index - 1] || min;
61-
const maxValue = this.__value[index + 1] || max;
83+
const minValue = fullValue[index - 1] !== undefined ? fullValue[index - 1] : min;
84+
const maxValue = fullValue[index + 1] !== undefined ? fullValue[index + 1] : max;
6285

6386
const safeValue = Math.min(Math.max(value, minValue), maxValue);
6487

65-
const offset = safeValue - min;
88+
const offset = safeValue - minValue;
6689
const nearestOffset = Math.round(offset / step) * step;
67-
const nearestValue = min + nearestOffset;
90+
const nearestValue = minValue + nearestOffset;
6891

6992
const newValue = Math.round(nearestValue);
7093

71-
this.__value = this.__value.with(index, newValue);
94+
this.__value = fullValue.with(index, newValue);
7295
}
7396

7497
/**
75-
* @return {{ min: number, max: number}}
98+
* @return {{ min: number, max: number, step: number}}
7699
* @private
77100
*/
78101
__getConstraints() {
@@ -93,24 +116,141 @@ export const SliderMixin = (superClass) =>
93116
return (100 * (value - min)) / (max - min);
94117
}
95118

119+
/**
120+
* @param {number} percent
121+
* @return {number}
122+
* @private
123+
*/
124+
__getValueFromPercent(percent) {
125+
const { min, max } = this.__getConstraints();
126+
return min + percent * (max - min);
127+
}
128+
129+
/**
130+
* @param {PointerEvent} event
131+
* @return {number}
132+
* @private
133+
*/
134+
__getEventPercent(event) {
135+
const offset = event.offsetX;
136+
const size = this.offsetWidth;
137+
const safeOffset = Math.min(Math.max(offset, 0), size);
138+
return safeOffset / size;
139+
}
140+
141+
/**
142+
* @param {PointerEvent} event
143+
* @return {number}
144+
* @private
145+
*/
146+
__getEventValue(event) {
147+
const percent = this.__getEventPercent(event);
148+
return this.__getValueFromPercent(percent);
149+
}
150+
151+
/**
152+
* @param {PointerEvent} event
153+
* @private
154+
*/
155+
__onMouseDown(event) {
156+
const part = event.composedPath()[0].getAttribute('part');
157+
if (!part || (!part.startsWith('track') && !part.startsWith('thumb'))) {
158+
return;
159+
}
160+
161+
// Prevent losing focus
162+
event.preventDefault();
163+
164+
this.__focusInput(event);
165+
}
166+
167+
/**
168+
* @param {PointerEvent} event
169+
* @private
170+
*/
171+
__onPointerDown(event) {
172+
if (event.button !== 0) {
173+
return;
174+
}
175+
176+
const part = event.composedPath()[0].getAttribute('part');
177+
if (!part || (!part.startsWith('track') && !part.startsWith('thumb'))) {
178+
return;
179+
}
180+
181+
this.setPointerCapture(event.pointerId);
182+
this.addEventListener('pointermove', this.__onPointerMove);
183+
this.addEventListener('pointerup', this.__onPointerUp);
184+
this.addEventListener('pointercancel', this.__onPointerUp);
185+
186+
this.__thumbIndex = this.__getThumbIndex(event);
187+
188+
// Update value on track click
189+
if (part.startsWith('track')) {
190+
const newValue = this.__getEventValue(event);
191+
this.__updateValue(newValue, this.__thumbIndex);
192+
this.__commitValue();
193+
}
194+
}
195+
196+
/**
197+
* @param {PointerEvent} event
198+
* @private
199+
*/
200+
__onPointerMove(event) {
201+
const newValue = this.__getEventValue(event);
202+
this.__updateValue(newValue, this.__thumbIndex);
203+
this.__commitValue();
204+
}
205+
206+
/**
207+
* @param {PointerEvent} event
208+
* @private
209+
*/
210+
__onPointerUp(event) {
211+
this.__thumbIndex = null;
212+
213+
this.releasePointerCapture(event.pointerId);
214+
this.removeEventListener('pointermove', this.__onPointerMove);
215+
this.removeEventListener('pointerup', this.__onPointerUp);
216+
this.removeEventListener('pointercancel', this.__onPointerUp);
217+
218+
this.__detectAndDispatchChange();
219+
}
220+
221+
/**
222+
* @param {Event} event
223+
* @private
224+
*/
225+
__focusInput(_event) {
226+
this.focus({ focusVisible: false });
227+
}
228+
96229
/** @private */
97230
__detectAndDispatchChange() {
98-
if (this.__lastCommittedValue !== this.value) {
231+
if (JSON.stringify(this.__lastCommittedValue) !== JSON.stringify(this.value)) {
99232
this.__lastCommittedValue = this.value;
100233
this.dispatchEvent(new Event('change', { bubbles: true }));
101234
}
102235
}
103236

104-
/** @private */
237+
/**
238+
* @param {Event} event
239+
* @private
240+
*/
105241
__onInput(event) {
106-
this.__updateValue(event.target.value);
242+
const index = this.__getThumbIndex(event);
243+
this.__updateValue(event.target.value, index);
107244
this.__commitValue();
108-
this.__detectAndDispatchChange();
109245
}
110246

111-
/** @private */
247+
/**
248+
* @param {Event} event
249+
* @private
250+
*/
112251
__onChange(event) {
113252
event.stopPropagation();
253+
this.__detectAndDispatchChange();
114254
}
115255

116256
/**

packages/slider/src/vaadin-slider.js

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ class Slider extends SliderMixin(
9595

9696
this.__value = [this.value];
9797
this.__inputId = `slider-${generateUniqueId()}`;
98-
99-
this.addEventListener('mousedown', (e) => this._onMouseDown(e));
10098
}
10199

102100
/** @protected */
@@ -167,18 +165,6 @@ class Slider extends SliderMixin(
167165
__commitValue() {
168166
this.value = this.__value[0];
169167
}
170-
171-
/**
172-
* @param {PointerEvent} event
173-
* @protected
174-
*/
175-
_onMouseDown(event) {
176-
// Prevent blur if already focused
177-
event.preventDefault();
178-
179-
// Focus the input to allow modifying value using keyboard
180-
this.focus({ focusVisible: false });
181-
}
182168
}
183169

184170
defineCustomElement(Slider);

0 commit comments

Comments
 (0)