참고자료
Radix UI - Accordion
아코디언 UI를 보고 사람들이 기대하는 기능은
이걸 개발쪽으로 생각해보면
An accordion is a vertically stacked set of interactive headings that each contain a title, content snippet, or thumbnail representing a section of content. The headings function as controls that enable users to reveal or hide their associated sections of content. Accordions are commonly used to reduce the need to scroll when presenting multiple sections of content on a single page.
W3C에서 설명하는 Accordion 또한 비슷한데,
radix 이외에도 여러 자료를 살펴보니 보통 이런 식으로 컴포넌트를 나눠놓는다
Accordion : 아코디언들을 다 모아놓는 wrapper의 역할
AccordionItem : 열고 닫히는 아코디언 한개를 나타낸다
- AccordionTrigger : 아코디언의 열림/닫힘을 제어하는 역할
- AccordionContent : 실제 내용이 있는 곳으로, Trigger에 의해 보일지 말지가 결정됨
예시
<Accordion type="single">
<AccordionItem value="1">
<AccordionTrigger>누르면 content가 열고닫힘</AccordionTrigger>
<AccordionContent>내용임</AccordionContent>
</AccordionItem>
<AccordionItem value="2">
<AccordionTrigger>누르면 content가 열고닫힘</AccordionTrigger>
<AccordionContent>내용임</AccordionContent>
</AccordionItem>
<AccordionItem value="3">
<AccordionTrigger>누르면 content가 열고닫힘</AccordionTrigger>
<AccordionContent>내용임</AccordionContent>
</AccordionItem>
</Accordion>
export type TAccordionContext = {
selected?: string[]
onItemOpen: (value: string) => void
onItemClose: (value: string) => void
}
const [AccordionProvider, useAccordionContext] =
createContext<TAccordionContext>("accordion")
export interface AccordionProps extends PropsWithChildren {
value?: string[]
defaultValue?: string[]
onValueChange?: (value: string[]) => void
type?: "single" | "multi"
}
//ref 관련 문제 해결 필요
export const Accordion = forwardRef<HTMLElement, AccordionProps>((props, _) => {
const { type = "single", ...accordionProps } = props
if (type === "single") {
return <SingleAccordion {...accordionProps} />
}
if (type === "multi") {
return <MultiAccordion {...accordionProps} />
}
})
가장 최상위 태그에서 할 일이 무엇일까 생각해보면
선택된 아코디언들에 대한 데이터를 최상위 컴포넌트에서 관리하면서, context 를 통해 Item들로 내려주는 게 맞는 방법이라는 생각이 들었다
단일/다중 선택에 관한 것도 prop을 통해 주입받아야 한다
export interface AccordionProps extends PropsWithChildren {
value?: string[]//선택된 아코디언 value값
defaultValue?: string[]//기본값
onValueChange?: (value: string[]) => void//아코디언을 클릭했을 때
type?: "single" | "multi"
}
AccordionItem 의 value 값을 저장한다한 컴포넌트에서 단일/다중 선택을 다루려면 value가 string | string[] 타입이 되는게 원칙상 맞겠지만 그냥 string[] 으로 통일해서 단일 타입일 경우 배열을 한칸만 사용하는 것이 더 간단하게 해결할 수 있을 것 같았다
아코디언에 클릭 이벤트가 발생했을 때 type에 따라 하는일이 달라져야 하기 때문에
export const Accordion = forwardRef<HTMLElement, AccordionProps>((props, _) => {
const { type = "single", ...accordionProps } = props
if (type === "single") {
return <SingleAccordion {...accordionProps} />
}
if (type === "multi") {
return <MultiAccordion {...accordionProps} />
}
})
이런 식으로 SingleAccordion , MultiAccordion으로 나누어 렌더링한다
interface AccordionImplSingleProps extends PropsWithChildren {
value?: string[]
defaultValue?: string[]
onValueChange?: (value: string[]) => void
}
const SingleAccordion = ({
value,
defaultValue = [],
onValueChange,
children,
}: AccordionImplSingleProps) => {
const [selected = [], setSelected] = useControlledState({
prop: value,
defaultProp: defaultValue,
onChange: onValueChange,
})
return (
<AccordionProvider
selected={selected}
onItemOpen={(value) => setSelected((_) => [value])}
onItemClose={() => setSelected((_) => [])}
>
{children}
</AccordionProvider>
)
}
useControlledState hook을 통해 외부에서 value를 주입하게 되면 controlled 로, 아니면 uncontrolled 로 동작되게 설정했고,interface AccordionImplMultiProps extends PropsWithChildren {
value?: string[]
defaultValue?: string[]
onValueChange?: (value: string[]) => void
}
const MultiAccordion = ({
value,
defaultValue,
onValueChange,
children,
}: AccordionImplMultiProps) => {
const [selected, setSelected] = useControlledState({
prop: value,
defaultProp: defaultValue,
onChange: onValueChange,
})
const handleItemOpen = useCallback(
(item: string) => {
setSelected((prev = []) => [...prev, item])
},
[setSelected],
)
const handleItemClose = useCallback(
(item: string) =>
setSelected((prev = []) => prev.filter((value) => value !== item)),
[setSelected],
)
return (
<AccordionProvider
selected={selected}
onItemOpen={handleItemOpen}
onItemClose={handleItemClose}
>
{children}
</AccordionProvider>
)
}
single과 달리 다중 선택이 가능해야 하기 때문에 onItemOpen,onItemClose 함수만 변경했고, 나머지는 다 동일하다