Skip to content

Commit

Permalink
WIP - updates to carousel
Browse files Browse the repository at this point in the history
  • Loading branch information
Fercas123 committed Feb 7, 2025
1 parent af2edcf commit da3e255
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 88 deletions.
34 changes: 0 additions & 34 deletions packages/lab/src/carousel/Carousel.css
Original file line number Diff line number Diff line change
@@ -1,34 +0,0 @@
/*.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";*/
/*}*/

/*.saltCarousel.saltCarousel-compact {*/
/* grid-template-areas: "slider slider slider" "prev-button dots next-button";*/
/*}*/

/*.saltCarousel-scroll {*/

/*}*/

/*.saltCarousel-prev-button {*/
/* grid-area: prev-button;*/
/* height: 100%;*/
/*}*/

/*.saltCarousel-next-button {*/
/* grid-area: next-button;*/
/* height: 100%;*/
/*}*/

/*.saltCarousel-slider {*/
/* grid-area: slider;*/
/*}*/

/*.saltCarousel-dots {*/
/* grid-area: dots;*/
/* justify-self: center;*/
/*}*/
21 changes: 21 additions & 0 deletions packages/lab/src/carousel/CarouselContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createContext } from "@salt-ds/core";
import {
type ReactNode,
type RefObject,
type SyntheticEvent,
useContext,
useRef,
useState,
} from "react";

Expand All @@ -11,8 +13,10 @@ export interface CarouselContextValue {
nextSlide: (event: SyntheticEvent) => void;
prevSlide: (event: SyntheticEvent) => void;
goToSlide: (index: number) => void;
updateActiveFromScroll: (scrollLeft: number, sliderW: number) => void;
slides: string[];
registerSlide: (slideId: string) => void;
containerRef: RefObject<HTMLDivElement>;
}

export const CarouselContext = createContext<CarouselContextValue | null>(
Expand All @@ -32,11 +36,26 @@ export function CarouselProvider({ children }: { children: ReactNode }) {
// TODO: check active slide to initialy set the carousel prop
const [activeSlide, setActiveSlide] = useState(0);
const [slides, setSlides] = useState<string[]>([]);
const containerRef = useRef<HTMLDivElement>(null);

const registerSlide = (slideId: string) => {
setSlides((prev) => [...prev, slideId]);
};

const updateActiveFromScroll = (scrollLeft: number, slideW: number) => {
const newIndex = Math.round(scrollLeft / slideW);
if (newIndex !== activeSlide) {
setActiveSlide(newIndex);
}
};
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);
Expand All @@ -48,10 +67,12 @@ export function CarouselProvider({ children }: { children: ReactNode }) {
value={{
activeSlide,
nextSlide,
updateActiveFromScroll,
prevSlide,
goToSlide,
slides,
registerSlide,
containerRef,
}}
>
{children}
Expand Down
6 changes: 6 additions & 0 deletions packages/lab/src/carousel/CarouselControls.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.saltCarouselControls {
display: flex;
gap: var(--salt-spacing-100);
padding: var(--salt-spacing-100);
align-items: center;
}
50 changes: 19 additions & 31 deletions packages/lab/src/carousel/CarouselControls.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import {
Button,
makePrefixer,
RadioButton,
RadioButtonGroup,
useIcon,
} from "@salt-ds/core";
import { Button, makePrefixer, Text, useIcon } from "@salt-ds/core";

Check failure on line 1 in packages/lab/src/carousel/CarouselControls.tsx

View workflow job for this annotation

GitHub Actions / lint

organizeImports

Import statements differs from the output
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { forwardRef, type HTMLAttributes } from "react";
import { useCarousel } from "./CarouselContext";

import carouselControlsCss from "./CarouselControls.css";

const withBaseName = makePrefixer("saltCarousel");
const withBaseName = makePrefixer("saltCarouselControls");

export interface CarouselControlsProps extends HTMLAttributes<HTMLDivElement> {
/**
* TODO: check w design
* This prop will enable compact / reduced width mode.
* The navigation buttons would be part of indicators
* Optional. Defaults to false
Expand All @@ -33,48 +28,41 @@ export const CarouselControls = forwardRef<
css: carouselControlsCss,
window: targetWindow,
});
const { slides, activeSlide, nextSlide, prevSlide, goToSlide } =
useCarousel();
const { slides, activeSlide, nextSlide, prevSlide } = useCarousel();
const { NextIcon, PreviousIcon } = useIcon();

const slidesCount = slides.length;

console.log(activeSlide, slidesCount, slides);
const isOnFirstSlide = activeSlide === 0;
const isOnLastSlide = activeSlide === slidesCount - 1;
return (
<div ref={ref} className={withBaseName()} {...rest}>
<div ref={ref} role="group" className={withBaseName()} {...rest}>
<Button
appearance="transparent"
appearance="bordered"
sentiment="neutral"
className={withBaseName("prev-button")}
onClick={prevSlide}
disabled={activeSlide === 0}
disabled={isOnFirstSlide}
aria-label="Previous slide"
>
<PreviousIcon />
<PreviousIcon aria-hidden />
</Button>
{children}
<Button
appearance="transparent"
appearance="bordered"
sentiment="neutral"
className={withBaseName("next-button")}
onClick={nextSlide}
disabled={activeSlide === slidesCount - 1}
disabled={isOnLastSlide}
aria-label="Next slide"
>
<NextIcon />
<NextIcon aria-hidden />
</Button>
<div className={withBaseName("dots")}>
<RadioButtonGroup
aria-label="Carousel buttons"
onChange={(e) => goToSlide(Number(e.target.value))}
value={`${activeSlide}`}
direction={"horizontal"}
>
{Array.from({ length: slidesCount }, (_, index) => ({
value: `${index}`,
})).map((radio) => (
<RadioButton {...radio} key={radio.value} />
))}
</RadioButtonGroup>
</div>
{/* TODO: check color with design*/}
<Text as="span" color="secondary">
{activeSlide + 1} of {slidesCount}
</Text>
</div>
);
});
4 changes: 2 additions & 2 deletions packages/lab/src/carousel/CarouselSlide.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.saltCarouselSlide {
scroll-snap-align: center;
scroll-snap-align: start; /* was center */
display: flex;
flex-direction: column;
flex: 0 0 100%;
max-width: 100%;
width: 100%;
}
3 changes: 2 additions & 1 deletion packages/lab/src/carousel/CarouselSlider.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.saltCarouselSlider {
display: flex;
scroll-snap-type: x mandatory;
overflow-x: scroll;
overflow-x: auto;
scroll-behavior: smooth;
scrollbar-width: none;
width: 100%;
}
54 changes: 40 additions & 14 deletions packages/lab/src/carousel/CarouselSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
Children,
forwardRef,
type HTMLAttributes,
type KeyboardEvent,
type ReactElement,
useEffect,
useRef,
useState,
} from "react";
import { useCarousel } from "./CarouselContext";
import carouselSliderCss from "./CarouselSlider.css";
Expand All @@ -29,17 +30,31 @@ export interface CarouselSliderProps extends HTMLAttributes<HTMLDivElement> {
const withBaseName = makePrefixer("saltCarouselSlider");

export const CarouselSlider = forwardRef<HTMLDivElement, CarouselSliderProps>(
function CarouselSlider({ animation, children }, ref) {
function CarouselSlider({ animation, children, onKeyDown }, ref) {
const targetWindow = useWindow();
useComponentCssInjection({
testId: "salt-carousel-slide",
css: carouselSliderCss,
window: targetWindow,
});

const containerRef = useRef<HTMLDivElement>(null);

const [sliderW, setSliderW] = useState(0);
const { updateActiveFromScroll, activeSlide, containerRef, goToSlide } =
useCarousel();
const slidesCount = Children.count(children);

useEffect(() => {
if (containerRef.current) {
setSliderW(containerRef.current.offsetWidth);
}
const handleResize = () => {
if (containerRef.current) {
setSliderW(containerRef.current.offsetWidth);
}
};
targetWindow?.addEventListener("resize", handleResize);
return () => targetWindow?.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (process.env.NODE_ENV !== "production") {
if (slidesCount < 1) {
Expand All @@ -49,24 +64,35 @@ export const CarouselSlider = forwardRef<HTMLDivElement, CarouselSliderProps>(
}
}
}, [slidesCount]);

const { activeSlide } = useCarousel();

useEffect(() => {
if (containerRef.current) {
const slideW = containerRef.current.offsetWidth;
containerRef.current.scrollTo({
left: activeSlide * slideW,
//TODO: double check animations
behavior: "smooth",
});
containerRef.current?.addEventListener("scroll", handleScroll);
}
return () =>
containerRef?.current?.removeEventListener("scroll", handleScroll);
}, [sliderW, activeSlide]);

// Handlers
const handleScroll = () => {
if (containerRef.current) {
const scrollLeft = containerRef.current.scrollLeft;
updateActiveFromScroll(scrollLeft, sliderW);
}
}, [activeSlide]);
};
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowRight") goToSlide(activeSlide + 1);
if (event.key === "ArrowLeft") goToSlide(activeSlide - 1);
onKeyDown?.(event);
};

return (
<div
ref={useForkRef(ref, containerRef)}
className={withBaseName()}
aria-live="polite"
role="region"
tabIndex={0}
onKeyDown={() => handleKeyDown}
>
{children}
</div>
Expand Down
6 changes: 5 additions & 1 deletion packages/lab/stories/carousel/carousel.stories.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
.carousel-container {
max-width: 600px;
width: 80vw;
}
.carousel-image-placeholder {
height: 250px;
width: 500px;
width: 100%;
}

.carousel-image-placeholder-1 {
Expand Down
13 changes: 8 additions & 5 deletions packages/lab/stories/carousel/carousel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
CarouselSlider,
} from "@salt-ds/lab";
import "./carousel.stories.css";
import { Button, H2, SplitLayout, Text } from "@salt-ds/core";
import { Button, H2, SplitLayout, StackLayout, Text } from "@salt-ds/core";

export default {
title: "Lab/Carousel",
Expand All @@ -16,7 +16,7 @@ export default {

const CarouselExample: StoryFn<typeof Carousel> = (args) => {
return (
<div style={{ maxWidth: 600 }}>
<div className="carousel-container">
<Carousel {...args}>
<CarouselControls />
<CarouselSlider>
Expand All @@ -27,11 +27,14 @@ const CarouselExample: StoryFn<typeof Carousel> = (args) => {
index + 1
}`}
/>
<div style={{ padding: " 1rem" }}>
<StackLayout gap={0}>
<H2>Header of the slider {index + 1}</H2>
<Text> This is a slider in a carousel</Text>
<Text>
We offer solutions to the world's moast important
corporations, governments and institution.
</Text>
<SplitLayout endItem={<Button>Learn more</Button>} />
</div>
</StackLayout>
</CarouselSlide>
))}
</CarouselSlider>
Expand Down

0 comments on commit da3e255

Please sign in to comment.