-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #173 from choco-team/172-fe-타이머
[FE] 타이머 기능 개발
- Loading branch information
Showing
31 changed files
with
1,094 additions
and
34 deletions.
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
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
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,42 @@ | ||
import { ComponentPropsWithoutRef } from 'react'; | ||
|
||
type Direction = 'down' | 'up' | 'left' | 'right'; | ||
|
||
type ArrowIconProps = { | ||
color?: string; | ||
direction: Direction; | ||
}; | ||
|
||
const DIRECTION: Record<Direction, string> = { | ||
down: 'M11.6667 16.6667L20 25.0001L28.3333 16.6667', | ||
up: 'M28.3333 23.3333L20 15L11.6667 23.3333', | ||
left: 'M23.3333 11.6667L15 20.0001L23.3333 28.3334', | ||
right: 'M16.6667 28.3334L25 20.0001L16.6667 11.6667', | ||
}; | ||
|
||
const ArrowIcon = ({ | ||
color = 'black', | ||
direction, | ||
...rest | ||
}: ArrowIconProps & ComponentPropsWithoutRef<'svg'>) => { | ||
return ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="20" | ||
height="20" | ||
viewBox="0 0 40 40" | ||
fill={color} | ||
{...rest} | ||
> | ||
<path | ||
d={DIRECTION[direction]} | ||
stroke={color} | ||
strokeWidth="1.66667" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
/> | ||
</svg> | ||
); | ||
}; | ||
|
||
export default ArrowIcon; |
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,56 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import Select from '.'; | ||
|
||
type Story = StoryObj<typeof Select>; | ||
|
||
/** | ||
* `Select` 컴포넌트는 사용자가 텍스트를 입력하고 편집할 수 있는 컴포넌트입니다. | ||
*/ | ||
const meta: Meta<typeof Select> = { | ||
title: 'INPUTS/Select', | ||
component: Select, | ||
}; | ||
|
||
export default meta; | ||
|
||
/** | ||
* `DefaultSelect`는 가징 기본적인 `Select` 스토리입니다. | ||
*/ | ||
export const DefaultSelect: Story = { | ||
args: { | ||
label: 'DEFAULT', | ||
options: ['Option1', 'Option2', 'Option3', 'Option4', 'Option5'], | ||
onChangeOption: (selected) => { | ||
console.log(selected); | ||
}, | ||
}, | ||
}; | ||
|
||
/** | ||
* `HasDefaultOptionSelect`는 defaultOption이 존재하는 `Select` 스토리입니다. | ||
*/ | ||
export const HasDefaultOptionSelect: Story = { | ||
args: { | ||
label: 'Has Default Option', | ||
options: ['Option1', 'Option2', 'Option3', 'Option4', 'Option5'], | ||
defaultOption: 'Option1', | ||
onChangeOption: (selected) => { | ||
console.log(selected); | ||
}, | ||
}, | ||
}; | ||
|
||
/** | ||
* `HasPlaceholderSelect`는 placeholder이 존재하는 `Select` 스토리입니다. | ||
*/ | ||
export const HasPlaceholderSelect: Story = { | ||
args: { | ||
label: 'Has Placeholder', | ||
options: ['Option1', 'Option2', 'Option3', 'Option4', 'Option5'], | ||
placeholder: '커스튬 가능한 문구입니다.', | ||
onChangeOption: (selected) => { | ||
console.log(selected); | ||
}, | ||
}, | ||
}; |
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,142 @@ | ||
import { | ||
MouseEventHandler, | ||
useCallback, | ||
useEffect, | ||
useLayoutEffect, | ||
useRef, | ||
useState, | ||
} from 'react'; | ||
|
||
import useOutsideClick from '@Hooks/useOutsideClick'; | ||
|
||
import ArrowIcon from '@Components/Icon/ArrowIcon/ArrowIcon'; | ||
|
||
import * as S from './style'; | ||
|
||
type SelectProps<Options extends readonly (string | number)[]> = { | ||
label?: string; | ||
placeholder?: string; | ||
defaultOption?: Options[number]; | ||
options: Options; | ||
onChangeOption?: (selected: Options[number]) => void; | ||
}; | ||
|
||
const Select = <Options extends readonly (string | number)[]>({ | ||
label, | ||
placeholder = '옵션을 선택해 주세요.', | ||
defaultOption = '', | ||
options, | ||
onChangeOption, | ||
}: SelectProps<Options>) => { | ||
const optionsRef = useRef<HTMLUListElement>(null); | ||
|
||
const [selectedOption, setSelectedOption] = useState(defaultOption); | ||
const [isOpenOptions, setIsOptionOptions] = useState(false); | ||
|
||
const ref = useOutsideClick<HTMLDivElement>(() => setIsOptionOptions(false)); | ||
|
||
const [offsetTop, setOffsetTop] = useState(ref?.current?.offsetTop || 0); | ||
const [clientWidth, setClientWidth] = useState( | ||
ref?.current?.clientWidth || 0, | ||
); | ||
|
||
const handleClickArrowIcon: MouseEventHandler<SVGSVGElement> = (event) => { | ||
event.preventDefault(); | ||
|
||
setIsOptionOptions((prev) => !prev); | ||
}; | ||
|
||
const handleChangeOption = (value: Options[number]) => { | ||
setIsOptionOptions(false); | ||
setSelectedOption(value); | ||
|
||
onChangeOption?.(value); | ||
}; | ||
|
||
const setOptionsPositionWidth = useCallback(() => { | ||
const offsetTop = ref?.current?.offsetTop; | ||
const clientWidth = ref?.current?.clientWidth; | ||
|
||
if (!offsetTop || !clientWidth) return; | ||
|
||
setOffsetTop(offsetTop); | ||
setClientWidth(clientWidth); | ||
}, [ref]); | ||
|
||
useEffect(() => { | ||
window.addEventListener('resize', setOptionsPositionWidth); | ||
|
||
return () => window.removeEventListener('resize', setOptionsPositionWidth); | ||
}, [setOptionsPositionWidth]); | ||
|
||
useLayoutEffect(() => { | ||
if (!isOpenOptions) return; | ||
|
||
setOptionsPositionWidth(); | ||
}, [isOpenOptions, setOptionsPositionWidth]); | ||
|
||
useEffect(() => { | ||
if (defaultOption) setSelectedOption(defaultOption); | ||
}, [defaultOption]); | ||
|
||
// 스크롤 계산 | ||
useEffect(() => { | ||
if (!isOpenOptions || !optionsRef?.current) return; | ||
|
||
const { scrollHeight, offsetHeight, children } = optionsRef.current; | ||
if (scrollHeight < offsetHeight) return; | ||
|
||
const options = Object.values(children) as HTMLLIElement[]; | ||
|
||
let top = 10; | ||
for (let i = 0; i < options.length; i++) { | ||
const { offsetHeight, innerText } = options[i]; | ||
top += offsetHeight + 8; | ||
|
||
if (innerText === selectedOption) break; | ||
} | ||
|
||
optionsRef.current.scrollTo({ | ||
top: top - 160, | ||
}); | ||
}, [isOpenOptions, selectedOption]); | ||
|
||
return ( | ||
<S.Select ref={ref}> | ||
<label> | ||
<S.LabelText>{label}</S.LabelText> | ||
<S.InputLayout $isFocus={isOpenOptions}> | ||
<S.Input | ||
readOnly | ||
value={selectedOption} | ||
onClick={() => setIsOptionOptions((prev) => !prev)} | ||
placeholder={placeholder} | ||
/> | ||
<ArrowIcon | ||
direction={isOpenOptions ? 'up' : 'down'} | ||
onClick={handleClickArrowIcon} | ||
/> | ||
</S.InputLayout> | ||
</label> | ||
{isOpenOptions && ( | ||
<S.Options | ||
ref={optionsRef} | ||
top={label ? `${offsetTop + 60}px` : `${offsetTop + 45}px`} | ||
width={`${clientWidth}px`} | ||
> | ||
{options.map((option) => ( | ||
<S.Option | ||
key={option} | ||
onClick={() => handleChangeOption(option)} | ||
$isSelected={selectedOption === option} | ||
> | ||
{option} | ||
</S.Option> | ||
))} | ||
</S.Options> | ||
)} | ||
</S.Select> | ||
); | ||
}; | ||
|
||
export default Select; |
Oops, something went wrong.