>;
/**
* Emitted when the range has focus.
*/
@@ -1360,7 +1371,7 @@ export class IonRange {
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
- proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
+ proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
}
}
diff --git a/angular/test/apps/ng12/src/app/form/form.component.ts b/angular/test/apps/ng12/src/app/form/form.component.ts
index 157bc9a1a8..beecc614db 100644
--- a/angular/test/apps/ng12/src/app/form/form.component.ts
+++ b/angular/test/apps/ng12/src/app/form/form.component.ts
@@ -19,7 +19,6 @@ export class FormComponent {
input: ['', Validators.required],
input2: ['Default Value'],
checkbox: [false],
- range: [5, Validators.min(10)],
}, {
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
});
@@ -41,8 +40,7 @@ export class FormComponent {
toggle: true,
input: 'Some value',
input2: 'Another values',
- checkbox: true,
- range: 50
+ checkbox: true
});
}
diff --git a/angular/test/apps/ng13/src/app/form/form.component.ts b/angular/test/apps/ng13/src/app/form/form.component.ts
index 157bc9a1a8..beecc614db 100644
--- a/angular/test/apps/ng13/src/app/form/form.component.ts
+++ b/angular/test/apps/ng13/src/app/form/form.component.ts
@@ -19,7 +19,6 @@ export class FormComponent {
input: ['', Validators.required],
input2: ['Default Value'],
checkbox: [false],
- range: [5, Validators.min(10)],
}, {
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
});
@@ -41,8 +40,7 @@ export class FormComponent {
toggle: true,
input: 'Some value',
input2: 'Another values',
- checkbox: true,
- range: 50
+ checkbox: true
});
}
diff --git a/angular/test/base/e2e/src/form-controls/range.spec.ts b/angular/test/base/e2e/src/form-controls/range.spec.ts
new file mode 100644
index 0000000000..386ba7ac60
--- /dev/null
+++ b/angular/test/base/e2e/src/form-controls/range.spec.ts
@@ -0,0 +1,32 @@
+describe('Form Controls: Range', () => {
+
+ beforeEach(() => {
+ cy.visit('/form-controls/range');
+ });
+
+ it('should have form control initial value', () => {
+ // Cypress does not support checking numeric values of custom elements
+ // see: https://github.com/cypress-io/cypress/blob/bf6560691436a5a953f7e03e0ea3de38f3d2a632/packages/driver/src/dom/elements/elementHelpers.ts#L7
+ cy.get('ion-range').invoke('prop', 'value').should('eq', 5);
+ });
+
+ it('should reflect Ionic form control status classes', () => {
+ // Control is initially invalid
+ cy.get('ion-range').should('have.class', 'ion-invalid');
+ cy.get('ion-range').should('have.class', 'ion-pristine');
+ cy.get('ion-range').should('have.class', 'ion-untouched');
+
+ // Cypress does not support typing unless the element is focusable.
+ cy.get('ion-range').shadow()
+ .find('.range-knob-handle')
+ .click()
+ .focus()
+ .type('{rightarrow}'.repeat(5));
+
+ cy.get('ion-range').should('have.class', 'ion-valid');
+ cy.get('ion-range').should('have.class', 'ion-dirty');
+ cy.get('ion-range').should('have.class', 'ion-touched');
+ cy.get('ion-range').invoke('prop', 'value').should('eq', 10);
+ });
+
+});
diff --git a/angular/test/base/e2e/src/form.spec.ts b/angular/test/base/e2e/src/form.spec.ts
index 6fe74df9f0..bb55004ae1 100644
--- a/angular/test/base/e2e/src/form.spec.ts
+++ b/angular/test/base/e2e/src/form.spec.ts
@@ -30,8 +30,7 @@ describe('Form', () => {
toggle: false,
input: '',
input2: 'Default Value',
- checkbox: false,
- range: 5
+ checkbox: false
});
});
@@ -51,9 +50,6 @@ describe('Form', () => {
// Click confirm button
cy.get('ion-alert .alert-button:not(.alert-button-role-cancel)').click();
- testStatus('INVALID');
-
- cy.get('ion-range').invoke('prop', 'value', 40);
testStatus('VALID');
testData({
@@ -62,8 +58,7 @@ describe('Form', () => {
toggle: false,
input: 'Some value',
input2: 'Default Value',
- checkbox: false,
- range: 40
+ checkbox: false
});
});
@@ -75,8 +70,7 @@ describe('Form', () => {
toggle: true,
input: '',
input2: 'Default Value',
- checkbox: false,
- range: 5
+ checkbox: false
});
});
@@ -88,8 +82,7 @@ describe('Form', () => {
toggle: false,
input: '',
input2: 'Default Value',
- checkbox: true,
- range: 5
+ checkbox: true
});
});
@@ -109,8 +102,7 @@ describe('Form', () => {
toggle: true,
input: '',
input2: 'Default Value',
- checkbox: false,
- range: 5
+ checkbox: false
});
cy.get('ion-checkbox').click();
testData({
@@ -119,8 +111,7 @@ describe('Form', () => {
toggle: true,
input: '',
input2: 'Default Value',
- checkbox: true,
- range: 5
+ checkbox: true
});
});
});
diff --git a/angular/test/base/e2e/src/inputs.spec.ts b/angular/test/base/e2e/src/inputs.spec.ts
index f921c98d82..873ae2374f 100644
--- a/angular/test/base/e2e/src/inputs.spec.ts
+++ b/angular/test/base/e2e/src/inputs.spec.ts
@@ -9,7 +9,6 @@ describe('Inputs', () => {
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
- cy.get('ion-range').should('have.prop', 'value').and('equal', 10);
});
it('should have reset value', () => {
@@ -20,7 +19,6 @@ describe('Inputs', () => {
cy.get('ion-input').should('have.prop', 'value').and('equal', '');
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '');
cy.get('ion-select').should('have.prop', 'value').and('equal', '');
- cy.get('ion-range').should('have.prop', 'value').and('be.NaN');
});
it('should get some value', () => {
@@ -32,7 +30,6 @@ describe('Inputs', () => {
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
- cy.get('ion-range').should('have.prop', 'value').and('equal', 10);
});
it('change values should update angular', () => {
@@ -54,19 +51,10 @@ describe('Inputs', () => {
// Click confirm button
cy.get('ion-alert .alert-button:not(.alert-button-role-cancel)').click();
- cy.get('ion-range').invoke('prop', 'value', 20);
-
cy.get('#checkbox-note').should('have.text', 'true');
cy.get('#toggle-note').should('have.text', 'true');
cy.get('#input-note').should('have.text', 'hola');
cy.get('#datetime-note').should('have.text', '1994-03-14');
cy.get('#select-note').should('have.text', 'ps');
- cy.get('#range-note').should('have.text', '20');
});
-
- it('nested components should not interfere with NgModel', () => {
- cy.get('#range-note').should('have.text', '10');
- cy.get('#nested-toggle').click();
- cy.get('#range-note').should('have.text', '10');
- });
-})
+});
diff --git a/angular/test/base/src/app/app-routing.module.ts b/angular/test/base/src/app/app-routing.module.ts
index d2e421c7a5..b6efd0f479 100644
--- a/angular/test/base/src/app/app-routing.module.ts
+++ b/angular/test/base/src/app/app-routing.module.ts
@@ -69,6 +69,10 @@ const routes: Routes = [
}
]
},
+ {
+ path: 'form-controls/range',
+ loadChildren: () => import('./form-controls/range/range.module').then(m => m.RangeModule)
+ }
];
@NgModule({
diff --git a/angular/test/base/src/app/form-controls/range/range-routing.module.ts b/angular/test/base/src/app/form-controls/range/range-routing.module.ts
new file mode 100644
index 0000000000..b42aaf651a
--- /dev/null
+++ b/angular/test/base/src/app/form-controls/range/range-routing.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { RangeComponent } from './range.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ { path: '', component: RangeComponent }
+ ])
+ ]
+})
+export class RangeRoutingModule { }
diff --git a/angular/test/base/src/app/form-controls/range/range.component.html b/angular/test/base/src/app/form-controls/range/range.component.html
new file mode 100644
index 0000000000..91c7df9d62
--- /dev/null
+++ b/angular/test/base/src/app/form-controls/range/range.component.html
@@ -0,0 +1,16 @@
+
+
+ Range
+
+
+
+
+
diff --git a/angular/test/base/src/app/form-controls/range/range.component.ts b/angular/test/base/src/app/form-controls/range/range.component.ts
new file mode 100644
index 0000000000..fa471e740e
--- /dev/null
+++ b/angular/test/base/src/app/form-controls/range/range.component.ts
@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+
+@Component({
+ selector: 'app-range',
+ templateUrl: './range.component.html'
+})
+export class RangeComponent {
+
+ form: FormGroup;
+
+ constructor(private fb: FormBuilder) {
+ this.form = this.fb.group({
+ range: [5, Validators.min(10)]
+ });
+ }
+
+}
diff --git a/angular/test/base/src/app/form-controls/range/range.module.ts b/angular/test/base/src/app/form-controls/range/range.module.ts
new file mode 100644
index 0000000000..a665a40653
--- /dev/null
+++ b/angular/test/base/src/app/form-controls/range/range.module.ts
@@ -0,0 +1,19 @@
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { IonicModule } from '@ionic/angular';
+
+import { RangeRoutingModule } from './range-routing.module';
+import { RangeComponent } from './range.component';
+
+@NgModule({
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ IonicModule,
+ RangeRoutingModule
+ ],
+ declarations: [
+ RangeComponent
+ ]
+})
+export class RangeModule { }
diff --git a/angular/test/base/src/app/form/form.component.html b/angular/test/base/src/app/form/form.component.html
index c9426e5fca..b7c73e4c32 100644
--- a/angular/test/base/src/app/form/form.component.html
+++ b/angular/test/base/src/app/form/form.component.html
@@ -51,11 +51,6 @@
-
- Range
-
-
-
Form Status: {{ profileForm.status }}
diff --git a/angular/test/base/src/app/form/form.component.ts b/angular/test/base/src/app/form/form.component.ts
index 65c6f5a4ce..0e9499d9bb 100644
--- a/angular/test/base/src/app/form/form.component.ts
+++ b/angular/test/base/src/app/form/form.component.ts
@@ -18,8 +18,7 @@ export class FormComponent {
toggle: [false],
input: ['', Validators.required],
input2: ['Default Value'],
- checkbox: [false],
- range: [5, Validators.min(10)],
+ checkbox: [false]
}, {
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
});
@@ -41,8 +40,7 @@ export class FormComponent {
toggle: true,
input: 'Some value',
input2: 'Another values',
- checkbox: true,
- range: 50
+ checkbox: true
});
}
diff --git a/angular/test/base/src/app/inputs/inputs.component.html b/angular/test/base/src/app/inputs/inputs.component.html
index 60f13a7b95..65391e5731 100644
--- a/angular/test/base/src/app/inputs/inputs.component.html
+++ b/angular/test/base/src/app/inputs/inputs.component.html
@@ -85,20 +85,6 @@
{{checkbox}}
-
- Range
-
- {{range}}
-
-
-
- Range Mirror
-
-
-
- {{range}}
-
-
Set values
diff --git a/angular/test/base/src/app/inputs/inputs.component.ts b/angular/test/base/src/app/inputs/inputs.component.ts
index 78f14765a0..abcbc6a8a0 100644
--- a/angular/test/base/src/app/inputs/inputs.component.ts
+++ b/angular/test/base/src/app/inputs/inputs.component.ts
@@ -6,12 +6,11 @@ import { Component } from '@angular/core';
})
export class InputsComponent {
- datetime = '1994-03-15';
- input = 'some text';
+ datetime? = '1994-03-15';
+ input? = 'some text';
checkbox = true;
toggle = true;
- select = 'nes';
- range = 10;
+ select? = 'nes';
changes = 0;
setValues() {
@@ -21,7 +20,6 @@ export class InputsComponent {
this.checkbox = true;
this.toggle = true;
this.select = 'nes';
- this.range = 10;
}
resetValues() {
@@ -31,8 +29,8 @@ export class InputsComponent {
this.checkbox = false;
this.toggle = false;
this.select = undefined;
- this.range = undefined;
}
+
counter() {
this.changes++;
return Math.floor(this.changes / 2);
diff --git a/core/api.txt b/core/api.txt
index 5494461da3..9a9ad389c9 100644
--- a/core/api.txt
+++ b/core/api.txt
@@ -982,7 +982,7 @@ ion-radio-group,event,ionChange,RadioGroupChangeEventDetail,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 | 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
diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index a282dd6f5d..ff0cf3c212 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -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;
/**
- * Emitted when the value property has changed.
+ * The `ionChange` event is fired for `` 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) => void;
/**
* Emitted when the range has focus.
*/
"onIonFocus"?: (event: IonRangeCustomEvent) => void;
+ /**
+ * The `ionInput` event is fired for `` elements when the value is modified. Unlike `ionChange`, `ionInput` is fired continuously while the user is dragging the knob.
+ */
+ "onIonInput"?: (event: IonRangeCustomEvent) => void;
/**
* Emitted when the user finishes moving the range knob, whether through mouse drag, touch gesture, or keyboard interaction.
*/
diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx
index 622cbea86c..f11c69a2dc 100644
--- a/core/src/components/range/range.tsx
+++ b/core/src/components/range/range.tsx
@@ -54,6 +54,7 @@ export class Range implements ComponentInterface {
private inheritedAttributes: Attributes = {};
private contentEl: HTMLElement | null = null;
private initialContentScrollY = true;
+ private originalIonInput?: EventEmitter;
@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 `` 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;
+ /**
+ * The `ionInput` event is fired for `` elements when the value
+ * is modified. Unlike `ionChange`, `ionInput` is fired continuously
+ * while the user is dragging the knob.
+ */
+ @Event() ionInput!: EventEmitter;
+
/**
* 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;
}
diff --git a/core/src/components/range/test/range-events.e2e.ts b/core/src/components/range/test/range-events.e2e.ts
new file mode 100644
index 0000000000..6513197f5b
--- /dev/null
+++ b/core/src/components/range/test/range-events.e2e.ts
@@ -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(``);
+
+ 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(``);
+
+ 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(``);
+
+ 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(``);
+
+ 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(``);
+
+ 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 });
+ });
+ });
+});
diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts
index 89ab65a6c8..38214f38e7 100644
--- a/packages/vue/src/proxies.ts
+++ b/packages/vue/src/proxies.ts
@@ -601,6 +601,7 @@ export const IonRange = /*@__PURE__*/ defineContainer('ion-range',
'disabled',
'value',
'ionChange',
+ 'ionInput',
'ionStyle',
'ionFocus',
'ionBlur',