Skip to content

Commit 799cf41

Browse files
Fercas123chrispcodejake-costa
authored
Carousel (#4635)
Co-authored-by: Kristiyan Serafimov <[email protected]> Co-authored-by: Jake <[email protected]>
1 parent 1d3f744 commit 799cf41

39 files changed

+1751
-285
lines changed

.changeset/purple-beers-laugh.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@salt-ds/lab": minor
3+
---
4+
5+
Updated `Carousel` component
6+
7+
- Renamed `initialIndex` to `defaultActiveSlideIndex`
8+
- Added controlled `activeSlideIndex`
9+
- Added `visibleSlides` to control how many slides can be visible at a time.
10+
- Added `CarouselSlider` and extracted the controls to its own component, `CarouselControls` to improve composition.
11+
- Added appearance in `CarouselSlide` to allow for border items.
12+
- Added keyboard navigation.
13+
- Removed usage of `DeckLayout`.
14+
15+
before:
16+
17+
```tsx
18+
<Carousel>
19+
{items.map((item, index) => (
20+
<CarouselSlide
21+
key={index}
22+
ButtonBar={<Button variant="cta">Learn more</Button>}
23+
description="Lorem ipsum"
24+
title="Carousel slide title"
25+
/>
26+
))}
27+
</Carousel>
28+
```
29+
30+
after:
31+
32+
```tsx
33+
<Carousel>
34+
<CarouselControls />
35+
<CarouselSlider>
36+
{items.map((slide) => (
37+
<CarouselSlide
38+
key={slide.title}
39+
header={<H3>{slide.title}</H3>}
40+
actions={<Link href="#">{slide.link}</Link>}
41+
>
42+
<Text>{slide.content}</Text>
43+
</CarouselSlide>
44+
))}
45+
</CarouselSlider>
46+
</Carousel>
47+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import * as carouselStories from "@stories/carousel/carousel.stories";
2+
import { composeStories } from "@storybook/react";
3+
import { checkAccessibility } from "../../../../../../cypress/tests/checkAccessibility";
4+
5+
const composedStories = composeStories(carouselStories);
6+
const { Default, WithActions, Controlled } = composedStories;
7+
describe("GIVEN a 100% width slides carousel", () => {
8+
checkAccessibility(composedStories);
9+
describe("WHEN the default is rendered with slides", () => {
10+
it("SHOULD render carousel", () => {
11+
cy.mount(<Default />);
12+
cy.findByRole("region").should("exist");
13+
});
14+
});
15+
describe("WHEN moving slides with buttons", () => {
16+
it("SHOULD move to the next slide with button", () => {
17+
cy.mount(<Default />);
18+
cy.findAllByText("1 of 4").should("exist");
19+
cy.findAllByRole("button", { name: "Previous slide" }).should(
20+
"have.attr",
21+
"aria-disabled",
22+
"true",
23+
);
24+
cy.findAllByRole("button", { name: "Next slide" }).click();
25+
cy.findAllByText("2 of 4").should("exist");
26+
});
27+
28+
it("SHOULD move to the previous slide with button", () => {
29+
cy.mount(<Default />);
30+
cy.findAllByText("1 of 4").should("exist");
31+
// move one to the left
32+
cy.findAllByRole("button", { name: "Next slide" }).click();
33+
cy.findAllByText("2 of 4").should("exist");
34+
// test back button
35+
cy.findAllByRole("button", { name: "Previous slide" }).click();
36+
cy.findAllByText("1 of 4").should("exist");
37+
});
38+
it("SHOULD disable previous button when reaching far left", () => {
39+
cy.mount(<Default defaultActiveSlideIndex={1} />);
40+
cy.findAllByText("2 of 4").should("exist");
41+
cy.findAllByRole("button", { name: "Previous slide" }).click();
42+
cy.findAllByRole("button", { name: "Previous slide" }).should(
43+
"have.attr",
44+
"aria-disabled",
45+
"true",
46+
);
47+
});
48+
49+
it("SHOULD disable next button when reaching far right", () => {
50+
cy.mount(<Default defaultActiveSlideIndex={2} />);
51+
cy.findAllByText("3 of 4").should("exist");
52+
cy.findAllByRole("button", { name: "Next slide" }).click();
53+
cy.findAllByText("4 of 4").should("exist");
54+
cy.findAllByRole("button", { name: "Next slide" }).should(
55+
"have.attr",
56+
"aria-disabled",
57+
"true",
58+
);
59+
});
60+
it.skip("SHOULD update labels when scrolling", () => {
61+
// flaky with react 16/17
62+
cy.mount(<Default />);
63+
cy.findAllByText("1 of 4").should("exist");
64+
cy.get(".saltCarouselSlider").scrollTo("right");
65+
cy.wait(100);
66+
cy.findAllByText("4 of 4").should("exist");
67+
});
68+
it("SHOULD support keyboard navigation", () => {
69+
cy.mount(<Default />);
70+
cy.findAllByText("1 of 4").should("exist");
71+
cy.findAllByRole("group").get('[tabindex="0"]').focus();
72+
cy.findAllByRole("group").get('[tabindex="0"]').realPress("ArrowRight");
73+
cy.findAllByText("2 of 4").should("exist");
74+
cy.wait(100);
75+
cy.findAllByRole("group").get('[tabindex="0"]').realPress("ArrowLeft");
76+
cy.findAllByText("1 of 4").should("exist");
77+
});
78+
describe("WHEN navigating with keyboard keys", () => {
79+
it("SHOULD NOT move to hidden slides when using tab navigation", () => {
80+
cy.mount(<WithActions />);
81+
cy.findAllByText("1 - 2 of 4").should("exist");
82+
cy.findAllByRole("button", { name: "Next slides" }).focus();
83+
// tab through visible elements
84+
cy.realPress("Tab");
85+
cy.realPress("Tab");
86+
cy.findByText("Open an account").should("be.focused");
87+
cy.realPress("Tab");
88+
cy.realPress("Tab");
89+
cy.findByText("Go to dashboard").should("be.focused");
90+
// next tab should exit the carousel
91+
cy.realPress("Tab");
92+
// slides should not have been changed
93+
cy.findAllByText("1 - 2 of 4").should("exist");
94+
});
95+
});
96+
});
97+
});
98+
describe("GIVEN a carousel with responsive visibleItems", () => {
99+
it("SHOULD render properly with different visible items based on viewport", () => {
100+
cy.viewport(590, 900); // xs viewport
101+
cy.mount(<Default visibleSlides={{ xs: 1, sm: 2, md: 3 }} />);
102+
cy.findAllByText("1 of 4").should("exist");
103+
104+
cy.viewport(700, 900); // sm viewport
105+
cy.mount(<Default visibleSlides={{ xs: 1, sm: 2, md: 3 }} />);
106+
cy.findAllByText("1 - 2 of 4").should("exist");
107+
108+
cy.viewport(961, 1200); // md viewport
109+
cy.mount(<Default visibleSlides={{ xs: 1, sm: 2, md: 3 }} />);
110+
cy.findAllByText("1 - 3 of 4").should("exist");
111+
});
112+
});
113+
describe("WHEN mounted as an uncontrolled component", () => {
114+
it("THEN should move slide when controlled slide index is passed", () => {
115+
cy.mount(<Controlled />);
116+
cy.findAllByRole("button", { name: "Right" }).click();
117+
cy.findByText("Current slide: 2").should("exist");
118+
cy.findAllByRole("button", { name: "Left" }).click();
119+
cy.findByText("Current slide: 1").should("exist");
120+
});
121+
it("THEN should be able to update controlled value when navigating slider", () => {
122+
cy.mount(<Controlled />);
123+
cy.findByText("Current slide: 1").should("exist");
124+
cy.findAllByRole("group").get('[tabindex="0"]').focus();
125+
cy.findAllByRole("group").get('[tabindex="0"]').realPress("ArrowRight");
126+
cy.findByText("Current slide: 2").should("exist");
127+
});
128+
});
+8-24
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,11 @@
1-
.saltGridLayout.saltCarousel {
2-
grid-template-columns: min-content auto min-content;
3-
grid-template-areas: "prev-button slider next-button" "dots dots dots";
1+
.saltCarousel {
2+
display: grid;
3+
grid-row-gap: var(--salt-spacing-100);
4+
grid-template-areas: "controls" "slider";
5+
grid-template-rows: auto 1fr;
46
}
57

6-
.saltCarousel.saltCarousel-compact {
7-
grid-template-areas: "slider slider slider" "prev-button dots next-button";
8-
}
9-
10-
.saltCarousel-prev-button {
11-
grid-area: prev-button;
12-
height: 100%;
13-
}
14-
15-
.saltCarousel-next-button {
16-
grid-area: next-button;
17-
height: 100%;
18-
}
19-
20-
.saltCarousel-slider {
21-
grid-area: slider;
22-
}
23-
24-
.saltCarousel-dots {
25-
grid-area: dots;
26-
justify-self: center;
8+
.saltCarousel.saltCarousel-bottom {
9+
grid-template-areas: "slider" "controls";
10+
grid-template-rows: 1fr auto;
2711
}

0 commit comments

Comments
 (0)