File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-14
08-14
08-18
08-18
08-14
08-18
08-18
08-18
File name
Commit message
Commit date
"use client";
import { useId, useState, type ReactNode } from "react";
import { cx } from "../../utils";
export interface FoxDisclosureProps {
title: ReactNode;
children?: ReactNode;
/** 주면 호출부가 열림을 소유한다. 없으면 컴포넌트가 스스로 여닫는다. */
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
hidden?: boolean;
/** 배치 조정용. */
className?: string;
}
/**
* @fox 디스클로저. 제목 한 줄을 누르면 아래 내용이 열린다.
*
* FoxAccordion과 겹쳐 보이지만 시안이 따로 둔 별개 컴포넌트다 — 바탕·테두리·크기 축이
* 없고 캐럿이 제목 앞에 온다. 목록을 나누는 아코디언과 달리 글 사이에 끼우는 접기다.
*
* ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
* (또는 개별 파티셜)로 한 번 불러와야 한다.
*/
export function FoxDisclosure({
title,
children,
open,
defaultOpen = false,
onOpenChange,
hidden = false,
className,
}: FoxDisclosureProps) {
const autoId = useId();
const [inner, setInner] = useState(defaultOpen);
if (hidden) {
return null;
}
const expanded = open ?? inner;
const panelId = `${autoId}-panel`;
const toggle = () => {
if (open === undefined) {
setInner(!expanded);
}
onOpenChange?.(!expanded);
};
return (
<div
className={cx(
"fox-disclosure",
expanded && "fox-disclosure--open",
className
)}
>
<button
className="fox-disclosure__tit"
type="button"
aria-expanded={expanded}
aria-controls={panelId}
onClick={toggle}
>
<span className="fox-disclosure__icon" aria-hidden="true">
{expanded ? <i className="fox-ico fox-ico-CaretCircleUp" aria-hidden="true" /> : <i className="fox-ico fox-ico-CaretCircleRight" aria-hidden="true" />}
</span>
<span className="fox-disclosure__title">{title}</span>
</button>
{/* 닫혔을 때는 DOM에서 뺀다 — 안의 초점 가능한 것들이 탭 순서에 남으면 안 된다. */}
{expanded && (
<div className="fox-disclosure__contents" id={panelId}>
{children}
</div>
)}
</div>
);
}