Skip to content

Commit

Permalink
updates to carousel
Browse files Browse the repository at this point in the history
  • Loading branch information
Fercas123 committed Jan 28, 2025
1 parent 3b1c265 commit dd86a8d
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 202 deletions.
12 changes: 11 additions & 1 deletion packages/lab/src/carousel/Carousel.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.saltGridLayout.saltCarousel {
.saltCarousel {
position: relative;
overflow: hidden;
display: grid;
grid-template-columns: min-content auto min-content;
grid-template-areas: "prev-button slider next-button" "dots dots dots";
}
Expand All @@ -7,6 +10,13 @@
grid-template-areas: "slider slider slider" "prev-button dots next-button";
}

.saltCarousel-scroll {
display: flex;
scroll-snap-type: x mandatory;
overflow-x: scroll;
scroll-behavior: smooth;
}

.saltCarousel-prev-button {
grid-area: prev-button;
height: 100%;
Expand Down
156 changes: 85 additions & 71 deletions packages/lab/src/carousel/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
Button,
GridLayout,
RadioButton,
RadioButtonGroup,
makePrefixer,
useIcon,
useId,
} from "@salt-ds/core";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
Expand All @@ -17,9 +15,10 @@ import {
type ReactElement,
forwardRef,
useEffect,
useRef,
useState,
} from "react";
import { DeckLayout } from "../deck-layout";
import { useSlideSelection } from "../utils";
import { CarouselContext } from "./CarouselContext";
import type { CarouselSlideProps } from "./CarouselSlide";

import carouselCss from "./Carousel.css";
Expand All @@ -31,7 +30,7 @@ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
* The initial Index enables you to select the active slide in the carousel.
* Optional, default 0.
**/
initialIndex?: number;
activeSlideIndex?: number;
/**
* The animation when the slides are shown.
* Optional. Defaults to `slide`
Expand All @@ -53,23 +52,17 @@ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
* Optional. Defaults to false
**/
compact?: boolean;
/**
* It sets the id for the Carousel Container.
* String. Optional
*/
id?: string;
}

export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
function Carousel(
{
initialIndex,
activeSlideIndex = 0,
animation = "slide",
carouselDescription,
children,
className,
compact,
id: idProp,
...rest
},
ref,
Expand All @@ -81,26 +74,35 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
window: targetWindow,
});
const { NextIcon, PreviousIcon } = useIcon();
const id = useId(idProp);

const containerRef = useRef<HTMLDivElement>(null);
const slidesCount = Children.count(children);
const [activeSlide, setActiveSlide] = useState(activeSlideIndex);
const [slides, setSlides] = useState<string[]>([]);

const [_, selectedSlide, handleSlideSelection] =
useSlideSelection(initialIndex);
const registerSlide = (slideId: string) => {
setSlides((prev) => [...prev, slideId]);
};

const moveSlide = (direction: "left" | "right") => {
const moveLeft =
selectedSlide === 0 ? slidesCount - 1 : selectedSlide - 1;
const moveRight =
selectedSlide === slidesCount - 1 ? 0 : selectedSlide + 1;
const newSelection = direction === "left" ? moveLeft : moveRight;
const newTransition = direction === "left" ? "decrease" : "increase";
handleSlideSelection(newSelection, newTransition);
const scrollToSlide = (index: number) => {
if (containerRef.current) {
const slideW = containerRef.current.offsetWidth;
containerRef.current.scrollTo({
left: index * slideW,
behavior: "smooth",
});
setActiveSlide(index);
}
};
const nextSlide = () => scrollToSlide(activeSlide + 1);
const prevSlide = () => scrollToSlide(activeSlide - 1);
const goToSlide = (index: number) => scrollToSlide(index);

// TODO: implement on scroll
const handleRadioChange: ChangeEventHandler<HTMLInputElement> = ({
target: { value },
}) => {
handleSlideSelection(Number(value));
goToSlide(Number(value));
};

useEffect(() => {
Expand All @@ -114,57 +116,69 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
}, [slidesCount]);

return (
<GridLayout
aria-label={carouselDescription}
aria-roledescription="carousel"
id={id}
role="region"
ref={ref}
gap={0}
columns={3}
className={clsx(
withBaseName(),
compact && withBaseName("compact"),
className,
)}
{...rest}
<CarouselContext.Provider
value={{
activeSlide,
nextSlide,
prevSlide,
goToSlide,
slides,
registerSlide,
}}
>
<Button
variant="secondary"
className={withBaseName("prev-button")}
onClick={() => moveSlide("left")}
<div
aria-label={carouselDescription}
aria-roledescription="carousel"
role="region"
ref={ref}
className={clsx(
withBaseName(),
compact && withBaseName("compact"),
className,
)}
{...rest}
>
<PreviousIcon size={2} />
</Button>
<DeckLayout
activeIndex={selectedSlide}
animation={animation}
className={withBaseName("slider")}
>
{children}
</DeckLayout>
<Button
variant="secondary"
className={withBaseName("next-button")}
onClick={() => moveSlide("right")}
>
<NextIcon size={2} />
</Button>
<div className={withBaseName("dots")}>
<RadioButtonGroup
aria-label="Carousel buttons"
onChange={handleRadioChange}
value={`${selectedSlide}`}
direction={"horizontal"}
<Button
appearance="transparent"
sentiment="neutral"
className={withBaseName("prev-button")}
onClick={prevSlide}
disabled={activeSlide === 0}
>
<PreviousIcon size={2} />
</Button>
<div
ref={containerRef}
className={withBaseName("scroll")}
aria-live="polite"
>
{children}
</div>
<Button
appearance="transparent"
sentiment="neutral"
className={withBaseName("next-button")}
onClick={nextSlide}
disabled={activeSlide === slidesCount - 1}
>
{Array.from({ length: slidesCount }, (_, index) => ({
value: `${index}`,
})).map((radio) => (
<RadioButton {...radio} key={radio.value} />
))}
</RadioButtonGroup>
<NextIcon size={2} />
</Button>
<div className={withBaseName("dots")}>
<RadioButtonGroup
aria-label="Carousel buttons"
onChange={handleRadioChange}
value={`${activeSlide}`}
direction={"horizontal"}
>
{Array.from({ length: slidesCount }, (_, index) => ({
value: `${index}`,
})).map((radio) => (
<RadioButton {...radio} key={radio.value} />
))}
</RadioButtonGroup>
</div>
</div>
</GridLayout>
</CarouselContext.Provider>
);
},
);
28 changes: 28 additions & 0 deletions packages/lab/src/carousel/CarouselContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createContext } from "@salt-ds/core";
import { type SyntheticEvent, useContext } from "react";

export interface CarouselContextValue {
activeSlide: number;

nextSlide: (event: SyntheticEvent) => void;
prevSlide: (event: SyntheticEvent) => void;
goToSlide: (index: number) => void;
slides: string[];
registerSlide: (slideId: string) => void;
}

export const CarouselContext = createContext<CarouselContextValue>(
"CarouselContext",
{
activeSlide: 0,
nextSlide: () => undefined,
prevSlide: () => undefined,
goToSlide: () => undefined,
slides: [],
registerSlide: () => undefined,
},
);

export function useCarousel() {
return useContext(CarouselContext);
}
9 changes: 9 additions & 0 deletions packages/lab/src/carousel/CarouselSlide.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.saltCarouselSlide {
scroll-snap-align: center;
display: flex;
flex-direction: column;
/*display: none;*/
}
.saltCarouselSlide-active {
/*display: block;*/
}
95 changes: 30 additions & 65 deletions packages/lab/src/carousel/CarouselSlide.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,41 @@
import { makePrefixer } from "@salt-ds/core";
import {
type ElementType,
type HTMLAttributes,
type ReactElement,
forwardRef,
useRef,
} from "react";

import { makePrefixer, useId } from "@salt-ds/core";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
import { type HTMLAttributes, forwardRef, useEffect, useRef } from "react";
import { useCarousel } from "./CarouselContext";
import carouselSlideCss from "./CarouselSlide.css";

export interface CarouselSlideProps extends HTMLAttributes<HTMLDivElement> {
ButtonBar?: ElementType;
Media: ReactElement;
description?: string;
title?: string;
contentAlignment?: "center" | "left" | "right";
}
export interface CarouselSlideProps extends HTMLAttributes<HTMLDivElement> {}

const withBaseName = makePrefixer("saltCarouselSlide");

export const CarouselSlide = forwardRef<HTMLDivElement, CarouselSlideProps>(
function CarouselSlide(
{ ButtonBar, Media, description, title, contentAlignment },
ref,
) {
const buttonBarRef = useRef(null);
function CarouselSlide({ children }, ref) {
const targetWindow = useWindow();
useComponentCssInjection({
testId: "salt-carousel-slide",
css: carouselSlideCss,
window: targetWindow,
});

const slideRef = useRef(useId());
const { activeSlide, registerSlide, slides } = useCarousel();

useEffect(() => {
slideRef.current && registerSlide(slideRef.current);
}, []);

const isActive = slides[activeSlide] === slideRef.current;
return (
<div ref={ref}>
{Media && <div className={withBaseName("mediaContainer")}>{Media}</div>}
<div className={withBaseName("fixedContainer")} ref={buttonBarRef}>
<div
className={clsx({
[withBaseName("textContainer")]: contentAlignment === "center",
[withBaseName("textContainerLeft")]: contentAlignment === "left",
})}
>
{title && (
<div
aria-level={1}
className={withBaseName("titleContainer")}
role="heading"
>
{title}
</div>
)}
{description && (
<div className={withBaseName("descriptionContainer")}>
{description}
</div>
)}
</div>
{ButtonBar && (
<div
className={clsx({
[withBaseName("buttonBarOverride")]:
contentAlignment === "center",
[withBaseName("buttonBarOverrideLeft")]:
contentAlignment === "left",
})}
>
<ButtonBar
className={clsx({
[withBaseName("buttonBarContainer")]:
contentAlignment === "center",
[withBaseName("buttonBarContainerLeft")]:
contentAlignment === "left",
})}
/>
</div>
)}
</div>
<div
role="group"
ref={ref}
className={clsx(withBaseName(), {
[withBaseName("active")]: isActive,
})}
>
{children}
</div>
);
},
Expand Down
Loading

0 comments on commit dd86a8d

Please sign in to comment.