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;