mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
feat(range): ionChange will only emit from user committed changes (#26089)
This commit is contained in:
28
BREAKING.md
28
BREAKING.md
@ -103,21 +103,27 @@ Ionic now listens on the `keydown` event instead of the `keyup` event when deter
|
|||||||
|
|
||||||
<h4 id="version-7x-range">Range</h4>
|
<h4 id="version-7x-range">Range</h4>
|
||||||
|
|
||||||
Range is updated to align with the design specification for supported modes.
|
- Range is updated to align with the design specification for supported modes.
|
||||||
|
|
||||||
**Design tokens**
|
**Design tokens**
|
||||||
|
|
||||||
|
iOS:
|
||||||
|
|
||||||
iOS:
|
| Token | Previous Value | New Value |
|
||||||
|
| --------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||||
|
| `--bar-border-radius` | `0px` | `$range-ios-bar-border-radius` (`2px` default) |
|
||||||
|
| `--knob-size` | `28px` | `$range-ios-knob-width` (`26px` default) |
|
||||||
|
| `$range-ios-bar-height` | `2px` | `4px` |
|
||||||
|
| `$range-ios-bar-background-color` | `rgba(var(--ion-text-color-rgb, 0, 0, 0), .1)` | `var(--ion-color-step-900, #e6e6e6)` |
|
||||||
|
| `$range-ios-knob-box-shadow` | `0 3px 1px rgba(0, 0, 0, .1), 0 4px 8px rgba(0, 0, 0, .13), 0 0 0 1px rgba(0, 0, 0, .02)` | `0px 0.5px 4px rgba(0, 0, 0, 0.12), 0px 6px 13px rgba(0, 0, 0, 0.12)` |
|
||||||
|
| `$range-ios-knob-width` | `28px` | `26px` |
|
||||||
|
|
||||||
|Token|Previous Value|New Value|
|
- `ionChange` is no longer emitted when the `value` of `ion-range` is modified externally. `ionChange` is only emitted from user committed changes, such as dragging and releasing the range knob or selecting a new value with the keyboard arrows.
|
||||||
|-----|--------------|---------|
|
- If your application requires immediate feedback based on the user actively dragging the range knob, consider migrating your event listeners to using `ionInput` instead.
|
||||||
|`--bar-border-radius`|`0px`|`$range-ios-bar-border-radius` (`2px` default)|
|
|
||||||
|`--knob-size`|`28px`|`$range-ios-knob-width` (`26px` default)|
|
- The `debounce` property's value value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||||
|`$range-ios-bar-height`|`2px`|`4px`|
|
|
||||||
|`$range-ios-bar-background-color`|`rgba(var(--ion-text-color-rgb, 0, 0, 0), .1)`|`var(--ion-color-step-900, #e6e6e6)`|
|
- Range no longer clamps assigned values within bounds. Developers will need to validate the value they are assigning to `ion-range` is within the `min` and `max` bounds when programmatically assigning a value.
|
||||||
|`$range-ios-knob-box-shadow`|`0 3px 1px rgba(0, 0, 0, .1), 0 4px 8px rgba(0, 0, 0, .13), 0 0 0 1px rgba(0, 0, 0, .02)`|`0px 0.5px 4px rgba(0, 0, 0, 0.12), 0px 6px 13px rgba(0, 0, 0, 0.12)`|
|
|
||||||
|`$range-ios-knob-width`|`28px`|`26px`|
|
|
||||||
|
|
||||||
<h4 id="version-7x-searchbar">Searchbar</h4>
|
<h4 id="version-7x-searchbar">Searchbar</h4>
|
||||||
|
|
||||||
|
@ -1321,9 +1321,20 @@ import type { RangeKnobMoveStartEventDetail as IRangeRangeKnobMoveStartEventDeta
|
|||||||
import type { RangeKnobMoveEndEventDetail as IRangeRangeKnobMoveEndEventDetail } from '@ionic/core';
|
import type { RangeKnobMoveEndEventDetail as IRangeRangeKnobMoveEndEventDetail } from '@ionic/core';
|
||||||
export declare interface IonRange extends Components.IonRange {
|
export declare interface IonRange extends Components.IonRange {
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
ionChange: EventEmitter<CustomEvent<IRangeRangeChangeEventDetail>>;
|
ionChange: EventEmitter<CustomEvent<IRangeRangeChangeEventDetail>>;
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
ionInput: EventEmitter<CustomEvent<IRangeRangeChangeEventDetail>>;
|
||||||
/**
|
/**
|
||||||
* Emitted when the range has focus.
|
* Emitted when the range has focus.
|
||||||
*/
|
*/
|
||||||
@ -1360,7 +1371,7 @@ export class IonRange {
|
|||||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||||
c.detach();
|
c.detach();
|
||||||
this.el = r.nativeElement;
|
this.el = r.nativeElement;
|
||||||
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
|
proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ export class FormComponent {
|
|||||||
input: ['', Validators.required],
|
input: ['', Validators.required],
|
||||||
input2: ['Default Value'],
|
input2: ['Default Value'],
|
||||||
checkbox: [false],
|
checkbox: [false],
|
||||||
range: [5, Validators.min(10)],
|
|
||||||
}, {
|
}, {
|
||||||
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
|
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
|
||||||
});
|
});
|
||||||
@ -41,8 +40,7 @@ export class FormComponent {
|
|||||||
toggle: true,
|
toggle: true,
|
||||||
input: 'Some value',
|
input: 'Some value',
|
||||||
input2: 'Another values',
|
input2: 'Another values',
|
||||||
checkbox: true,
|
checkbox: true
|
||||||
range: 50
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ export class FormComponent {
|
|||||||
input: ['', Validators.required],
|
input: ['', Validators.required],
|
||||||
input2: ['Default Value'],
|
input2: ['Default Value'],
|
||||||
checkbox: [false],
|
checkbox: [false],
|
||||||
range: [5, Validators.min(10)],
|
|
||||||
}, {
|
}, {
|
||||||
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
|
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
|
||||||
});
|
});
|
||||||
@ -41,8 +40,7 @@ export class FormComponent {
|
|||||||
toggle: true,
|
toggle: true,
|
||||||
input: 'Some value',
|
input: 'Some value',
|
||||||
input2: 'Another values',
|
input2: 'Another values',
|
||||||
checkbox: true,
|
checkbox: true
|
||||||
range: 50
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
32
angular/test/base/e2e/src/form-controls/range.spec.ts
Normal file
32
angular/test/base/e2e/src/form-controls/range.spec.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -30,8 +30,7 @@ describe('Form', () => {
|
|||||||
toggle: false,
|
toggle: false,
|
||||||
input: '',
|
input: '',
|
||||||
input2: 'Default Value',
|
input2: 'Default Value',
|
||||||
checkbox: false,
|
checkbox: false
|
||||||
range: 5
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,9 +50,6 @@ describe('Form', () => {
|
|||||||
// Click confirm button
|
// Click confirm button
|
||||||
cy.get('ion-alert .alert-button:not(.alert-button-role-cancel)').click();
|
cy.get('ion-alert .alert-button:not(.alert-button-role-cancel)').click();
|
||||||
|
|
||||||
testStatus('INVALID');
|
|
||||||
|
|
||||||
cy.get('ion-range').invoke('prop', 'value', 40);
|
|
||||||
testStatus('VALID');
|
testStatus('VALID');
|
||||||
|
|
||||||
testData({
|
testData({
|
||||||
@ -62,8 +58,7 @@ describe('Form', () => {
|
|||||||
toggle: false,
|
toggle: false,
|
||||||
input: 'Some value',
|
input: 'Some value',
|
||||||
input2: 'Default Value',
|
input2: 'Default Value',
|
||||||
checkbox: false,
|
checkbox: false
|
||||||
range: 40
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -75,8 +70,7 @@ describe('Form', () => {
|
|||||||
toggle: true,
|
toggle: true,
|
||||||
input: '',
|
input: '',
|
||||||
input2: 'Default Value',
|
input2: 'Default Value',
|
||||||
checkbox: false,
|
checkbox: false
|
||||||
range: 5
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,8 +82,7 @@ describe('Form', () => {
|
|||||||
toggle: false,
|
toggle: false,
|
||||||
input: '',
|
input: '',
|
||||||
input2: 'Default Value',
|
input2: 'Default Value',
|
||||||
checkbox: true,
|
checkbox: true
|
||||||
range: 5
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -109,8 +102,7 @@ describe('Form', () => {
|
|||||||
toggle: true,
|
toggle: true,
|
||||||
input: '',
|
input: '',
|
||||||
input2: 'Default Value',
|
input2: 'Default Value',
|
||||||
checkbox: false,
|
checkbox: false
|
||||||
range: 5
|
|
||||||
});
|
});
|
||||||
cy.get('ion-checkbox').click();
|
cy.get('ion-checkbox').click();
|
||||||
testData({
|
testData({
|
||||||
@ -119,8 +111,7 @@ describe('Form', () => {
|
|||||||
toggle: true,
|
toggle: true,
|
||||||
input: '',
|
input: '',
|
||||||
input2: 'Default Value',
|
input2: 'Default Value',
|
||||||
checkbox: true,
|
checkbox: true
|
||||||
range: 5
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,6 @@ describe('Inputs', () => {
|
|||||||
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
|
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-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
|
||||||
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
|
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', () => {
|
it('should have reset value', () => {
|
||||||
@ -20,7 +19,6 @@ describe('Inputs', () => {
|
|||||||
cy.get('ion-input').should('have.prop', 'value').and('equal', '');
|
cy.get('ion-input').should('have.prop', 'value').and('equal', '');
|
||||||
cy.get('ion-datetime').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-select').should('have.prop', 'value').and('equal', '');
|
||||||
cy.get('ion-range').should('have.prop', 'value').and('be.NaN');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get some value', () => {
|
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-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-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
|
||||||
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
|
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', () => {
|
it('change values should update angular', () => {
|
||||||
@ -54,19 +51,10 @@ describe('Inputs', () => {
|
|||||||
// Click confirm button
|
// Click confirm button
|
||||||
cy.get('ion-alert .alert-button:not(.alert-button-role-cancel)').click();
|
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('#checkbox-note').should('have.text', 'true');
|
||||||
cy.get('#toggle-note').should('have.text', 'true');
|
cy.get('#toggle-note').should('have.text', 'true');
|
||||||
cy.get('#input-note').should('have.text', 'hola');
|
cy.get('#input-note').should('have.text', 'hola');
|
||||||
cy.get('#datetime-note').should('have.text', '1994-03-14');
|
cy.get('#datetime-note').should('have.text', '1994-03-14');
|
||||||
cy.get('#select-note').should('have.text', 'ps');
|
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');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
@ -69,6 +69,10 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'form-controls/range',
|
||||||
|
loadChildren: () => import('./form-controls/range/range.module').then(m => m.RangeModule)
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -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 { }
|
@ -0,0 +1,16 @@
|
|||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Range</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Range</ion-label>
|
||||||
|
<ion-range formControlName="range" min="0" max="20"></ion-range>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button type="submit">Submit</ion-button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
@ -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)]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 { }
|
@ -51,11 +51,6 @@
|
|||||||
<ion-checkbox formControlName="checkbox" slot="start"></ion-checkbox>
|
<ion-checkbox formControlName="checkbox" slot="start"></ion-checkbox>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item>
|
|
||||||
<ion-label>Range</ion-label>
|
|
||||||
<ion-range formControlName="range"></ion-range>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<p>
|
<p>
|
||||||
Form Status: <span id="status">{{ profileForm.status }}</span>
|
Form Status: <span id="status">{{ profileForm.status }}</span>
|
||||||
|
@ -18,8 +18,7 @@ export class FormComponent {
|
|||||||
toggle: [false],
|
toggle: [false],
|
||||||
input: ['', Validators.required],
|
input: ['', Validators.required],
|
||||||
input2: ['Default Value'],
|
input2: ['Default Value'],
|
||||||
checkbox: [false],
|
checkbox: [false]
|
||||||
range: [5, Validators.min(10)],
|
|
||||||
}, {
|
}, {
|
||||||
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
|
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
|
||||||
});
|
});
|
||||||
@ -41,8 +40,7 @@ export class FormComponent {
|
|||||||
toggle: true,
|
toggle: true,
|
||||||
input: 'Some value',
|
input: 'Some value',
|
||||||
input2: 'Another values',
|
input2: 'Another values',
|
||||||
checkbox: true,
|
checkbox: true
|
||||||
range: 50
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,20 +85,6 @@
|
|||||||
<ion-note slot="end">{{checkbox}}</ion-note>
|
<ion-note slot="end">{{checkbox}}</ion-note>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item>
|
|
||||||
<ion-label>Range</ion-label>
|
|
||||||
<ion-range [(ngModel)]="range"></ion-range>
|
|
||||||
<ion-note slot="end" id="range-note">{{range}}</ion-note>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item color="dark">
|
|
||||||
<ion-label>Range Mirror</ion-label>
|
|
||||||
<ion-range [(ngModel)]="range">
|
|
||||||
<ion-toggle slot="start" id="nested-toggle" [(ngModel)]="toggle"></ion-toggle>
|
|
||||||
</ion-range>
|
|
||||||
<ion-note slot="end">{{range}}</ion-note>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<p>
|
<p>
|
||||||
<ion-button (click)="setValues()" id="set-button">Set values</ion-button>
|
<ion-button (click)="setValues()" id="set-button">Set values</ion-button>
|
||||||
|
@ -6,12 +6,11 @@ import { Component } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class InputsComponent {
|
export class InputsComponent {
|
||||||
|
|
||||||
datetime = '1994-03-15';
|
datetime? = '1994-03-15';
|
||||||
input = 'some text';
|
input? = 'some text';
|
||||||
checkbox = true;
|
checkbox = true;
|
||||||
toggle = true;
|
toggle = true;
|
||||||
select = 'nes';
|
select? = 'nes';
|
||||||
range = 10;
|
|
||||||
changes = 0;
|
changes = 0;
|
||||||
|
|
||||||
setValues() {
|
setValues() {
|
||||||
@ -21,7 +20,6 @@ export class InputsComponent {
|
|||||||
this.checkbox = true;
|
this.checkbox = true;
|
||||||
this.toggle = true;
|
this.toggle = true;
|
||||||
this.select = 'nes';
|
this.select = 'nes';
|
||||||
this.range = 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetValues() {
|
resetValues() {
|
||||||
@ -31,8 +29,8 @@ export class InputsComponent {
|
|||||||
this.checkbox = false;
|
this.checkbox = false;
|
||||||
this.toggle = false;
|
this.toggle = false;
|
||||||
this.select = undefined;
|
this.select = undefined;
|
||||||
this.range = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
counter() {
|
counter() {
|
||||||
this.changes++;
|
this.changes++;
|
||||||
return Math.floor(this.changes / 2);
|
return Math.floor(this.changes / 2);
|
||||||
|
@ -982,7 +982,7 @@ ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
|||||||
ion-range,shadow
|
ion-range,shadow
|
||||||
ion-range,prop,activeBarStart,number | undefined,undefined,false,false
|
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,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,disabled,boolean,false,false,false
|
||||||
ion-range,prop,dualKnobs,boolean,false,false,false
|
ion-range,prop,dualKnobs,boolean,false,false,false
|
||||||
ion-range,prop,max,number,100,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,ionBlur,void,true
|
||||||
ion-range,event,ionChange,RangeChangeEventDetail,true
|
ion-range,event,ionChange,RangeChangeEventDetail,true
|
||||||
ion-range,event,ionFocus,void,true
|
ion-range,event,ionFocus,void,true
|
||||||
|
ion-range,event,ionInput,RangeChangeEventDetail,true
|
||||||
ion-range,event,ionKnobMoveEnd,RangeKnobMoveEndEventDetail,true
|
ion-range,event,ionKnobMoveEnd,RangeKnobMoveEndEventDetail,true
|
||||||
ion-range,event,ionKnobMoveStart,RangeKnobMoveStartEventDetail,true
|
ion-range,event,ionKnobMoveStart,RangeKnobMoveStartEventDetail,true
|
||||||
ion-range,css-prop,--bar-background
|
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;
|
"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.
|
* If `true`, the user cannot interact with the range.
|
||||||
*/
|
*/
|
||||||
@ -5843,7 +5843,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"color"?: Color;
|
"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;
|
||||||
/**
|
/**
|
||||||
@ -5875,13 +5875,17 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonBlur"?: (event: IonRangeCustomEvent<void>) => void;
|
"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;
|
"onIonChange"?: (event: IonRangeCustomEvent<RangeChangeEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
* Emitted when the range has focus.
|
* Emitted when the range has focus.
|
||||||
*/
|
*/
|
||||||
"onIonFocus"?: (event: IonRangeCustomEvent<void>) => void;
|
"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.
|
* 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 inheritedAttributes: Attributes = {};
|
||||||
private contentEl: HTMLElement | null = null;
|
private contentEl: HTMLElement | null = null;
|
||||||
private initialContentScrollY = true;
|
private initialContentScrollY = true;
|
||||||
|
private originalIonInput?: EventEmitter<RangeChangeEventDetail>;
|
||||||
|
|
||||||
@Element() el!: HTMLIonRangeElement;
|
@Element() el!: HTMLIonRangeElement;
|
||||||
|
|
||||||
@ -70,14 +71,18 @@ export class Range implements ComponentInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* How long, in milliseconds, to wait to trigger the
|
* How long, in milliseconds, to wait to trigger the
|
||||||
* `ionChange` event after each change in the range value.
|
* `ionInput` event after each change in the range value.
|
||||||
* This also impacts form bindings such as `ngModel` or `v-model`.
|
|
||||||
*/
|
*/
|
||||||
@Prop() debounce = 0;
|
@Prop() debounce?: number;
|
||||||
|
|
||||||
@Watch('debounce')
|
@Watch('debounce')
|
||||||
protected debounceChanged() {
|
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.
|
// 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;
|
@Prop({ mutable: true }) value: RangeValue = 0;
|
||||||
@Watch('value')
|
@Watch('value')
|
||||||
protected valueChanged(value: RangeValue) {
|
protected valueChanged() {
|
||||||
if (!this.noUpdate) {
|
if (!this.noUpdate) {
|
||||||
this.updateRatio();
|
this.updateRatio();
|
||||||
}
|
}
|
||||||
|
|
||||||
value = this.ensureValueInBounds(value);
|
|
||||||
|
|
||||||
this.ionChange.emit({ value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private clampBounds = (value: any): number => {
|
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>;
|
@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.
|
* Emitted when the styles change.
|
||||||
* @internal
|
* @internal
|
||||||
@ -270,6 +283,7 @@ export class Range implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidLoad() {
|
componentDidLoad() {
|
||||||
|
this.originalIonInput = this.ionInput;
|
||||||
this.setupGesture();
|
this.setupGesture();
|
||||||
this.didLoad = true;
|
this.didLoad = true;
|
||||||
}
|
}
|
||||||
@ -317,6 +331,7 @@ export class Range implements ComponentInterface {
|
|||||||
|
|
||||||
this.ionKnobMoveStart.emit({ value: ensureValueInBounds(this.value) });
|
this.ionKnobMoveStart.emit({ value: ensureValueInBounds(this.value) });
|
||||||
this.updateValue();
|
this.updateValue();
|
||||||
|
this.emitValueChange();
|
||||||
this.ionKnobMoveEnd.emit({ value: ensureValueInBounds(this.value) });
|
this.ionKnobMoveEnd.emit({ value: ensureValueInBounds(this.value) });
|
||||||
};
|
};
|
||||||
private getValue(): RangeValue {
|
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) {
|
private onStart(detail: GestureDetail) {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
@ -382,6 +408,7 @@ export class Range implements ComponentInterface {
|
|||||||
this.update(detail.currentX);
|
this.update(detail.currentX);
|
||||||
this.pressedKnob = undefined;
|
this.pressedKnob = undefined;
|
||||||
|
|
||||||
|
this.emitValueChange();
|
||||||
this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
|
this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,6 +485,8 @@ export class Range implements ComponentInterface {
|
|||||||
upper: Math.max(valA, valB),
|
upper: Math.max(valA, valB),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.ionInput.emit({ value: this.value });
|
||||||
|
|
||||||
this.noUpdate = false;
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -601,6 +601,7 @@ export const IonRange = /*@__PURE__*/ defineContainer<JSX.IonRange>('ion-range',
|
|||||||
'disabled',
|
'disabled',
|
||||||
'value',
|
'value',
|
||||||
'ionChange',
|
'ionChange',
|
||||||
|
'ionInput',
|
||||||
'ionStyle',
|
'ionStyle',
|
||||||
'ionFocus',
|
'ionFocus',
|
||||||
'ionBlur',
|
'ionBlur',
|
||||||
|
Reference in New Issue
Block a user