From 7cf25cff4a7a5f486a9dbbfa546d813805211d8f Mon Sep 17 00:00:00 2001
From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Date: Tue, 18 Aug 2026 16:15:43 -0400
Subject: [PATCH 1/3] fix(range): update knob positions when dualKnobs changes
---
core/src/components/range/range.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx
index 26e5ef785dd..44d0468e0a5 100644
--- a/core/src/components/range/range.tsx
+++ b/core/src/components/range/range.tsx
@@ -138,6 +138,13 @@ export class Range implements ComponentInterface {
*/
@Prop() dualKnobs = false;
+ @Watch('dualKnobs')
+ protected dualKnobsChanged() {
+ if (!this.noUpdate) {
+ this.updateRatio();
+ }
+ }
+
/**
* Minimum integer value of the range.
*/
From 75ace2a491c78762c0a59526125b2b842a3717da Mon Sep 17 00:00:00 2001
From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Date: Tue, 18 Aug 2026 16:46:44 -0400
Subject: [PATCH 2/3] test(range): add e2e test for dualKnobs behavior
---
.../range/test/dual-knobs/index.html | 122 ++++++++++++++++++
.../range/test/dual-knobs/range.e2e.ts | 82 ++++++++++++
2 files changed, 204 insertions(+)
create mode 100644 core/src/components/range/test/dual-knobs/index.html
create mode 100644 core/src/components/range/test/dual-knobs/range.e2e.ts
diff --git a/core/src/components/range/test/dual-knobs/index.html b/core/src/components/range/test/dual-knobs/index.html
new file mode 100644
index 00000000000..a477dae62d8
--- /dev/null
+++ b/core/src/components/range/test/dual-knobs/index.html
@@ -0,0 +1,122 @@
+
+
+
+
+ Range - Dual Knobs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Range - Dual Knobs
+
+
+
+
+
+ Every range below is assigned { lower: 20, upper: 80 }. The lower knob is purple while the upper
+ is blue.
+
+
+ dualKnobs set on the component
+
+ 20 - 80
+
+
+ dualKnobs assigned before value
+
+ 20 - 80
+
+
+ dualKnobs assigned after value
+
+ 20 - 80
+
+
+ dualKnobs toggled
+
+
+
+ Toggle Dual Knobs
+
+
+
+
+
+
diff --git a/core/src/components/range/test/dual-knobs/range.e2e.ts b/core/src/components/range/test/dual-knobs/range.e2e.ts
new file mode 100644
index 00000000000..f3e8e610568
--- /dev/null
+++ b/core/src/components/range/test/dual-knobs/range.e2e.ts
@@ -0,0 +1,82 @@
+import type { Locator } from '@playwright/test';
+import { expect } from '@playwright/test';
+import { configs, test } from '@utils/test/playwright';
+
+const DUAL_VALUE = { lower: 20, upper: 80 };
+
+/**
+ * Returns the value each knob is positioned at. Sorted low to high because knob
+ * A is not guaranteed to be the knob holding the lower value.
+ */
+const knobValues = async (range: Locator) => {
+ const values: string[] = await range
+ .locator('.range-knob-handle')
+ .evaluateAll((els: HTMLElement[]) => els.map((el) => el.getAttribute('aria-valuenow')!));
+
+ return values.sort((a, b) => parseFloat(a) - parseFloat(b));
+};
+
+/**
+ * This behavior does not vary across modes/directions
+ */
+configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, config }) => {
+ test.describe(title('range: dual knobs'), () => {
+ test('should position knobs when dualKnobs is assigned before value', async ({ page }) => {
+ await page.setContent(``, config);
+
+ const range = page.locator('ion-range');
+ await range.evaluate((el: HTMLIonRangeElement, value) => {
+ el.dualKnobs = true;
+ el.value = value;
+ }, DUAL_VALUE);
+ await page.waitForChanges();
+
+ expect(await knobValues(range)).toEqual(['20', '80']);
+
+ // A single knob range ignores the lower half of an object value.
+ await range.evaluate((el: HTMLIonRangeElement) => (el.dualKnobs = false));
+ await page.waitForChanges();
+
+ expect(await knobValues(range)).toEqual(['80']);
+ });
+
+ test('should position knobs when dualKnobs is assigned after value', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/31026',
+ });
+
+ await page.setContent(``, config);
+
+ const range = page.locator('ion-range');
+ await range.evaluate((el: HTMLIonRangeElement, value) => {
+ el.value = value;
+ el.dualKnobs = true;
+ }, DUAL_VALUE);
+ await page.waitForChanges();
+
+ expect(await knobValues(range)).toEqual(['20', '80']);
+ });
+
+ test('should reposition knobs when dualKnobs is toggled at runtime', async ({ page }) => {
+ await page.setContent(``, config);
+
+ const range = page.locator('ion-range');
+ await range.evaluate((el: HTMLIonRangeElement, value) => (el.value = value), DUAL_VALUE);
+ await page.waitForChanges();
+
+ // A single knob range ignores the lower half of an object value.
+ expect(await knobValues(range)).toEqual(['80']);
+
+ await range.evaluate((el: HTMLIonRangeElement) => (el.dualKnobs = true));
+ await page.waitForChanges();
+
+ expect(await knobValues(range)).toEqual(['20', '80']);
+
+ await range.evaluate((el: HTMLIonRangeElement) => (el.dualKnobs = false));
+ await page.waitForChanges();
+
+ expect(await knobValues(range)).toEqual(['80']);
+ });
+ });
+});
From bf66519c868cde1c940213f9a4e2c7d943f9842d Mon Sep 17 00:00:00 2001
From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Date: Tue, 18 Aug 2026 16:49:12 -0400
Subject: [PATCH 3/3] docs(range): improve dualKnobs description
---
core/src/components.d.ts | 4 ++--
core/src/components/range/range.tsx | 4 +++-
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index 173a0537548..102f7472b98 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -2662,7 +2662,7 @@ export namespace Components {
*/
"disabled": boolean;
/**
- * Show two knobs.
+ * If `true`, the range shows two knobs and `value` is an object with `lower` and `upper` properties. If `false`, the range shows one knob and `value` is a number.
* @default false
*/
"dualKnobs": boolean;
@@ -7939,7 +7939,7 @@ declare namespace LocalJSX {
*/
"disabled"?: boolean;
/**
- * Show two knobs.
+ * If `true`, the range shows two knobs and `value` is an object with `lower` and `upper` properties. If `false`, the range shows one knob and `value` is a number.
* @default false
*/
"dualKnobs"?: boolean;
diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx
index 44d0468e0a5..c3875c8e30e 100644
--- a/core/src/components/range/range.tsx
+++ b/core/src/components/range/range.tsx
@@ -134,7 +134,9 @@ export class Range implements ComponentInterface {
@Prop() label?: string;
/**
- * Show two knobs.
+ * If `true`, the range shows two knobs and `value` is an object with `lower`
+ * and `upper` properties. If `false`, the range shows one knob and `value` is
+ * a number.
*/
@Prop() dualKnobs = false;