Skip to content

Instantly share code, notes, and snippets.

@gabrielmlinassi
Created November 16, 2022 06:12
Show Gist options
  • Save gabrielmlinassi/83aa2a12d42dbc9324c4efab72862ed6 to your computer and use it in GitHub Desktop.
Save gabrielmlinassi/83aa2a12d42dbc9324c4efab72862ed6 to your computer and use it in GitHub Desktop.
Carousel
import cn from 'classnames'
import useEmblaCarousel, { UseEmblaCarouselType } from 'embla-carousel-react'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { NextButton, PrevButton } from './CarouselButtons'
const Context = React.createContext<{
emblaRef: UseEmblaCarouselType[0]
thumbRef: UseEmblaCarouselType[0]
selectedIdx: number
prevBtnEnabled: boolean
nextBtnEnabled: boolean
scrollPrev: () => void
scrollNext: () => void
scrollTo: (idx: number) => void
}>({
emblaRef: null,
thumbRef: null,
selectedIdx: 0,
prevBtnEnabled: false,
nextBtnEnabled: true,
scrollPrev: () => null,
scrollNext: () => null,
scrollTo: () => null
})
const Provider = ({ children }: { children: React.ReactNode }) => {
const [emblaRef, embla] = useEmblaCarousel()
const [thumbRef, emblaThumbs] = useEmblaCarousel()
const [selectedIdx, setSelectedIdx] = useState(0)
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false)
const [nextBtnEnabled, setNextBtnEnabled] = useState(false)
const scrollPrev = useCallback(() => embla && embla.scrollPrev(), [embla])
const scrollNext = useCallback(() => embla && embla.scrollNext(), [embla])
const scrollTo = useCallback((idx) => embla && embla.scrollTo(idx), [embla])
const onSelect = useCallback(() => {
if (!embla || !emblaThumbs) return
setSelectedIdx(embla.selectedScrollSnap())
setPrevBtnEnabled(embla.canScrollPrev())
setNextBtnEnabled(embla.canScrollNext())
emblaThumbs.scrollTo(embla.selectedScrollSnap())
}, [embla, emblaThumbs, setSelectedIdx])
useEffect(() => {
if (!embla) return
onSelect()
embla.on('select', onSelect)
}, [embla, onSelect])
return (
<Context.Provider
value={{
emblaRef,
thumbRef,
selectedIdx,
prevBtnEnabled,
nextBtnEnabled,
scrollPrev,
scrollNext,
scrollTo
}}
>
{children}
</Context.Provider>
)
}
type CarouselProps = {
children: React.ReactNode
}
const Carousel = ({ children }: CarouselProps) => {
const { emblaRef, thumbRef } = useContext(Context)
const Slots = {
slides: null,
thumbnails: null,
prevButton: null,
nextButton: null
}
React.Children.forEach(children, (child) => {
if (React.isValidElement(child)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
switch ((child.type as any)?.displayName) {
case 'Slides':
Slots.slides = child
break
case 'Thumbnails':
Slots.thumbnails = child
break
case 'PrevButton':
Slots.prevButton = child
break
case 'NextButton':
Slots.nextButton = child
break
}
}
})
return (
<div className="relative h-full">
<div className="overflow-hidden h-full" ref={emblaRef}>
<div className="flex h-full">{Slots.slides}</div>
</div>
<div className="absolute bottom-2 left-1/2 -translate-x-1/2">
<div ref={thumbRef} className="flex gap-2">
{Slots.thumbnails}
</div>
</div>
{Slots.prevButton}
{Slots.nextButton}
</div>
)
}
const Slides = ({ children }: { children: React.ReactNode }) => {
return (
<>
{React.Children.map(children, (child, idx) => (
<div key={idx} className="relative basis-full grow-0 shrink-0">
{child}
</div>
))}
</>
)
}
const Thumbnails = ({ children }: { children: React.ReactNode }) => {
const { scrollTo, selectedIdx } = useContext(Context)
return (
<>
{React.Children.map(children, (child, idx) => (
<div
key={idx}
onClick={() => scrollTo(idx)}
className={cn([
'relative w-[70px] h-[70px]',
selectedIdx === idx && 'outline outline-4 outline-primary'
])}
>
{child}
</div>
))}
</>
)
}
const Prev = () => {
const { scrollPrev, prevBtnEnabled } = useContext(Context)
return <PrevButton onClick={scrollPrev} enabled={prevBtnEnabled} />
}
const Next = () => {
const { scrollNext, nextBtnEnabled } = useContext(Context)
return <NextButton onClick={scrollNext} enabled={nextBtnEnabled} />
}
const Root = ({ children }: { children: React.ReactNode }) => {
return (
<Provider>
<Carousel>{children}</Carousel>
</Provider>
)
}
Slides.displayName = 'Slides'
Thumbnails.displayName = 'Thumbnails'
Prev.displayName = 'PrevButton'
Next.displayName = 'NextButton'
Carousel.Root = Root
Carousel.Slides = Slides
Carousel.Thumbnails = Thumbnails
Carousel.PrevButton = Prev
Carousel.NextButton = Next
export default Carousel
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment