返回 presentation-ai
carousel.tsx
根目录 / src / components / ui / carousel.tsx
1 "use client";
2
3 import useEmblaCarousel, {
4 type UseEmblaCarouselType,
5 } from "embla-carousel-react";
6 import { ArrowLeft, ArrowRight } from "lucide-react";
7 import * as React from "react";
8
9 import { Button } from "@/components/ui/button";
10 import { cn } from "@/lib/utils";
11
12 type CarouselApi = UseEmblaCarouselType[1];
13 type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
14 type CarouselOptions = UseCarouselParameters[0];
15 type CarouselPlugin = UseCarouselParameters[1];
16
17 type CarouselProps = {
18 opts?: CarouselOptions;
19 plugins?: CarouselPlugin;
20 orientation?: "horizontal" | "vertical";
21 setApi?: (api: CarouselApi) => void;
22 };
23
24 type CarouselContextProps = {
25 carouselRef: ReturnType<typeof useEmblaCarousel>[0];
26 api: ReturnType<typeof useEmblaCarousel>[1];
27 scrollPrev: () => void;
28 scrollNext: () => void;
29 canScrollPrev: boolean;
30 canScrollNext: boolean;
31 } & CarouselProps;
32
33 const CarouselContext = React.createContext<CarouselContextProps | null>(null);
34
35 function useCarousel() {
36 const context = React.useContext(CarouselContext);
37
38 if (!context) {
39 throw new Error("useCarousel must be used within a <Carousel />");
40 }
41
42 return context;
43 }
44
45 const Carousel = React.forwardRef<
46 HTMLDivElement,
47 React.HTMLAttributes<HTMLDivElement> & CarouselProps
48 >(
49 (
50 {
51 orientation = "horizontal",
52 opts,
53 setApi,
54 plugins,
55 className,
56 children,
57 ...props
58 },
59 ref,
60 ) => {
61 const [carouselRef, api] = useEmblaCarousel(
62 {
63 ...opts,
64 axis: orientation === "horizontal" ? "x" : "y",
65 },
66 plugins,
67 );
68 const [canScrollPrev, setCanScrollPrev] = React.useState(false);
69 const [canScrollNext, setCanScrollNext] = React.useState(false);
70
71 const onSelect = React.useCallback((api: CarouselApi) => {
72 if (!api) {
73 return;
74 }
75
76 setCanScrollPrev(api.canScrollPrev());
77 setCanScrollNext(api.canScrollNext());
78 }, []);
79
80 const scrollPrev = React.useCallback(() => {
81 api?.scrollPrev();
82 }, [api]);
83
84 const scrollNext = React.useCallback(() => {
85 api?.scrollNext();
86 }, [api]);
87
88 const handleKeyDown = React.useCallback(
89 (event: React.KeyboardEvent<HTMLDivElement>) => {
90 if (event.key === "ArrowLeft") {
91 event.preventDefault();
92 scrollPrev();
93 } else if (event.key === "ArrowRight") {
94 event.preventDefault();
95 scrollNext();
96 }
97 },
98 [scrollPrev, scrollNext],
99 );
100
101 React.useEffect(() => {
102 if (!api || !setApi) {
103 return;
104 }
105
106 setApi(api);
107 }, [api, setApi]);
108
109 React.useEffect(() => {
110 if (!api) {
111 return;
112 }
113
114 onSelect(api);
115 api.on("reInit", onSelect);
116 api.on("select", onSelect);
117
118 return () => {
119 api?.off("select", onSelect);
120 };
121 }, [api, onSelect]);
122
123 return (
124 <CarouselContext.Provider
125 value={{
126 carouselRef,
127 api: api,
128 opts,
129 orientation:
130 orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
131 scrollPrev,
132 scrollNext,
133 canScrollPrev,
134 canScrollNext,
135 }}
136 >
137 <div
138 ref={ref}
139 onKeyDownCapture={handleKeyDown}
140 className={cn("relative", className)}
141 role="region"
142 aria-roledescription="carousel"
143 {...props}
144 >
145 {children}
146 </div>
147 </CarouselContext.Provider>
148 );
149 },
150 );
151 Carousel.displayName = "Carousel";
152
153 const CarouselContent = React.forwardRef<
154 HTMLDivElement,
155 React.HTMLAttributes<HTMLDivElement>
156 >(({ className, ...props }, ref) => {
157 const { carouselRef, orientation } = useCarousel();
158
159 return (
160 <div ref={carouselRef} className="overflow-hidden">
161 <div
162 ref={ref}
163 className={cn(
164 "flex",
165 orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
166 className,
167 )}
168 {...props}
169 />
170 </div>
171 );
172 });
173 CarouselContent.displayName = "CarouselContent";
174
175 const CarouselItem = React.forwardRef<
176 HTMLDivElement,
177 React.HTMLAttributes<HTMLDivElement>
178 >(({ className, ...props }, ref) => {
179 const { orientation } = useCarousel();
180
181 return (
182 <div
183 ref={ref}
184 role="group"
185 aria-roledescription="slide"
186 className={cn(
187 "min-w-0 shrink-0 grow-0 basis-full",
188 orientation === "horizontal" ? "pl-4" : "pt-4",
189 className,
190 )}
191 {...props}
192 />
193 );
194 });
195 CarouselItem.displayName = "CarouselItem";
196
197 const CarouselPrevious = React.forwardRef<
198 HTMLButtonElement,
199 React.ComponentProps<typeof Button>
200 >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201 const { orientation, scrollPrev, canScrollPrev } = useCarousel();
202
203 return (
204 <Button
205 ref={ref}
206 variant={variant}
207 size={size}
208 type="button"
209 className={cn(
210 "absolute h-8 w-8 rounded-full",
211 orientation === "horizontal"
212 ? "top-1/2 -left-12 -translate-y-1/2"
213 : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
214 "disabled:pointer-events-auto disabled:cursor-not-allowed",
215 className,
216 )}
217 disabled={!canScrollPrev}
218 onClick={scrollPrev}
219 {...props}
220 >
221 <ArrowLeft className="h-4 w-4" />
222 <span className="sr-only">Previous slide</span>
223 </Button>
224 );
225 });
226 CarouselPrevious.displayName = "CarouselPrevious";
227
228 const CarouselNext = React.forwardRef<
229 HTMLButtonElement,
230 React.ComponentProps<typeof Button>
231 >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
232 const { orientation, scrollNext, canScrollNext } = useCarousel();
233
234 return (
235 <Button
236 ref={ref}
237 variant={variant}
238 size={size}
239 type="button"
240 className={cn(
241 "absolute h-8 w-8 rounded-full",
242 orientation === "horizontal"
243 ? "top-1/2 -right-12 -translate-y-1/2"
244 : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
245 "disabled:pointer-events-auto disabled:cursor-not-allowed",
246 className,
247 )}
248 disabled={!canScrollNext}
249 onClick={scrollNext}
250 {...props}
251 >
252 <ArrowRight className="h-4 w-4" />
253 <span className="sr-only">Next slide</span>
254 </Button>
255 );
256 });
257 CarouselNext.displayName = "CarouselNext";
258
259 export {
260 Carousel,
261 CarouselContent,
262 CarouselItem,
263 CarouselNext,
264 CarouselPrevious,
265 type CarouselApi,
266 };
267
267 lines Plain Text