From bb2a127de374cbca01d0c6f2efb2921e869a911f Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Wed, 11 Dec 2024 10:30:36 -0800 Subject: [PATCH 01/10] feat(slider): Implement vertical orientation --- .../src/components/slider/resources.ts | 1 + .../src/components/slider/slider.e2e.ts | 2152 +++++++++-------- .../src/components/slider/slider.scss | 9 + .../src/components/slider/slider.stories.ts | 1009 ++++++-- .../src/components/slider/slider.tsx | 181 +- .../calcite-components/src/demos/slider.html | 44 + 6 files changed, 2149 insertions(+), 1247 deletions(-) diff --git a/packages/calcite-components/src/components/slider/resources.ts b/packages/calcite-components/src/components/slider/resources.ts index d81781a2731..b59b41f9e88 100644 --- a/packages/calcite-components/src/components/slider/resources.ts +++ b/packages/calcite-components/src/components/slider/resources.ts @@ -20,6 +20,7 @@ export const CSS = { tickActive: "tick--active", tickLabel: "tick__label", tickMax: "tick__label--max", + tickLabelVertical: "tick__label--vertical", tickMin: "tick__label--min", ticks: "ticks", track: "track", diff --git a/packages/calcite-components/src/components/slider/slider.e2e.ts b/packages/calcite-components/src/components/slider/slider.e2e.ts index 2e6fda8b870..22dc51a5161 100644 --- a/packages/calcite-components/src/components/slider/slider.e2e.ts +++ b/packages/calcite-components/src/components/slider/slider.e2e.ts @@ -79,6 +79,10 @@ describe("calcite-slider", () => { { propertyName: "status", defaultValue: "idle" }, { propertyName: "validationIcon", defaultValue: undefined }, { propertyName: "validationMessage", defaultValue: undefined }, + { + propertyName: "layout", + defaultValue: "horizontal", + }, ]); }); @@ -107,1219 +111,1351 @@ describe("calcite-slider", () => { disabled("calcite-slider"); }); - it("sets aria attributes properly for single value", async () => { - const page = await newE2EPage(); - await page.setContent(` - - - `); - const button = await page.find("calcite-slider >>> .thumb"); - expect(button).toEqualAttribute("role", "slider"); - expect(button).toEqualAttribute("aria-label", "Yeah! Slider!"); - expect(button).toEqualAttribute("aria-valuenow", "23"); - expect(button).toEqualAttribute("aria-valuemin", "0"); - expect(button).toEqualAttribute("aria-valuemax", "100"); - expect(button).toEqualAttribute("aria-orientation", "horizontal"); - }); - - it("sets aria attributes properly for range values", async () => { - const page = await newE2EPage(); - await page.setContent(` - - - `); - const maxButton = await page.find("calcite-slider >>> .thumb--value"); - const minButton = await page.find("calcite-slider >>> .thumb--minValue"); - expect(minButton).toEqualAttribute("role", "slider"); - expect(maxButton).toEqualAttribute("role", "slider"); - expect(minButton).toEqualAttribute("aria-label", "Min Label"); - expect(maxButton).toEqualAttribute("aria-label", "Max Label"); - expect(minButton).toEqualAttribute("aria-valuenow", "23"); - expect(maxButton).toEqualAttribute("aria-valuenow", "47"); - expect(minButton).toEqualAttribute("aria-valuemin", "0"); - expect(maxButton).toEqualAttribute("aria-valuemin", "0"); - expect(minButton).toEqualAttribute("aria-valuemax", "100"); - expect(maxButton).toEqualAttribute("aria-valuemax", "100"); - expect(minButton).toEqualAttribute("aria-orientation", "horizontal"); - expect(maxButton).toEqualAttribute("aria-orientation", "horizontal"); - }); + describe.each(["horizontal", "vertical"])("%s layout", (layout) => { + it("sets aria attributes properly for single value", async () => { + const page = await newE2EPage(); + await page.setContent(` + + + `); + const button = await page.find("calcite-slider >>> .thumb"); + expect(button).toEqualAttribute("role", "slider"); + expect(button).toEqualAttribute("aria-label", "Yeah! Slider!"); + expect(button).toEqualAttribute("aria-valuenow", "23"); + expect(button).toEqualAttribute("aria-valuemin", "0"); + expect(button).toEqualAttribute("aria-valuemax", "100"); + expect(button).toEqualAttribute("aria-orientation", `${layout}`); + }); - it("can be controlled via keyboard", async () => { - const page = await newE2EPage(); - await page.setContent(` - - - `); - const slider = await page.find("calcite-slider"); - const handle = await page.find("calcite-slider >>> .thumb"); - await page.waitForChanges(); - const value = await slider.getProperty("value"); - expect(value).toBe(30); - await handle.press("ArrowRight"); - expect(await slider.getProperty("value")).toBe(31); - await handle.press("ArrowLeft"); - expect(await slider.getProperty("value")).toBe(30); - await handle.press("ArrowUp"); - expect(await slider.getProperty("value")).toBe(31); - await handle.press("ArrowDown"); - expect(await slider.getProperty("value")).toBe(30); - await handle.press("PageUp"); - expect(await slider.getProperty("value")).toBe(40); - await handle.press("PageDown"); - expect(await slider.getProperty("value")).toBe(30); - await handle.press("Home"); - expect(await slider.getProperty("value")).toBe(0); - await handle.press("End"); - expect(await slider.getProperty("value")).toBe(100); - - // activation keys should not affect the value - await handle.press(" "); - await handle.press("Enter"); - expect(await slider.getProperty("value")).toBe(100); - }); + it("sets aria attributes properly for range values", async () => { + const page = await newE2EPage(); + await page.setContent(` + + + `); + const maxButton = await page.find("calcite-slider >>> .thumb--value"); + const minButton = await page.find("calcite-slider >>> .thumb--minValue"); + expect(minButton).toEqualAttribute("role", "slider"); + expect(maxButton).toEqualAttribute("role", "slider"); + expect(minButton).toEqualAttribute("aria-label", "Min Label"); + expect(maxButton).toEqualAttribute("aria-label", "Max Label"); + expect(minButton).toEqualAttribute("aria-valuenow", "23"); + expect(maxButton).toEqualAttribute("aria-valuenow", "47"); + expect(minButton).toEqualAttribute("aria-valuemin", "0"); + expect(maxButton).toEqualAttribute("aria-valuemin", "0"); + expect(minButton).toEqualAttribute("aria-valuemax", "100"); + expect(maxButton).toEqualAttribute("aria-valuemax", "100"); + expect(minButton).toEqualAttribute("aria-orientation", `${layout}`); + expect(maxButton).toEqualAttribute("aria-orientation", `${layout}`); + }); - describe("slider taking the precision of the provided step", () => { - it("takes the precision of the decimal step when controlled through keyboard", async () => { + it("can be controlled via keyboard", async () => { const page = await newE2EPage(); - await page.setContent(html` `); + await page.setContent(` + + + `); const slider = await page.find("calcite-slider"); const handle = await page.find("calcite-slider >>> .thumb"); await page.waitForChanges(); const value = await slider.getProperty("value"); expect(value).toBe(30); - await handle.press("ArrowRight"); - expect(await slider.getProperty("value")).toBe(31.12); + expect(await slider.getProperty("value")).toBe(31); await handle.press("ArrowLeft"); expect(await slider.getProperty("value")).toBe(30); await handle.press("ArrowUp"); - expect(await slider.getProperty("value")).toBe(31.12); + expect(await slider.getProperty("value")).toBe(31); await handle.press("ArrowDown"); expect(await slider.getProperty("value")).toBe(30); + await handle.press("PageUp"); + expect(await slider.getProperty("value")).toBe(40); + await handle.press("PageDown"); + expect(await slider.getProperty("value")).toBe(30); + await handle.press("Home"); + expect(await slider.getProperty("value")).toBe(0); + await handle.press("End"); + expect(await slider.getProperty("value")).toBe(100); + + // activation keys should not affect the value + await handle.press(" "); + await handle.press("Enter"); + expect(await slider.getProperty("value")).toBe(100); }); - it("single handle: takes the precision of the decimal step when clicking and dragging the track", async () => { + describe("slider taking the precision of the provided step", () => { + it("takes the precision of the decimal step when controlled through keyboard", async () => { + const page = await newE2EPage(); + await page.setContent(html` + + `); + const slider = await page.find("calcite-slider"); + const handle = await page.find("calcite-slider >>> .thumb"); + await page.waitForChanges(); + const value = await slider.getProperty("value"); + expect(value).toBe(30); + + await handle.press("ArrowRight"); + expect(await slider.getProperty("value")).toBe(31.12); + await handle.press("ArrowLeft"); + expect(await slider.getProperty("value")).toBe(30); + await handle.press("ArrowUp"); + expect(await slider.getProperty("value")).toBe(31.12); + await handle.press("ArrowDown"); + expect(await slider.getProperty("value")).toBe(30); + }); + + it("single handle: takes the precision of the decimal step when clicking and dragging the track", async () => { + const page = await newE2EPage(); + await page.setContent(html` + + `); + const slider = await page.find("calcite-slider"); + expect(await slider.getProperty("value")).toBe(0); + const trackRect = await getElementRect(page, "calcite-slider", ".track"); + + await page.mouse.move(trackRect.x, trackRect.y); + await page.mouse.down(); + if (layout === "horizontal") { + await page.mouse.move(trackRect.x + 5, trackRect.y); + } else { + await page.mouse.move(trackRect.x, trackRect.y + 95); + } + await page.mouse.up(); + await page.waitForChanges(); + + expect(await slider.getProperty("value")).toBe(4.48); + }); + }); + + // skipped due to a bug where value is rounded down instead of up: + // https://github.com/Esri/calcite-design-system/issues/9684 + it.skip("step floating point precision", async () => { const page = await newE2EPage(); - await page.setContent(html` - - `); + await page.setContent( + html``, + ); const slider = await page.find("calcite-slider"); - expect(await slider.getProperty("value")).toBe(0); - const trackRect = await getElementRect(page, "calcite-slider", ".track"); - await page.mouse.move(trackRect.x, trackRect.y); - await page.mouse.down(); - await page.mouse.move(trackRect.x + 5, trackRect.y); - await page.mouse.up(); await page.waitForChanges(); + expect((await slider.getProperty("value")).toString()).toBe("1.4"); + }); - expect(await slider.getProperty("value")).toBe(4.48); + it("only selects values on step interval when snap prop is passed", async () => { + const page = await newE2EPage(); + await page.setContent(` + + + `); + const slider = await page.find("calcite-slider"); + const handle = await page.find("calcite-slider >>> .thumb--value"); + await page.waitForChanges(); + let value = await slider.getProperty("value"); + expect(value).toBe(20); + await handle.press("ArrowRight"); + value = await slider.getProperty("value"); + expect(value).toBe(30); }); - }); - // skipped due to a bug where value is rounded down instead of up: - // https://github.com/Esri/calcite-design-system/issues/9684 - it.skip("step floating point precision", async () => { - const page = await newE2EPage(); - await page.setContent( - html``, - ); - const slider = await page.find("calcite-slider"); - - await page.waitForChanges(); - expect((await slider.getProperty("value")).toString()).toBe("1.4"); - }); + it("displays tick marks when ticks prop is passed", async () => { + const page = await newE2EPage(); + await page.setContent(` + + + `); + const ticks = await page.findAll("calcite-slider >>> .tick"); + expect(ticks.length).toBe(11); + }); - it("only selects values on step interval when snap prop is passed", async () => { - const page = await newE2EPage(); - await page.setContent(` - { + const page = await newE2EPage(); + await page.setContent(` + - - `); - const slider = await page.find("calcite-slider"); - const handle = await page.find("calcite-slider >>> .thumb--value"); - await page.waitForChanges(); - let value = await slider.getProperty("value"); - expect(value).toBe(20); - await handle.press("ArrowRight"); - value = await slider.getProperty("value"); - expect(value).toBe(30); - }); - - it("displays tick marks when ticks prop is passed", async () => { - const page = await newE2EPage(); - await page.setContent(` - - - `); - const ticks = await page.findAll("calcite-slider >>> .tick"); - expect(ticks.length).toBe(11); - }); - - it("should cap the rendered last tick label to the slider's provided max", async () => { - const page = await newE2EPage(); - await page.setContent(` - - - `); - const slider = await page.find("calcite-slider"); - const maxTickLabel = await page.find("calcite-slider >>> .tick:nth-of-type(11)"); - expect(parseFloat(maxTickLabel.textContent)).toBe(await slider.getProperty("max")); - }); - - it("key press should change the value and emit input and change events", async () => { - const page = await newE2EPage(); - await page.setContent(` - - - `); - const slider = await page.find("calcite-slider"); - const handle = await page.find("calcite-slider >>> .thumb"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - expect(inputEvent).toHaveReceivedEventTimes(0); - expect(changeEvent).toHaveReceivedEventTimes(0); - - await handle.press("ArrowRight"); - await page.waitForChanges(); - - expect(await slider.getProperty("value")).toBe(24); - expect(inputEvent).toHaveReceivedEventTimes(1); - expect(changeEvent).toHaveReceivedEventTimes(1); - }); + label-handles + label-ticks + layout=${layout} + > + + `); + const slider = await page.find("calcite-slider"); + const maxTickLabel = await page.find("calcite-slider >>> .tick:nth-of-type(11)"); + expect(parseFloat(maxTickLabel.textContent)).toBe(await slider.getProperty("max")); + }); - describe("thumb focus for single value", () => { - const sliderForThumbFocusTests = html``; - - it("should focus thumb when clicked near", async () => { + it("key press should change the value and emit input and change events", async () => { const page = await newE2EPage(); - await page.setContent(html`${sliderForThumbFocusTests}`); + await page.setContent(` + + + `); const slider = await page.find("calcite-slider"); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + const handle = await page.find("calcite-slider >>> .thumb"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + expect(inputEvent).toHaveReceivedEventTimes(0); + expect(changeEvent).toHaveReceivedEventTimes(0); - await page.mouse.move(trackX + 50, trackY); - await page.mouse.down(); - await page.mouse.up(); + await handle.press("ArrowRight"); await page.waitForChanges(); - let isThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), - ); - - expect(isThumbFocused).toBe(true); - expect(await slider.getProperty("value")).toBe(50); - - await page.mouse.move(trackX + 40, trackY); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); + expect(await slider.getProperty("value")).toBe(24); + expect(inputEvent).toHaveReceivedEventTimes(1); + expect(changeEvent).toHaveReceivedEventTimes(1); + }); - isThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), - ); + describe("thumb focus for single value", () => { + const sliderForThumbFocusTests = html``; - expect(isThumbFocused).toBe(true); - expect(await slider.getProperty("value")).toBe(40); + it("should focus thumb when clicked near", async () => { + const page = await newE2EPage(); + await page.setContent(html`${sliderForThumbFocusTests}`); + const slider = await page.find("calcite-slider"); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + + layout === "horizontal" + ? await page.mouse.move(trackX + 50, trackY) + : await page.mouse.move(trackX, trackY + 50); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); - await page.mouse.move(trackX + 60, trackY); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); + let isThumbFocused = await page.$eval("calcite-slider", (slider) => + slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + ); - isThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), - ); + expect(isThumbFocused).toBe(true); + expect(await slider.getProperty("value")).toBe(50); - expect(isThumbFocused).toBe(true); - expect(await slider.getProperty("value")).toBe(60); - }); - }); + layout === "horizontal" + ? await page.mouse.move(trackX + 40, trackY) + : await page.mouse.move(trackX, trackY + 60); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); - describe("thumb focus in range", () => { - const sliderForThumbFocusTests = html``; - - it("should focus the min thumb when clicked on track close to minValue", async () => { - const page = await newE2EPage({ - html: `${sliderForThumbFocusTests}`, - }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + isThumbFocused = await page.$eval("calcite-slider", (slider) => + slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + ); - await page.mouse.move(trackX + 30, trackY); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); + expect(isThumbFocused).toBe(true); + expect(await slider.getProperty("value")).toBe(40); - const isMinThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--minValue"), - ); + layout === "horizontal" + ? await page.mouse.move(trackX + 60, trackY) + : await page.mouse.move(trackX, trackY + 40); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); - expect(await slider.getProperty("minValue")).toBe(0); - expect(await slider.getProperty("maxValue")).toBe(100); - expect(isMinThumbFocused).toBe(true); - }); + isThumbFocused = await page.$eval("calcite-slider", (slider) => + slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + ); - it("should focus the max thumb when clicked on track close to maxValue", async () => { - const page = await newE2EPage({ - html: `${sliderForThumbFocusTests}`, + expect(isThumbFocused).toBe(true); + expect(await slider.getProperty("value")).toBe(60); }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + }); - await page.mouse.move(trackX + 60, trackY); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); + describe("thumb focus in range", () => { + const sliderForThumbFocusTests = html``; - const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), - ); + it("should focus the min thumb when clicked on track close to minValue", async () => { + const page = await newE2EPage({ + html: `${sliderForThumbFocusTests}`, + }); + await page.waitForChanges(); + const slider = await page.find("calcite-slider"); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + + layout === "horizontal" + ? await page.mouse.move(trackX + 30, trackY) + : await page.mouse.move(trackX, trackY + 95); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); - expect(await slider.getProperty("minValue")).toBe(0); - expect(await slider.getProperty("maxValue")).toBe(100); - expect(isMaxThumbFocused).toBe(true); - }); + const isMinThumbFocused = await page.$eval("calcite-slider", (slider) => + slider.shadowRoot.activeElement?.classList.contains("thumb--minValue"), + ); - it("should focus the max thumb when clicked on middle of the track", async () => { - const page = await newE2EPage({ - html: `${sliderForThumbFocusTests}`, + expect(await slider.getProperty("minValue")).toBe(0); + expect(await slider.getProperty("maxValue")).toBe(100); + expect(isMinThumbFocused).toBe(true); }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - await page.mouse.move(trackX + 50, trackY); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); - - const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), - ); + it("should focus the max thumb when clicked on track close to maxValue", async () => { + const page = await newE2EPage({ + html: `${sliderForThumbFocusTests}`, + }); + await page.waitForChanges(); + const slider = await page.find("calcite-slider"); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + + layout === "horizontal" + ? await page.mouse.move(trackX + 60, trackY) + : await page.mouse.move(trackX, trackY + 40); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); - expect(await slider.getProperty("minValue")).toBe(0); - expect(await slider.getProperty("maxValue")).toBe(100); - expect(isMaxThumbFocused).toBe(true); - }); - }); + const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => + slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + ); - describe("mouse interaction", () => { - it("single handle: clicking the track changes value on mousedown, emits on mouseup", async () => { - const page = await newE2EPage({ - html: ``, + expect(await slider.getProperty("minValue")).toBe(0); + expect(await slider.getProperty("maxValue")).toBe(100); + expect(isMaxThumbFocused).toBe(true); }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - expect(await slider.getProperty("value")).toBe(0); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + it("should focus the max thumb when clicked on middle of the track", async () => { + const page = await newE2EPage({ + html: `${sliderForThumbFocusTests}`, + }); + await page.waitForChanges(); + const slider = await page.find("calcite-slider"); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + + layout === "horizontal" + ? await page.mouse.move(trackX + 50, trackY) + : await page.mouse.move(trackX, trackY + 50); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); - await page.mouse.move(trackX + 50, trackY); - await page.mouse.down(); - await page.waitForChanges(); - await page.mouse.up(); + const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => + slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + ); - expect(await slider.getProperty("value")).toBe(50); - expect(inputEvent).toHaveReceivedEventTimes(1); - expect(changeEvent).toHaveReceivedEventTimes(1); + expect(await slider.getProperty("minValue")).toBe(0); + expect(await slider.getProperty("maxValue")).toBe(100); + expect(isMaxThumbFocused).toBe(true); + }); }); - it("single handle: clicking and dragging the track changes and emits the value", async () => { - const page = await newE2EPage({ - html: ``, - }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - expect(await slider.getProperty("value")).toBe(0); + describe("mouse interaction", () => { + it("single handle: clicking the track changes value on mousedown, emits on mouseup", async () => { + const page = await newE2EPage({ + html: ``, + }); + await page.waitForChanges(); + const slider = await page.find("calcite-slider"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + expect(await slider.getProperty("value")).toBe(0); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - await page.mouse.move(trackX, trackY); - await page.mouse.down(); - await page.mouse.move(trackX + 1, trackY); - await page.mouse.move(trackX + 2, trackY); - await page.mouse.move(trackX + 3, trackY); - await page.mouse.move(trackX + 4, trackY); - await page.mouse.move(trackX + 5, trackY); - await page.waitForChanges(); - await page.mouse.up(); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - expect(await slider.getProperty("value")).toBe(5); - expect(inputEvent).toHaveReceivedEventTimes(5); - expect(changeEvent).toHaveReceivedEventTimes(1); - }); + layout === "horizontal" + ? await page.mouse.move(trackX + 50, trackY) + : await page.mouse.move(trackX, trackY + 50); + await page.mouse.down(); + await page.waitForChanges(); + await page.mouse.up(); - it("range: clicking the track to the left of the min handle changes minValue on mousedown, emits on mouseup", async () => { - const page = await newE2EPage({ - html: ``, + expect(await slider.getProperty("value")).toBe(50); + expect(inputEvent).toHaveReceivedEventTimes(1); + expect(changeEvent).toHaveReceivedEventTimes(1); }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - expect(await slider.getProperty("minValue")).toBe(50); - expect(await slider.getProperty("maxValue")).toBe(75); + it("single handle: clicking and dragging the track changes and emits the value", async () => { + const page = await newE2EPage({ + html: ``, + }); + await page.waitForChanges(); + const slider = await page.find("calcite-slider"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + expect(await slider.getProperty("value")).toBe(0); + + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + await page.mouse.move(trackX, trackY); + await page.mouse.down(); + if (layout === "horizontal") { + await page.mouse.move(trackX + 1, trackY); + await page.mouse.move(trackX + 2, trackY); + await page.mouse.move(trackX + 3, trackY); + await page.mouse.move(trackX + 4, trackY); + await page.mouse.move(trackX + 5, trackY); + } else { + await page.mouse.move(trackX, trackY + 99); + await page.mouse.move(trackX, trackY + 98); + await page.mouse.move(trackX, trackY + 97); + await page.mouse.move(trackX, trackY + 96); + await page.mouse.move(trackX, trackY + 95); + } + await page.waitForChanges(); + await page.mouse.up(); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + expect(await slider.getProperty("value")).toBe(5); + expect(inputEvent).toHaveReceivedEventTimes(layout === "horizontal" ? 5 : 6); + expect(changeEvent).toHaveReceivedEventTimes(1); + }); - await page.mouse.move(trackX + 25, trackY); - await page.mouse.down(); - await page.waitForChanges(); + it("range: clicking the track to the left of the min handle changes minValue on mousedown, emits on mouseup", async () => { + const page = await newE2EPage({ + html: `>`, + }); + await page.waitForChanges(); - expect(await slider.getProperty("minValue")).toBe(25); - expect(await slider.getProperty("maxValue")).toBe(75); - expect(changeEvent).toHaveReceivedEventTimes(0); + const slider = await page.find("calcite-slider"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + expect(await slider.getProperty("minValue")).toBe(50); + expect(await slider.getProperty("maxValue")).toBe(75); - await page.mouse.up(); - await page.waitForChanges(); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - expect(changeEvent).toHaveReceivedEventTimes(1); - expect(inputEvent).toHaveReceivedEventTimes(1); - }); + layout === "horizontal" + ? await page.mouse.move(trackX + 25, trackY) + : await page.mouse.move(trackX, trackY + 75); + await page.mouse.down(); + await page.waitForChanges(); - it("range: clicking and dragging the track to the left of the min handle changes minValue and emits", async () => { - const page = await newE2EPage({ - html: ``, - }); - await page.waitForChanges(); + expect(await slider.getProperty("minValue")).toBe(25); + expect(await slider.getProperty("maxValue")).toBe(75); + expect(changeEvent).toHaveReceivedEventTimes(0); - const slider = await page.find("calcite-slider"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - expect(await slider.getProperty("minValue")).toBe(50); - expect(await slider.getProperty("maxValue")).toBe(75); + await page.mouse.up(); + await page.waitForChanges(); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + expect(changeEvent).toHaveReceivedEventTimes(1); + expect(inputEvent).toHaveReceivedEventTimes(1); + }); - await page.mouse.move(trackX + 21, trackY); - await page.mouse.down(); - await page.mouse.move(trackX + 22, trackY); - await page.mouse.move(trackX + 23, trackY); - await page.mouse.move(trackX + 24, trackY); - await page.mouse.move(trackX + 25, trackY); - await page.waitForChanges(); - await page.mouse.up(); + it("range: clicking and dragging the track to the left of the min handle changes minValue and emits", async () => { + const page = await newE2EPage({ + html: ``, + }); + await page.waitForChanges(); - expect(await slider.getProperty("minValue")).toBe(25); - expect(await slider.getProperty("maxValue")).toBe(75); - expect(inputEvent).toHaveReceivedEventTimes(5); - expect(changeEvent).toHaveReceivedEventTimes(1); - }); + const slider = await page.find("calcite-slider"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + expect(await slider.getProperty("minValue")).toBe(50); + expect(await slider.getProperty("maxValue")).toBe(75); + + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + + if (layout === "horizontal") { + await page.mouse.move(trackX + 21, trackY); + await page.mouse.down(); + await page.mouse.move(trackX + 22, trackY); + await page.mouse.move(trackX + 23, trackY); + await page.mouse.move(trackX + 24, trackY); + await page.mouse.move(trackX + 25, trackY); + } else { + await page.mouse.move(trackX, trackY + 79); + await page.mouse.down(); + await page.mouse.move(trackX, trackY + 78); + await page.mouse.move(trackX, trackY + 77); + await page.mouse.move(trackX, trackY + 76); + await page.mouse.move(trackX, trackY + 75); + } + await page.waitForChanges(); + await page.mouse.up(); - it("range: clicking the track to the right of the max handle changes maxValue on mousedown, emits on mouseup", async () => { - const page = await newE2EPage({ - html: ``, + expect(await slider.getProperty("minValue")).toBe(25); + expect(await slider.getProperty("maxValue")).toBe(75); + expect(inputEvent).toHaveReceivedEventTimes(5); + expect(changeEvent).toHaveReceivedEventTimes(1); }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + it("range: clicking the track to the right of the max handle changes maxValue on mousedown, emits on mouseup", async () => { + const page = await newE2EPage({ + html: ``, + }); + await page.waitForChanges(); - expect(await slider.getProperty("minValue")).toBe(25); - expect(await slider.getProperty("maxValue")).toBe(50); + const slider = await page.find("calcite-slider"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + expect(await slider.getProperty("minValue")).toBe(25); + expect(await slider.getProperty("maxValue")).toBe(50); - await page.mouse.move(trackX + 75, trackY); - await page.mouse.down(); - await page.waitForChanges(); - await page.mouse.up(); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - expect(await slider.getProperty("minValue")).toBe(25); - expect(await slider.getProperty("maxValue")).toBe(75); - expect(changeEvent).toHaveReceivedEventTimes(1); - expect(inputEvent).toHaveReceivedEventTimes(1); - }); + layout === "horizontal" + ? await page.mouse.move(trackX + 75, trackY) + : await page.mouse.move(trackX, trackY + 25); + await page.mouse.down(); + await page.waitForChanges(); + await page.mouse.up(); - it("range: clicking and dragging the track to the right of the max handle changes maxValue on mousedown, emits on mouseup", async () => { - const page = await newE2EPage({ - html: ``, + expect(await slider.getProperty("minValue")).toBe(25); + expect(await slider.getProperty("maxValue")).toBe(75); + expect(changeEvent).toHaveReceivedEventTimes(1); + expect(inputEvent).toHaveReceivedEventTimes(1); }); - await page.waitForChanges(); - const slider = await page.find("calcite-slider"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - expect(await slider.getProperty("minValue")).toBe(25); - expect(await slider.getProperty("maxValue")).toBe(50); - - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - - await page.mouse.move(trackX + 71, trackY); - await page.mouse.down(); - await page.mouse.move(trackX + 72, trackY); - await page.mouse.move(trackX + 73, trackY); - await page.mouse.move(trackX + 74, trackY); - await page.mouse.move(trackX + 75, trackY); - await page.mouse.up(); - await page.waitForChanges(); - expect(await slider.getProperty("minValue")).toBe(25); - expect(await slider.getProperty("maxValue")).toBe(75); - expect(inputEvent).toHaveReceivedEventTimes(5); - expect(changeEvent).toHaveReceivedEventTimes(1); - }); + it("range: clicking and dragging the track to the right of the max handle changes maxValue on mousedown, emits on mouseup", async () => { + const page = await newE2EPage({ + html: ``, + }); + await page.waitForChanges(); + const slider = await page.find("calcite-slider"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + expect(await slider.getProperty("minValue")).toBe(25); + expect(await slider.getProperty("maxValue")).toBe(50); + + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + + if (layout === "horizontal") { + await page.mouse.move(trackX + 71, trackY); + await page.mouse.down(); + await page.mouse.move(trackX + 72, trackY); + await page.mouse.move(trackX + 73, trackY); + await page.mouse.move(trackX + 74, trackY); + await page.mouse.move(trackX + 75, trackY); + } else { + await page.mouse.move(trackX, trackY + 29); + await page.mouse.down(); + await page.mouse.move(trackX, trackY + 28); + await page.mouse.move(trackX, trackY + 27); + await page.mouse.move(trackX, trackY + 26); + await page.mouse.move(trackX, trackY + 25); + } + await page.mouse.up(); + await page.waitForChanges(); - it("range: clicking and dragging the range changes minValue and maxValue on mousedown, emits on mouseup", async () => { - const page = await newE2EPage(); - await page.setContent( - html``, - ); - const slider = await page.find("calcite-slider"); - const inputEvent = await slider.spyOnEvent("calciteSliderInput"); - const changeEvent = await slider.spyOnEvent("calciteSliderChange"); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - - await page.mouse.move(trackX + 25, trackY); - await page.mouse.down(); - await page.mouse.move(trackX + 26, trackY); - await page.mouse.move(trackX + 27, trackY); - await page.mouse.move(trackX + 28, trackY); - await page.mouse.move(trackX + 29, trackY); - await page.mouse.move(trackX + 30, trackY); - await page.mouse.move(trackX + 31, trackY); - await page.mouse.up(); - await page.waitForChanges(); + expect(await slider.getProperty("minValue")).toBe(25); + expect(await slider.getProperty("maxValue")).toBe(75); + expect(inputEvent).toHaveReceivedEventTimes(5); + expect(changeEvent).toHaveReceivedEventTimes(1); + }); - expect(await slider.getProperty("minValue")).toBe(5); - expect(await slider.getProperty("maxValue")).toBe(55); - expect(inputEvent).toHaveReceivedEventTimes(6); - expect(changeEvent).toHaveReceivedEventTimes(1); - }); + it("range: clicking and dragging the range changes minValue and maxValue on mousedown, emits on mouseup", async () => { + const page = await newE2EPage(); + await page.setContent( + html``, + ); + const slider = await page.find("calcite-slider"); + const inputEvent = await slider.spyOnEvent("calciteSliderInput"); + const changeEvent = await slider.spyOnEvent("calciteSliderChange"); + const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); + + if (layout === "horizontal") { + await page.mouse.move(trackX + 25, trackY); + await page.mouse.down(); + await page.mouse.move(trackX + 26, trackY); + await page.mouse.move(trackX + 27, trackY); + await page.mouse.move(trackX + 28, trackY); + await page.mouse.move(trackX + 29, trackY); + await page.mouse.move(trackX + 30, trackY); + await page.mouse.move(trackX + 31, trackY); + } else { + await page.mouse.move(trackX, trackY + 75); + await page.mouse.down(); + await page.mouse.move(trackX, trackY + 74); + await page.mouse.move(trackX, trackY + 73); + await page.mouse.move(trackX, trackY + 72); + await page.mouse.move(trackX, trackY + 71); + await page.mouse.move(trackX, trackY + 70); + await page.mouse.move(trackX, trackY + 69); + } + await page.mouse.up(); + await page.waitForChanges(); - it("does not allow text selection when slider is used", async () => { - const page = await newE2EPage({ - html: ` - `, + expect(await slider.getProperty("minValue")).toBe(5); + expect(await slider.getProperty("maxValue")).toBe(55); + expect(inputEvent).toHaveReceivedEventTimes(6); + expect(changeEvent).toHaveReceivedEventTimes(1); }); - await page.waitForChanges(); - const thumbRect = await getElementRect(page, "calcite-slider", ".thumb"); + it("does not allow text selection when slider is used", async () => { + const page = await newE2EPage({ + html: ` + `, + }); + await page.waitForChanges(); - await page.mouse.move(thumbRect.x, thumbRect.y); - await page.mouse.down(); - await page.mouse.move(thumbRect.x + 500, thumbRect.y + 200); - await page.mouse.up(); - await page.waitForChanges(); + const thumbRect = await getElementRect(page, "calcite-slider", ".thumb"); - expect(await page.evaluate(() => window.getSelection().type)).toBe("None"); - }); - }); + await page.mouse.move(thumbRect.x, thumbRect.y); + await page.mouse.down(); + await page.mouse.move(thumbRect.x + 500, thumbRect.y + 200); + await page.mouse.up(); + await page.waitForChanges(); - describe("histogram", () => { - it("creates calcite-graph with color stops", async () => { - const page = await newE2EPage({ html: `` }); + expect(await page.evaluate(() => window.getSelection().type)).toBe("None"); + }); + }); - const props = { - histogram: [ - [0, 4], - [1, 7], - [4, 6], - [6, 2], - ], - histogramStops: [ - { offset: 0, color: "red" }, - { offset: 0.5, color: "green" }, - { offset: 1, color: "blue" }, - ], - }; + describe("histogram", () => { + it("creates calcite-graph with color stops", async () => { + const page = await newE2EPage({ html: `` }); + + const props = { + histogram: [ + [0, 4], + [1, 7], + [4, 6], + [6, 2], + ], + histogramStops: [ + { offset: 0, color: "red" }, + { offset: 0.5, color: "green" }, + { offset: 1, color: "blue" }, + ], + }; - await page.$eval( - "calcite-slider", - (elm: any, { histogram, histogramStops }) => { - elm.histogram = histogram; - elm.histogramStops = histogramStops; - }, - props, - ); + await page.$eval( + "calcite-slider", + (elm: any, { histogram, histogramStops }) => { + elm.histogram = histogram; + elm.histogramStops = histogramStops; + }, + props, + ); - await page.waitForChanges(); + await page.waitForChanges(); - const graph = await page.find("calcite-slider >>> calcite-graph"); + const graph = await page.find("calcite-slider >>> calcite-graph"); - const linearGradient = await page.find("pierce/linearGradient"); - const linearGradientId = linearGradient.getAttribute("id"); + const linearGradient = await page.find("pierce/linearGradient"); + const linearGradientId = linearGradient.getAttribute("id"); - const path = await graph.find("pierce/path.graph-path"); - const fill = path.getAttribute("fill"); - expect(fill).toBe(`url(#${linearGradientId})`); + const path = await graph.find("pierce/path.graph-path"); + const fill = path.getAttribute("fill"); + expect(fill).toBe(`url(#${linearGradientId})`); - for (let i = 0; i < props.histogramStops.length; i += 1) { - const { offset, color } = props.histogramStops[i]; - const stop = await linearGradient.find(`stop[offset="${offset * 100}%"][stop-color="${color}"]`); - expect(stop).toBeTruthy(); - } + for (let i = 0; i < props.histogramStops.length; i += 1) { + const { offset, color } = props.histogramStops[i]; + const stop = await linearGradient.find(`stop[offset="${offset * 100}%"][stop-color="${color}"]`); + expect(stop).toBeTruthy(); + } + }); }); - }); - describe("when range thumbs overlap", () => { - let page: E2EPage; - let changeEvent: EventSpy; - let inputEvent: EventSpy; - let element: E2EElement; - let trackRect: DOMRect; - - const commonSliderAttrs = ` - min="5" - max="100" - step="10" - ticks="10" - label-handles - label-ticks - snap - style="width:${sliderWidthFor1To1PixelValueTrack}"`; - - async function assertValuesUnchanged(minMaxValue: number): Promise { - expect(await element.getProperty("minValue")).toBe(minMaxValue); - expect(await element.getProperty("maxValue")).toBe(minMaxValue); - expect(changeEvent).toHaveReceivedEventTimes(0); - expect(inputEvent).toHaveReceivedEventTimes(0); - } + describe("when range thumbs overlap", () => { + let page: E2EPage; + let changeEvent: EventSpy; + let inputEvent: EventSpy; + let element: E2EElement; + let trackRect: DOMRect; - async function setUpTest(sliderAttrs: string): Promise { - page = await newE2EPage(); - await page.setContent(html``); + const commonSliderAttrs = ` + min="5" + max="100" + step="10" + ticks="10" + label-handles + label-ticks + snap + style="width:${sliderWidthFor1To1PixelValueTrack}; margin-top: 100px"`; - element = await page.find("calcite-slider"); - trackRect = await getElementRect(page, "calcite-slider", `.${CSS.track}`); - changeEvent = await element.spyOnEvent("calciteSliderChange"); - inputEvent = await element.spyOnEvent("calciteSliderInput"); - } + async function assertValuesUnchanged(minMaxValue: number): Promise { + expect(await element.getProperty("minValue")).toBe(minMaxValue); + expect(await element.getProperty("maxValue")).toBe(minMaxValue); + expect(changeEvent).toHaveReceivedEventTimes(0); + expect(inputEvent).toHaveReceivedEventTimes(0); + } - describe("at min edge", () => { - const expectedValue = 5; + async function setUpTest(sliderAttrs: string): Promise { + page = await newE2EPage(); + await page.setContent(html``); - it("click/tap should grab the max value thumb", async () => { - await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}"`); + element = await page.find("calcite-slider"); + trackRect = await getElementRect(page, "calcite-slider", `.${CSS.track}`); + changeEvent = await element.spyOnEvent("calciteSliderChange"); + inputEvent = await element.spyOnEvent("calciteSliderInput"); + } - await assertValuesUnchanged(5); + describe("at min edge", () => { + const expectedValue = 5; - await page.mouse.click(trackRect.x, trackRect.y); - await page.waitForChanges(); + it("click/tap should grab the max value thumb", async () => { + await setUpTest( + `${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" layout=${layout}`, + ); - const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); + await assertValuesUnchanged(5); - expect(isMaxThumbFocused).toBe(true); - await assertValuesUnchanged(5); - }); + layout === "horizontal" + ? await page.mouse.click(trackRect.x, trackRect.y) + : await page.mouse.click(trackRect.x, trackRect.y + trackRect.height); + await page.waitForChanges(); - it("mirrored: click/tap should grab the max value thumb", async () => { - await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored`); + const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); - await assertValuesUnchanged(5); + expect(isMaxThumbFocused).toBe(true); + await assertValuesUnchanged(5); + }); - await page.mouse.click(trackRect.x + trackRect.width, trackRect.y); - await page.waitForChanges(); + it("mirrored: click/tap should grab the max value thumb", async () => { + await setUpTest( + `${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored layout=${layout}`, + ); - const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); + await assertValuesUnchanged(5); - expect(isMaxThumbFocused).toBe(true); - await assertValuesUnchanged(5); + layout === "horizontal" + ? await page.mouse.click(trackRect.x + trackRect.width, trackRect.y) + : await page.mouse.click(trackRect.x, trackRect.y); + await page.waitForChanges(); + + const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); + + expect(isMaxThumbFocused).toBe(true); + await assertValuesUnchanged(5); + }); }); - }); - describe("at max edge", () => { - const expectedValue = 100; + describe("at max edge", () => { + const expectedValue = 100; - it("click/tap should grab the min value thumb", async () => { - await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}"`); + it("click/tap should grab the min value thumb", async () => { + await setUpTest( + `${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" layout=${layout}`, + ); - await page.mouse.click(trackRect.x + trackRect.width, trackRect.y); - await page.waitForChanges(); + layout === "horizontal" + ? await page.mouse.click(trackRect.x + trackRect.width, trackRect.y) + : await page.mouse.click(trackRect.x, trackRect.y); + await page.waitForChanges(); - const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); + const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); - expect(isMinThumbFocused).toBe(true); - await assertValuesUnchanged(expectedValue); - }); + expect(isMinThumbFocused).toBe(true); + await assertValuesUnchanged(expectedValue); + }); - it("mirrored: click/tap should grab the max value thumb", async () => { - await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored`); + it("mirrored: click/tap should grab the max value thumb", async () => { + await setUpTest( + `${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored layout=${layout}`, + ); - await assertValuesUnchanged(expectedValue); + await assertValuesUnchanged(expectedValue); - await page.mouse.click(trackRect.x, trackRect.y); - await page.waitForChanges(); + layout === "horizontal" + ? await page.mouse.click(trackRect.x, trackRect.y) + : await page.mouse.click(trackRect.x, trackRect.y + trackRect.height); + await page.waitForChanges(); - const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); + const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); - expect(isMinThumbFocused).toBe(true); - await assertValuesUnchanged(expectedValue); + expect(isMinThumbFocused).toBe(true); + await assertValuesUnchanged(expectedValue); + }); }); }); - }); - describe("when a range has 0 for both minValue and maxValue", () => { - const slider = `${slider}>`; - const mirroredSlider = `
${slider} mirrored>
`; - - it("should position the minValue thumb beside the maxValue thumb", async () => { - const page = await newE2EPage({ html: nonMirroredSlider }); - const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); - const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); - const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; - const maxHandleRight = await (await maxValueThumb.getComputedStyle()).right; - expect(minHandleLeft).toBe("260px"); - expect(maxHandleRight).toBe("26px"); - }); + describe("when a range has 0 for both minValue and maxValue", () => { + const slider = `${slider}>`; + const mirroredSlider = `
${slider} mirrored>
`; + + it("should position the minValue thumb beside the maxValue thumb", async () => { + const page = await newE2EPage({ html: nonMirroredSlider.replace("{layout}", layout) }); + const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); + const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); + const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; + const maxHandleRight = await (await maxValueThumb.getComputedStyle()).right; + expect(minHandleLeft).toBe("260px"); + expect(maxHandleRight).toBe("26px"); + }); - it("should position the minValue thumb beside the maxValue thumb when mirrored", async () => { - const page = await newE2EPage({ html: mirroredSlider }); - const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); - const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); - const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; - const maxHandleRight = await (await maxValueThumb.getComputedStyle()).right; - expect(minHandleLeft).toBe("26px"); - expect(maxHandleRight).toBe("260px"); - }); + it("should position the minValue thumb beside the maxValue thumb when mirrored", async () => { + const page = await newE2EPage({ html: mirroredSlider.replace("{layout}", layout) }); + const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); + const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); + const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; + const maxHandleRight = await (await maxValueThumb.getComputedStyle()).right; + expect(minHandleLeft).toBe("26px"); + expect(maxHandleRight).toBe("260px"); + }); - it("should position the minValue thumb beside the maxValue thumb when it's a histogram range", async () => { - const page = await newE2EPage({ html: nonMirroredSlider }); - await page.$eval("calcite-slider", (slider: Slider["el"]) => { - slider.histogram = [ - [0, 0], - [20, 12], - [40, 25], - [60, 55], - [80, 10], - [100, 0], - ]; + it("should position the minValue thumb beside the maxValue thumb when it's a histogram range", async () => { + const page = await newE2EPage({ html: nonMirroredSlider }); + await page.$eval("calcite-slider", (slider: Slider["el"]) => { + slider.histogram = [ + [0, 0], + [20, 12], + [40, 25], + [60, 55], + [80, 10], + [100, 0], + ]; + }); + await page.waitForChanges(); + const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); + const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); + const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; + const maxHandleRight = await (await maxValueThumb.getComputedStyle()).right; + expect(minHandleLeft).toBe("260px"); + expect(maxHandleRight).toBe("26px"); }); - await page.waitForChanges(); - const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); - const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); - const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; - const maxHandleRight = await (await maxValueThumb.getComputedStyle()).right; - expect(minHandleLeft).toBe("260px"); - expect(maxHandleRight).toBe("26px"); }); - }); - describe("is form-associated", () => { - describe("single value", () => { - formAssociated("calcite-slider", { testValue: 5 }); - }); + describe("is form-associated", () => { + describe("single value", () => { + formAssociated("calcite-slider", { testValue: 5 }); + }); - describe("range", () => { - formAssociated("calcite-slider", { testValue: [5, 10] }); + describe("range", () => { + formAssociated("calcite-slider", { testValue: [5, 10] }); + }); }); - }); - - describe("number locale support", () => { - let page: E2EPage; - let noSeparator: string[]; - const expectedNotSeparatedValueArray = { - en: ["2500", "500000.5", "1000", "1000000.5"], - fr: ["2500", "500000,5", "1000", "1000000,5"], - }; - let withSeparator: string[]; - let getDisplayedValuesArray: () => Promise; - let element: E2EElement; - const formattedValuesPerLanguageObject = { - "de-CH": ["2’500", "500’000.5", "1’000", "1’000’000.5"], - en: ["2,500", "500,000.5", "1,000", "1,000,000.5"], - es: ["2.500", "500.000,5", "1.000", "1.000.000,5"], - fr: [ - ["2", "500"].join("\u00A0"), - ["500", "000,5"].join("\u00A0"), - ["1", "000"].join("\u00A0"), - ["1", "000", "000,5"].join("\u00A0"), - ], - hi: ["2,500", "5,00,000.5", "1,000", "10,00,000.5"], - }; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent( - html` - `, - ); - element = await page.find("calcite-slider"); - - getDisplayedValuesArray = async (): Promise => { - const labelMinVal = (await element.shadowRoot.querySelector(`.${CSS.handleLabelMinValue}`)) as HTMLElement; - const labelVal = (await element.shadowRoot.querySelector(`.${CSS.handleLabelValue}`)) as HTMLElement; - - const tickMin = (await element.shadowRoot.querySelector(`.${CSS.tickMin}`)) as HTMLElement; - const tickMax = (await element.shadowRoot.querySelector(`.${CSS.tickMax}`)) as HTMLElement; - return [labelMinVal.innerText, labelVal.innerText, tickMin.innerText, tickMax.innerText]; + describe("number locale support", () => { + let page: E2EPage; + let noSeparator: string[]; + const expectedNotSeparatedValueArray = { + en: ["2500", "500000.5", "1000", "1000000.5"], + fr: ["2500", "500000,5", "1000", "1000000,5"], + }; + let withSeparator: string[]; + let getDisplayedValuesArray: () => Promise; + let element: E2EElement; + const formattedValuesPerLanguageObject = { + "de-CH": ["2’500", "500’000.5", "1’000", "1’000’000.5"], + en: ["2,500", "500,000.5", "1,000", "1,000,000.5"], + es: ["2.500", "500.000,5", "1.000", "1.000.000,5"], + fr: [ + ["2", "500"].join("\u00A0"), + ["500", "000,5"].join("\u00A0"), + ["1", "000"].join("\u00A0"), + ["1", "000", "000,5"].join("\u00A0"), + ], + hi: ["2,500", "5,00,000.5", "1,000", "10,00,000.5"], }; - await page.exposeFunction("getDisplayedValuesArray", getDisplayedValuesArray); - }); - it("does not render separated when groupSeparator prop is false", async () => { - element.setProperty("groupSeparator", false); - await page.waitForChanges(); + beforeEach(async () => { + page = await newE2EPage(); + await page.setContent( + html` + `, + ); + element = await page.find("calcite-slider"); - noSeparator = await page.$eval("calcite-slider", async (): Promise => { - return await getDisplayedValuesArray(); - }); - expect(await element.getProperty("groupSeparator")).toBe(false); - expect(noSeparator).toEqual(expectedNotSeparatedValueArray.en); + getDisplayedValuesArray = async (): Promise => { + const labelMinVal = (await element.shadowRoot.querySelector(`.${CSS.handleLabelMinValue}`)) as HTMLElement; + const labelVal = (await element.shadowRoot.querySelector(`.${CSS.handleLabelValue}`)) as HTMLElement; - element.setProperty("lang", "fr"); - await page.waitForChanges(); + const tickMin = (await element.shadowRoot.querySelector(`.${CSS.tickMin}`)) as HTMLElement; + const tickMax = (await element.shadowRoot.querySelector(`.${CSS.tickMax}`)) as HTMLElement; - noSeparator = await page.$eval("calcite-slider", async (): Promise => { - return await getDisplayedValuesArray(); + return [labelMinVal.innerText, labelVal.innerText, tickMin.innerText, tickMax.innerText]; + }; + await page.exposeFunction("getDisplayedValuesArray", getDisplayedValuesArray); }); - expect(noSeparator).toEqual(expectedNotSeparatedValueArray.fr); - }); - it("displays group separator for multiple locales", async () => { - const testLocalizedGroupSeparator = async (lang: string, formattedValuesArr: string[]): Promise => { - element.setProperty("lang", lang); + it("does not render separated when groupSeparator prop is false", async () => { + element.setProperty("groupSeparator", false); await page.waitForChanges(); - withSeparator = await page.$eval("calcite-slider", async (): Promise => { + noSeparator = await page.$eval("calcite-slider", async (): Promise => { return await getDisplayedValuesArray(); }); - expect(withSeparator).toEqual(formattedValuesArr); - }; - - for (const lang in formattedValuesPerLanguageObject) { - await testLocalizedGroupSeparator(lang, formattedValuesPerLanguageObject[lang]); - } - }); - }); - - describe("snap + step interaction", () => { - let page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - }); - - async function dragThumbToMax(): Promise { - const trackRect = await getElementRect(page, "calcite-slider", ".track"); - const thumbRect = await getElementRect(page, "calcite-slider", ".thumb--value"); - const thumbWidth = thumbRect.width; - const trackWidth = trackRect.width; - const dragDistance = trackWidth - thumbWidth; - - await page.mouse.move(trackRect.x, trackRect.y); - await page.mouse.down(); - await page.mouse.move(trackRect.x + dragDistance, trackRect.y); - await page.mouse.up(); - await page.waitForChanges(); - } - - it("honors snap value with step", async () => { - await page.setContent(html``); - const slider = await page.find("calcite-slider"); - - expect(await slider.getProperty("value")).toBe(1); - - await dragThumbToMax(); - expect(await slider.getProperty("value")).toBe(9); - }); - - it("honors snap value with step (fractional)", async () => { - await page.setContent(html``); - const slider = await page.find("calcite-slider"); + expect(await element.getProperty("groupSeparator")).toBe(false); + expect(noSeparator).toEqual(expectedNotSeparatedValueArray.en); - expect(await slider.getProperty("value")).toBe(1.5); + element.setProperty("lang", "fr"); + await page.waitForChanges(); - await dragThumbToMax(); - expect(await slider.getProperty("value")).toBe(9.5); - }); + noSeparator = await page.$eval("calcite-slider", async (): Promise => { + return await getDisplayedValuesArray(); + }); + expect(noSeparator).toEqual(expectedNotSeparatedValueArray.fr); + }); - it("snaps to max limit beyond upper bound", async () => { - await page.setContent( - html``, - ); - const slider = await page.find("calcite-slider"); + it("displays group separator for multiple locales", async () => { + const testLocalizedGroupSeparator = async (lang: string, formattedValuesArr: string[]): Promise => { + element.setProperty("lang", lang); + await page.waitForChanges(); - expect(await slider.getProperty("value")).toBe(10); + withSeparator = await page.$eval("calcite-slider", async (): Promise => { + return await getDisplayedValuesArray(); + }); + expect(withSeparator).toEqual(formattedValuesArr); + }; - await dragThumbToMax(); - expect(await slider.getProperty("value")).toBe(10); + for (const lang in formattedValuesPerLanguageObject) { + await testLocalizedGroupSeparator(lang, formattedValuesPerLanguageObject[lang]); + } + }); }); - it("snaps to max limit at upper bound", async () => { - await page.setContent( - html``, - ); - const slider = await page.find("calcite-slider"); + describe("snap + step interaction", () => { + let page: E2EPage; - expect(await slider.getProperty("value")).toBe(10); - - await dragThumbToMax(); - expect(await slider.getProperty("value")).toBe(10); - }); - }); + beforeEach(async () => { + page = await newE2EPage(); + }); - describe("labelFormatter", () => { - const frGroupSeparator = " "; + async function dragThumbToMax(): Promise { + const trackRect = await getElementRect(page, "calcite-slider", ".track"); + const thumbRect = await getElementRect(page, "calcite-slider", ".thumb--value"); + const thumbWidth = layout === "horizontal" ? thumbRect.width : thumbRect.height; + const trackWidth = layout === "horizontal" ? trackRect.width : trackRect.height; + const dragDistance = trackWidth - thumbWidth; + + layout === "horizontal" + ? await page.mouse.move(trackRect.x, trackRect.y) + : await page.mouse.move(trackRect.x, trackRect.y + dragDistance); + await page.mouse.down(); + layout === "horizontal" + ? await page.mouse.move(trackRect.x + dragDistance, trackRect.y) + : await page.mouse.move(trackRect.x, trackRect.y - dragDistance); + await page.mouse.up(); + await page.waitForChanges(); + } - describe("single value", () => { - it("allows formatting of the handle and ticks", async () => { - const page = await newE2EPage(); + it("honors snap value with step", async () => { await page.setContent( - html` `, + html`>`, ); + const slider = await page.find("calcite-slider"); - await page.$eval( - "calcite-slider", - (slider: Slider["el"]) => - (slider.labelFormatter = (value, type) => { - if (type === "value") { - return `${value}%`; - } - - if (type === "tick") { - return "^"; - } - - return undefined; - }), - ); - await page.waitForChanges(); + expect(await slider.getProperty("value")).toBe(1); - const valueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); - const tickLabels = await page.findAll(`calcite-slider >>> .${CSS.tickLabel}`); - - expect(valueLabel.innerText).toBe("50%"); - - expect(tickLabels).toHaveLength(5); - tickLabels.forEach((tickLabel) => { - expect(tickLabel.innerText).toBe("^"); - }); + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(9); }); - it("allows formatting with the default formatter", async () => { - const page = await newE2EPage(); + it("honors snap value with step (fractional)", async () => { await page.setContent( - html` `, + html``, ); + const slider = await page.find("calcite-slider"); - await page.$eval( - "calcite-slider", - (slider: Slider["el"]) => - (slider.labelFormatter = (value, type, defaultFormatter) => { - if (type === "value") { - return defaultFormatter(value); - } - - return undefined; - }), - ); - await page.waitForChanges(); + expect(await slider.getProperty("value")).toBe(1.5); - const valueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); - - expect(valueLabel.innerText).toBe(`5${frGroupSeparator}000`); + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(9.5); }); - }); - describe("min/max value", () => { - it("allows formatting of the min/max handle and ticks", async () => { - const page = await newE2EPage(); + it("snaps to max limit beyond upper bound", async () => { await page.setContent( - html` `, ); + const slider = await page.find("calcite-slider"); - await page.$eval( - "calcite-slider", - (slider: Slider["el"]) => - (slider.labelFormatter = (value, type) => { - if (type === "min") { - return `-${value}%`; - } - - if (type === "max") { - return `+${value}%`; - } - - if (type === "tick") { - return "^"; - } - - return undefined; - }), - ); - await page.waitForChanges(); - - const minValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelMinValue}`); - const maxValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); - const tickLabels = await page.findAll(`calcite-slider >>> .${CSS.tickLabel}`); - - expect(minValueLabel.innerText).toBe("-25%"); - expect(maxValueLabel.innerText).toBe("+75%"); + expect(await slider.getProperty("value")).toBe(10); - expect(tickLabels).toHaveLength(5); - tickLabels.forEach((tickLabel) => { - expect(tickLabel.innerText).toBe("^"); - }); + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(10); }); - it("allows formatting with the default formatter", async () => { - const page = await newE2EPage(); + it("snaps to max limit at upper bound", async () => { await page.setContent( - html` `, ); + const slider = await page.find("calcite-slider"); - await page.$eval( - "calcite-slider", - (slider: Slider["el"]) => - (slider.labelFormatter = (value, type, defaultFormatter) => - type === "min" - ? // default formatting - undefined - : // using the default formatter - defaultFormatter(value)), - ); - await page.waitForChanges(); - - const minValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelMinValue}`); - const maxValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); + expect(await slider.getProperty("value")).toBe(10); - expect(minValueLabel.innerText).toBe(`2${frGroupSeparator}500`); - expect(maxValueLabel.innerText).toBe(`7${frGroupSeparator}500`); + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(10); }); }); - }); - describe("themed", () => { - describe("default", () => { - themed(html``, { - "--calcite-slider-track-color": { - shadowSelector: `.${CSS.track}`, - targetProp: "backgroundColor", - }, - "--calcite-slider-track-fill-color": { - shadowSelector: `.${CSS.trackRange}`, - targetProp: "backgroundColor", - }, - "--calcite-slider-handle-fill-color": { - shadowSelector: `.${CSS.handle}`, - targetProp: "backgroundColor", - }, + describe("labelFormatter", () => { + const frGroupSeparator = " "; + + describe("single value", () => { + it("allows formatting of the handle and ticks", async () => { + const page = await newE2EPage(); + await page.setContent( + html` `, + ); + + await page.$eval( + "calcite-slider", + (slider: Slider["el"]) => + (slider.labelFormatter = (value, type) => { + if (type === "value") { + return `${value}%`; + } + + if (type === "tick") { + return "^"; + } + + return undefined; + }), + ); + await page.waitForChanges(); + + const valueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); + const tickLabels = await page.findAll(`calcite-slider >>> .${CSS.tickLabel}`); + + expect(valueLabel.innerText).toBe("50%"); + + expect(tickLabels).toHaveLength(5); + tickLabels.forEach((tickLabel) => { + expect(tickLabel.innerText).toBe("^"); + }); + }); + + it("allows formatting with the default formatter", async () => { + const page = await newE2EPage(); + await page.setContent( + html` `, + ); + + await page.$eval( + "calcite-slider", + (slider: Slider["el"]) => + (slider.labelFormatter = (value, type, defaultFormatter) => { + if (type === "value") { + return defaultFormatter(value); + } + + return undefined; + }), + ); + await page.waitForChanges(); + + const valueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); + + expect(valueLabel.innerText).toBe(`5${frGroupSeparator}000`); + }); + }); + + describe("min/max value", () => { + it("allows formatting of the min/max handle and ticks", async () => { + const page = await newE2EPage(); + await page.setContent( + html` `, + ); + + await page.$eval( + "calcite-slider", + (slider: Slider["el"]) => + (slider.labelFormatter = (value, type) => { + if (type === "min") { + return `-${value}%`; + } + + if (type === "max") { + return `+${value}%`; + } + + if (type === "tick") { + return "^"; + } + + return undefined; + }), + ); + await page.waitForChanges(); + + const minValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelMinValue}`); + const maxValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); + const tickLabels = await page.findAll(`calcite-slider >>> .${CSS.tickLabel}`); + + expect(minValueLabel.innerText).toBe("-25%"); + expect(maxValueLabel.innerText).toBe("+75%"); + + expect(tickLabels).toHaveLength(5); + tickLabels.forEach((tickLabel) => { + expect(tickLabel.innerText).toBe("^"); + }); + }); + + it("allows formatting with the default formatter", async () => { + const page = await newE2EPage(); + await page.setContent( + html` `, + ); + + await page.$eval( + "calcite-slider", + (slider: Slider["el"]) => + (slider.labelFormatter = (value, type, defaultFormatter) => + type === "min" + ? // default formatting + undefined + : // using the default formatter + defaultFormatter(value)), + ); + await page.waitForChanges(); + + const minValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelMinValue}`); + const maxValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`); + + expect(minValueLabel.innerText).toBe(`2${frGroupSeparator}500`); + expect(maxValueLabel.innerText).toBe(`7${frGroupSeparator}500`); + }); }); }); - describe("text color", () => { - describe("should apply handle label", () => { - themed(html``, { - "--calcite-slider-text-color": { - shadowSelector: `.${CSS.handleLabel}`, - targetProp: "color", + describe("themed", () => { + describe("default", () => { + themed(html``, { + "--calcite-slider-track-color": { + shadowSelector: `.${CSS.track}`, + targetProp: "backgroundColor", + }, + "--calcite-slider-track-fill-color": { + shadowSelector: `.${CSS.trackRange}`, + targetProp: "backgroundColor", + }, + "--calcite-slider-handle-fill-color": { + shadowSelector: `.${CSS.handle}`, + targetProp: "backgroundColor", }, }); }); - describe("should apply tick labels", () => { - themed( - html``, - { + + describe("text color", () => { + describe("should apply handle label", () => { + themed(html``, { "--calcite-slider-text-color": { - shadowSelector: `.${CSS.tickLabel}`, + shadowSelector: `.${CSS.handleLabel}`, targetProp: "color", }, - }, - ); - }); - }); - - describe("handle extension", () => { - describe("should apply handle extension", () => { - themed(html``, { - "--calcite-slider-handle-extension-color": { - shadowSelector: `.${CSS.handleExtension}`, - targetProp: "backgroundColor", - }, + }); + }); + describe("should apply tick labels", () => { + themed( + html``, + { + "--calcite-slider-text-color": { + shadowSelector: `.${CSS.tickLabel}`, + targetProp: "color", + }, + }, + ); }); }); - }); - describe("ticks", () => { - describe("should apply ticks", () => { - themed( - html``, - { - "--calcite-slider-tick-color": { - shadowSelector: `.${CSS.tick}:not(.${CSS.tickActive})`, + describe("handle extension", () => { + describe("should apply handle extension", () => { + themed(html``, { + "--calcite-slider-handle-extension-color": { + shadowSelector: `.${CSS.handleExtension}`, targetProp: "backgroundColor", }, - }, - ); + }); + }); }); - describe("should apply ticks border", () => { - themed( - html``, - { - "--calcite-slider-tick-border-color": { - shadowSelector: `.${CSS.tick}`, - targetProp: "borderColor", + + describe("ticks", () => { + describe("should apply ticks", () => { + themed( + html``, + { + "--calcite-slider-tick-color": { + shadowSelector: `.${CSS.tick}:not(.${CSS.tickActive})`, + targetProp: "backgroundColor", + }, }, - }, - ); - }); - describe("should apply ticks in selected range", () => { - themed( - html``, - { - "--calcite-slider-tick-selected-color": { - shadowSelector: `.${CSS.tickActive}`, - targetProp: "backgroundColor", + ); + }); + describe("should apply ticks border", () => { + themed( + html``, + { + "--calcite-slider-tick-border-color": { + shadowSelector: `.${CSS.tick}`, + targetProp: "borderColor", + }, }, - }, - ); + ); + }); + describe("should apply ticks in selected range", () => { + themed( + html``, + { + "--calcite-slider-tick-selected-color": { + shadowSelector: `.${CSS.tickActive}`, + targetProp: "backgroundColor", + }, + }, + ); + }); }); - }); - describe("--calcite-slider-graph-color", () => { - describe("should apply graph", () => { - themed( - html` - `, - { - "--calcite-slider-graph-color": { - shadowSelector: `.${CSS.graph}`, - targetProp: "color", + describe("--calcite-slider-graph-color", () => { + describe("should apply graph", () => { + themed( + html` + `, + { + "--calcite-slider-graph-color": { + shadowSelector: `.${CSS.graph}`, + targetProp: "color", + }, }, - }, - ); + ); + }); }); }); }); diff --git a/packages/calcite-components/src/components/slider/slider.scss b/packages/calcite-components/src/components/slider/slider.scss index 8b744c8c887..5f7c0c8a03b 100644 --- a/packages/calcite-components/src/components/slider/slider.scss +++ b/packages/calcite-components/src/components/slider/slider.scss @@ -56,6 +56,10 @@ :host { @apply block; + &:host([layout="vertical"]) { + transform: rotate(-90deg); + } + // remove top padding applied by validation component. .validation-container { padding-block-start: 0 !important; @@ -138,6 +142,7 @@ content: "\2014"; display: inline-block; inline-size: 1em; + margin-inline-start: 0.1875rem; } &.hyphen--wrap { display: flex; @@ -400,6 +405,10 @@ } } +.tick__label--vertical { + transform: rotate(90deg); +} + @include form-validation-message(); @include hidden-form-input(); @include base-component(); diff --git a/packages/calcite-components/src/components/slider/slider.stories.ts b/packages/calcite-components/src/components/slider/slider.stories.ts index 699f40fb07d..3fa0fc69e28 100644 --- a/packages/calcite-components/src/components/slider/slider.stories.ts +++ b/packages/calcite-components/src/components/slider/slider.stories.ts @@ -3,7 +3,6 @@ import { html } from "../../../support/formatting"; import { iconNames } from "../../../.storybook/helpers"; import { ATTRIBUTES } from "../../../.storybook/resources"; import { Slider } from "./slider"; -import type { Slider as HTMLCalciteSliderElement } from "./slider"; const { scale, status } = ATTRIBUTES; @@ -27,6 +26,7 @@ interface SliderStoryArgs | "status" | "validationMessage" | "validationIcon" + | "layout" > { temperature: string; } @@ -51,6 +51,7 @@ export default { status: status.defaultValue, validationMessage: "", validationIcon: "", + layout: "horizontal", }, argTypes: { scale: { @@ -75,6 +76,8 @@ export default { }, }; +const verticalAdjustment = "padding-top: 200px; padding-left: 100px; height: 200px"; + export const simple = (args: SliderStoryArgs): string => html` html` status="${args.status}" validation-message="${args.validationMessage}" validation-icon="${args.validationIcon}" + layout="${args.layout}" > `; @@ -109,6 +113,21 @@ export const range = (): string => html` ticks="20" snap scale="m" + layout="horizontal" + > + `; @@ -130,6 +149,25 @@ export const darkModeMirroredRange_TestOnly = (): string => html` snap scale="m" > + `; darkModeMirroredRange_TestOnly.story = { @@ -149,6 +187,20 @@ export const rangeLabeledTicks_TestOnly = (): string => html` label-ticks snap > + `; rangeLabeledTicks_TestOnly.parameters = { @@ -168,6 +220,20 @@ export const rangeLabeledTicksOverlappingAtMax_TestOnly = (): string => html` label-ticks snap > + `; rangeLabeledTicksOverlappingAtMax_TestOnly.parameters = { @@ -187,6 +253,20 @@ export const rangeLabeledTicksOverlappingAtMin_TestOnly = (): string => html` label-ticks snap > + `; rangeLabeledTicksOverlappingAtMin_TestOnly.parameters = { @@ -206,6 +286,20 @@ export const rangeLabeledTicksEdgePositioningAtMax_TestOnly = (): string => html label-ticks snap > + `; rangeLabeledTicksEdgePositioningAtMax_TestOnly.parameters = { @@ -225,13 +319,28 @@ export const rangeLabeledTicksEdgePositioningAtMin_TestOnly = (): string => html label-ticks snap > + `; rangeLabeledTicksEdgePositioningAtMin_TestOnly.parameters = { chromatic: { diffThreshold: 1 }, }; -export const Histogram = (): HTMLCalciteSliderElement["el"]["el"] => { +export const Histogram = (): HTMLDivElement => { + const div = document.createElement("div"); const slider = document.createElement("calcite-slider"); slider.min = -100; slider.minValue = -33.32; @@ -248,10 +357,33 @@ export const Histogram = (): HTMLCalciteSliderElement["el"]["el"] => { slider.ticks = 10; slider.scale = "m"; slider.style.minWidth = "60vw"; - return slider; + div.appendChild(slider); + const sliderVertical = document.createElement("calcite-slider"); + sliderVertical.min = -100; + sliderVertical.minValue = -33.32; + sliderVertical.max = 100; + sliderVertical.maxValue = 30.87; + sliderVertical.histogram = [ + [-90, 0], + [-60, 12], + [-20, 25], + [20, 55], + [60, 10], + [90, 0], + ] as any; + sliderVertical.ticks = 10; + sliderVertical.scale = "m"; + sliderVertical.style.minWidth = "60vw"; + sliderVertical.style.paddingTop = "350px"; + sliderVertical.style.paddingLeft = "100px"; + sliderVertical.style.height = "600px"; + sliderVertical.layout = "vertical"; + div.appendChild(sliderVertical); + return div; }; -export const HistogramWithColors = (): HTMLCalciteSliderElement["el"]["el"] => { +export const HistogramWithColors = (): HTMLDivElement => { + const div = document.createElement("div"); const slider = document.createElement("calcite-slider"); slider.min = 0; slider.minValue = 35; @@ -270,10 +402,33 @@ export const HistogramWithColors = (): HTMLCalciteSliderElement["el"]["el"] => { const offsets = colors.map((_, i) => `${(1 / (colors.length - 1)) * i}`); slider.histogramStops = colors.map((color, i) => ({ offset: parseFloat(offsets[i]), color })); slider.scale = "m"; - return slider; + div.appendChild(slider); + const sliderVertical = document.createElement("calcite-slider"); + sliderVertical.min = 0; + sliderVertical.minValue = 35; + sliderVertical.max = 100; + sliderVertical.maxValue = 55; + sliderVertical.histogram = [ + [0, 0], + [20, 12], + [40, 25], + [60, 55], + [80, 10], + [100, 0], + ] as any; + sliderVertical.style.minWidth = "60vw"; + sliderVertical.histogramStops = colors.map((color, i) => ({ offset: parseFloat(offsets[i]), color })); + sliderVertical.scale = "m"; + sliderVertical.style.paddingTop = "350px"; + sliderVertical.style.paddingLeft = "100px"; + sliderVertical.style.height = "600px"; + sliderVertical.layout = "vertical"; + div.appendChild(sliderVertical); + return div; }; -export const darkModeHistogramRTL_TestOnly = (): HTMLCalciteSliderElement["el"]["el"] => { +export const darkModeHistogramRTL_TestOnly = (): HTMLDivElement => { + const div = document.createElement("div"); const slider = document.createElement("calcite-slider"); slider.min = 0; slider.minValue = 25; @@ -291,27 +446,66 @@ export const darkModeHistogramRTL_TestOnly = (): HTMLCalciteSliderElement["el"][ slider.scale = "m"; slider.style.minWidth = "60vw"; slider.className = "calcite-mode-dark"; - return slider; + div.appendChild(slider); + const sliderVertical = document.createElement("calcite-slider"); + sliderVertical.min = 0; + sliderVertical.minValue = 25; + sliderVertical.max = 100; + sliderVertical.maxValue = 75; + sliderVertical.histogram = [ + [0, 0], + [20, 12], + [40, 25], + [60, 55], + [80, 10], + [100, 0], + ]; + sliderVertical.ticks = 10; + sliderVertical.scale = "m"; + sliderVertical.style.minWidth = "60vw"; + sliderVertical.className = "calcite-mode-dark"; + sliderVertical.style.paddingTop = "350px"; + sliderVertical.style.paddingLeft = "100px"; + sliderVertical.style.height = "600px"; + sliderVertical.layout = "vertical"; + div.appendChild(sliderVertical); + return div; }; darkModeHistogramRTL_TestOnly.parameters = { themes: modesDarkDefault }; -export const disabled_TestOnly = (): string => html``; +export const disabled_TestOnly = (): string => + html` + `; export const wordBreakDoesNotAffectLabels_TestOnly = (): string => html``; + min="-100" + max="100" + min-value="-100" + max-value="100" + step="10" + ticks="10" + label-handles + label-ticks + style="word-break: break-all" + > + `; -export const WithLabelHandlesAndNoValue_TestOnly = (): string => html` `; +export const WithLabelHandlesAndNoValue_TestOnly = (): string => + html` + `; export const WithLargeFontSize_TestOnly = (): string => html` @@ -323,107 +517,261 @@ export const WithLargeFontSize_TestOnly = (): string => calcite-label { padding: 10px; } + calcite-slider[layout="vertical"] { + height: 100px; + margin-top: 90px; + } + div.container { + display: flex; + } + div.side { + flex: 1; + } -
- - precise with label-handles - - - precise with label-handles mirrored - - - - precise with label-handles & label-ticks - - - - precise with label-handles & label-ticks mirrored - - - - range slider with label-handles & label-ticks - - - - precise range slider with label-handles & label-ticks - - - - precise range slider with label-handles & label-ticks mirrored - - +
+
+
+ + precise with label-handles + +
+
+ + precise with label-handles mirrored + + +
+
+ + precise with label-handles & label-ticks + + +
+
+ + precise with label-handles & label-ticks mirrored + + +
+
+ + range slider with label-handles & label-ticks + + +
+
+ + precise range slider with label-handles & label-ticks + + +
+
+ + precise range slider with label-handles & label-ticks mirrored + + +
+
+
+
+ + precise with label-handles + +
+
+ + precise with label-handles mirrored + + +
+
+ + precise with label-handles & label-ticks + + +
+
+ + precise with label-handles & label-ticks mirrored + + +
+
+ + range slider with label-handles & label-ticks + + +
+
+ + precise range slider with label-handles & label-ticks + + +
+
+ + precise range slider with label-handles & label-ticks mirrored + + +
+
`; @@ -433,35 +781,130 @@ export const maxTickRendering_TestOnly = (): string => html` calcite-slider { width: 60vw; } + div.container { + display: flex; + } + div.side { + flex: 1; + } + div.side.vertical calcite-slider { + height: 100px; + width: 400px; + max-width: none; + margin-bottom: 350px; + } + div.side.vertical calcite-slider.first { + margin-top: 200px; + } - - - - - - - - - - - - +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
`; export const rendersWhenTrackRelatedPropChanges_TestOnly = (): string => html` + `; @@ -481,6 +924,18 @@ export const spaceGroupSeparatorNoBreak_TestOnly = (): string => html` max="10000" ticks="2000" > + `; export const fillPlacements = (): string => html` @@ -615,86 +1070,238 @@ export const fillPlacements = (): string => html` > `; +export const fillPlacementsVertical = (): string => html` + +
+

single

+

start (default)

+ +
+ + + + + + +
+

none

+
+ + + + + + +
+

end

+
+ + + + + + +
+

range

+

start (default)

+ +
+ + + + + + +
+

none

+
+ + + + + + +
+

end

+
+ + + + + + +
+
+`; + export const customLabelsAndTicks = (): string => html` - - + +
+
+ + - - + + -
-
+
+
- - + + - - + + +
- `; diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index 1ae5bdde64f..144786991f7 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -100,7 +100,9 @@ export class Slider event.preventDefault(); if (this.dragProp) { - const value = this.mapToRange(event.clientX || event.pageX); + const valueToTranslate = + this.layout === "horizontal" ? event.clientX || event.pageX : event.clientY || event.pageY; + const value = this.mapToRange(valueToTranslate); if (isRange(this.value) && this.dragProp === "minMaxValue") { if (this.minValueDragRange && this.maxValueDragRange && this.minMaxValueRange) { const newMinValue = value - this.minValueDragRange; @@ -129,9 +131,9 @@ export class Slider formEl: HTMLFormElement; /** - * Returns a string representing the localized label value based if the groupSeparator prop is parsed. + * @returns Returns a string representing the localized label value based if the groupSeparator prop is parsed. * - * @param value + * @param value the value to format */ private formatValue = (value: number): string => { numberStringFormatter.numberFormatOptions = { @@ -319,6 +321,9 @@ export class Slider /** The component's value. */ @property({ type: Number, reflect: true }) value: null | number | number[] = 0; + /** The orientation of the slider */ + @property({ reflect: true }) layout: "horizontal" | "vertical" = "horizontal"; + // #endregion // #region Public Methods @@ -477,8 +482,9 @@ export class Slider return; } - const x = event.clientX || event.pageX; - const position = this.mapToRange(x); + const pointerPos = + this.layout === "horizontal" ? event.clientX || event.pageX : event.clientY || event.pageY; + const position = this.mapToRange(pointerPos); let prop: ActiveSliderProperty = "value"; if (isRange(this.value)) { const inRange = position >= this.minValue && position <= this.maxValue; @@ -495,7 +501,7 @@ export class Slider if (!isThumbActive) { this.setValue({ [prop as SetValueProperty]: this.clamp(position, prop) }); } - this.focusActiveHandle(x); + this.focusActiveHandle(pointerPos); } private handleTouchStart(event: TouchEvent): void { @@ -737,13 +743,16 @@ export class Slider /** * Translate a pixel position to value along the range * - * @param x + * @param pixel * @private */ - private mapToRange(x: number): number { + private mapToRange(pixel: number): number { const range = this.max - this.min; - const { left, width } = this.trackEl.getBoundingClientRect(); - const percent = (x - left) / width; + const rect = this.trackEl.getBoundingClientRect(); + const percent = + this.layout === "horizontal" + ? (pixel - rect.left) / rect.width + : (rect.bottom - pixel) / rect.height; const mirror = this.shouldMirror(); const clampedValue = this.clamp(this.min + range * (mirror ? 1 - percent : percent)); const value = Number(clampedValue.toFixed(decimalPlaces(this.step))); @@ -803,7 +812,9 @@ export class Slider } private adjustHostObscuredHandleLabel(name: "value" | "minValue"): void { - const label: HTMLSpanElement = this.el.shadowRoot.querySelector(`.handle__label--${name}`); + const label: HTMLSpanElement = this.el.shadowRoot.querySelector( + `.handle__label--${name}:not(.static):not(.transformed)`, + ); const labelStatic: HTMLSpanElement = this.el.shadowRoot.querySelector( `.handle__label--${name}.static`, ); @@ -811,12 +822,34 @@ export class Slider `.handle__label--${name}.transformed`, ); const labelStaticBounds = labelStatic.getBoundingClientRect(); - const labelStaticOffset = this.getHostOffset(labelStaticBounds.left, labelStaticBounds.right); - label.style.transform = `translateX(${labelStaticOffset}px)`; - labelTransformed.style.transform = `translateX(${labelStaticOffset}px)`; + const labelStaticOffset = this.getHostOffset( + this.layout === "horizontal" ? labelStaticBounds.left : labelStaticBounds.top, + this.layout === "horizontal" ? labelStaticBounds.right : labelStaticBounds.bottom, + ); + + let verticalLabelStaticOffset = 0; + if (this.layout === "vertical" && !this.precise) { + const trackBoundingBox = this.trackEl.getBoundingClientRect(); + verticalLabelStaticOffset = Math.max(labelStaticBounds.right - trackBoundingBox.left + 8, 0); + } + + const transform = + this.layout === "vertical" + ? `translateX(${labelStaticOffset}px) translateY(-${verticalLabelStaticOffset}px) rotate(90deg)` + : `translateX(${labelStaticOffset}px)`; + label.style.transform = transform; + labelTransformed.style.transform = transform; } private hyphenateCollidingRangeHandleLabels(): void { + if (this.layout === "horizontal") { + this.hyphenateHorizontalCollidingRangeHandleLabels(); + } else { + this.hyphenateVerticalCollidingRangeHandleLabels(); + } + } + + private hyphenateHorizontalCollidingRangeHandleLabels(): void { const { shadowRoot } = this.el; const mirror = this.shouldMirror(); @@ -934,6 +967,61 @@ export class Slider } } + private hyphenateVerticalCollidingRangeHandleLabels(): void { + const minHandle: HTMLDivElement | null = this.el.shadowRoot.querySelector( + `.${CSS.thumbMinValue}`, + ); + const maxHandle: HTMLDivElement | null = this.el.shadowRoot.querySelector(`.${CSS.thumbValue}`); + + if (!minHandle || !maxHandle) { + return; + } + + const minHandleBounds = minHandle.getBoundingClientRect(); + const maxHandleBounds = maxHandle.getBoundingClientRect(); + const { shadowRoot } = this.el; + const mirror = this.shouldMirror(); + const leftModifier = mirror ? "value" : "minValue"; + const rightModifier = mirror ? "minValue" : "value"; + + const leftValueLabel: HTMLSpanElement = shadowRoot.querySelector( + `.handle__label--${leftModifier}`, + ); + const leftValueLabelStatic: HTMLSpanElement = shadowRoot.querySelector( + `.handle__label--${leftModifier}.static`, + ); + const rightValueLabel: HTMLSpanElement = shadowRoot.querySelector( + `.handle__label--${rightModifier}`, + ); + const rightValueLabelStatic: HTMLSpanElement = shadowRoot.querySelector( + `.handle__label--${rightModifier}.static`, + ); + + if (intersects(minHandleBounds, maxHandleBounds)) { + leftValueLabel.textContent = `${leftValueLabelStatic.textContent} \u2014 ${rightValueLabelStatic.textContent}`; + rightValueLabel.style.visibility = "hidden"; + const width = this.getStringWidth( + leftValueLabel.textContent, + window.getComputedStyle(leftValueLabel)["font"], + ); + leftValueLabel.style.transform = `translateY(-${width / 2}px) rotate(90deg)`; + leftValueLabel.style.width = `${width}px`; + } else if (rightValueLabel.style.visibility === "hidden") { + leftValueLabel.textContent = leftValueLabelStatic.textContent; + leftValueLabel.style.transform = leftValueLabelStatic.style.transform; + leftValueLabel.style.width = leftValueLabelStatic.style.width; + rightValueLabel.style.visibility = "visible"; + } + } + + private getStringWidth(text: string, font: string): number { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + context.font = font; + const metrics = context.measureText(text); + return metrics.width; + } + /** Hides bounding tick labels that are obscured by either handle. */ private hideObscuredBoundingTickLabels(): void { const valueIsRange = isRange(this.value); @@ -988,47 +1076,55 @@ export class Slider } /** - * Returns an integer representing the number of pixels to offset on the left or right side based on desired position behavior. + * Returns an integer representing the number of pixels to offset on the left/top or right/bottom side based on desired position behavior. * - * @param leftBounds - * @param rightBounds + * @param startBounds + * @param endBounds * @private */ - private getHostOffset(leftBounds: number, rightBounds: number): number { - const { left, right } = this.el.getBoundingClientRect(); + private getHostOffset(startBounds: number, endBounds: number): number { + const hostBounds = this.el.getBoundingClientRect(); + const hostStart = this.layout === "horizontal" ? hostBounds.left : hostBounds.top; + const hostEnd = this.layout === "horizontal" ? hostBounds.right : hostBounds.bottom; - if (leftBounds < left) { - return left - leftBounds; + if (startBounds < hostStart) { + return hostStart - startBounds; } - if (rightBounds > right) { - return -(rightBounds - right); + if (endBounds > hostEnd) { + return -(endBounds - hostEnd); } return 0; } /** - * Returns an integer representing the number of pixels that the two given span elements are overlapping, taking into account + * @returns an integer representing the number of pixels that the two given span elements are overlapping, taking into account * a space in between the two spans equal to the font-size set on them to account for the space needed to render a hyphen. * - * @param leftLabel - * @param rightLabel + * @param leftLabel the left label element + * @param rightLabel the right label element */ private getRangeLabelOverlap(leftLabel: HTMLSpanElement, rightLabel: HTMLSpanElement): number { const leftLabelBounds = leftLabel.getBoundingClientRect(); const rightLabelBounds = rightLabel.getBoundingClientRect(); - const leftLabelFontSize = this.getFontSizeForElement(leftLabel); - const rangeLabelOverlap = leftLabelBounds.right + leftLabelFontSize - rightLabelBounds.left; + const labelFontSize = + this.layout === "horizontal" + ? this.getFontSizeForElement(leftLabel) + : this.getFontSizeForElement(rightLabel); + const rangeLabelOverlap = + this.layout === "horizontal" + ? leftLabelBounds.right + labelFontSize - rightLabelBounds.left + : rightLabelBounds.bottom + labelFontSize - leftLabelBounds.top; return Math.max(rangeLabelOverlap, 0); } /** - * Returns a boolean value representing if the minLabel span element is obscured (being overlapped) by the given handle div element. + * @returns a boolean value representing if the minLabel span element is obscured (being overlapped) by the given handle div element. * - * @param minLabel - * @param handle + * @param minLabel the minimum label element + * @param handle the handle element */ private isMinTickLabelObscured(minLabel: HTMLSpanElement, handle: HTMLDivElement): boolean { const minLabelBounds = minLabel.getBoundingClientRect(); @@ -1037,10 +1133,10 @@ export class Slider } /** - * Returns a boolean value representing if the maxLabel span element is obscured (being overlapped) by the given handle div element. + * @returns a boolean value representing if the maxLabel span element is obscured (being overlapped) by the given handle div element. * - * @param maxLabel - * @param handle + * @param maxLabel the maximum label element + * @param handle the handle element */ private isMaxTickLabelObscured(maxLabel: HTMLSpanElement, handle: HTMLDivElement): boolean { const maxLabelBounds = maxLabel.getBoundingClientRect(); @@ -1081,7 +1177,8 @@ export class Slider const thumbTypes = this.buildThumbType("max"); const thumb = this.renderThumb({ type: thumbTypes, - thumbPlacement: thumbTypes.includes("histogram") ? "below" : "above", + thumbPlacement: + this.layout === "horizontal" && thumbTypes.includes("histogram") ? "below" : "above", maxInterval, minInterval, mirror, @@ -1092,7 +1189,9 @@ export class Slider ? this.renderThumb({ type: minThumbTypes, thumbPlacement: - minThumbTypes.includes("histogram") || minThumbTypes.includes("precise") + (this.layout === "horizontal" && + (minThumbTypes.includes("histogram") || minThumbTypes.includes("precise"))) || + (this.layout === "vertical" && valueIsRange && this.precise) ? "below" : "above", maxInterval, @@ -1233,7 +1332,12 @@ export class Slider const labels = isLabeled ? [ - + , @@ -1258,7 +1362,7 @@ export class Slider return (
{this.internalLabelFormatter(tick, "tick")} diff --git a/packages/calcite-components/src/demos/slider.html b/packages/calcite-components/src/demos/slider.html index b7344a04996..c1d8c53443c 100644 --- a/packages/calcite-components/src/demos/slider.html +++ b/packages/calcite-components/src/demos/slider.html @@ -13,6 +13,14 @@ margin: 25px 0; } + .parent-vertical { + height: 25rem; + + calcite-slider { + width: 25rem; + } + } + .font, .header { color: var(--calcite-color-text-3); @@ -1663,6 +1671,42 @@

Slider


+
+
vertical slider
+ +
+ +
+
+ +
+
+ +
+
histogram
From a93e2332a06c4ea839d3562bcb024b1e6add1bc8 Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Wed, 11 Dec 2024 11:22:18 -0800 Subject: [PATCH 02/10] Fixed the separation between the vertical sliders; also fixed the display of the long labels for horizontal and vertical --- .../src/components/slider/slider.stories.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.stories.ts b/packages/calcite-components/src/components/slider/slider.stories.ts index 3fa0fc69e28..37255af53aa 100644 --- a/packages/calcite-components/src/components/slider/slider.stories.ts +++ b/packages/calcite-components/src/components/slider/slider.stories.ts @@ -1190,11 +1190,11 @@ export const customLabelsAndTicks = (): string => html`
- + html` step="1" min-label="Temperature" layout="vertical" - style="margin-top: 6.25rem" + style="margin-top: 6.25rem; margin-bottom: 7.5rem" > -
-
- @@ -1254,7 +1251,7 @@ export const customLabelsAndTicks = (): string => html` min-value="0" max-value="100" layout="vertical" - style="margin-top: 6.25rem" + style="margin-top: 7.25rem; margin-bottom: 7.5rem" >
From 9eaec7b3ea652d699f8c115449ec7997cc9c5fc8 Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Mon, 16 Dec 2024 08:46:50 -0800 Subject: [PATCH 03/10] Improved display of dash separating handle values --- .../src/components/slider/slider.tsx | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index 144786991f7..7a0ffc26297 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -982,35 +982,19 @@ export class Slider const { shadowRoot } = this.el; const mirror = this.shouldMirror(); const leftModifier = mirror ? "value" : "minValue"; - const rightModifier = mirror ? "minValue" : "value"; - const leftValueLabel: HTMLSpanElement = shadowRoot.querySelector( `.handle__label--${leftModifier}`, ); - const leftValueLabelStatic: HTMLSpanElement = shadowRoot.querySelector( - `.handle__label--${leftModifier}.static`, - ); - const rightValueLabel: HTMLSpanElement = shadowRoot.querySelector( - `.handle__label--${rightModifier}`, - ); - const rightValueLabelStatic: HTMLSpanElement = shadowRoot.querySelector( - `.handle__label--${rightModifier}.static`, - ); if (intersects(minHandleBounds, maxHandleBounds)) { - leftValueLabel.textContent = `${leftValueLabelStatic.textContent} \u2014 ${rightValueLabelStatic.textContent}`; - rightValueLabel.style.visibility = "hidden"; + leftValueLabel.classList.add(CSS.hyphen, CSS.hyphenWrap); const width = this.getStringWidth( leftValueLabel.textContent, window.getComputedStyle(leftValueLabel)["font"], ); - leftValueLabel.style.transform = `translateY(-${width / 2}px) rotate(90deg)`; - leftValueLabel.style.width = `${width}px`; - } else if (rightValueLabel.style.visibility === "hidden") { - leftValueLabel.textContent = leftValueLabelStatic.textContent; - leftValueLabel.style.transform = leftValueLabelStatic.style.transform; - leftValueLabel.style.width = leftValueLabelStatic.style.width; - rightValueLabel.style.visibility = "visible"; + leftValueLabel.style.transform = `translateY(-${width / 2 - 4}px) rotate(90deg)`; + } else { + leftValueLabel.classList.remove(CSS.hyphen, CSS.hyphenWrap); } } From 27ff97315e2e6bfb1e0822d0ecf6165e5d973e29 Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Mon, 13 Jan 2025 17:32:18 -0800 Subject: [PATCH 04/10] Moved host rule per code review --- .../calcite-components/src/components/slider/slider.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.scss b/packages/calcite-components/src/components/slider/slider.scss index 5f7c0c8a03b..eb63adf0c6d 100644 --- a/packages/calcite-components/src/components/slider/slider.scss +++ b/packages/calcite-components/src/components/slider/slider.scss @@ -56,16 +56,16 @@ :host { @apply block; - &:host([layout="vertical"]) { - transform: rotate(-90deg); - } - // remove top padding applied by validation component. .validation-container { padding-block-start: 0 !important; } } +:host([layout="vertical"]) { + transform: rotate(-90deg); +} + .container { @apply relative block break-normal; padding-inline: calc(var(--calcite-slider-handle-size) * 0.5); From ba3ee46815430de45407acee79f720a0cde89b97 Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Tue, 14 Jan 2025 15:30:04 -0800 Subject: [PATCH 05/10] Fixed spacing around hyphen of range label; reused getTextWidth; removed mention of slider in comment; moved definition of shadowRoot up --- .../src/components/slider/slider.scss | 6 ++++- .../src/components/slider/slider.tsx | 24 ++++++------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.scss b/packages/calcite-components/src/components/slider/slider.scss index eb63adf0c6d..b5f2cdcc84f 100644 --- a/packages/calcite-components/src/components/slider/slider.scss +++ b/packages/calcite-components/src/components/slider/slider.scss @@ -64,6 +64,10 @@ :host([layout="vertical"]) { transform: rotate(-90deg); + + .thumb .handle__label.hyphen::after { + margin-inline-start: 8px; + } } .container { @@ -142,7 +146,7 @@ content: "\2014"; display: inline-block; inline-size: 1em; - margin-inline-start: 0.1875rem; + margin-inline-start: 3px; } &.hyphen--wrap { display: flex; diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index 1d1d9b2aab5..2d428073013 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -43,6 +43,7 @@ import { BigDecimal } from "../../utils/number"; import { IconNameOrString } from "../icon/interfaces"; import { useT9n } from "../../controllers/useT9n"; import type { Label } from "../label/label"; +import { getTextWidth } from "../../utils/dom"; import { CSS, IDS, maxTickElementThreshold } from "./resources"; import { ActiveSliderProperty, SetValueProperty, SideOffset, ThumbType } from "./interfaces"; import { styles } from "./slider.scss"; @@ -322,7 +323,7 @@ export class Slider /** The component's value. */ @property({ type: Number, reflect: true }) value: null | number | number[] = 0; - /** The orientation of the slider */ + /** Defines the layout of the component */ @property({ reflect: true }) layout: "horizontal" | "vertical" = "horizontal"; // #endregion @@ -969,10 +970,9 @@ export class Slider } private hyphenateVerticalCollidingRangeHandleLabels(): void { - const minHandle: HTMLDivElement | null = this.el.shadowRoot.querySelector( - `.${CSS.thumbMinValue}`, - ); - const maxHandle: HTMLDivElement | null = this.el.shadowRoot.querySelector(`.${CSS.thumbValue}`); + const { shadowRoot } = this.el; + const minHandle: HTMLDivElement | null = shadowRoot.querySelector(`.${CSS.thumbMinValue}`); + const maxHandle: HTMLDivElement | null = shadowRoot.querySelector(`.${CSS.thumbValue}`); if (!minHandle || !maxHandle) { return; @@ -980,16 +980,14 @@ export class Slider const minHandleBounds = minHandle.getBoundingClientRect(); const maxHandleBounds = maxHandle.getBoundingClientRect(); - const { shadowRoot } = this.el; - const mirror = this.shouldMirror(); - const leftModifier = mirror ? "value" : "minValue"; + const leftModifier = this.shouldMirror() ? "value" : "minValue"; const leftValueLabel: HTMLSpanElement = shadowRoot.querySelector( `.handle__label--${leftModifier}`, ); if (intersects(minHandleBounds, maxHandleBounds)) { leftValueLabel.classList.add(CSS.hyphen, CSS.hyphenWrap); - const width = this.getStringWidth( + const width = getTextWidth( leftValueLabel.textContent, window.getComputedStyle(leftValueLabel)["font"], ); @@ -999,14 +997,6 @@ export class Slider } } - private getStringWidth(text: string, font: string): number { - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - context.font = font; - const metrics = context.measureText(text); - return metrics.width; - } - /** Hides bounding tick labels that are obscured by either handle. */ private hideObscuredBoundingTickLabels(): void { const valueIsRange = isRange(this.value); From 0f0c3fd13b51dbd775ffcf68d86b9f0fa52bc257 Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Tue, 14 Jan 2025 16:07:24 -0800 Subject: [PATCH 06/10] Changed how the shorthand font is obtained --- .../calcite-components/src/components/slider/slider.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index 2d428073013..9e535ff6f2a 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -987,10 +987,10 @@ export class Slider if (intersects(minHandleBounds, maxHandleBounds)) { leftValueLabel.classList.add(CSS.hyphen, CSS.hyphenWrap); - const width = getTextWidth( - leftValueLabel.textContent, - window.getComputedStyle(leftValueLabel)["font"], - ); + const computedStyle = getComputedStyle(leftValueLabel); + // we recreate the shorthand vs using computedStyle.font because browsers will return "" instead of the expected value + const shorthandFont = `${computedStyle.fontStyle} ${computedStyle.fontVariant} ${computedStyle.fontWeight} ${computedStyle.fontSize}/${computedStyle.lineHeight} ${computedStyle.fontFamily}`; + const width = getTextWidth(leftValueLabel.textContent, shorthandFont); leftValueLabel.style.transform = `translateY(-${width / 2 - 4}px) rotate(90deg)`; } else { leftValueLabel.classList.remove(CSS.hyphen, CSS.hyphenWrap); From cf773447672907364f0befb04d149986e57d9bd7 Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Tue, 14 Jan 2025 16:11:05 -0800 Subject: [PATCH 07/10] Changed name of variable --- .../calcite-components/src/components/slider/slider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index 9e535ff6f2a..310e7dfc292 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -484,9 +484,9 @@ export class Slider return; } - const pointerPos = + const coordinate = this.layout === "horizontal" ? event.clientX || event.pageX : event.clientY || event.pageY; - const position = this.mapToRange(pointerPos); + const position = this.mapToRange(coordinate); let prop: ActiveSliderProperty = "value"; if (isRange(this.value)) { const inRange = position >= this.minValue && position <= this.maxValue; @@ -503,7 +503,7 @@ export class Slider if (!isThumbActive) { this.setValue({ [prop as SetValueProperty]: this.clamp(position, prop) }); } - this.focusActiveHandle(pointerPos); + this.focusActiveHandle(coordinate); } private handleTouchStart(event: TouchEvent): void { From 9eb46f69790dc72b6fd74146148b463f0ed5349f Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Wed, 15 Jan 2025 09:54:36 -0800 Subject: [PATCH 08/10] Changed name of 'pixel' to 'value': --- .../src/components/slider/slider.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index 310e7dfc292..a15fa50c32d 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -745,21 +745,21 @@ export class Slider /** * Translate a pixel position to value along the range * - * @param pixel + * @param value * @private */ - private mapToRange(pixel: number): number { + private mapToRange(value: number): number { const range = this.max - this.min; const rect = this.trackEl.getBoundingClientRect(); const percent = this.layout === "horizontal" - ? (pixel - rect.left) / rect.width - : (rect.bottom - pixel) / rect.height; + ? (value - rect.left) / rect.width + : (rect.bottom - value) / rect.height; const mirror = this.shouldMirror(); const clampedValue = this.clamp(this.min + range * (mirror ? 1 - percent : percent)); - const value = Number(clampedValue.toFixed(decimalPlaces(this.step))); + const mappedValue = Number(clampedValue.toFixed(decimalPlaces(this.step))); - return !(this.snap && this.step) ? value : this.getClosestStep(value); + return !(this.snap && this.step) ? mappedValue : this.getClosestStep(mappedValue); } /** From 46f4fe7aea352ba70c6e7e51b2d2292d9d48251d Mon Sep 17 00:00:00 2001 From: Jose Carcamo Date: Wed, 15 Jan 2025 15:14:41 -0800 Subject: [PATCH 09/10] Removed style to preview-head.html --- packages/calcite-components/.storybook/preview-head.html | 5 +++++ .../src/components/slider/slider.stories.ts | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/calcite-components/.storybook/preview-head.html b/packages/calcite-components/.storybook/preview-head.html index 5391aee10ea..fc41e81a52c 100644 --- a/packages/calcite-components/.storybook/preview-head.html +++ b/packages/calcite-components/.storybook/preview-head.html @@ -13,6 +13,11 @@ max-width: 100%; } + calcite-slider[layout="vertical"] { + height: 100px; + margin-top: 90px; + } + calcite-meter { width: 450px; max-width: 100%; diff --git a/packages/calcite-components/src/components/slider/slider.stories.ts b/packages/calcite-components/src/components/slider/slider.stories.ts index 37255af53aa..0e63d77aa64 100644 --- a/packages/calcite-components/src/components/slider/slider.stories.ts +++ b/packages/calcite-components/src/components/slider/slider.stories.ts @@ -517,10 +517,6 @@ export const WithLargeFontSize_TestOnly = (): string => calcite-label { padding: 10px; } - calcite-slider[layout="vertical"] { - height: 100px; - margin-top: 90px; - } div.container { display: flex; } From 7a08c49330a61bfbb5ac4b60c60b0a16ae818acc Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Thu, 16 Jan 2025 13:54:26 -0800 Subject: [PATCH 10/10] test: fix linting errors --- .../src/components/slider/slider.e2e.ts | 90 ++++++++----------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/packages/calcite-components/src/components/slider/slider.e2e.ts b/packages/calcite-components/src/components/slider/slider.e2e.ts index 8bb31dee22b..9b5ccd92590 100644 --- a/packages/calcite-components/src/components/slider/slider.e2e.ts +++ b/packages/calcite-components/src/components/slider/slider.e2e.ts @@ -243,11 +243,9 @@ describe("calcite-slider", () => { await page.mouse.move(trackRect.x, trackRect.y); await page.mouse.down(); - if (layout === "horizontal") { - await page.mouse.move(trackRect.x + 5, trackRect.y); - } else { - await page.mouse.move(trackRect.x, trackRect.y + 95); - } + await (layout === "horizontal" + ? page.mouse.move(trackRect.x + 5, trackRect.y) + : page.mouse.move(trackRect.x, trackRect.y + 95)); await page.mouse.up(); await page.waitForChanges(); @@ -372,9 +370,7 @@ describe("calcite-slider", () => { const slider = await page.find("calcite-slider"); const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - layout === "horizontal" - ? await page.mouse.move(trackX + 50, trackY) - : await page.mouse.move(trackX, trackY + 50); + await (layout === "horizontal" ? page.mouse.move(trackX + 50, trackY) : page.mouse.move(trackX, trackY + 50)); await page.mouse.down(); await page.mouse.up(); await page.waitForChanges(); @@ -386,9 +382,7 @@ describe("calcite-slider", () => { expect(isThumbFocused).toBe(true); expect(await slider.getProperty("value")).toBe(50); - layout === "horizontal" - ? await page.mouse.move(trackX + 40, trackY) - : await page.mouse.move(trackX, trackY + 60); + await (layout === "horizontal" ? page.mouse.move(trackX + 40, trackY) : page.mouse.move(trackX, trackY + 60)); await page.mouse.down(); await page.mouse.up(); await page.waitForChanges(); @@ -400,9 +394,7 @@ describe("calcite-slider", () => { expect(isThumbFocused).toBe(true); expect(await slider.getProperty("value")).toBe(40); - layout === "horizontal" - ? await page.mouse.move(trackX + 60, trackY) - : await page.mouse.move(trackX, trackY + 40); + await (layout === "horizontal" ? page.mouse.move(trackX + 60, trackY) : page.mouse.move(trackX, trackY + 40)); await page.mouse.down(); await page.mouse.up(); await page.waitForChanges(); @@ -435,9 +427,7 @@ describe("calcite-slider", () => { const slider = await page.find("calcite-slider"); const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - layout === "horizontal" - ? await page.mouse.move(trackX + 30, trackY) - : await page.mouse.move(trackX, trackY + 95); + await (layout === "horizontal" ? page.mouse.move(trackX + 30, trackY) : page.mouse.move(trackX, trackY + 95)); await page.mouse.down(); await page.mouse.up(); await page.waitForChanges(); @@ -459,9 +449,7 @@ describe("calcite-slider", () => { const slider = await page.find("calcite-slider"); const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - layout === "horizontal" - ? await page.mouse.move(trackX + 60, trackY) - : await page.mouse.move(trackX, trackY + 40); + await (layout === "horizontal" ? page.mouse.move(trackX + 60, trackY) : page.mouse.move(trackX, trackY + 40)); await page.mouse.down(); await page.mouse.up(); await page.waitForChanges(); @@ -483,9 +471,7 @@ describe("calcite-slider", () => { const slider = await page.find("calcite-slider"); const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - layout === "horizontal" - ? await page.mouse.move(trackX + 50, trackY) - : await page.mouse.move(trackX, trackY + 50); + await (layout === "horizontal" ? page.mouse.move(trackX + 50, trackY) : page.mouse.move(trackX, trackY + 50)); await page.mouse.down(); await page.mouse.up(); await page.waitForChanges(); @@ -513,9 +499,7 @@ describe("calcite-slider", () => { const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - layout === "horizontal" - ? await page.mouse.move(trackX + 50, trackY) - : await page.mouse.move(trackX, trackY + 50); + await (layout === "horizontal" ? page.mouse.move(trackX + 50, trackY) : page.mouse.move(trackX, trackY + 50)); await page.mouse.down(); await page.waitForChanges(); await page.mouse.up(); @@ -573,9 +557,7 @@ describe("calcite-slider", () => { const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - layout === "horizontal" - ? await page.mouse.move(trackX + 25, trackY) - : await page.mouse.move(trackX, trackY + 75); + await (layout === "horizontal" ? page.mouse.move(trackX + 25, trackY) : page.mouse.move(trackX, trackY + 75)); await page.mouse.down(); await page.waitForChanges(); @@ -643,9 +625,7 @@ describe("calcite-slider", () => { const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - layout === "horizontal" - ? await page.mouse.move(trackX + 75, trackY) - : await page.mouse.move(trackX, trackY + 25); + await (layout === "horizontal" ? page.mouse.move(trackX + 75, trackY) : page.mouse.move(trackX, trackY + 25)); await page.mouse.down(); await page.waitForChanges(); await page.mouse.up(); @@ -819,6 +799,8 @@ describe("calcite-slider", () => { let inputEvent: EventSpy; let element: E2EElement; let trackRect: DOMRect; + let trackRectX: number; + let trackRectY: number; const commonSliderAttrs = ` min="5" @@ -843,6 +825,8 @@ describe("calcite-slider", () => { element = await page.find("calcite-slider"); trackRect = await getElementRect(page, "calcite-slider", `.${CSS.track}`); + trackRectX = trackRect.x + trackRect.width; + trackRectY = trackRect.y + trackRect.height; changeEvent = await element.spyOnEvent("calciteSliderChange"); inputEvent = await element.spyOnEvent("calciteSliderInput"); } @@ -857,9 +841,9 @@ describe("calcite-slider", () => { await assertValuesUnchanged(5); - layout === "horizontal" - ? await page.mouse.click(trackRect.x, trackRect.y) - : await page.mouse.click(trackRect.x, trackRect.y + trackRect.height); + await (layout === "horizontal" + ? page.mouse.click(trackRect.x, trackRect.y) + : page.mouse.click(trackRect.x, trackRectY)); await page.waitForChanges(); const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); @@ -875,9 +859,9 @@ describe("calcite-slider", () => { await assertValuesUnchanged(5); - layout === "horizontal" - ? await page.mouse.click(trackRect.x + trackRect.width, trackRect.y) - : await page.mouse.click(trackRect.x, trackRect.y); + await (layout === "horizontal" + ? page.mouse.click(trackRectX, trackRect.y) + : page.mouse.click(trackRect.x, trackRect.y)); await page.waitForChanges(); const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); @@ -895,9 +879,9 @@ describe("calcite-slider", () => { `${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" layout=${layout}`, ); - layout === "horizontal" - ? await page.mouse.click(trackRect.x + trackRect.width, trackRect.y) - : await page.mouse.click(trackRect.x, trackRect.y); + await (layout === "horizontal" + ? page.mouse.click(trackRectX, trackRect.y) + : page.mouse.click(trackRect.x, trackRect.y)); await page.waitForChanges(); const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); @@ -913,9 +897,9 @@ describe("calcite-slider", () => { await assertValuesUnchanged(expectedValue); - layout === "horizontal" - ? await page.mouse.click(trackRect.x, trackRect.y) - : await page.mouse.click(trackRect.x, trackRect.y + trackRect.height); + await (layout === "horizontal" + ? page.mouse.click(trackRect.x, trackRect.y) + : page.mouse.click(trackRect.x, trackRectY)); await page.waitForChanges(); const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); @@ -932,12 +916,12 @@ describe("calcite-slider", () => { max="1" min-value="0" max-value="0" - layout={layout}`; + layout=${layout}`; const nonMirroredSlider = `
${slider}>
`; const mirroredSlider = `
${slider} mirrored>
`; it("should position the minValue thumb beside the maxValue thumb", async () => { - const page = await newE2EPage({ html: nonMirroredSlider.replace("{layout}", layout) }); + const page = await newE2EPage({ html: nonMirroredSlider }); const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; @@ -947,7 +931,7 @@ describe("calcite-slider", () => { }); it("should position the minValue thumb beside the maxValue thumb when mirrored", async () => { - const page = await newE2EPage({ html: mirroredSlider.replace("{layout}", layout) }); + const page = await newE2EPage({ html: mirroredSlider }); const minValueThumb = await page.find("calcite-slider >>> .thumb--minValue"); const maxValueThumb = await page.find("calcite-slider >>> .thumb--value"); const minHandleLeft = await (await minValueThumb.getComputedStyle()).left; @@ -1093,13 +1077,13 @@ describe("calcite-slider", () => { const trackWidth = layout === "horizontal" ? trackRect.width : trackRect.height; const dragDistance = trackWidth - thumbWidth; - layout === "horizontal" - ? await page.mouse.move(trackRect.x, trackRect.y) - : await page.mouse.move(trackRect.x, trackRect.y + dragDistance); + await (layout === "horizontal" + ? page.mouse.move(trackRect.x, trackRect.y) + : page.mouse.move(trackRect.x, trackRect.y + dragDistance)); await page.mouse.down(); - layout === "horizontal" - ? await page.mouse.move(trackRect.x + dragDistance, trackRect.y) - : await page.mouse.move(trackRect.x, trackRect.y - dragDistance); + await (layout === "horizontal" + ? page.mouse.move(trackRect.x + dragDistance, trackRect.y) + : page.mouse.move(trackRect.x, trackRect.y - dragDistance)); await page.mouse.up(); await page.waitForChanges(); }