Cadenza
EN

一个触发器加一块可展开的面板 —— 高度过渡是默认值,不用自己写动画

基于 Base UI 的展开/收起。封装层只补一件事:面板的高度过渡。Base UI 把面板量好的高度 发布成 --collapsible-panel-height,再用 data-starting-style / data-ending-style 标出过渡的两端,封装层把这三样接成一条 CSS transition —— 不写它,面板就是一帧之内直接 弹开。根和触发器则不带任何外观:布局归调用方,触发器是一个裸 <button>,套什么样子用 render

部件名跟的是 Base UI 的词表:页内展开的内容叫 Panel(shadcn 那边叫 CollapsibleContent,是 Radix 时代的叫法,Base UI 自己导出的就是 Collapsible.Panel)。

使用

import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@gedatou/cadenza-ui'
<Collapsible>
  <CollapsibleTrigger render={<Button variant="outline" />}>详情</CollapsibleTrigger>
  <CollapsiblePanel>
    <p className="p-4 text-sm">面板内容。</p>
  </CollapsiblePanel>
</Collapsible>

内边距写在面板里面的元素上,不要写在面板自己身上 —— 动画动的就是面板的高度, 它自己的 padding 会被一路压扁。

组成

三个部件,全部对外导出:

Collapsible                ← 根:持有开合状态,自己不带样式
├── CollapsibleTrigger     ← 真 <button>,aria-expanded / aria-controls 由 Base UI 接线
└── CollapsiblePanel       ← 展开的内容,高度过渡在这一层

顺序是自由的:面板放在触发器前面(向上展开)同样成立,aria-controls 认的是 id 不是位置。

受控

open + onOpenChange 把状态交出去,回调第二参 details.reason 说明这次是谁改的 (trigger-press 或程序性的 none):

不传 open 就是非受控,初始值用 defaultOpen。受控性在首渲染锁定,中途换会在开发模式 下报错。

设置面板

最常见的用法:常用项常驻,不常改的收进面板。

文件树

嵌套:每个目录是一个独立的 Collapsible,面板里再放下一层。

每层各记各的开合,层与层之间没有共享状态。触发器上的 data-panel-open 是 chevron 转向 和文件夹图标换脸的钩子 —— 注意是 data-panel-open,不是面板那边的 data-open

什么时候用 Collapsible

  • 一块内容的显示/隐藏 → Collapsible。它管的是「这块要不要看见」,没有兄弟概念。
  • 一组互斥的视图,同时只看一个Tabs。它有选中值, 有 roving focus 和方向键导航,Collapsible 这些都没有。
  • 一叠可折叠区块,展开一个自动收起其他 → 那是 Accordion,库里还没有公开封装。 可以叠多个 Collapsible 自己管互斥(把 open 提到外面),或直接用 @base-ui/react/accordion

状态与 className

className / style / render 一律双形态:值,或 (state) => 值 的函数。下表左列是挂在 DOM 上的 data-*,Tailwind 直接当变体写(data-open:border-primary);右列是同一个状态在 函数形态里的名字。

部件data-*出现时机函数 className 里的名字
根 / 面板data-open面板展开opentrue
根 / 面板data-closed面板收起openfalse
触发器data-panel-open面板展开(触发器没有 data-closed,收起时属性直接消失)opentrue
根 / 触发器 / 面板data-disabled根传了 disableddisabled
根 / 面板data-starting-style进场的第一帧transitionStatus'starting'
根 / 面板data-ending-style退场动画进行中transitionStatus'ending'

根与面板的 state 是同一组字段,都含 transitionStatus('starting' / 'ending' / 'idle' / undefined),默认的高度过渡就是拿 data-starting-style / data-ending-style 把两端压到 h-0 做的。 要改成横向折叠,把默认的 h-(--collapsible-panel-height) 换成 w-(--collapsible-panel-width)transition-[height] 换成 transition-[width] 即可 (cn 合并,后写的赢)。

需要从外部定位时用 data-slot:

data-slot是什么
collapsible根元素
collapsible-trigger触发器按钮
collapsible-panel面板

键盘交互

按键行为
Tab触发器在 Tab 序里;面板展开后,面板内的可聚焦元素接在它后面
Enter / Space在触发器上切换开合

面板收起时默认从 DOM 卸载,里面的东西自然不在 Tab 序里。keepMounted 会把它留在 DOM 中 —— Base UI 同时挂 hidden,所以留下的内容依然不可聚焦、不被朗读。

Props

顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className

Collapsible

根,持有开合状态。渲染一个 <div>

Prop类型默认值说明
defaultOpenbooleanfalse非受控初始开合
openboolean受控开合。传了就锁定受控,首渲染之后不许换
onOpenChange(open: boolean, details: CollapsibleChangeEventDetails) => void开合变化。details.reason'trigger-press''none'
disabledbooleanfalse整个组件不响应交互;三个部件都会带上 data-disabled
classNamestring | (state) => string函数形态收到 { open, disabled, transitionStatus }
其余Base UI Collapsible.Root 的 props(元素原生属性 + ref)透传
<Collapsible defaultOpen onOpenChange={(open, details) => log(details.reason)}></Collapsible>

CollapsibleTrigger

切换开合的按钮。默认渲染真 <button type="button">,aria-expanded / aria-controls 由 Base UI 接线。

Prop类型默认值说明
nativeButtonbooleantruerender 换成非 button 元素(如 <div>)时置 false,Base UI 会补上键盘与 role
renderReactElement | (props, state) => ReactElement换掉宿主元素。给触发器上妆的正道:render={<Button />}
classNamestring | (state) => string函数形态收到 { open, disabled, transitionStatus }
其余Base UI Collapsible.Trigger 的 props(<button> 原生属性 + ref)透传
<CollapsibleTrigger className="group/trigger" render={<Button variant="ghost" />}>
  高级选项
  <IconChevronDown className="transition-transform group-data-panel-open/trigger:rotate-180" />
</CollapsibleTrigger>

CollapsiblePanel

展开的内容。渲染一个 <div>,自带高度过渡。

Prop类型默认值说明
keepMountedbooleanfalse收起时保留在 DOM 里(带 hidden,不可聚焦、不被朗读)。hiddenUntilFound 开启时此项被忽略
hiddenUntilFoundbooleanfalse改用 hidden="until-found":浏览器自带的页内查找能命中收起的文本并自动展开。代价是内容始终在 DOM 中
classNamestring | (state) => string与默认的过渡类名合并(cn,后写的赢)。函数形态收到 { open, disabled, transitionStatus }
其余Base UI Collapsible.Panel 的 props(元素原生属性 + ref)透传
<CollapsiblePanel hiddenUntilFound>
  <div className="p-4">Ctrl+F 能搜到这里的文字</div>
</CollapsiblePanel>

七个类型一并导出:CollapsibleProps / CollapsibleState / CollapsibleChangeEventDetails / CollapsibleTriggerProps / CollapsibleTriggerState / CollapsiblePanelProps / CollapsiblePanelState