DisclosureRow 折叠行

一行 [前缀] 标题 · 摘要 的骨架,展开后显示正文。<r-reasoning><r-tool-card> 用的是同一个它,因此同时含有两者的会话只有一套折叠语言,而不是两套。

适用场景:一行紧凑的文字代表着更大的一团内容(一次工具调用、一段思维链、一组日志),而细节值得先藏起来、需要时再看。

快速开始

基础用法

展开后显示的正文。
<r-disclosure-row heading="Read file" summary="packages/ranui/index.ts" expandable>
  <div>展开后显示的正文。</div>
</r-disclosure-row>

heading 是定宽的左半边summary 是会截断的右半边,因此不管每行摘要多长,一列行都对齐在同一条竖线上。摘要为空时,分隔符也会一并消失。

工作进行中

busy 会让一道微光扫过该行。转圈只说明「某处有事在发生」,扫过这一行则说明正是这一行还在跑。

带前缀指示

leading 插槽与折叠箭头共用同一个网格单元,因此两者互换不产生布局开销,标题不会在指针下方移动。

没有 leading 内容时,箭头一直显示,它是这一行唯一能表明可展开的标记;有 leading 内容时,箭头改为在悬停、聚焦或展开时出现,其余时候显示的是状态点。

产物超过体积上限。
<r-disclosure-row heading="Build" summary="failed in 4.2s" tone="error" expandable>
  <r-state-dot slot="leading" state="error"></r-state-dot>
  <div>产物超过体积上限。</div>
</r-disclosure-row>

API 参考

属性

属性值 属性 类型 默认值 说明
heading heading string '' 定宽的左半边。
summary summary string '' 会截断的右半边;为空时分隔符一并消失。
open open boolean false 是否展开正文。会反射到属性上,因此 :has([open]) 可用。
expandable expandable boolean false 这一行是否有值得展开的正文。
busy busy boolean false 这一行代表的工作是否仍在进行。
tone tone string '' error 会把摘要染成错误色,其余为普通色调。
name name string '' 把若干行归为一组,展开一行时收起同组其余行。
sheet sheet string '' 注入 shadow root 的 CSS。

事件

事件 detail 派发选项 说明
disclosurebeforetoggle { open: boolean } bubbles, composed, cancelable 该行即将被展开或收起。
disclosuretoggle { open: boolean } bubbles, composed 该行已被展开或收起。
row.addEventListener('disclosuretoggle', () => {
  console.log(row.open ? '已展开' : '已收起');
});

disclosurebeforetoggle 先触发,并且可以被拒绝。「第一次展开时才去拉取正文」「有未保存的修改时不许收起」这两件事都要靠它。平台本身没有对应能力:<details> 只有事后的 toggle,给它加可取消的 beforetoggle 的提案至今还没落地。

row.addEventListener('disclosurebeforetoggle', async (event) => {
  if (!event.detail.open || row.dataset.loaded) return;
  event.preventDefault(); // 正文到位之前先按住不展开
  row.append(await fetchBody());
  row.dataset.loaded = 'true';
  row.open = true;
});

只有点击或按键才会触发它。row.open = true 这种程序化修改是应用自己改主意,没有谁需要被征求意见。

同时只展开一行

name 的分组方式和 <details>name 一致:展开一行会收起同组其余行。分组范围是整个文档,同组的行不必相邻。

<r-disclosure-row name="run" heading="Install" expandable>…</r-disclosure-row>
<r-disclosure-row name="run" heading="Build" expandable>…</r-disclosure-row>
<r-disclosure-row name="run" heading="Test" expandable>…</r-disclosure-row>

无障碍

只有确实能展开的行才是控件。带 expandable 时,该行会有 role="button"、一个 Tab 停靠点、 aria-expanded,以及指向正文的 aria-controls;不带时这些一个都没有,因为把一行纯文本报成按钮,只会引来一次什么都不会发生的点击。busy 会写上 aria-busy,这样那道扫光就不是唯一的进行中信号。

收起时正文是被裁切而不是被移除,这样才能做动画。同时它会被标记为 inert,内容用 content-visibility: hidden 跳过渲染,因此收起期间既不在 Tab 顺序里,也不占渲染开销。

行高 24px,正好卡在 WCAG 2.5.8 的最小值上,而且行与行之间没有间隔。在粗指针(触摸)设备上默认行高会变成 32px:命中区无法在不与上一行重叠的前提下继续扩大,而重叠只会把「目标偏小」换成「点错行」。设置 --ran-disclosure-row-height 可以在任何输入方式下固定行高。

插槽

插槽 内容
default 正文,在 open 时显示。
leading 标题前的指示物,通常是 <r-state-dot>
heading 左半边的标记内容,替代 heading 属性的纯文本。
summary 右半边的标记内容,替代 summary 属性的纯文本。

headingsummary 作为属性只能传纯文本,对工具调用行来说通常够用。如果这一半需要带标记(代码、链接、缩写),改用插槽。属性文本正是插槽的后备内容,所以插入内容会直接把它顶掉:

<r-disclosure-row expandable>
  <code slot="heading">fetch()</code>
  <a slot="summary" href="https://example.com">https://example.com</a>
  <pre>…</pre>
</r-disclosure-row>

插槽内容同样算作这一行的一半,因此分隔符的出现和消失规则与用属性时完全一致。

Part

row · leading · title · separator · summary · disclosure · body

自定义样式

<r-disclosure-row> 自身暴露了 15 个 CSS 自定义属性,另外还会读取主题里的语义令牌。令牌设在任何能继承到的地方都有效,比如 :root、外层容器,或元素本身:

r-disclosure-row {
  --ran-disclosure-hover-background: var(--ran-color-bg-subtle);
}

Part:body · disclosure · leading · row · separator · summary · title

完整清单见样式令牌;该选哪个令牌见设计系统

最佳实践

  • 要么给行配正文,要么别让它可展开。 展开后却是空的,箭头就没有意义;不加 expandable,这一行就保持单行。
  • heading 用固定词表Read fileRun testsSearch),把变化的部分放进 summary。这正是一列行能被快速扫读的原因。
  • tone="error" 必须配文字,不能只靠颜色:摘要要说清失败的是什么。