Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slider update #4614

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .storybook/decorators/withResponsiveWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ToggleButton, ToggleButtonGroup, Tooltip } from "@salt-ds/core";
import { Slider, type SliderValue, StepperInput } from "@salt-ds/lab";
import { Slider, StepperInput } from "@salt-ds/lab";
import type { Decorator } from "@storybook/react";
import { type ReactNode, type SyntheticEvent, useState } from "react";
import "./ResponsiveContainer.css";
Expand All @@ -10,6 +10,9 @@ const ResponsiveContainer = ({ children }: { children?: ReactNode }) => {
const [selected, setSelected] = useState<string>("vw/vh");
const inPixels = selected === "px";
const maxUnits = inPixels ? 1000 : 100;
const toFloat = (value: string | number) =>
typeof value === "string" ? Number.parseFloat(value) : value;

return (
<div className="StoryContainer">
<div className="StoryContainer-sliders">
Expand Down Expand Up @@ -39,8 +42,8 @@ const ResponsiveContainer = ({ children }: { children?: ReactNode }) => {
id="width"
max={maxUnits}
min={10}
onChange={(value: SliderValue) => setWidth(value)}
value={containerWidth as SliderValue}
onChange={(event, value) => setWidth([toFloat(value)])}
value={containerWidth[0]}
/>
<StepperInput
value={containerHeight[0]}
Expand All @@ -53,8 +56,8 @@ const ResponsiveContainer = ({ children }: { children?: ReactNode }) => {
id="height"
max={maxUnits}
min={10}
onChange={(value: SliderValue) => setHeight(value)}
value={containerHeight as SliderValue}
onChange={(event, value) => setHeight([toFloat(value)])}
value={containerHeight[0]}
/>
</div>

Expand Down
143 changes: 143 additions & 0 deletions packages/lab/src/__tests__/__e2e__/slider/RangeSlider.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { RangeSlider } from "@salt-ds/lab";

describe("Given a Range Slider", () => {
it("should have ARIA roles and attributes", () => {
cy.mount(
<RangeSlider
style={{ width: "400px" }}
min={5}
max={125}
step={5}
defaultValue={[50, 100]}
/>,
);

// TODO Finish tests

cy.findAllByRole("slider")
.should("have.attr", "aria-valuemin", "5")
.and("have.attr", "aria-valuemax", "125");
});

it("should trigger onChange when clicked on the track", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(<RangeSlider style={{ width: "400px" }} onChange={changeSpy} />);
cy.get(".saltSlider-track").trigger("pointerdown", {
button: 0,
clientX: 500,
clientY: 50,
});
cy.get("@changeSpy").should("have.callCount", 1);
cy.get(".saltSlider-track").trigger("pointerup");
});

it("should set thumb positions correctly when clicked on the track", () => {
cy.mount(<RangeSlider style={{ width: "400px" }} />);
cy.get(".saltSlider-track").trigger("pointerdown", {
button: 0,
clientX: 500,
clientY: 50,
});
cy.get(".saltSlider-track").trigger("pointerup");
cy.get(".saltSlider-track").trigger("pointerdown", {
button: 0,
clientX: 700,
clientY: 50,
});
cy.get(".saltSlider-track").trigger("pointerup");

// First thumb
cy.findAllByRole("slider").eq(0).should("have.attr", "aria-valuenow", "1");
// First thumb
cy.findAllByRole("slider").eq(1).should("have.attr", "aria-valuenow", "7");
});

it("should change thumb positions based on keyboard navigation", () => {
cy.mount(<RangeSlider defaultValue={[4, 8]} style={{ width: "400px" }} />);

// Focus and move first thumb
cy.findAllByRole("slider").eq(0).focus().realPress("ArrowRight");
cy.findAllByRole("slider").eq(0).should("have.attr", "aria-valuenow", "5");

// Focus and move second thumb
cy.findAllByRole("slider").eq(1).focus().realPress("ArrowLeft");
cy.findAllByRole("slider").eq(1).should("have.attr", "aria-valuenow", "7");

// Focus first thumb and press and Home key
cy.findAllByRole("slider").eq(0).focus().realPress("Home");
cy.findAllByRole("slider").eq(0).should("have.attr", "aria-valuenow", "0");

// Focus second thumb and press and End key
cy.findAllByRole("slider").eq(1).focus().realPress("End");
cy.findAllByRole("slider").eq(1).should("have.attr", "aria-valuenow", "10");
});

it("should not allow thumbs to overlap", () => {
cy.mount(<RangeSlider defaultValue={[4, 8]} style={{ width: "400px" }} />);

// Focus first thumb and press and End key
cy.findAllByRole("slider").eq(0).focus().realPress("End");
cy.findAllByRole("slider").eq(0).should("have.attr", "aria-valuenow", "8");

// Focus second thumb and press and Home key
cy.findAllByRole("slider").eq(1).focus().realPress("Home");
cy.findAllByRole("slider").eq(1).should("have.attr", "aria-valuenow", "8");
});

it("should display a tooltip with correct value only when thumb is hovered", () => {
cy.mount(<RangeSlider style={{ width: "400px" }} defaultValue={[2, 5]} />);

// Hover the first thumb
cy.get(".saltSliderThumb").eq(0).trigger("pointerover");
// First thumb's tooltip should be visible and have correct value
cy.get(".saltSliderTooltip").eq(0).should("be.visible");
cy.get(".saltSliderTooltip").eq(0).should("have.text", "2");
// Second thumb's tooltip should not be visible
cy.get(".saltSliderTooltip").eq(1).should("not.be.visible");

cy.get(".saltSliderThumb").eq(0).trigger("pointerout");
cy.wait(250);
cy.get(".saltSliderTooltip").should("not.be.visible");

// Hover the second thumb
cy.get(".saltSliderThumb").eq(1).trigger("pointerover");
// Second thumb's tooltip should be visible and have correct value
cy.get(".saltSliderTooltip").eq(1).should("be.visible");
cy.get(".saltSliderTooltip").eq(1).should("have.text", "5");
// First thumb's tooltip should not be visible
cy.get(".saltSliderTooltip").eq(0).should("not.be.visible");

cy.get(".saltSliderThumb").eq(1).trigger("pointerout");
cy.wait(250);
cy.get(".saltSliderTooltip").should("not.be.visible");
});

it("should render markers when provided", () => {
cy.mount(
<RangeSlider
style={{ width: "400px" }}
markers={[
{ value: 2, label: "2" },
{ value: 3, label: "3" },
]}
/>,
);

cy.get(".saltSlider-markerLabel").eq(0).should("have.text", "2");
cy.get(".saltSlider-markerLabel").eq(1).should("have.text", "3");
});

it("should not render inline min/max labels when markers are provided", () => {
cy.mount(
<RangeSlider
style={{ width: "400px" }}
markers={[
{ value: 2, label: "2" },
{ value: 3, label: "3" },
]}
/>,
);

cy.get(".saltSlider").should("not.have.class", ".saltSlider-inlineLabels");
});
});
126 changes: 30 additions & 96 deletions packages/lab/src/__tests__/__e2e__/slider/Slider.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Slider } from "@salt-ds/lab";

describe("Given a Slider", () => {
describe("Given a Slider with a single value", () => {
it("THEN it should have ARIA roles and attributes", () => {
it("should have ARIA roles and attributes", () => {
cy.mount(
<Slider
style={{ width: "400px" }}
min={5}
max={125}
step={5}
defaultValue={[100]}
defaultValue={100}
/>,
);
cy.findByRole("slider")
Expand All @@ -18,26 +18,26 @@ describe("Given a Slider", () => {
.and("have.attr", "aria-valuenow", "100");
});

it("THEN onChange should fire on pointer down on slider track", () => {
it("should fire onChange on pointer down on slider track", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(<Slider style={{ width: "400px" }} onChange={changeSpy} />);
cy.get(".saltSliderTrack").trigger("pointerdown", {
cy.get(".saltSlider-track").trigger("pointerdown", {
button: 0,
clientX: 50,
clientY: 50,
});
cy.get("@changeSpy").should("have.callCount", 1);
});

it("THEN keyboard navigation can be used to change the slider position", () => {
it("should change the thumb position on slider based on keyboard navigation", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(
<Slider
style={{ width: "400px" }}
min={5}
max={125}
step={5}
defaultValue={[100]}
defaultValue={100}
onChange={changeSpy}
/>,
);
Expand All @@ -58,112 +58,46 @@ describe("Given a Slider", () => {
cy.get("@changeSpy").should("have.callCount", 4);
});

it("THEN it should display a tooltip on pointerover", () => {
cy.mount(<Slider style={{ width: "400px" }} />);
cy.get(".saltSliderThumb-container").trigger("pointerover");
cy.get(".saltSliderThumb-tooltip").should("be.visible");
cy.get(".saltSliderThumb-tooltip").should(
"have.attr",
"aria-expanded",
"true",
);

cy.get(".saltSliderThumb-container").trigger("pointerout");
cy.get(".saltSliderThumb-tooltip").should("not.be.visible");
cy.get(".saltSliderThumb-tooltip").should(
"have.attr",
"aria-expanded",
"false",
);
it("should display a tooltip on pointerover with correct value", () => {
cy.mount(<Slider style={{ width: "400px" }} defaultValue={2} />);
cy.get(".saltSliderThumb").trigger("pointerover");
cy.get(".saltSliderTooltip").should("be.visible");
cy.get(".saltSliderTooltip").should("have.text", "2");
// And hide tooltip on pointerout
cy.get(".saltSliderThumb").trigger("pointerout");
cy.get(".saltSliderTooltip").should("not.be.visible");
});
});

describe("Given a Slider with a range value", () => {
it("THEN it should have ARIA roles and attributes", () => {
it("should render markers when provided", () => {
cy.mount(
<Slider
style={{ width: "400px" }}
min={-100}
max={100}
step={10}
defaultValue={[20, 40]}
markers={[
{ value: 2, label: "2" },
{ value: 3, label: "3" },
]}
/>,
);
cy.findAllByRole("slider").should("have.length", 2);
cy.findAllByRole("slider")
.eq(0)
.should("have.attr", "aria-valuenow", "20");
cy.findAllByRole("slider")
.eq(1)
.should("have.attr", "aria-valuenow", "40");
});

it("THEN the nearest slider thumb should move on pointer down track", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(
<Slider
style={{ width: "400px" }}
min={0}
max={10}
step={1}
defaultValue={[2, 8]}
onChange={changeSpy}
/>,
);
cy.get(".saltSliderTrack").trigger("pointerdown", {
button: 0,
clientX: 0,
clientY: 0,
});
cy.get("@changeSpy").should("have.callCount", 1);
cy.findAllByRole("slider")
.eq(0)
.should("have.attr", "aria-valuenow", "0");
cy.findAllByRole("slider")
.eq(1)
.should("have.attr", "aria-valuenow", "8");
cy.get(".saltSlider-markerLabel").eq(0).should("have.text", "2");
cy.get(".saltSlider-markerLabel").eq(1).should("have.text", "3");
});

it("THEN slider thumbs should not cross when using keyboard nav", () => {
it("should not render inline min/max labels when markers are provided", () => {
cy.mount(
<Slider
style={{ width: "400px" }}
min={0}
max={10}
step={1}
defaultValue={[5, 8]}
markers={[
{ value: 2, label: "2" },
{ value: 3, label: "3" },
]}
/>,
);
cy.findAllByRole("slider")
.eq(0)
.focus()
.realPress("ArrowRight")
.realPress("ArrowRight")
.realPress("ArrowRight");
cy.findAllByRole("slider")
.eq(0)
.should("have.attr", "aria-valuenow", "8");
});
it("THEN slider thumbs should not cross when using cursor nav", () => {
cy.mount(
<Slider
style={{ width: "400px" }}
min={0}
max={10}
step={1}
defaultValue={[2, 5]}
/>,

cy.get(".saltSlider").should(
"not.have.class",
".saltSlider-inlineLabels",
);
cy.findAllByRole("slider")
.eq(0)
.trigger("pointerdown", { button: 0 })
.trigger("pointermove", {
clientX: 1000,
clientY: 1000,
});
cy.findAllByRole("slider")
.eq(0)
.should("have.attr", "aria-valuenow", "5");
});
});
});
Loading
Loading