mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 07:41:51 +08:00
feat(range): ionChange will only emit from user committed changes (#26089)
This commit is contained in:
@ -982,7 +982,7 @@ ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
||||
ion-range,shadow
|
||||
ion-range,prop,activeBarStart,number | undefined,undefined,false,false
|
||||
ion-range,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
|
||||
ion-range,prop,debounce,number,0,false,false
|
||||
ion-range,prop,debounce,number | undefined,undefined,false,false
|
||||
ion-range,prop,disabled,boolean,false,false,false
|
||||
ion-range,prop,dualKnobs,boolean,false,false,false
|
||||
ion-range,prop,max,number,100,false,false
|
||||
@ -998,6 +998,7 @@ ion-range,prop,value,number | { lower: number; upper: number; },0,false,false
|
||||
ion-range,event,ionBlur,void,true
|
||||
ion-range,event,ionChange,RangeChangeEventDetail,true
|
||||
ion-range,event,ionFocus,void,true
|
||||
ion-range,event,ionInput,RangeChangeEventDetail,true
|
||||
ion-range,event,ionKnobMoveEnd,RangeKnobMoveEndEventDetail,true
|
||||
ion-range,event,ionKnobMoveStart,RangeKnobMoveStartEventDetail,true
|
||||
ion-range,css-prop,--bar-background
|
||||
|
||||
12
core/src/components.d.ts
vendored
12
core/src/components.d.ts
vendored
@ -2109,9 +2109,9 @@ export namespace Components {
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* How long, in milliseconds, to wait to trigger the `ionChange` event after each change in the range value. This also impacts form bindings such as `ngModel` or `v-model`.
|
||||
* How long, in milliseconds, to wait to trigger the `ionInput` event after each change in the range value.
|
||||
*/
|
||||
"debounce": number;
|
||||
"debounce"?: number;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the range.
|
||||
*/
|
||||
@ -5843,7 +5843,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* How long, in milliseconds, to wait to trigger the `ionChange` event after each change in the range value. This also impacts form bindings such as `ngModel` or `v-model`.
|
||||
* How long, in milliseconds, to wait to trigger the `ionInput` event after each change in the range value.
|
||||
*/
|
||||
"debounce"?: number;
|
||||
/**
|
||||
@ -5875,13 +5875,17 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"onIonBlur"?: (event: IonRangeCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted when the value property has changed.
|
||||
* The `ionChange` event is fired for `<ion-range>` elements when the user modifies the element's value: - When the user releases the knob after dragging; - When the user moves the knob with keyboard arrows `ionChange` is not fired when the value is changed programmatically.
|
||||
*/
|
||||
"onIonChange"?: (event: IonRangeCustomEvent<RangeChangeEventDetail>) => void;
|
||||
/**
|
||||
* Emitted when the range has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: IonRangeCustomEvent<void>) => void;
|
||||
/**
|
||||
* The `ionInput` event is fired for `<ion-range>` elements when the value is modified. Unlike `ionChange`, `ionInput` is fired continuously while the user is dragging the knob.
|
||||
*/
|
||||
"onIonInput"?: (event: IonRangeCustomEvent<RangeChangeEventDetail>) => void;
|
||||
/**
|
||||
* Emitted when the user finishes moving the range knob, whether through mouse drag, touch gesture, or keyboard interaction.
|
||||
*/
|
||||
|
||||
@ -54,6 +54,7 @@ export class Range implements ComponentInterface {
|
||||
private inheritedAttributes: Attributes = {};
|
||||
private contentEl: HTMLElement | null = null;
|
||||
private initialContentScrollY = true;
|
||||
private originalIonInput?: EventEmitter<RangeChangeEventDetail>;
|
||||
|
||||
@Element() el!: HTMLIonRangeElement;
|
||||
|
||||
@ -70,14 +71,18 @@ export class Range implements ComponentInterface {
|
||||
|
||||
/**
|
||||
* How long, in milliseconds, to wait to trigger the
|
||||
* `ionChange` event after each change in the range value.
|
||||
* This also impacts form bindings such as `ngModel` or `v-model`.
|
||||
* `ionInput` event after each change in the range value.
|
||||
*/
|
||||
@Prop() debounce = 0;
|
||||
@Prop() debounce?: number;
|
||||
|
||||
@Watch('debounce')
|
||||
protected debounceChanged() {
|
||||
this.ionChange = debounceEvent(this.ionChange, this.debounce);
|
||||
const { ionInput, debounce, originalIonInput } = this;
|
||||
/**
|
||||
* If debounce is undefined, we have to manually revert the ionInput emitter in case
|
||||
* debounce used to be set to a number. Otherwise, the event would stay debounced.
|
||||
*/
|
||||
this.ionInput = debounce === undefined ? originalIonInput ?? ionInput : debounceEvent(ionInput, debounce);
|
||||
}
|
||||
|
||||
// TODO: In Ionic Framework v6 this should initialize to this.rangeId like the other form components do.
|
||||
@ -185,14 +190,10 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop({ mutable: true }) value: RangeValue = 0;
|
||||
@Watch('value')
|
||||
protected valueChanged(value: RangeValue) {
|
||||
protected valueChanged() {
|
||||
if (!this.noUpdate) {
|
||||
this.updateRatio();
|
||||
}
|
||||
|
||||
value = this.ensureValueInBounds(value);
|
||||
|
||||
this.ionChange.emit({ value });
|
||||
}
|
||||
|
||||
private clampBounds = (value: any): number => {
|
||||
@ -211,10 +212,22 @@ export class Range implements ComponentInterface {
|
||||
};
|
||||
|
||||
/**
|
||||
* Emitted when the value property has changed.
|
||||
* The `ionChange` event is fired for `<ion-range>` elements when the user
|
||||
* modifies the element's value:
|
||||
* - When the user releases the knob after dragging;
|
||||
* - When the user moves the knob with keyboard arrows
|
||||
*
|
||||
* `ionChange` is not fired when the value is changed programmatically.
|
||||
*/
|
||||
@Event() ionChange!: EventEmitter<RangeChangeEventDetail>;
|
||||
|
||||
/**
|
||||
* The `ionInput` event is fired for `<ion-range>` elements when the value
|
||||
* is modified. Unlike `ionChange`, `ionInput` is fired continuously
|
||||
* while the user is dragging the knob.
|
||||
*/
|
||||
@Event() ionInput!: EventEmitter<RangeChangeEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted when the styles change.
|
||||
* @internal
|
||||
@ -270,6 +283,7 @@ export class Range implements ComponentInterface {
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.originalIonInput = this.ionInput;
|
||||
this.setupGesture();
|
||||
this.didLoad = true;
|
||||
}
|
||||
@ -317,6 +331,7 @@ export class Range implements ComponentInterface {
|
||||
|
||||
this.ionKnobMoveStart.emit({ value: ensureValueInBounds(this.value) });
|
||||
this.updateValue();
|
||||
this.emitValueChange();
|
||||
this.ionKnobMoveEnd.emit({ value: ensureValueInBounds(this.value) });
|
||||
};
|
||||
private getValue(): RangeValue {
|
||||
@ -344,6 +359,17 @@ export class Range implements ComponentInterface {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an `ionChange` event.
|
||||
*
|
||||
* This API should be called for user committed changes.
|
||||
* This API should not be used for external value changes.
|
||||
*/
|
||||
private emitValueChange() {
|
||||
this.value = this.ensureValueInBounds(this.value);
|
||||
this.ionChange.emit({ value: this.value });
|
||||
}
|
||||
|
||||
private onStart(detail: GestureDetail) {
|
||||
const { contentEl } = this;
|
||||
if (contentEl) {
|
||||
@ -382,6 +408,7 @@ export class Range implements ComponentInterface {
|
||||
this.update(detail.currentX);
|
||||
this.pressedKnob = undefined;
|
||||
|
||||
this.emitValueChange();
|
||||
this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
|
||||
}
|
||||
|
||||
@ -458,6 +485,8 @@ export class Range implements ComponentInterface {
|
||||
upper: Math.max(valA, valB),
|
||||
};
|
||||
|
||||
this.ionInput.emit({ value: this.value });
|
||||
|
||||
this.noUpdate = false;
|
||||
}
|
||||
|
||||
|
||||
131
core/src/components/range/test/range-events.e2e.ts
Normal file
131
core/src/components/range/test/range-events.e2e.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('range: events:', () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('md');
|
||||
});
|
||||
|
||||
test.describe(' ionChange', () => {
|
||||
test('should not emit if the value is set programmatically', async ({ page }) => {
|
||||
await page.setContent(`<ion-range></ion-range>`);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await range.evaluate((el: HTMLIonRangeElement) => {
|
||||
el.value = 50;
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
|
||||
|
||||
// Update the value again to make sure it doesn't emit a second time
|
||||
await range.evaluate((el: HTMLIonRangeElement) => {
|
||||
el.value = 60;
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
|
||||
});
|
||||
|
||||
test('should emit when the knob is released', async ({ page }) => {
|
||||
await page.setContent(`<ion-range></ion-range>`);
|
||||
|
||||
const rangeHandle = page.locator('ion-range .range-knob-handle');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
const boundingBox = await rangeHandle.boundingBox();
|
||||
|
||||
await rangeHandle.hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(boundingBox!.x + 100, boundingBox!.y);
|
||||
|
||||
await page.mouse.up();
|
||||
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
|
||||
});
|
||||
|
||||
test('should emit when the knob is moved with the keyboard', async ({ page }) => {
|
||||
await page.setContent(`<ion-range value="50"></ion-range>`);
|
||||
|
||||
const rangeHandle = page.locator('ion-range .range-knob-handle');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await rangeHandle.click();
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 49 });
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 50 });
|
||||
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 51 });
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 50 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('ionInput', () => {
|
||||
test('should emit when the knob is dragged', async ({ page }) => {
|
||||
await page.setContent(`<ion-range></ion-range>`);
|
||||
|
||||
const rangeHandle = page.locator('ion-range .range-knob-handle');
|
||||
const ionInputSpy = await page.spyOnEvent('ionInput');
|
||||
|
||||
const boundingBox = await rangeHandle.boundingBox();
|
||||
|
||||
await rangeHandle.hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(boundingBox!.x + 100, boundingBox!.y);
|
||||
|
||||
await ionInputSpy.next();
|
||||
|
||||
expect(ionInputSpy).toHaveReceivedEvent();
|
||||
});
|
||||
|
||||
test('should emit when the knob is moved with the keyboard', async ({ page }) => {
|
||||
await page.setContent(`<ion-range value="50"></ion-range>`);
|
||||
|
||||
const rangeHandle = page.locator('ion-range .range-knob-handle');
|
||||
const ionInputSpy = await page.spyOnEvent('ionInput');
|
||||
|
||||
await rangeHandle.click();
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await ionInputSpy.next();
|
||||
|
||||
expect(ionInputSpy).toHaveReceivedEventDetail({ value: 49 });
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await ionInputSpy.next();
|
||||
|
||||
expect(ionInputSpy).toHaveReceivedEventDetail({ value: 50 });
|
||||
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await ionInputSpy.next();
|
||||
|
||||
expect(ionInputSpy).toHaveReceivedEventDetail({ value: 51 });
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await ionInputSpy.next();
|
||||
|
||||
expect(ionInputSpy).toHaveReceivedEventDetail({ value: 50 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user