Skip to content

Instantly share code, notes, and snippets.

@superbahbi
Last active December 31, 2023 02:18
Show Gist options
  • Save superbahbi/85ee6ba70f2e0b0a30222b709e85fc44 to your computer and use it in GitHub Desktop.
Save superbahbi/85ee6ba70f2e0b0a30222b709e85fc44 to your computer and use it in GitHub Desktop.

Revisions

  1. superbahbi renamed this gist Dec 31, 2023. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. superbahbi created this gist Dec 31, 2023.
    340 changes: 340 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,340 @@
    import useEmblaCarousel, {
    type EmblaCarouselType as CarouselApi,
    type EmblaOptionsType as CarouselOptions,
    type EmblaPluginType as CarouselPlugin,
    } from "embla-carousel-react";
    import { ArrowLeft, ArrowRight } from "lucide-react";
    import * as React from "react";

    import { Button, ButtonProps } from "@/components/ui/button";
    import { cn } from "@/lib/utils";

    type CarouselProps = {
    opts?: CarouselOptions;
    plugins?: CarouselPlugin[];
    orientation?: "horizontal" | "vertical";
    setApi?: (api: CarouselApi) => void;
    };

    type CarouselContextProps = {
    carouselRef: ReturnType<typeof useEmblaCarousel>[0];
    api: ReturnType<typeof useEmblaCarousel>[1];
    scrollPrev: () => void;
    scrollNext: () => void;
    canScrollPrev: boolean;
    canScrollNext: boolean;
    scrollTo: (index: number) => void;
    selectedIndex: number;
    } & CarouselProps;

    const CarouselContext = React.createContext<CarouselContextProps | null>(null);

    function useCarousel() {
    const context = React.useContext(CarouselContext);

    if (!context) {
    throw new Error("useCarousel must be used within a <Carousel />");
    }

    return context;
    }

    const Carousel = React.forwardRef<
    HTMLDivElement,
    React.HTMLAttributes<HTMLDivElement> & CarouselProps
    >(
    (
    {
    orientation = "horizontal",
    opts,
    setApi,
    plugins,
    className,
    children,
    ...props
    },
    ref,
    ) => {
    const [carouselRef, api] = useEmblaCarousel(
    {
    ...opts,
    axis: orientation === "horizontal" ? "x" : "y",
    },
    plugins,
    );
    const [canScrollPrev, setCanScrollPrev] = React.useState(false);
    const [canScrollNext, setCanScrollNext] = React.useState(false);
    const [selectedIndex, setSelectedIndex] = React.useState(0);

    const onSelect = React.useCallback((api: CarouselApi) => {
    if (!api) {
    return;
    }

    setCanScrollPrev(api.canScrollPrev());
    setCanScrollNext(api.canScrollNext());
    setSelectedIndex(api.selectedScrollSnap());
    }, []);

    const scrollPrev = React.useCallback(() => {
    api?.scrollPrev();
    }, [api]);

    const scrollNext = React.useCallback(() => {
    api?.scrollNext();
    }, [api]);

    const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
    if (event.key === "ArrowLeft") {
    event.preventDefault();
    scrollPrev();
    } else if (event.key === "ArrowRight") {
    event.preventDefault();
    scrollNext();
    }
    },
    [scrollPrev, scrollNext],
    );

    React.useEffect(() => {
    if (!api || !setApi) {
    return;
    }

    setApi(api);
    }, [api, setApi]);

    React.useEffect(() => {
    if (!api) {
    return;
    }

    onSelect(api);
    api.on("reInit", onSelect);
    api.on("select", onSelect);

    return () => {
    api?.off("select", onSelect);
    };
    }, [api, onSelect]);

    const scrollTo = React.useCallback(
    (index: number) => api && api.scrollTo(index),
    [api],
    );

    return (
    <CarouselContext.Provider
    value={{
    carouselRef,
    api: api,
    opts,
    orientation:
    orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
    scrollPrev,
    scrollNext,
    canScrollPrev,
    canScrollNext,
    scrollTo,
    selectedIndex,
    }}
    >
    <div
    ref={ref}
    onKeyDownCapture={handleKeyDown}
    className={cn("relative", className)}
    role="region"
    aria-roledescription="carousel"
    {...props}
    >
    {children}
    </div>
    </CarouselContext.Provider>
    );
    },
    );
    Carousel.displayName = "Carousel";

    const CarouselContent = React.forwardRef<
    HTMLDivElement,
    React.HTMLAttributes<HTMLDivElement>
    >(({ className, ...props }, ref) => {
    const { carouselRef, orientation } = useCarousel();

    return (
    <div ref={carouselRef} className="overflow-hidden">
    <div
    ref={ref}
    className={cn(
    "flex",
    orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
    className,
    )}
    {...props}
    />
    </div>
    );
    });
    CarouselContent.displayName = "CarouselContent";

    const CarouselItem = React.forwardRef<
    HTMLDivElement,
    React.HTMLAttributes<HTMLDivElement>
    >(({ className, ...props }, ref) => {
    const { orientation } = useCarousel();

    return (
    <div
    ref={ref}
    role="group"
    aria-roledescription="slide"
    className={cn(
    "min-w-0 shrink-0 grow-0 basis-full",
    orientation === "horizontal" ? "pl-4" : "pt-4",
    className,
    )}
    {...props}
    />
    );
    });

    CarouselItem.displayName = "CarouselItem";

    const CarouselPrevious = React.forwardRef<
    HTMLButtonElement,
    React.ComponentProps<typeof Button>
    >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
    const { orientation, scrollPrev, canScrollPrev } = useCarousel();

    return (
    <Button
    ref={ref}
    variant={variant}
    size={size}
    className={cn(
    "absolute h-8 w-8 rounded-full",
    orientation === "horizontal"
    ? "-left-12 top-1/2 -translate-y-1/2"
    : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
    className,
    )}
    disabled={!canScrollPrev}
    onClick={scrollPrev}
    {...props}
    >
    <ArrowLeft className="h-4 w-4" />
    <span className="sr-only">Previous slide</span>
    </Button>
    );
    });
    CarouselPrevious.displayName = "CarouselPrevious";

    const CarouselNext = React.forwardRef<
    HTMLButtonElement,
    React.ComponentProps<typeof Button>
    >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
    const { orientation, scrollNext, canScrollNext } = useCarousel();

    return (
    <Button
    ref={ref}
    variant={variant}
    size={size}
    className={cn(
    "absolute h-8 w-8 rounded-full",
    orientation === "horizontal"
    ? "-right-12 top-1/2 -translate-y-1/2"
    : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
    className,
    )}
    disabled={!canScrollNext}
    onClick={scrollNext}
    {...props}
    >
    <ArrowRight className="h-4 w-4" />
    <span className="sr-only">Next slide</span>
    </Button>
    );
    });
    CarouselNext.displayName = "CarouselNext";

    const DotSvg = ({ isSelected }: { isSelected: boolean }) => (
    <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="1"
    strokeLinecap="round"
    strokeLinejoin="round"
    className={cn(
    isSelected
    ? " fill-blue-500 text-blue-500"
    : "fill-slate-400 text-slate-400",
    "h-5 w-5",
    )}
    >
    <circle cx="12.1" cy="12.1" r="5" />
    </svg>
    );

    type DotButtonProps = ButtonProps & {
    isSelected: boolean;
    onClick: () => void;
    };

    const DotButton = React.forwardRef<HTMLButtonElement, DotButtonProps>(
    ({ className, isSelected, onClick, ...props }, ref) => {
    return (
    <button
    className={cn(isSelected ? " fill-blue-500" : "fill-slate-400")}
    ref={ref}
    onClick={onClick}
    {...props}
    >
    <DotSvg isSelected={isSelected} />
    </button>
    );
    },
    );

    DotButton.displayName = "DotButton";

    const CarouselNavigation = React.forwardRef<
    HTMLDivElement,
    React.HTMLAttributes<HTMLDivElement>
    >((props, ref) => {
    const { api, selectedIndex, scrollTo } = useCarousel();

    return (
    <div
    ref={ref}
    className={cn("flex items-center justify-center")}
    {...props}
    >
    {api &&
    api
    .scrollSnapList()
    .map((_, index) => (
    <DotButton
    key={index}
    isSelected={index === selectedIndex}
    onClick={() => scrollTo(index)}
    />
    ))}
    </div>
    );
    });

    CarouselNavigation.displayName = "CarouselNavigation";

    export {
    Carousel,
    CarouselContent,
    CarouselItem,
    CarouselNavigation,
    CarouselNext,
    CarouselPrevious,
    type CarouselApi,
    };