From 09a52ca7522e4a7d978fbaaecb80d720601e4c74 Mon Sep 17 00:00:00 2001 From: Eugenio Tavares Date: Mon, 15 Jul 2024 10:39:31 -0300 Subject: [PATCH 1/3] feat: adding forwardRef in Carousel --- .../components/Carousel/Carousel.stories.tsx | 33 +- .../ui/src/components/Carousel/Carousel.tsx | 300 ++++++++++-------- 2 files changed, 198 insertions(+), 135 deletions(-) diff --git a/packages/ui/src/components/Carousel/Carousel.stories.tsx b/packages/ui/src/components/Carousel/Carousel.stories.tsx index d20313a58..343c0b962 100644 --- a/packages/ui/src/components/Carousel/Carousel.stories.tsx +++ b/packages/ui/src/components/Carousel/Carousel.stories.tsx @@ -1,7 +1,12 @@ import type { Meta, StoryFn } from "@storybook/react"; -import type { CarouselProps } from "./Carousel"; +import { useRef } from "react"; +import { CarouselProps, CarouselRef } from "./Carousel"; import { Carousel } from "./Carousel"; + + + + export default { title: "Components/Carousel", component: Carousel, @@ -45,3 +50,29 @@ WithNoIndicators.storyName = "With no indicators"; WithNoIndicators.args = { indicators: false, }; + +const ControlledTemplate: StoryFn = () => { + const carouselRef = useRef(null); + + return ( + <> +
+ + ... + ... + ... + ... + ... + +
+
+ + + +
+ + ); +}; + +export const ControlledCarousel = ControlledTemplate.bind({}); +ControlledCarousel.args = {}; diff --git a/packages/ui/src/components/Carousel/Carousel.tsx b/packages/ui/src/components/Carousel/Carousel.tsx index e07823a20..0ea43939c 100644 --- a/packages/ui/src/components/Carousel/Carousel.tsx +++ b/packages/ui/src/components/Carousel/Carousel.tsx @@ -1,7 +1,20 @@ "use client"; -import type { ComponentProps, FC, ReactElement, ReactNode } from "react"; -import { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Children, + cloneElement, + ComponentProps, + FC, + forwardRef, + ReactElement, + ReactNode, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; import { HiOutlineChevronLeft, HiOutlineChevronRight } from "react-icons/hi"; import { twMerge } from "tailwind-merge"; import ScrollContainer from "../../helpers/drag-scroll"; @@ -62,152 +75,171 @@ export interface DefaultLeftRightControlProps extends ComponentProps<"div"> { theme?: DeepPartial; } -export const Carousel: FC = ({ - children, - indicators = true, - leftControl, - rightControl, - slide = true, - draggable = true, - slideInterval, - className, - theme: customTheme = {}, - onSlideChange = null, - pauseOnHover = false, - ...props -}) => { - const theme = mergeDeep(getTheme().carousel, customTheme); +export interface CarouselRef { + nextSlide: () => void; + prevSlide: () => void; + goToSlide: (item: number) => void; + currentSlide: number; +} - const isDeviceMobile = isClient() && navigator.userAgent.indexOf("IEMobile") !== -1; - const carouselContainer = useRef(null); - const [activeItem, setActiveItem] = useState(0); - const [isDragging, setIsDragging] = useState(false); - const [isHovering, setIsHovering] = useState(false); - - const didMountRef = useRef(false); - - const items = useMemo( - () => - Children.map(children as ReactElement[], (child: ReactElement) => - cloneElement(child, { - className: twMerge(theme.item.base, child.props.className), - }), - ), - [children, theme.item.base], - ); +export const Carousel = forwardRef( + ( + { + children, + indicators = true, + leftControl, + rightControl, + slide = true, + draggable = true, + slideInterval, + className, + theme: customTheme = {}, + onSlideChange = null, + pauseOnHover = false, + ...props + }, + ref, + ) => { + const theme = mergeDeep(getTheme().carousel, customTheme); + + const isDeviceMobile = isClient() && navigator.userAgent.indexOf("IEMobile") !== -1; + const carouselContainer = useRef(null); + const [activeItem, setActiveItem] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [isHovering, setIsHovering] = useState(false); + + const didMountRef = useRef(false); - const navigateTo = useCallback( - (item: number) => () => { - if (!items) return; - item = (item + items.length) % items.length; - if (carouselContainer.current) { - carouselContainer.current.scrollLeft = carouselContainer.current.clientWidth * item; + const items = useMemo( + () => + Children.map(children as ReactElement[], (child: ReactElement) => + cloneElement(child, { + className: twMerge(theme.item.base, child.props.className), + }), + ), + [children, theme.item.base], + ); + + const navigateTo = useCallback( + (item: number) => () => { + if (!items) return; + item = (item + items.length) % items.length; + if (carouselContainer.current) { + carouselContainer.current.scrollLeft = carouselContainer.current.clientWidth * item; + } + setActiveItem(item); + }, + [items], + ); + + useEffect(() => { + if (carouselContainer.current && !isDragging && carouselContainer.current.scrollLeft !== 0) { + setActiveItem(Math.round(carouselContainer.current.scrollLeft / carouselContainer.current.clientWidth)); } - setActiveItem(item); - }, - [items], - ); + }, [isDragging]); - useEffect(() => { - if (carouselContainer.current && !isDragging && carouselContainer.current.scrollLeft !== 0) { - setActiveItem(Math.round(carouselContainer.current.scrollLeft / carouselContainer.current.clientWidth)); - } - }, [isDragging]); + useEffect(() => { + if (slide && !(pauseOnHover && isHovering)) { + const intervalId = setInterval(() => !isDragging && navigateTo(activeItem + 1)(), slideInterval ?? 3000); - useEffect(() => { - if (slide && !(pauseOnHover && isHovering)) { - const intervalId = setInterval(() => !isDragging && navigateTo(activeItem + 1)(), slideInterval ?? 3000); + return () => clearInterval(intervalId); + } + }, [activeItem, isDragging, navigateTo, slide, slideInterval, pauseOnHover, isHovering]); - return () => clearInterval(intervalId); - } - }, [activeItem, isDragging, navigateTo, slide, slideInterval, pauseOnHover, isHovering]); + useEffect(() => { + if (didMountRef.current) { + onSlideChange && onSlideChange(activeItem); + } else { + didMountRef.current = true; + } + }, [onSlideChange, activeItem]); - useEffect(() => { - if (didMountRef.current) { - onSlideChange && onSlideChange(activeItem); - } else { - didMountRef.current = true; - } - }, [onSlideChange, activeItem]); + useImperativeHandle(ref, () => ({ + nextSlide: () => navigateTo(activeItem + 1)(), + prevSlide: () => navigateTo(activeItem - 1)(), + goToSlide: (item: number) => navigateTo(item)(), + currentSlide: activeItem, + })); - const handleDragging = (dragging: boolean) => () => setIsDragging(dragging); + const handleDragging = (dragging: boolean) => () => setIsDragging(dragging); - const setHoveringTrue = useCallback(() => setIsHovering(true), [setIsHovering]); - const setHoveringFalse = useCallback(() => setIsHovering(false), [setIsHovering]); + const setHoveringTrue = useCallback(() => setIsHovering(true), [setIsHovering]); + const setHoveringFalse = useCallback(() => setIsHovering(false), [setIsHovering]); - return ( -
- - {items?.map((item, index) => ( -
- {item} -
- ))} -
- {indicators && ( -
- {items?.map((_, index) => ( - -
-
- + {item} +
+ ))} + + {indicators && ( +
+ {items?.map((_, index) => ( +
- - )} -
- ); -}; + )} + + {items && ( + <> +
+ +
+
+ +
+ + )} + + ); + }, +); const DefaultLeftControl: FC = ({ theme: customTheme = {} }) => { const theme = mergeDeep(getTheme().carousel, customTheme); From 0ea034137b5e7e4f3c0d5ad43bb21067648ed6e4 Mon Sep 17 00:00:00 2001 From: Eugenio Tavares Date: Mon, 15 Jul 2024 11:17:00 -0300 Subject: [PATCH 2/3] feat: fix lint carousel --- packages/ui/src/components/Carousel/Carousel.stories.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/ui/src/components/Carousel/Carousel.stories.tsx b/packages/ui/src/components/Carousel/Carousel.stories.tsx index 343c0b962..8f8de4b77 100644 --- a/packages/ui/src/components/Carousel/Carousel.stories.tsx +++ b/packages/ui/src/components/Carousel/Carousel.stories.tsx @@ -1,11 +1,6 @@ import type { Meta, StoryFn } from "@storybook/react"; import { useRef } from "react"; -import { CarouselProps, CarouselRef } from "./Carousel"; -import { Carousel } from "./Carousel"; - - - - +import { Carousel, CarouselProps, CarouselRef } from "./Carousel"; export default { title: "Components/Carousel", From 0c2406253a2d04d0bd9ad77c78a06a3f30c14428 Mon Sep 17 00:00:00 2001 From: Eugenio Tavares Date: Mon, 15 Jul 2024 11:35:08 -0300 Subject: [PATCH 3/3] Create ten-pumpkins-clean.md --- .changeset/ten-pumpkins-clean.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-pumpkins-clean.md diff --git a/.changeset/ten-pumpkins-clean.md b/.changeset/ten-pumpkins-clean.md new file mode 100644 index 000000000..6934110c3 --- /dev/null +++ b/.changeset/ten-pumpkins-clean.md @@ -0,0 +1,5 @@ +--- +"flowbite-react": patch +--- + +feat: adding forwardRef in Carousel