Skip to content

Commit d1fb7b0

Browse files
authored
feat(range): ionChange will only emit from user committed changes (#26089)
1 parent 04ed860 commit d1fb7b0

File tree

21 files changed

+328
-91
lines changed

21 files changed

+328
-91
lines changed

BREAKING.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,21 +103,27 @@ Ionic now listens on the `keydown` event instead of the `keyup` event when deter
103103

104104
<h4 id="version-7x-range">Range</h4>
105105

106-
Range is updated to align with the design specification for supported modes.
106+
- Range is updated to align with the design specification for supported modes.
107107

108-
**Design tokens**
108+
**Design tokens**
109109

110+
iOS:
110111

111-
iOS:
112+
| Token | Previous Value | New Value |
113+
| --------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
114+
| `--bar-border-radius` | `0px` | `$range-ios-bar-border-radius` (`2px` default) |
115+
| `--knob-size` | `28px` | `$range-ios-knob-width` (`26px` default) |
116+
| `$range-ios-bar-height` | `2px` | `4px` |
117+
| `$range-ios-bar-background-color` | `rgba(var(--ion-text-color-rgb, 0, 0, 0), .1)` | `var(--ion-color-step-900, #e6e6e6)` |
118+
| `$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)` |
119+
| `$range-ios-knob-width` | `28px` | `26px` |
112120

113-
|Token|Previous Value|New Value|
114-
|-----|--------------|---------|
115-
|`--bar-border-radius`|`0px`|`$range-ios-bar-border-radius` (`2px` default)|
116-
|`--knob-size`|`28px`|`$range-ios-knob-width` (`26px` default)|
117-
|`$range-ios-bar-height`|`2px`|`4px`|
118-
|`$range-ios-bar-background-color`|`rgba(var(--ion-text-color-rgb, 0, 0, 0), .1)`|`var(--ion-color-step-900, #e6e6e6)`|
119-
|`$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)`|
120-
|`$range-ios-knob-width`|`28px`|`26px`|
121+
- `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.
122+
- If your application requires immediate feedback based on the user actively dragging the range knob, consider migrating your event listeners to using `ionInput` instead.
123+
124+
- The `debounce` property's value value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
125+
126+
- 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.
121127

122128
<h4 id="version-7x-searchbar">Searchbar</h4>
123129

angular/src/directives/proxies.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,9 +1321,20 @@ import type { RangeKnobMoveStartEventDetail as IRangeRangeKnobMoveStartEventDeta
13211321
import type { RangeKnobMoveEndEventDetail as IRangeRangeKnobMoveEndEventDetail } from '@ionic/core';
13221322
export declare interface IonRange extends Components.IonRange {
13231323
/**
1324-
* Emitted when the value property has changed.
1324+
* The `ionChange` event is fired for `<ion-range>` elements when the user
1325+
modifies the element's value:
1326+
- When the user releases the knob after dragging;
1327+
- When the user moves the knob with keyboard arrows
1328+
1329+
`ionChange` is not fired when the value is changed programmatically.
13251330
*/
13261331
ionChange: EventEmitter<CustomEvent<IRangeRangeChangeEventDetail>>;
1332+
/**
1333+
* The `ionInput` event is fired for `<ion-range>` elements when the value
1334+
is modified. Unlike `ionChange`, `ionInput` is fired continuously
1335+
while the user is dragging the knob.
1336+
*/
1337+
ionInput: EventEmitter<CustomEvent<IRangeRangeChangeEventDetail>>;
13271338
/**
13281339
* Emitted when the range has focus.
13291340
*/
@@ -1360,7 +1371,7 @@ export class IonRange {
13601371
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
13611372
c.detach();
13621373
this.el = r.nativeElement;
1363-
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
1374+
proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
13641375
}
13651376
}
13661377

angular/test/apps/ng12/src/app/form/form.component.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export class FormComponent {
1919
input: ['', Validators.required],
2020
input2: ['Default Value'],
2121
checkbox: [false],
22-
range: [5, Validators.min(10)],
2322
}, {
2423
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
2524
});
@@ -41,8 +40,7 @@ export class FormComponent {
4140
toggle: true,
4241
input: 'Some value',
4342
input2: 'Another values',
44-
checkbox: true,
45-
range: 50
43+
checkbox: true
4644
});
4745
}
4846

angular/test/apps/ng13/src/app/form/form.component.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export class FormComponent {
1919
input: ['', Validators.required],
2020
input2: ['Default Value'],
2121
checkbox: [false],
22-
range: [5, Validators.min(10)],
2322
}, {
2423
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'
2524
});
@@ -41,8 +40,7 @@ export class FormComponent {
4140
toggle: true,
4241
input: 'Some value',
4342
input2: 'Another values',
44-
checkbox: true,
45-
range: 50
43+
checkbox: true
4644
});
4745
}
4846

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
describe('Form Controls: Range', () => {
2+
3+
beforeEach(() => {
4+
cy.visit('/form-controls/range');
5+
});
6+
7+
it('should have form control initial value', () => {
8+
// Cypress does not support checking numeric values of custom elements
9+
// see: https://github.com/cypress-io/cypress/blob/bf6560691436a5a953f7e03e0ea3de38f3d2a632/packages/driver/src/dom/elements/elementHelpers.ts#L7
10+
cy.get('ion-range').invoke('prop', 'value').should('eq', 5);
11+
});
12+
13+
it('should reflect Ionic form control status classes', () => {
14+
// Control is initially invalid
15+
cy.get('ion-range').should('have.class', 'ion-invalid');
16+
cy.get('ion-range').should('have.class', 'ion-pristine');
17+
cy.get('ion-range').should('have.class', 'ion-untouched');
18+
19+
// Cypress does not support typing unless the element is focusable.
20+
cy.get('ion-range').shadow()
21+
.find('.range-knob-handle')
22+
.click()
23+
.focus()
24+
.type('{rightarrow}'.repeat(5));
25+
26+
cy.get('ion-range').should('have.class', 'ion-valid');
27+
cy.get('ion-range').should('have.class', 'ion-dirty');
28+
cy.get('ion-range').should('have.class', 'ion-touched');
29+
cy.get('ion-range').invoke('prop', 'value').should('eq', 10);
30+
});
31+
32+
});

angular/test/base/e2e/src/form.spec.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ describe('Form', () => {
3030
toggle: false,
3131
input: '',
3232
input2: 'Default Value',
33-
checkbox: false,
34-
range: 5
33+
checkbox: false
3534
});
3635
});
3736

@@ -51,9 +50,6 @@ describe('Form', () => {
5150
// Click confirm button
5251
cy.get('ion-alert .alert-button:not(.alert-button-role-cancel)').click();
5352

54-
testStatus('INVALID');
55-
56-
cy.get('ion-range').invoke('prop', 'value', 40);
5753
testStatus('VALID');
5854

5955
testData({
@@ -62,8 +58,7 @@ describe('Form', () => {
6258
toggle: false,
6359
input: 'Some value',
6460
input2: 'Default Value',
65-
checkbox: false,
66-
range: 40
61+
checkbox: false
6762
});
6863
});
6964

@@ -75,8 +70,7 @@ describe('Form', () => {
7570
toggle: true,
7671
input: '',
7772
input2: 'Default Value',
78-
checkbox: false,
79-
range: 5
73+
checkbox: false
8074
});
8175
});
8276

@@ -88,8 +82,7 @@ describe('Form', () => {
8882
toggle: false,
8983
input: '',
9084
input2: 'Default Value',
91-
checkbox: true,
92-
range: 5
85+
checkbox: true
9386
});
9487
});
9588

@@ -109,8 +102,7 @@ describe('Form', () => {
109102
toggle: true,
110103
input: '',
111104
input2: 'Default Value',
112-
checkbox: false,
113-
range: 5
105+
checkbox: false
114106
});
115107
cy.get('ion-checkbox').click();
116108
testData({
@@ -119,8 +111,7 @@ describe('Form', () => {
119111
toggle: true,
120112
input: '',
121113
input2: 'Default Value',
122-
checkbox: true,
123-
range: 5
114+
checkbox: true
124115
});
125116
});
126117
});

angular/test/base/e2e/src/inputs.spec.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ describe('Inputs', () => {
99
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
1010
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
1111
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
12-
cy.get('ion-range').should('have.prop', 'value').and('equal', 10);
1312
});
1413

1514
it('should have reset value', () => {
@@ -20,7 +19,6 @@ describe('Inputs', () => {
2019
cy.get('ion-input').should('have.prop', 'value').and('equal', '');
2120
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '');
2221
cy.get('ion-select').should('have.prop', 'value').and('equal', '');
23-
cy.get('ion-range').should('have.prop', 'value').and('be.NaN');
2422
});
2523

2624
it('should get some value', () => {
@@ -32,7 +30,6 @@ describe('Inputs', () => {
3230
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
3331
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
3432
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
35-
cy.get('ion-range').should('have.prop', 'value').and('equal', 10);
3633
});
3734

3835
it('change values should update angular', () => {
@@ -54,19 +51,10 @@ describe('Inputs', () => {
5451
// Click confirm button
5552
cy.get('ion-alert .alert-button:not(.alert-button-role-cancel)').click();
5653

57-
cy.get('ion-range').invoke('prop', 'value', 20);
58-
5954
cy.get('#checkbox-note').should('have.text', 'true');
6055
cy.get('#toggle-note').should('have.text', 'true');
6156
cy.get('#input-note').should('have.text', 'hola');
6257
cy.get('#datetime-note').should('have.text', '1994-03-14');
6358
cy.get('#select-note').should('have.text', 'ps');
64-
cy.get('#range-note').should('have.text', '20');
65-
});
66-
67-
it('nested components should not interfere with NgModel', () => {
68-
cy.get('#range-note').should('have.text', '10');
69-
cy.get('#nested-toggle').click();
70-
cy.get('#range-note').should('have.text', '10');
7159
});
72-
})
60+
});

angular/test/base/src/app/app-routing.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ const routes: Routes = [
6969
}
7070
]
7171
},
72+
{
73+
path: 'form-controls/range',
74+
loadChildren: () => import('./form-controls/range/range.module').then(m => m.RangeModule)
75+
}
7276
];
7377

7478
@NgModule({
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NgModule } from '@angular/core';
2+
import { RouterModule } from '@angular/router';
3+
4+
import { RangeComponent } from './range.component';
5+
6+
@NgModule({
7+
imports: [
8+
RouterModule.forChild([
9+
{ path: '', component: RangeComponent }
10+
])
11+
]
12+
})
13+
export class RangeRoutingModule { }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<ion-header>
2+
<ion-toolbar>
3+
<ion-title>Range</ion-title>
4+
</ion-toolbar>
5+
</ion-header>
6+
<ion-content>
7+
<form [formGroup]="form">
8+
<ion-list>
9+
<ion-item>
10+
<ion-label>Range</ion-label>
11+
<ion-range formControlName="range" min="0" max="20"></ion-range>
12+
</ion-item>
13+
</ion-list>
14+
<ion-button type="submit">Submit</ion-button>
15+
</form>
16+
</ion-content>

0 commit comments

Comments
 (0)