diff --git a/src/components/GeneralCarousel/GeneralCarousel.style.ts b/src/components/GeneralCarousel/Carousel.style.ts similarity index 73% rename from src/components/GeneralCarousel/GeneralCarousel.style.ts rename to src/components/GeneralCarousel/Carousel.style.ts index cf96616..29ec322 100644 --- a/src/components/GeneralCarousel/GeneralCarousel.style.ts +++ b/src/components/GeneralCarousel/Carousel.style.ts @@ -2,9 +2,10 @@ import { css } from '@emotion/react'; import { Theme } from '@styles/Theme'; -export const getContainerStyling = (width: number, height: number) => { +export const containerStyling = (width: number, height: number) => { return css({ position: 'relative', + width, height, minWidth: width, @@ -20,16 +21,29 @@ export const getContainerStyling = (width: number, height: number) => { }); }; -export const sliderWrapperStyling = css({ - display: 'flex', - width: '100%', - margin: 0, - padding: 0, +export const sliderWrapperStyling = (width: number, height: number) => + css({ + display: 'flex', + width: '100%', + margin: 0, + padding: 0, + height, - overflow: 'hidden', -}); + overflow: 'hidden', + }); + +export const carouselItemStyling = (width: number, height: number) => + css({ + display: 'flex', + + '& > *': { + objectFit: 'cover', + width, + height, + }, + }); -export const getItemWrapperStyling = (width: number, height: number) => { +export const itemWrapperStyling = (width: number, height: number) => { return css({ minWidth: width, width, @@ -47,7 +61,7 @@ export const getItemWrapperStyling = (width: number, height: number) => { }); }; -export const getButtonContainerStyling = (showOnHover: boolean) => +export const buttonContainerStyling = (showOnHover: boolean) => css({ transition: 'opacity .1s ease-in', diff --git a/src/components/GeneralCarousel/Carousel.tsx b/src/components/GeneralCarousel/Carousel.tsx new file mode 100644 index 0000000..a1e4852 --- /dev/null +++ b/src/components/GeneralCarousel/Carousel.tsx @@ -0,0 +1,98 @@ +import LeftIcon from '@assets/svg/left-icon.svg'; +import RightIcon from '@assets/svg/right-icon.svg'; +import { createContext, useMemo } from 'react'; +import type { PropsWithChildren } from 'react'; + +import useCarousel from '@hooks/useCarousel'; + +import Box from '@components/Box/Box'; + +import { + buttonContainerStyling, + containerStyling, + leftButtonStyling, + rightButtonStyling, + sliderWrapperStyling, +} from './Carousel.style'; +import CarouselItem from './CarouselItem'; +import Dots from './Dots'; + +export interface CarouselProps extends PropsWithChildren { + width: number; + height: number; + length: number; + showNavigationOnHover?: boolean; + showArrows?: boolean; + showDots?: boolean; + children?: JSX.Element | JSX.Element[]; +} + +export const CarouselContext = createContext<{ + viewIndex: number; + width: number; + height: number; + itemRef: React.MutableRefObject; +} | null>(null); + +const Carousel = ({ + width, + height, + length, + showNavigationOnHover = true, + showArrows = true, + showDots = true, + children, +}: CarouselProps) => { + const { viewIndex, itemRef, carouselBoxRef, handleMoveImage, handleClickLeft, handleClickRight } = + useCarousel(length); + + const context = useMemo( + () => ({ + width, + height, + viewIndex, + itemRef, + carouselBoxRef, + handleMoveImage, + handleClickLeft, + handleClickRight, + }), + [ + width, + height, + viewIndex, + itemRef, + carouselBoxRef, + handleMoveImage, + handleClickLeft, + handleClickRight, + ] + ); + + return ( + +
+ {showArrows && length !== 1 && ( +
+ + +
+ )} + + {showDots && ( + + )} + + {children} +
+
+ ); +}; + +Carousel.Item = CarouselItem; + +export default Carousel; diff --git a/src/components/GeneralCarousel/CarouselItem.tsx b/src/components/GeneralCarousel/CarouselItem.tsx new file mode 100644 index 0000000..e088a00 --- /dev/null +++ b/src/components/GeneralCarousel/CarouselItem.tsx @@ -0,0 +1,33 @@ +import { useContext, useEffect, useRef } from 'react'; +import type { PropsWithChildren } from 'react'; + +import { CarouselContext } from '@components/GeneralCarousel/Carousel'; +import { carouselItemStyling } from '@components/GeneralCarousel/Carousel.style'; + +export interface CarouselItemProps extends PropsWithChildren { + index: number; +} + +const CarouselItem = ({ index, children }: CarouselItemProps) => { + const ref = useRef(null); + const context = useContext(CarouselContext); + + if (!context) throw Error('Carousel.Item is only available within Carousel.'); + + const { width, height, viewIndex, itemRef } = context; + + useEffect(() => { + if (ref.current) { + if (index === viewIndex) itemRef.current = ref.current; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [viewIndex]); + + return ( +
+ {children} +
+ ); +}; + +export default CarouselItem; diff --git a/src/components/GeneralCarousel/GeneralCarousel.tsx b/src/components/GeneralCarousel/GeneralCarousel.tsx deleted file mode 100644 index 69b97c1..0000000 --- a/src/components/GeneralCarousel/GeneralCarousel.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import LeftIcon from '@assets/svg/left-icon.svg'; -import RightIcon from '@assets/svg/right-icon.svg'; - -import useGeneralCarousel from '@hooks/useGeneralCarousel'; - -import Box from '@components/Box/Box'; -import Dots from '@components/GeneralCarousel/Dots'; -import { - getButtonContainerStyling, - getContainerStyling, - getItemWrapperStyling, - leftButtonStyling, - rightButtonStyling, - sliderWrapperStyling, -} from '@components/GeneralCarousel/GeneralCarousel.style'; - -export interface useGeneralCarouselProps { - width: number; - height: number; - items: React.FC>[] | string[]; - showNavigationOnHover?: boolean; - showArrows?: boolean; - showDots?: boolean; - children?: JSX.Element | JSX.Element[] | null; -} - -const GeneralCarousel = ({ - width, - height, - items, - showNavigationOnHover = true, - showArrows = true, - showDots = true, - children = null, -}: useGeneralCarouselProps) => { - const { viewIndex, itemRef, carouselBoxRef, handleMoveImage, handleClickLeft, handleClickRight } = - useGeneralCarousel(items); - - return ( -
- {showDots && ( - - )} - {showArrows && items.length !== 1 && ( -
- - -
- )} - - {children === null - ? items.map((Item, index) => { - if (typeof Item === 'string') { - return ( -
- 이미지 -
- ); - } - return ( -
- -
- ); - }) - : children} -
-
- ); -}; - -export default GeneralCarousel; diff --git a/src/hooks/useGeneralCarousel.ts b/src/hooks/useCarousel.ts similarity index 81% rename from src/hooks/useGeneralCarousel.ts rename to src/hooks/useCarousel.ts index 2a9a6ed..c719ead 100644 --- a/src/hooks/useGeneralCarousel.ts +++ b/src/hooks/useCarousel.ts @@ -2,7 +2,7 @@ import { useRef, useState } from 'react'; import type { MouseEvent } from 'react'; import { flushSync } from 'react-dom'; -const useGeneralCarousel = (items: React.FC>[] | string[]) => { +const useCarousel = (itemLength: number) => { const [viewIndex, setViewIndex] = useState(0); const carouselBoxRef = useRef(null); const itemRef = useRef(null); @@ -41,7 +41,7 @@ const useGeneralCarousel = (items: React.FC>[] | s e.stopPropagation(); if (itemRef.current) { flushSync(() => { - if (viewIndex === items.length - 1) setViewIndex(viewIndex); + if (viewIndex === itemLength - 1) setViewIndex(viewIndex); else setViewIndex(viewIndex + 1); }); @@ -53,7 +53,14 @@ const useGeneralCarousel = (items: React.FC>[] | s } }; - return { viewIndex, itemRef, carouselBoxRef, handleMoveImage, handleClickLeft, handleClickRight }; + return { + viewIndex, + itemRef, + carouselBoxRef, + handleMoveImage, + handleClickLeft, + handleClickRight, + }; }; -export default useGeneralCarousel; +export default useCarousel; diff --git a/src/index.tsx b/src/index.tsx index d844f52..e426fa8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,7 +17,7 @@ import DateRangePicker from '@components/DateRangePicker/DateRangePicker'; import Divider from '@components/Divider/Divider'; import Flex from '@components/Flex/Flex'; import FloatingButton from '@components/FloatingButton/FloatingButton'; -import GeneralCarousel from '@components/GeneralCarousel/GeneralCarousel'; +import GeneralCarousel from '@components/GeneralCarousel/Carousel'; import Heading from '@components/Heading/Heading'; import ImageCarousel from '@components/ImageCarousel/ImageCarousel'; import ImageUploadInput from '@components/ImageUploadInput/ImageUploadInput'; diff --git a/src/stories/GeneralCarousel.stories.tsx b/src/stories/GeneralCarousel.stories.tsx index ac22a7e..3faaa29 100644 --- a/src/stories/GeneralCarousel.stories.tsx +++ b/src/stories/GeneralCarousel.stories.tsx @@ -1,57 +1,43 @@ -import icon1 from '@assets/svg/add-icon.svg'; -import icon2 from '@assets/svg/checked-icon.svg'; -import icon3 from '@assets/svg/empty-star.svg'; +/* eslint-disable react/no-array-index-key */ + +/* eslint-disable jsx-a11y/img-redundant-alt */ import type { Meta, StoryObj } from '@storybook/react'; -import GeneralCarousel from '@components/GeneralCarousel/GeneralCarousel'; +import Carousel from '@components/GeneralCarousel/Carousel'; const meta = { - title: 'GeneralCarousel', - component: GeneralCarousel, + title: 'Carousel', + component: Carousel, argTypes: { width: { control: 'number' }, height: { control: 'number' }, - items: { control: false }, }, args: { width: 300, height: 200, + length: 3, }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +} satisfies Meta; -const items = [icon2, icon1, icon3]; -const items2 = [ - 'https://www.shutterstock.com/image-photo/red-apple-isolated-on-white-260nw-1727544364.jpg', - 'https://www.shutterstock.com/image-photo/red-apple-isolated-on-white-260nw-1727544364.jpg', - 'https://www.shutterstock.com/image-photo/red-apple-isolated-on-white-260nw-1727544364.jpg', +const images = [ + 'https://i.pinimg.com/236x/18/0e/c6/180ec6aaf4b5aab89d91f36752219569.jpg', + 'https://img.freepik.com/free-photo/many-ripe-juicy-red-apples-covered-with-water-drops-closeup-selective-focus-ripe-fruits-as-a-background_166373-2611.jpg?size=626&ext=jpg&ga=GA1.1.1546980028.1703808000&semt=sph', + 'https://img.freepik.com/premium-photo/a-red-apple-with-a-white-background-and-a-white-background_933356-5.jpg', ]; -export const Default: Story = { - render: ({ ...args }) => { - return ; - }, -}; - -export const WithArrowButtons: Story = { - render: ({ ...args }) => { - return ; - }, - args: {}, -}; - -export const WithDots: Story = { - render: ({ ...args }) => { - return ; - }, - args: {}, -}; +export default meta; +type Story = StoryObj; -export const ShowNavigationOnHover: Story = { +export const Default: Story = { render: ({ ...args }) => { - return ; + return ( + + {images.map((url, index) => ( + + image + + ))} + + ); }, - args: {}, };