본문으로 건너뛰기

Disclosure

Accordion

관련 콘텐츠를 펼치고 접어 화면의 정보 밀도를 조절합니다.

이 컴포넌트는 무엇인가요?
여러 콘텐츠 영역을 펼치거나 접는 컴포넌트입니다.
여러 항목을 열 수 있나요?
기본값은 multiple이며 동시에 여러 항목을 열 수 있습니다.

Usage

AccordionRoot 안에 AccordionItem, AccordionTitle, AccordionContent를 조합합니다. 열린 항목은 values로 초기화합니다.

Examples

Single

multiple={false}를 사용하면 한 번에 하나의 항목만 펼칩니다.

한 번에 하나만 열기
multiple을 false로 지정합니다.

Rounded

rounded를 지정하면 첫 항목과 마지막 항목의 바깥 모서리가 둥글게 표시됩니다. 위 기본 예제에서 확인할 수 있습니다.

Props

  • values: string[] — 처음 열 항목
  • multiple?: boolean — 여러 항목 동시 열기, 기본값 true
  • rounded?: boolean — 바깥 모서리 처리
  • onChange?: (values: string[]) => void — 열린 항목 변경 콜백

Source

accordion-root
import {
  createContext,
  PropsWithChildren,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from 'react'

type AccordionRootProps = {
  multiple?: boolean
  values: string[]
  onChange?: (values: string[]) => void
  rounded?: boolean
}

type TStateContext = {
  values: string[]
  rounded: boolean
}

type TActionContext = {
  setter: (item: string) => void
}

const AccordionStateContext = createContext<TStateContext>({ values: [], rounded: false })
const AccordionActionsContext = createContext<TActionContext>({ setter: () => null })

export const AccordionRoot = ({
  children,
  multiple = true,
  values = [],
  onChange,
  rounded = false,
}: PropsWithChildren<AccordionRootProps>) => {
  const [items, setItems] = useState<string[]>(values)

  const setter = useCallback(
    (item: string) => {
      let newItem = new Set([item])
      if (multiple) {
        newItem = new Set(items)
        if (newItem.has(item)) {
          newItem.delete(item)
        } else {
          newItem.add(item)
        }
      }
      setItems(Array.from(newItem))
    },
    [items, multiple],
  )

  const memoizedItems = useMemo(() => items, [items])
  const memoizedRounded = useMemo(() => rounded, [rounded])
  const memoizedSetter = useMemo(() => setter, [setter])

  useEffect(() => {
    if (onChange && typeof onChange === 'function') {
      onChange(memoizedItems)
    }
  }, [memoizedItems, onChange])

  return (
    <AccordionActionsContext.Provider value={{ setter: memoizedSetter }}>
      <AccordionStateContext.Provider value={{ values: memoizedItems, rounded: memoizedRounded }}>
        <div>{children}</div>
      </AccordionStateContext.Provider>
    </AccordionActionsContext.Provider>
  )
}

export const useAccordionState = () => useContext(AccordionStateContext)
export const useAccordionActions = () => useContext(AccordionActionsContext)
accordion-examples
'use client'

import {
  AccordionContent,
  AccordionItem,
  AccordionRoot,
  AccordionTitle,
} from '@/shared/ui/Accordion'

export function AccordionBasicExample() {
  return (
    <div className="w-full max-w-xl">
      <AccordionRoot values={['intro']} rounded>
        <AccordionItem value="intro">
          <AccordionTitle>이 컴포넌트는 무엇인가요?</AccordionTitle>
          <AccordionContent>여러 콘텐츠 영역을 펼치거나 접는 컴포넌트입니다.</AccordionContent>
        </AccordionItem>
        <AccordionItem value="usage">
          <AccordionTitle>여러 항목을 열 수 있나요?</AccordionTitle>
          <AccordionContent>
            기본값은 multiple이며 동시에 여러 항목을 열 수 있습니다.
          </AccordionContent>
        </AccordionItem>
      </AccordionRoot>
    </div>
  )
}

export function AccordionSingleExample() {
  return (
    <div className="w-full max-w-xl">
      <AccordionRoot values={[]} multiple={false}>
        <AccordionItem value="one">
          <AccordionTitle>한 번에 하나만 열기</AccordionTitle>
          <AccordionContent>multiple을 false로 지정합니다.</AccordionContent>
        </AccordionItem>
      </AccordionRoot>
    </div>
  )
}

Known limitations

현재 AccordionTitlediv 클릭으로 동작해 키보드와 ARIA 상태를 제공하지 않습니다. 현행 UI 보존 범위에서는 수정하지 않았으며, 접근성 개선 작업에서 button 기반 trigger로 교체해야 합니다.