Skip to content

Commit c783a9d

Browse files
web-padawanclaude
andauthored
feat: stop native input event, fire custom event on user interaction (#11014)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7e07afd commit c783a9d

13 files changed

+390
-241
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export type RangeSliderChangeEvent = Event & {
1616
target: RangeSlider;
1717
};
1818

19+
/**
20+
* Fired when the slider value changes during user interaction.
21+
*/
22+
export type RangeSliderInputEvent = Event & {
23+
target: RangeSlider;
24+
};
25+
1926
/**
2027
* Fired when the `value` property changes.
2128
*/
@@ -27,6 +34,7 @@ export interface RangeSliderCustomEventMap {
2734

2835
export interface RangeSliderEventMap extends HTMLElementEventMap, RangeSliderCustomEventMap {
2936
change: RangeSliderChangeEvent;
37+
input: RangeSliderInputEvent;
3038
}
3139

3240
/**
@@ -74,6 +82,7 @@ export interface RangeSliderEventMap extends HTMLElementEventMap, RangeSliderCus
7482
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
7583
*
7684
* @fires {Event} change - Fired when the user commits a value change.
85+
* @fires {Event} input - Fired when the slider value changes during user interaction.
7786
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
7887
*/
7988
declare class RangeSlider extends FieldMixin(SliderMixin(FocusMixin(ThemableMixin(ElementMixin(HTMLElement))))) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
6363
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
6464
*
6565
* @fires {Event} change - Fired when the user commits a value change.
66+
* @fires {Event} input - Fired when the slider value changes during user interaction.
6667
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
6768
*
6869
* @customElement
@@ -330,25 +331,31 @@ class RangeSlider extends FieldMixin(
330331

331332
/** @private */
332333
__onStartInput(event) {
334+
event.stopPropagation();
335+
333336
// Use second input value as first input max limit
334337
if (parseFloat(event.target.value) > this.__value[1]) {
335338
event.target.value = this.__value[1];
336339
}
337340

338341
const value = event.target.value;
339342
this.__updateValue(value, 0);
343+
this.__dispatchInputEvent();
340344
this.__commitValue();
341345
}
342346

343347
/** @private */
344348
__onEndInput(event) {
349+
event.stopPropagation();
350+
345351
// Use first input value as second input min limit
346352
if (parseFloat(event.target.value) < this.__value[0]) {
347353
event.target.value = this.__value[0];
348354
}
349355

350356
const value = event.target.value;
351357
this.__updateValue(value, 1);
358+
this.__dispatchInputEvent();
352359
this.__commitValue();
353360
}
354361

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ export const SliderMixin = (superClass) =>
163163
}
164164
}
165165

166+
/** @private */
167+
__dispatchInputEvent() {
168+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
169+
}
170+
166171
/** @private */
167172
__detectAndDispatchChange() {
168173
if (JSON.stringify(this.__lastCommittedValue) !== JSON.stringify(this.value)) {
@@ -180,6 +185,12 @@ export const SliderMixin = (superClass) =>
180185
this.__detectAndDispatchChange();
181186
}
182187

188+
/**
189+
* Fired when the slider value changes during user interaction.
190+
*
191+
* @event input
192+
*/
193+
183194
/**
184195
* Fired when the user commits a value change.
185196
*

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export type SliderChangeEvent = Event & {
1616
target: Slider;
1717
};
1818

19+
/**
20+
* Fired when the slider value changes during user interaction.
21+
*/
22+
export type SliderInputEvent = Event & {
23+
target: Slider;
24+
};
25+
1926
/**
2027
* Fired when the `value` property changes.
2128
*/
@@ -27,6 +34,7 @@ export interface SliderCustomEventMap {
2734

2835
export interface SliderEventMap extends HTMLElementEventMap, SliderCustomEventMap {
2936
change: SliderChangeEvent;
37+
input: SliderInputEvent;
3038
}
3139

3240
/**
@@ -72,6 +80,7 @@ export interface SliderEventMap extends HTMLElementEventMap, SliderCustomEventMa
7280
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
7381
*
7482
* @fires {Event} change - Fired when the user commits a value change.
83+
* @fires {Event} input - Fired when the slider value changes during user interaction.
7584
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
7685
*/
7786
declare class Slider extends FieldMixin(SliderMixin(FocusMixin(ThemableMixin(ElementMixin(HTMLElement))))) {

packages/slider/src/vaadin-slider.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { SliderMixin } from './vaadin-slider-mixin.js';
6060
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
6161
*
6262
* @fires {Event} change - Fired when the user commits a value change.
63+
* @fires {Event} input - Fired when the slider value changes during user interaction.
6364
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
6465
*
6566
* @customElement
@@ -253,7 +254,9 @@ class Slider extends FieldMixin(
253254

254255
/** @private */
255256
__onInput(event) {
257+
event.stopPropagation();
256258
this.__updateValue(event.target.value, 0);
259+
this.__dispatchInputEvent();
257260
this.__commitValue();
258261
}
259262

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { expect } from '@vaadin/chai-plugins';
2+
import { sendKeys } from '@vaadin/test-runner-commands';
3+
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
4+
import sinon from 'sinon';
5+
import '../vaadin-range-slider.js';
6+
import type { RangeSlider } from '../vaadin-range-slider.js';
7+
8+
window.Vaadin ??= {};
9+
window.Vaadin.featureFlags ??= {};
10+
window.Vaadin.featureFlags.sliderComponent = true;
11+
12+
describe('vaadin-range-slider - keyboard input', () => {
13+
let slider: RangeSlider;
14+
let inputs: HTMLInputElement[];
15+
16+
beforeEach(async () => {
17+
slider = fixtureSync('<vaadin-range-slider></vaadin-range-slider>');
18+
await nextRender();
19+
inputs = [...slider.querySelectorAll('input')];
20+
});
21+
22+
describe('start thumb', () => {
23+
beforeEach(() => {
24+
inputs[0].focus();
25+
});
26+
27+
it('should increase lower value boundary on first input Arrow Right', async () => {
28+
await sendKeys({ press: 'ArrowRight' });
29+
expect(slider.value).to.deep.equal([1, 100]);
30+
});
31+
32+
it('should increase lower value boundary on first input Arrow Up', async () => {
33+
await sendKeys({ press: 'ArrowUp' });
34+
expect(slider.value).to.deep.equal([1, 100]);
35+
});
36+
37+
it('should decrease lower value boundary on first input Arrow Left', async () => {
38+
slider.value = [10, 100];
39+
await sendKeys({ press: 'ArrowLeft' });
40+
expect(slider.value).to.deep.equal([9, 100]);
41+
});
42+
43+
it('should increase lower value boundary on first input Arrow Down', async () => {
44+
slider.value = [10, 100];
45+
await sendKeys({ press: 'ArrowDown' });
46+
expect(slider.value).to.deep.equal([9, 100]);
47+
});
48+
49+
it('should not decrease lower value boundary past min value', async () => {
50+
await sendKeys({ press: 'ArrowLeft' });
51+
expect(slider.value).to.deep.equal([0, 100]);
52+
});
53+
54+
it('should not increase lower value boundary past upper boundary', async () => {
55+
slider.value = [10, 10];
56+
await sendKeys({ press: 'ArrowRight' });
57+
expect(slider.value).to.deep.equal([10, 10]);
58+
});
59+
60+
it('should suppress input when trying to increase value past upper boundary on Arrow Right', async () => {
61+
const spy = sinon.spy();
62+
inputs[0].addEventListener('input', spy);
63+
64+
slider.value = [10, 10];
65+
66+
await sendKeys({ press: 'ArrowRight' });
67+
expect(spy).to.be.not.called;
68+
expect(inputs[0].value).to.equal('10');
69+
});
70+
71+
it('should suppress input when trying to increase value past upper boundary on Arrow Up', async () => {
72+
const spy = sinon.spy();
73+
inputs[0].addEventListener('input', spy);
74+
75+
slider.value = [10, 10];
76+
77+
await sendKeys({ press: 'ArrowUp' });
78+
expect(spy).to.be.not.called;
79+
expect(inputs[0].value).to.equal('10');
80+
});
81+
82+
it('should fire single change event on first input value change', async () => {
83+
const spy = sinon.spy();
84+
slider.addEventListener('change', spy);
85+
await sendKeys({ press: 'ArrowRight' });
86+
expect(spy).to.be.calledOnce;
87+
});
88+
89+
it('should fire single input event on first input value change', async () => {
90+
const spy = sinon.spy();
91+
slider.addEventListener('input', spy);
92+
await sendKeys({ press: 'ArrowRight' });
93+
expect(spy).to.be.calledOnce;
94+
});
95+
});
96+
97+
describe('end thumb', () => {
98+
beforeEach(() => {
99+
inputs[1].focus();
100+
});
101+
102+
it('should increase upper value boundary on second input Arrow Right', async () => {
103+
slider.value = [1, 50];
104+
await sendKeys({ press: 'ArrowRight' });
105+
expect(slider.value).to.deep.equal([1, 51]);
106+
});
107+
108+
it('should increase upper value boundary on second input Arrow Up', async () => {
109+
slider.value = [1, 50];
110+
await sendKeys({ press: 'ArrowUp' });
111+
expect(slider.value).to.deep.equal([1, 51]);
112+
});
113+
114+
it('should decrease upper value boundary on second input Arrow Left', async () => {
115+
await sendKeys({ press: 'ArrowLeft' });
116+
expect(slider.value).to.deep.equal([0, 99]);
117+
});
118+
119+
it('should decrease upper value boundary on second input Arrow Down', async () => {
120+
await sendKeys({ press: 'ArrowDown' });
121+
expect(slider.value).to.deep.equal([0, 99]);
122+
});
123+
124+
it('should not increase upper value boundary past max value', async () => {
125+
await sendKeys({ press: 'ArrowRight' });
126+
expect(slider.value).to.deep.equal([0, 100]);
127+
});
128+
129+
it('should not decrease lower value boundary past lower boundary', async () => {
130+
slider.value = [10, 10];
131+
await sendKeys({ press: 'ArrowLeft' });
132+
expect(slider.value).to.deep.equal([10, 10]);
133+
});
134+
135+
it('should suppress input when trying to decrease value past lower boundary on Arrow Left', async () => {
136+
const spy = sinon.spy();
137+
inputs[1].addEventListener('input', spy);
138+
139+
slider.value = [10, 10];
140+
141+
await sendKeys({ press: 'ArrowLeft' });
142+
expect(spy).to.be.not.called;
143+
expect(inputs[1].value).to.equal('10');
144+
});
145+
146+
it('should suppress input when trying to decrease value past lower boundary on Arrow Down', async () => {
147+
const spy = sinon.spy();
148+
inputs[1].addEventListener('input', spy);
149+
150+
slider.value = [10, 10];
151+
152+
await sendKeys({ press: 'ArrowDown' });
153+
expect(spy).to.be.not.called;
154+
expect(inputs[1].value).to.equal('10');
155+
});
156+
157+
it('should fire single change event on second input value change', async () => {
158+
const spy = sinon.spy();
159+
slider.addEventListener('change', spy);
160+
await sendKeys({ press: 'ArrowLeft' });
161+
expect(spy).to.be.calledOnce;
162+
});
163+
164+
it('should fire single input event on second input value change', async () => {
165+
const spy = sinon.spy();
166+
slider.addEventListener('input', spy);
167+
await sendKeys({ press: 'ArrowLeft' });
168+
expect(spy).to.be.calledOnce;
169+
});
170+
171+
it('should preserve decimals when changing value using fractional step', async () => {
172+
slider.value = [0, 3];
173+
slider.step = 1.5;
174+
await sendKeys({ press: 'ArrowLeft' });
175+
expect(slider.value).to.deep.equal([0, 1.5]);
176+
});
177+
178+
it('should not increase value past max allowed value with fractional step', async () => {
179+
slider.value = [0, 99];
180+
slider.step = 1.5;
181+
await sendKeys({ press: 'ArrowRight' });
182+
expect(slider.value).to.deep.equal([0, 99]);
183+
});
184+
185+
it('should not change value on arrow key when readonly', async () => {
186+
slider.readonly = true;
187+
await sendKeys({ press: 'ArrowRight' });
188+
expect(slider.value).to.deep.equal([0, 100]);
189+
});
190+
});
191+
});

packages/slider/test/range-slider-pointer.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,36 @@ window.Vaadin.featureFlags.sliderComponent = true;
149149
});
150150
});
151151

152+
describe('input event', () => {
153+
let spy: sinon.SinonSpy;
154+
155+
beforeEach(() => {
156+
spy = sinon.spy();
157+
slider.addEventListener('input', spy);
158+
});
159+
160+
it('should fire on thumb pointermove', async () => {
161+
const { x, y } = middleOfThumb(0);
162+
163+
await sendMouseToElement({ type: 'move', element: thumbs[0] });
164+
await sendMouse({ type: 'down' });
165+
await sendMouse({ type: 'move', position: [x + 20, y] });
166+
167+
expect(spy).to.be.calledOnce;
168+
});
169+
170+
it('should fire on track pointerdown', async () => {
171+
slider.value = [20, 80];
172+
173+
const { x, y } = middleOfThumb(0);
174+
175+
await sendMouse({ type: 'move', position: [x - 20, y] });
176+
await sendMouse({ type: 'down' });
177+
178+
expect(spy).to.be.calledOnce;
179+
});
180+
});
181+
152182
describe('track', () => {
153183
beforeEach(() => {
154184
slider.value = [20, 80];

0 commit comments

Comments
 (0)