-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SP2] 드롭다운 컴포넌트 추가, 프로젝트 페이지 필터에 적용 #223
Changes from all commits
20d7e50
04cfb1c
3083ecf
361a0d3
af6452a
50b14d0
81894c1
061972f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import useOutsideClickListener from '@src/hooks/useOutsideClickListener'; | ||
import { LabelKeyType } from '@src/lib/types/universal'; | ||
import { useCallback, useRef, useState } from 'react'; | ||
import { S } from './style'; | ||
|
||
interface SelectProps<T extends LabelKeyType> { | ||
options: T[]; | ||
baseValue: T; | ||
baseLabel: string; | ||
selectedValue: T; | ||
setSelectedValue: React.Dispatch<React.SetStateAction<T>>; | ||
labels: Record<T, string>; | ||
} | ||
|
||
export default function Select<T extends LabelKeyType>({ | ||
options, | ||
selectedValue, | ||
setSelectedValue, | ||
baseValue, | ||
baseLabel, | ||
labels, | ||
}: SelectProps<T>) { | ||
const [isOpen, setIsOpen] = useState(false); | ||
|
||
const toggleSelect = () => setIsOpen(!isOpen); | ||
|
||
const handleSelect = (value: T) => { | ||
setSelectedValue(value); | ||
}; | ||
|
||
const closeSelectItem = useCallback(() => setIsOpen(false), []); | ||
|
||
const selectItemWrapperRef = useRef<HTMLDivElement>(null); | ||
const selectTriggerRef = useRef<HTMLButtonElement>(null); | ||
useOutsideClickListener([selectItemWrapperRef, selectTriggerRef], closeSelectItem); | ||
|
||
return ( | ||
<div> | ||
<S.SelectTrigger | ||
ref={selectTriggerRef} | ||
onClick={toggleSelect} | ||
isSelectionExist={selectedValue !== baseValue} | ||
isOpened={isOpen} | ||
> | ||
{selectedValue === baseValue ? baseLabel : labels[selectedValue]} | ||
</S.SelectTrigger> | ||
{isOpen && ( | ||
<S.SelectItemWrapper ref={selectItemWrapperRef}> | ||
{options.map((option, index) => ( | ||
<S.SelectItem | ||
key={index} | ||
isSelected={selectedValue === option} | ||
onClick={() => handleSelect(option)} | ||
> | ||
{labels[option]} | ||
</S.SelectItem> | ||
))} | ||
</S.SelectItemWrapper> | ||
)} | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import styled from '@emotion/styled'; | ||
import arrowDown from '@src/assets/icons/arrow_down.svg'; | ||
import { colors } from '@src/lib/styles/colors'; | ||
|
||
const SelectWrapper = styled.div` | ||
position: relative; | ||
`; | ||
|
||
const SelectTrigger = styled.button<{ isSelectionExist: boolean; isOpened: boolean }>` | ||
cursor: pointer; | ||
position: relative; | ||
width: 110px; | ||
font-size: 16px; | ||
font-weight: 500; | ||
padding: 9px 22px; | ||
text-align: left; | ||
color: white; | ||
border-radius: 20px; | ||
background-color: ${({ isSelectionExist }) => | ||
isSelectionExist ? colors.gray700 : colors.gray600}; | ||
border: 1px solid; | ||
border-color: ${({ isSelectionExist }) => (isSelectionExist ? colors.gray200 : colors.gray700)}; | ||
&::after { | ||
content: ''; | ||
background-repeat: no-repeat; | ||
background-position: center; | ||
position: absolute; | ||
right: 22px; | ||
top: 9px; | ||
transition: 0.2s; | ||
width: 10px; | ||
height: 18px; | ||
background-image: url(${arrowDown}); | ||
transform: ${({ isOpened }) => (isOpened ? 'rotate(180deg)' : 'none')}; | ||
} | ||
`; | ||
|
||
const SelectItem = styled.div<{ isSelected: boolean }>` | ||
background-color: ${({ isSelected }) => (isSelected ? colors.gray400 : 'transparent')}; | ||
padding: 6px 10px; | ||
border-radius: 4px; | ||
cursor: pointer; | ||
transition: 0.1s; | ||
color: ${colors.gray30}; | ||
`; | ||
|
||
const SelectItemWrapper = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
position: absolute; | ||
background-color: ${colors.gray600}; | ||
z-index: 200; | ||
width: 110px; | ||
border-radius: 20px; | ||
padding: 9px 12px; | ||
margin-top: 8px; | ||
gap: 8px; | ||
&:hover { | ||
${SelectItem} { | ||
background-color: transparent; | ||
&:hover { | ||
background-color: ${colors.gray400}; | ||
} | ||
} | ||
} | ||
`; | ||
|
||
export const S = { SelectWrapper, SelectTrigger, SelectItem, SelectItemWrapper }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { useEffect } from 'react'; | ||
|
||
const useOutsideClickListener = (targetRefs: React.RefObject<Node>[], callback: () => void) => { | ||
useEffect(() => { | ||
const handleClickOutside = (event: MouseEvent) => { | ||
const target = event.target; | ||
if (target instanceof Node) { | ||
const isOutsideClick = targetRefs.every( | ||
(targetRef: React.RefObject<Node>) => | ||
targetRef.current === null || !targetRef.current.contains(target), | ||
); | ||
if (isOutsideClick) { | ||
callback(); | ||
} | ||
} | ||
}; | ||
|
||
document.addEventListener('mousedown', handleClickOutside); | ||
|
||
return () => { | ||
document.removeEventListener('mousedown', handleClickOutside); | ||
}; | ||
}, [targetRefs, callback]); | ||
}; | ||
|
||
export default useOutsideClickListener; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,18 @@ | ||
import { ProjectCategoryType } from '@src/lib/types/project'; | ||
|
||
export const projectCategoryList: { | ||
type: ProjectCategoryType; | ||
name: string; | ||
}[] = [ | ||
{ type: ProjectCategoryType.ALL, name: '전체' }, | ||
{ type: ProjectCategoryType.APPJAM, name: '🎊 앱잼' }, | ||
{ type: ProjectCategoryType.SOPKATHON, name: '💡 솝커톤' }, | ||
{ type: ProjectCategoryType.SOPTERM, name: '🛎 솝텀 프로젝트' }, | ||
{ type: ProjectCategoryType.STUDY, name: '📖 스터디' }, | ||
{ type: ProjectCategoryType.JOINTSEMINAR, name: '👥 합동 세미나' }, | ||
{ type: ProjectCategoryType.ETC, name: '💬 기타' }, | ||
export const activeProjectCategoryList: ProjectCategoryType[] = [ | ||
ProjectCategoryType.ALL, | ||
ProjectCategoryType.APPJAM, | ||
ProjectCategoryType.SOPKATHON, | ||
ProjectCategoryType.SOPTERM, | ||
]; | ||
|
||
export const projectCategoryLabel: Record<ProjectCategoryType, string> = { | ||
[ProjectCategoryType.ALL]: '전체', | ||
[ProjectCategoryType.APPJAM]: '앱잼', | ||
[ProjectCategoryType.SOPKATHON]: '솝커톤', | ||
[ProjectCategoryType.SOPTERM]: '솝텀', | ||
[ProjectCategoryType.STUDY]: '스터디', | ||
[ProjectCategoryType.JOINTSEMINAR]: '합동 세미나', | ||
[ProjectCategoryType.ETC]: '기타', | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,3 +34,5 @@ type TabTypeOption<T> = { | |
|
||
export type TabType = TabTypeOption<Part>; | ||
export type ExtraTabType = TabTypeOption<ExtraPart>; | ||
|
||
export type LabelKeyType = string | number | symbol; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question; key 값의 타입으로 symbol이 오는 경우는 어떤게 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Record에서 key 자리에 올 수 있는 타입이 string, number, symbol이라서 일단 이렇게 적어두었어요 ..! |
This file was deleted.
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question;
Item과 Trigger 을 각각 div, button 분리해 다른 ref를 사용한 이유가 있나요?
둘 다 같은 ref를 참조해도 되는거 아닌가 싶어서요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
같은 ref를 참조하면 더 밑에 있는 ref가 위에 있는 ref를 덮어쓰게 되지 않나요 ..??!
같은 ref를 참조하는 상황에 대하여 더 자세히 설명 주시면 감사하겠습니닷..!