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 26e5ef785dd..c3875c8e30e 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -134,10 +134,19 @@ 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; + @Watch('dualKnobs') + protected dualKnobsChanged() { + if (!this.noUpdate) { + this.updateRatio(); + } + } + /** * Minimum integer value of the range. */ 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']); + }); + }); +});