diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx
index da4c2d75344..b36617ad3e1 100644
--- a/core/src/components/range/range.tsx
+++ b/core/src/components/range/range.tsx
@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
import type { Attributes } from '@utils/helpers';
-import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput } from '@utils/helpers';
+import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext } from '@utils/theme';
@@ -109,7 +109,11 @@ export class Range implements ComponentInterface {
*/
@Prop() min = 0;
@Watch('min')
- protected minChanged() {
+ protected minChanged(newValue: number) {
+ if (!isSafeNumber(newValue)) {
+ this.min = 0;
+ }
+
if (!this.noUpdate) {
this.updateRatio();
}
@@ -120,7 +124,11 @@ export class Range implements ComponentInterface {
*/
@Prop() max = 100;
@Watch('max')
- protected maxChanged() {
+ protected maxChanged(newValue: number) {
+ if (!isSafeNumber(newValue)) {
+ this.max = 100;
+ }
+
if (!this.noUpdate) {
this.updateRatio();
}
@@ -151,6 +159,12 @@ export class Range implements ComponentInterface {
* Specifies the value granularity.
*/
@Prop() step = 1;
+ @Watch('step')
+ protected stepChanged(newValue: number) {
+ if (!isSafeNumber(newValue)) {
+ this.step = 1;
+ }
+ }
/**
* If `true`, tick marks are displayed based on the step value.
@@ -300,6 +314,11 @@ export class Range implements ComponentInterface {
}
this.inheritedAttributes = inheritAriaAttributes(this.el);
+ // If min, max, or step are not safe, set them to 0, 100, and 1, respectively.
+ // Each watch does this, but not before the initial load.
+ this.min = isSafeNumber(this.min) ? this.min : 0;
+ this.max = isSafeNumber(this.max) ? this.max : 100;
+ this.step = isSafeNumber(this.step) ? this.step : 1;
}
componentDidLoad() {
diff --git a/core/src/components/range/test/range.spec.ts b/core/src/components/range/test/range.spec.ts
index 8698f9bbf06..0d83082d399 100644
--- a/core/src/components/range/test/range.spec.ts
+++ b/core/src/components/range/test/range.spec.ts
@@ -28,6 +28,25 @@ describe('Range', () => {
});
});
+ it('should handle undefined min and max values by falling back to defaults', async () => {
+ const page = await newSpecPage({
+ components: [Range],
+ html: `
+ Range
+ `,
+ });
+
+ const range = page.body.querySelector('ion-range')!;
+ // Here we have to cast this to any, but in its react wrapper it accepts undefined as a valid value
+ range.min = undefined as any;
+ range.max = undefined as any;
+ range.step = undefined as any;
+ await page.waitForChanges();
+ expect(range.min).toBe(0);
+ expect(range.max).toBe(100);
+ expect(range.step).toBe(1);
+ });
+
it('should return the clamped value for a range dual knob component', () => {
sharedRange.min = 0;
sharedRange.max = 100;
diff --git a/core/src/utils/floating-point/floating-point.spec.ts b/core/src/utils/floating-point/floating-point.spec.ts
index 80db4138b5b..d985699ba7b 100644
--- a/core/src/utils/floating-point/floating-point.spec.ts
+++ b/core/src/utils/floating-point/floating-point.spec.ts
@@ -11,6 +11,12 @@ describe('floating point utils', () => {
const n = getDecimalPlaces(5);
expect(n).toBe(0);
});
+
+ it('should handle nullish values', () => {
+ expect(getDecimalPlaces(undefined as any)).toBe(0);
+ expect(getDecimalPlaces(null as any)).toBe(0);
+ expect(getDecimalPlaces(NaN as any)).toBe(0);
+ });
});
describe('roundToMaxDecimalPlaces', () => {
@@ -18,5 +24,11 @@ describe('floating point utils', () => {
const n = roundToMaxDecimalPlaces(5.12345, 1.12, 2.123);
expect(n).toBe(5.123);
});
+
+ it('should handle nullish values', () => {
+ expect(roundToMaxDecimalPlaces(undefined as any)).toBe(0);
+ expect(roundToMaxDecimalPlaces(null as any)).toBe(0);
+ expect(roundToMaxDecimalPlaces(NaN as any)).toBe(0);
+ });
});
});
diff --git a/core/src/utils/floating-point/index.ts b/core/src/utils/floating-point/index.ts
index d8ed8570741..6f94b801296 100644
--- a/core/src/utils/floating-point/index.ts
+++ b/core/src/utils/floating-point/index.ts
@@ -1,4 +1,7 @@
+import { isSafeNumber } from '@utils/helpers';
+
export function getDecimalPlaces(n: number) {
+ if (!isSafeNumber(n)) return 0;
if (n % 1 === 0) return 0;
return n.toString().split('.')[1].length;
}
@@ -36,6 +39,7 @@ export function getDecimalPlaces(n: number) {
* be used as a reference for the desired specificity.
*/
export function roundToMaxDecimalPlaces(n: number, ...references: number[]) {
+ if (!isSafeNumber(n)) return 0;
const maxPlaces = Math.max(...references.map((r) => getDecimalPlaces(r)));
return Number(n.toFixed(maxPlaces));
}
diff --git a/core/src/utils/helpers.ts b/core/src/utils/helpers.ts
index a740ff98ac7..a005686b779 100644
--- a/core/src/utils/helpers.ts
+++ b/core/src/utils/helpers.ts
@@ -424,3 +424,10 @@ export const getNextSiblingOfType = (element: Element): T | n
}
return null;
};
+
+/**
+ * Checks input for usable number. Not NaN and not Infinite.
+ */
+export const isSafeNumber = (input: unknown): input is number => {
+ return typeof input === 'number' && !isNaN(input) && isFinite(input);
+};