Skip to content

Commit 3223193

Browse files
fudomShaneKbrandyscarney
authored
fix(range): handle unsupported values for range min and max (#30070)
Issue number: resolves #29667 --------- ## What is the current behavior? Currently, if min/max are set to undefined on `IonRange` (which is an accepted value), it breaks the DOM. ## What is the new behavior? After these changes, if min/max are set to undefined or any unsupported value (such as infinity or a NaN), it will fall back to the default values for min and max (currently, 1 and 100 respectively). ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information --------- Co-authored-by: ShaneK <[email protected]> Co-authored-by: Brandy Smith <[email protected]>
1 parent 4df0e0f commit 3223193

File tree

5 files changed

+64
-3
lines changed

5 files changed

+64
-3
lines changed

core/src/components/range/range.tsx

+22-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
33
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
44
import type { Attributes } from '@utils/helpers';
5-
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput } from '@utils/helpers';
5+
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from '@utils/helpers';
66
import { printIonWarning } from '@utils/logging';
77
import { isRTL } from '@utils/rtl';
88
import { createColorClasses, hostContext } from '@utils/theme';
@@ -109,7 +109,11 @@ export class Range implements ComponentInterface {
109109
*/
110110
@Prop() min = 0;
111111
@Watch('min')
112-
protected minChanged() {
112+
protected minChanged(newValue: number) {
113+
if (!isSafeNumber(newValue)) {
114+
this.min = 0;
115+
}
116+
113117
if (!this.noUpdate) {
114118
this.updateRatio();
115119
}
@@ -120,7 +124,11 @@ export class Range implements ComponentInterface {
120124
*/
121125
@Prop() max = 100;
122126
@Watch('max')
123-
protected maxChanged() {
127+
protected maxChanged(newValue: number) {
128+
if (!isSafeNumber(newValue)) {
129+
this.max = 100;
130+
}
131+
124132
if (!this.noUpdate) {
125133
this.updateRatio();
126134
}
@@ -151,6 +159,12 @@ export class Range implements ComponentInterface {
151159
* Specifies the value granularity.
152160
*/
153161
@Prop() step = 1;
162+
@Watch('step')
163+
protected stepChanged(newValue: number) {
164+
if (!isSafeNumber(newValue)) {
165+
this.step = 1;
166+
}
167+
}
154168

155169
/**
156170
* If `true`, tick marks are displayed based on the step value.
@@ -300,6 +314,11 @@ export class Range implements ComponentInterface {
300314
}
301315

302316
this.inheritedAttributes = inheritAriaAttributes(this.el);
317+
// If min, max, or step are not safe, set them to 0, 100, and 1, respectively.
318+
// Each watch does this, but not before the initial load.
319+
this.min = isSafeNumber(this.min) ? this.min : 0;
320+
this.max = isSafeNumber(this.max) ? this.max : 100;
321+
this.step = isSafeNumber(this.step) ? this.step : 1;
303322
}
304323

305324
componentDidLoad() {

core/src/components/range/test/range.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ describe('Range', () => {
2828
});
2929
});
3030

31+
it('should handle undefined min and max values by falling back to defaults', async () => {
32+
const page = await newSpecPage({
33+
components: [Range],
34+
html: `<ion-range id="my-custom-range">
35+
<div slot="label">Range</div>
36+
</ion-range>`,
37+
});
38+
39+
const range = page.body.querySelector('ion-range')!;
40+
// Here we have to cast this to any, but in its react wrapper it accepts undefined as a valid value
41+
range.min = undefined as any;
42+
range.max = undefined as any;
43+
range.step = undefined as any;
44+
await page.waitForChanges();
45+
expect(range.min).toBe(0);
46+
expect(range.max).toBe(100);
47+
expect(range.step).toBe(1);
48+
});
49+
3150
it('should return the clamped value for a range dual knob component', () => {
3251
sharedRange.min = 0;
3352
sharedRange.max = 100;

core/src/utils/floating-point/floating-point.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,24 @@ describe('floating point utils', () => {
1111
const n = getDecimalPlaces(5);
1212
expect(n).toBe(0);
1313
});
14+
15+
it('should handle nullish values', () => {
16+
expect(getDecimalPlaces(undefined as any)).toBe(0);
17+
expect(getDecimalPlaces(null as any)).toBe(0);
18+
expect(getDecimalPlaces(NaN as any)).toBe(0);
19+
});
1420
});
1521

1622
describe('roundToMaxDecimalPlaces', () => {
1723
it('should round to the highest number of places as references', async () => {
1824
const n = roundToMaxDecimalPlaces(5.12345, 1.12, 2.123);
1925
expect(n).toBe(5.123);
2026
});
27+
28+
it('should handle nullish values', () => {
29+
expect(roundToMaxDecimalPlaces(undefined as any)).toBe(0);
30+
expect(roundToMaxDecimalPlaces(null as any)).toBe(0);
31+
expect(roundToMaxDecimalPlaces(NaN as any)).toBe(0);
32+
});
2133
});
2234
});

core/src/utils/floating-point/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { isSafeNumber } from '@utils/helpers';
2+
13
export function getDecimalPlaces(n: number) {
4+
if (!isSafeNumber(n)) return 0;
25
if (n % 1 === 0) return 0;
36
return n.toString().split('.')[1].length;
47
}
@@ -36,6 +39,7 @@ export function getDecimalPlaces(n: number) {
3639
* be used as a reference for the desired specificity.
3740
*/
3841
export function roundToMaxDecimalPlaces(n: number, ...references: number[]) {
42+
if (!isSafeNumber(n)) return 0;
3943
const maxPlaces = Math.max(...references.map((r) => getDecimalPlaces(r)));
4044
return Number(n.toFixed(maxPlaces));
4145
}

core/src/utils/helpers.ts

+7
Original file line numberDiff line numberDiff line change
@@ -424,3 +424,10 @@ export const getNextSiblingOfType = <T extends Element>(element: Element): T | n
424424
}
425425
return null;
426426
};
427+
428+
/**
429+
* Checks input for usable number. Not NaN and not Infinite.
430+
*/
431+
export const isSafeNumber = (input: unknown): input is number => {
432+
return typeof input === 'number' && !isNaN(input) && isFinite(input);
433+
};

0 commit comments

Comments
 (0)