-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: carousel컴포넌트 추가 * chore: 주석제거 * fix: viewIndex 로직 분리 * feat: move버튼 throttle 적용 * chore: 범용 hook 경로 수정 --------- Co-authored-by: moonki kim <[email protected]>
- Loading branch information
1 parent
36aa38e
commit 2631941
Showing
9 changed files
with
396 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { createContext, useMemo } from 'react'; | ||
import type { MouseEvent, PropsWithChildren } from 'react'; | ||
|
||
import CarouselItem from './CarouselItem'; | ||
import useCarousel from './useCarousel'; | ||
import Move from './Move'; | ||
import Wrapper from './Wrapper'; | ||
import Dots from './Dots'; | ||
|
||
export interface CarouselProps extends PropsWithChildren { | ||
width: number; | ||
height: number; | ||
length: number; | ||
} | ||
|
||
// viewIndex context 분리 | ||
export const CarouselContext = createContext<{ | ||
width: number; | ||
height: number; | ||
length: number; | ||
itemRef: React.MutableRefObject<HTMLDivElement | null>; | ||
carouselBoxRef: React.MutableRefObject<HTMLDivElement | null>; | ||
handleMoveImage: (imageNumber: number) => void; | ||
handleMoveNext: (e: MouseEvent<HTMLButtonElement>) => void; | ||
handleMovePrev: (e: MouseEvent<HTMLButtonElement>) => void; | ||
} | null>(null); | ||
|
||
export const CarouselIndexContext = createContext<number>(0); | ||
|
||
const Carousel = ({ width, height, length, children }: CarouselProps) => { | ||
const { viewIndex, itemRef, carouselBoxRef, handleMoveImage, handleMoveNext, handleMovePrev } = useCarousel(length); | ||
|
||
const carouselContextValue = useMemo( | ||
() => ({ | ||
width, | ||
height, | ||
length, | ||
itemRef, | ||
carouselBoxRef, | ||
handleMoveImage, | ||
handleMoveNext, | ||
handleMovePrev, | ||
}), | ||
[width, height, length, itemRef, carouselBoxRef, handleMoveImage, handleMoveNext, handleMovePrev], | ||
); | ||
|
||
// viewIndex context 분리하여 다른 상태를 구독중인 component가 리렌더링 되지 않도록 함 | ||
return ( | ||
<CarouselContext.Provider value={carouselContextValue}> | ||
<CarouselIndexContext.Provider value={viewIndex}>{children}</CarouselIndexContext.Provider> | ||
</CarouselContext.Provider> | ||
); | ||
}; | ||
|
||
Carousel.Wrapper = Wrapper; | ||
Carousel.Item = CarouselItem; | ||
Carousel.Move = Move; | ||
Carousel.Dots = Dots; | ||
|
||
export default Carousel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { useContext, useEffect, useRef } from 'react'; | ||
import type { PropsWithChildren } from 'react'; | ||
import { CarouselContext, CarouselIndexContext } from './Carousel'; | ||
|
||
export interface CarouselItemProps extends PropsWithChildren { | ||
index: number; | ||
} | ||
|
||
const CarouselItem = ({ index, children }: CarouselItemProps) => { | ||
const ref = useRef<HTMLDivElement | null>(null); | ||
const context = useContext(CarouselContext); | ||
// viewIndex context 추가 | ||
const viewIndex = useContext(CarouselIndexContext); | ||
|
||
if (!context) throw Error('Carousel.Item is only available within Carousel.'); | ||
|
||
// viewIndex 분리 | ||
const { width, height, itemRef } = context; | ||
|
||
useEffect(() => { | ||
if (ref.current) { | ||
if (index === viewIndex) itemRef.current = ref.current; | ||
} | ||
}, [viewIndex]); | ||
|
||
return ( | ||
<div ref={ref} style={{ width, height }}> | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
export default CarouselItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useContext, type MouseEvent } from 'react'; | ||
import { CarouselContext, CarouselIndexContext } from './Carousel'; | ||
|
||
interface DotsProps { | ||
imageLength: number; | ||
size?: number; | ||
selectedColor?: string; | ||
unSelectedColor?: string; | ||
} | ||
|
||
const Dots = ({ imageLength, size = 6, selectedColor = '#fff', unSelectedColor = '#000' }: DotsProps) => { | ||
const images = Array.from({ length: imageLength }, () => ''); | ||
|
||
const context = useContext(CarouselContext); | ||
const viewIndex = useContext(CarouselIndexContext); | ||
|
||
if (!context) throw Error('Carousel.Dots is only available within Carousel.'); | ||
|
||
const { handleMoveImage } = context; | ||
|
||
return ( | ||
<div className='flex justify-between'> | ||
{images.map((_, index) => ( | ||
// viewIndex, index 비교 로직을 backgroundColor로 이동 | ||
<button | ||
type='button' | ||
key={crypto.randomUUID()} | ||
onClick={(e: MouseEvent<HTMLButtonElement>) => { | ||
e.stopPropagation(); | ||
handleMoveImage(index); | ||
}} | ||
className='border-none rounded-full cursor-pointer opacity-60' | ||
style={{ | ||
width: `${size}px`, | ||
height: `${size}px`, | ||
backgroundColor: viewIndex === index ? selectedColor : unSelectedColor, | ||
}} | ||
/> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export default Dots; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { ComponentPropsWithoutRef, MouseEvent, useContext } from 'react'; | ||
import { CarouselContext } from './Carousel'; | ||
import useThrottleCallback from '@/hooks/useThrottleCallback'; | ||
|
||
export interface MoveProps extends ComponentPropsWithoutRef<'button'> { | ||
direction: 'prev' | 'next'; | ||
addFunc?: CallableFunction; | ||
} | ||
|
||
const Move = ({ direction = 'next', addFunc, children, ...attributes }: MoveProps) => { | ||
const context = useContext(CarouselContext); | ||
|
||
if (!context) throw Error('Carousel.Move is only available within Carousel.'); | ||
|
||
const { handleMovePrev, handleMoveNext } = context; | ||
|
||
const handleMove = useThrottleCallback((e: MouseEvent<HTMLButtonElement>) => { | ||
if (addFunc) addFunc(); | ||
if (direction === 'prev') return handleMovePrev(e); | ||
return handleMoveNext(e); | ||
}, 500); | ||
|
||
return ( | ||
<button onClick={handleMove} {...attributes}> | ||
{children} | ||
</button> | ||
); | ||
}; | ||
|
||
export default Move; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { useContext } from 'react'; | ||
import type { PropsWithChildren } from 'react'; | ||
|
||
import { CarouselContext } from './Carousel'; | ||
|
||
interface WrapperProps extends PropsWithChildren { | ||
height?: string; | ||
} | ||
|
||
const Wrapper = ({ height = '100%', children }: WrapperProps) => { | ||
const context = useContext(CarouselContext); | ||
|
||
if (!context) throw Error('Carousel.Wrapper is only available within Carousel.'); | ||
|
||
const { carouselBoxRef, width } = context; | ||
|
||
return ( | ||
<div | ||
ref={carouselBoxRef} | ||
className='relative overflow-hidden' | ||
style={{ width, height, minWidth: width, minHeight: height }} | ||
> | ||
<div className='flex w-full p-0 m-0 overflow-hidden' style={{ height }}> | ||
{children} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Wrapper; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { useRef, useState } from 'react'; | ||
import type { MouseEvent } from 'react'; | ||
import { flushSync } from 'react-dom'; | ||
|
||
const useCarousel = (itemLength: number) => { | ||
const [viewIndex, setViewIndex] = useState(0); | ||
const carouselBoxRef = useRef<HTMLDivElement | null>(null); | ||
const itemRef = useRef<HTMLDivElement | null>(null); | ||
|
||
const handleMoveImage = (imageNumber: number) => { | ||
if (itemRef.current) { | ||
flushSync(() => { | ||
setViewIndex(imageNumber); | ||
}); | ||
|
||
itemRef.current.scrollIntoView({ | ||
behavior: 'smooth', | ||
block: 'nearest', | ||
inline: 'center', | ||
}); | ||
} | ||
}; | ||
|
||
const handleMovePrev = (e: MouseEvent<HTMLButtonElement>) => { | ||
e.stopPropagation(); | ||
if (itemRef.current) { | ||
flushSync(() => { | ||
if (viewIndex === 0) setViewIndex(0); | ||
else setViewIndex(viewIndex - 1); | ||
}); | ||
|
||
itemRef.current.scrollIntoView({ | ||
behavior: 'smooth', | ||
block: 'nearest', | ||
inline: 'center', | ||
}); | ||
} | ||
}; | ||
|
||
const handleMoveNext = (e: MouseEvent<HTMLButtonElement>) => { | ||
e.stopPropagation(); | ||
if (itemRef.current) { | ||
flushSync(() => { | ||
if (viewIndex === itemLength - 1) setViewIndex(viewIndex); | ||
else setViewIndex(viewIndex + 1); | ||
}); | ||
|
||
itemRef.current.scrollIntoView({ | ||
behavior: 'smooth', | ||
block: 'nearest', | ||
inline: 'center', | ||
}); | ||
} | ||
}; | ||
|
||
return { | ||
viewIndex, | ||
itemRef, | ||
carouselBoxRef, | ||
handleMoveImage, | ||
handleMovePrev, | ||
handleMoveNext, | ||
}; | ||
}; | ||
|
||
export default useCarousel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import Carousel from '@/components/Carousel/Carousel'; | ||
|
||
const meta = { | ||
title: 'Example/Carousel', | ||
component: Carousel, | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
|
||
tags: ['autodocs'], | ||
|
||
argTypes: {}, | ||
args: { width: 200, height: 200, length: 3 }, | ||
} satisfies Meta<typeof Carousel>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = { | ||
render: args => { | ||
return ( | ||
<Carousel {...args}> | ||
<Carousel.Wrapper> | ||
<Carousel.Item index={0}> | ||
<div style={{ width: '200px', height: '200px', backgroundColor: 'red' }}>1번컴포넌트</div> | ||
</Carousel.Item> | ||
<Carousel.Item index={1}> | ||
<div style={{ width: '200px', height: '200px', backgroundColor: 'green' }}>2번컴포넌트</div> | ||
</Carousel.Item> | ||
<Carousel.Item index={2}> | ||
<div style={{ width: '200px', height: '200px' }}>3번컴포넌트</div> | ||
</Carousel.Item> | ||
</Carousel.Wrapper> | ||
|
||
<div className='flex justify-center pt-4'> | ||
<div style={{ width: '80px' }}> | ||
<Carousel.Dots size={8} imageLength={3} unSelectedColor='black' selectedColor='blue' /> | ||
</div> | ||
</div> | ||
<div className='flex justify-between'> | ||
<Carousel.Move direction='prev' addFunc={() => console.log('이전')}> | ||
<button>이전버튼</button> | ||
</Carousel.Move> | ||
<Carousel.Move direction='next' addFunc={() => console.log('다음')}> | ||
<button>다음버튼</button> | ||
</Carousel.Move> | ||
</div> | ||
</Carousel> | ||
); | ||
}, | ||
}; | ||
|
||
export const NoDots: Story = { | ||
render: args => { | ||
return ( | ||
<Carousel {...args}> | ||
<Carousel.Wrapper> | ||
<Carousel.Item index={0}> | ||
<div style={{ width: '200px', height: '200px', backgroundColor: 'red' }}>1번컴포넌트</div> | ||
</Carousel.Item> | ||
<Carousel.Item index={1}> | ||
<div style={{ width: '200px', height: '200px', backgroundColor: 'green' }}>2번컴포넌트</div> | ||
</Carousel.Item> | ||
<Carousel.Item index={2}> | ||
<div style={{ width: '200px', height: '200px' }}>3번컴포넌트</div> | ||
</Carousel.Item> | ||
</Carousel.Wrapper> | ||
|
||
<div className='flex justify-between'> | ||
<Carousel.Move direction='prev' addFunc={() => console.log('이전')}> | ||
<button>이전버튼</button> | ||
</Carousel.Move> | ||
<Carousel.Move direction='next' addFunc={() => console.log('다음')}> | ||
<button>다음버튼</button> | ||
</Carousel.Move> | ||
</div> | ||
</Carousel> | ||
); | ||
}, | ||
}; | ||
|
||
export const OnlyDots: Story = { | ||
render: args => { | ||
return ( | ||
<Carousel {...args}> | ||
<Carousel.Wrapper> | ||
<Carousel.Item index={0}> | ||
<div style={{ width: '200px', height: '200px', backgroundColor: 'red' }}>1번컴포넌트</div> | ||
</Carousel.Item> | ||
<Carousel.Item index={1}> | ||
<div style={{ width: '200px', height: '200px', backgroundColor: 'green' }}>2번컴포넌트</div> | ||
</Carousel.Item> | ||
<Carousel.Item index={2}> | ||
<div style={{ width: '200px', height: '200px' }}>3번컴포넌트</div> | ||
</Carousel.Item> | ||
</Carousel.Wrapper> | ||
<div className='flex justify-center pt-4'> | ||
<div style={{ width: '80px' }}> | ||
<Carousel.Dots size={8} imageLength={3} unSelectedColor='black' selectedColor='blue' /> | ||
</div> | ||
</div> | ||
</Carousel> | ||
); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.