Skip to content

Commit

Permalink
Merge pull request #60 from hngngn/fix/carousel
Browse files Browse the repository at this point in the history
fix: `Carousel` touch button behavior (#58)
  • Loading branch information
hngngn authored Apr 19, 2024
2 parents 516c9f9 + a4ef35d commit e1ac285
Show file tree
Hide file tree
Showing 7 changed files with 17 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
"dependencies": [
"embla-carousel-solid"
],
"registryDependencies": [
"button"
],
"files": [
{
"name": "carousel.tsx",
"content": "import { cn } from \"@/libs/cn\";\nimport type { CreateEmblaCarouselType } from \"embla-carousel-solid\";\nimport createEmblaCarousel from \"embla-carousel-solid\";\n\nimport type { Accessor, ComponentProps, ParentProps, VoidProps } from \"solid-js\";\nimport {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n mergeProps,\n splitProps,\n useContext\n} from \"solid-js\";\nimport { Button } from \"./button\";\n\nexport type CarouselApi = CreateEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof createEmblaCarousel>;\ntype CarouselOptions = NonNullable<UseCarouselParameters[0]>;\ntype CarouselPlugin = NonNullable<UseCarouselParameters[1]>;\n\ntype CarouselProps = {\n opts?: ReturnType<CarouselOptions>;\n plugins?: ReturnType<CarouselPlugin>;\n orientation?: \"horizontal\" | \"vertical\";\n setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof createEmblaCarousel>[0];\n api: ReturnType<typeof createEmblaCarousel>[1];\n scrollPrev: () => void;\n scrollNext: () => void;\n canScrollPrev: Accessor<boolean>;\n canScrollNext: Accessor<boolean>;\n} & CarouselProps;\n\nconst CarouselContext = createContext<Accessor<CarouselContextProps> | null>(null);\n\nconst useCarousel = () => {\n const context = useContext(CarouselContext);\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\");\n }\n\n return context();\n};\n\nexport const Carousel = (props: ComponentProps<\"div\"> & CarouselProps) => {\n const merge = mergeProps<ParentProps<ComponentProps<\"div\"> & CarouselProps>[]>(\n { orientation: \"horizontal\" },\n props\n );\n\n const [local, rest] = splitProps(merge, [\n \"orientation\",\n \"opts\",\n \"setApi\",\n \"plugins\",\n \"class\",\n \"children\"\n ]);\n\n const [carouselRef, api] = createEmblaCarousel(\n () => ({\n ...local.opts,\n axis: local.orientation === \"horizontal\" ? \"x\" : \"y\"\n }),\n () => (local.plugins === undefined ? [] : local.plugins)\n );\n const [canScrollPrev, setCanScrollPrev] = createSignal(false);\n const [canScrollNext, setCanScrollNext] = createSignal(false);\n\n const onSelect = (api: NonNullable<ReturnType<CarouselApi>>) => {\n setCanScrollPrev(api.canScrollPrev());\n setCanScrollNext(api.canScrollNext());\n };\n\n const scrollPrev = () => {\n api()?.scrollPrev();\n };\n\n const scrollNext = () => {\n api()?.scrollNext();\n };\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n scrollPrev();\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault();\n scrollNext();\n }\n };\n\n createEffect(() => {\n if (!api() || !local.setApi) {\n return;\n }\n\n local.setApi(api);\n });\n\n createEffect(() => {\n if (!api()) {\n return;\n }\n\n onSelect(api()!);\n api()?.on(\"reInit\", onSelect);\n api()?.on(\"select\", onSelect);\n\n return () => {\n api()?.off(\"select\", onSelect);\n };\n });\n\n const value = createMemo(\n () =>\n ({\n carouselRef,\n api,\n opts: local.opts,\n orientation: local.orientation || (local.opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext\n }) satisfies CarouselContextProps\n );\n\n return (\n <CarouselContext.Provider value={value}>\n <div\n onKeyDown={handleKeyDown}\n class={cn(\"relative\", local.class)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n {...rest}\n >\n {local.children}\n </div>\n </CarouselContext.Provider>\n );\n};\n\nexport const CarouselContent = (props: ComponentProps<\"div\">) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { carouselRef, orientation } = useCarousel();\n\n return (\n <div ref={carouselRef} class=\"overflow-hidden\">\n <div\n class={cn(\"flex\", orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\", local.class)}\n {...rest}\n />\n </div>\n );\n};\n\nexport const CarouselItem = (props: ComponentProps<\"div\">) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { orientation } = useCarousel();\n\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n class={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport const CarouselPrevious = (props: VoidProps<ComponentProps<typeof Button>>) => {\n const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(\n { variant: \"outline\", size: \"icon\" },\n props\n );\n const [local, rest] = splitProps(merge, [\"class\", \"variant\", \"size\"]);\n const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n\n return (\n <Button\n variant={local.variant}\n size={local.size}\n class={cn(\n \"absolute h-8 w-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"-left-12 top-1/2 -translate-y-1/2\"\n : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n local.class\n )}\n disabled={!canScrollPrev()}\n onClick={scrollPrev}\n {...rest}\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" class=\"h-4 w-4\">\n <path\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M5 12h14M5 12l6 6m-6-6l6-6\"\n />\n </svg>\n <span class=\"sr-only\">Previous slide</span>\n </Button>\n );\n};\n\nexport const CarouselNext = (props: VoidProps<ComponentProps<typeof Button>>) => {\n const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(\n { variant: \"outline\", size: \"icon\" },\n props\n );\n const [local, rest] = splitProps(merge, [\"class\", \"variant\", \"size\"]);\n const { orientation, scrollNext, canScrollNext } = useCarousel();\n\n return (\n <Button\n variant={local.variant}\n size={local.size}\n class={cn(\n \"absolute h-8 w-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"-right-12 top-1/2 -translate-y-1/2\"\n : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n local.class\n )}\n disabled={!canScrollNext()}\n onClick={scrollNext}\n {...rest}\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" class=\"h-4 h-4\">\n <path\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M5 12h14m-4 4l4-4m-4-4l4 4\"\n />\n </svg>\n <span class=\"sr-only\">Next slide</span>\n </Button>\n );\n};\n"
"content": "import { cn } from \"@/libs/cn\";\nimport type { CreateEmblaCarouselType } from \"embla-carousel-solid\";\nimport createEmblaCarousel from \"embla-carousel-solid\";\n\nimport type { Accessor, ComponentProps, ParentProps, VoidProps } from \"solid-js\";\nimport {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n mergeProps,\n splitProps,\n useContext\n} from \"solid-js\";\nimport { Button } from \"./button\";\n\nexport type CarouselApi = CreateEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof createEmblaCarousel>;\ntype CarouselOptions = NonNullable<UseCarouselParameters[0]>;\ntype CarouselPlugin = NonNullable<UseCarouselParameters[1]>;\n\ntype CarouselProps = {\n opts?: ReturnType<CarouselOptions>;\n plugins?: ReturnType<CarouselPlugin>;\n orientation?: \"horizontal\" | \"vertical\";\n setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof createEmblaCarousel>[0];\n api: ReturnType<typeof createEmblaCarousel>[1];\n scrollPrev: () => void;\n scrollNext: () => void;\n canScrollPrev: Accessor<boolean>;\n canScrollNext: Accessor<boolean>;\n} & CarouselProps;\n\nconst CarouselContext = createContext<Accessor<CarouselContextProps> | null>(null);\n\nconst useCarousel = () => {\n const context = useContext(CarouselContext);\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\");\n }\n\n return context();\n};\n\nexport const Carousel = (props: ComponentProps<\"div\"> & CarouselProps) => {\n const merge = mergeProps<ParentProps<ComponentProps<\"div\"> & CarouselProps>[]>(\n { orientation: \"horizontal\" },\n props\n );\n\n const [local, rest] = splitProps(merge, [\n \"orientation\",\n \"opts\",\n \"setApi\",\n \"plugins\",\n \"class\",\n \"children\"\n ]);\n\n const [carouselRef, api] = createEmblaCarousel(\n () => ({\n ...local.opts,\n axis: local.orientation === \"horizontal\" ? \"x\" : \"y\"\n }),\n () => (local.plugins === undefined ? [] : local.plugins)\n );\n const [canScrollPrev, setCanScrollPrev] = createSignal(false);\n const [canScrollNext, setCanScrollNext] = createSignal(false);\n\n const onSelect = (api: NonNullable<ReturnType<CarouselApi>>) => {\n setCanScrollPrev(api.canScrollPrev());\n setCanScrollNext(api.canScrollNext());\n };\n\n const scrollPrev = () => {\n api()?.scrollPrev();\n };\n\n const scrollNext = () => {\n api()?.scrollNext();\n };\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n scrollPrev();\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault();\n scrollNext();\n }\n };\n\n createEffect(() => {\n if (!api() || !local.setApi) {\n return;\n }\n\n local.setApi(api);\n });\n\n createEffect(() => {\n if (!api()) {\n return;\n }\n\n onSelect(api()!);\n api()?.on(\"reInit\", onSelect);\n api()?.on(\"select\", onSelect);\n\n return () => {\n api()?.off(\"select\", onSelect);\n };\n });\n\n const value = createMemo(\n () =>\n ({\n carouselRef,\n api,\n opts: local.opts,\n orientation: local.orientation || (local.opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext\n }) satisfies CarouselContextProps\n );\n\n return (\n <CarouselContext.Provider value={value}>\n <div\n onKeyDown={handleKeyDown}\n class={cn(\"relative\", local.class)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n {...rest}\n >\n {local.children}\n </div>\n </CarouselContext.Provider>\n );\n};\n\nexport const CarouselContent = (props: ComponentProps<\"div\">) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { carouselRef, orientation } = useCarousel();\n\n return (\n <div ref={carouselRef} class=\"overflow-hidden\">\n <div\n class={cn(\"flex\", orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\", local.class)}\n {...rest}\n />\n </div>\n );\n};\n\nexport const CarouselItem = (props: ComponentProps<\"div\">) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { orientation } = useCarousel();\n\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n class={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport const CarouselPrevious = (props: VoidProps<ComponentProps<typeof Button>>) => {\n const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(\n { variant: \"outline\", size: \"icon\" },\n props\n );\n const [local, rest] = splitProps(merge, [\"class\", \"variant\", \"size\"]);\n const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n\n return (\n <Button\n variant={local.variant}\n size={local.size}\n class={cn(\n \"absolute h-8 w-8 rounded-full touch-manipulation\",\n orientation === \"horizontal\"\n ? \"-left-12 top-1/2 -translate-y-1/2\"\n : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n local.class\n )}\n disabled={!canScrollPrev()}\n onClick={scrollPrev}\n {...rest}\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" class=\"h-4 w-4\">\n <path\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M5 12h14M5 12l6 6m-6-6l6-6\"\n />\n </svg>\n <span class=\"sr-only\">Previous slide</span>\n </Button>\n );\n};\n\nexport const CarouselNext = (props: VoidProps<ComponentProps<typeof Button>>) => {\n const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(\n { variant: \"outline\", size: \"icon\" },\n props\n );\n const [local, rest] = splitProps(merge, [\"class\", \"variant\", \"size\"]);\n const { orientation, scrollNext, canScrollNext } = useCarousel();\n\n return (\n <Button\n variant={local.variant}\n size={local.size}\n class={cn(\n \"absolute h-8 w-8 rounded-full touch-manipulation\",\n orientation === \"horizontal\"\n ? \"-right-12 top-1/2 -translate-y-1/2\"\n : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n local.class\n )}\n disabled={!canScrollNext()}\n onClick={scrollNext}\n {...rest}\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" class=\"h-4 h-4\">\n <path\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M5 12h14m-4 4l4-4m-4-4l4 4\"\n />\n </svg>\n <span class=\"sr-only\">Next slide</span>\n </Button>\n );\n};\n"
}
],
"type": "components:ui"
Expand Down
Loading

0 comments on commit e1ac285

Please sign in to comment.