原创

details 与 summary 做折叠区:分组、布尔属性和原生键盘行为

折叠一段说明,不一定需要先写点击监听、维护 expanded 变量,再手动隐藏内容。details 与 summary 已经有原生展开行为,键盘可以操作,打开状态也有对应属性。真正容易出错的是把它包装过度以后,让脚本状态与浏览器状态分家。

若需求只是FAQ、配置说明或次要详情,可以先从原生元素开始。这里做一个同组最多打开一项的帮助区,再专门观察布尔属性和toggle事件,避免把一份简单交互写成两套互相校正的状态机。

完整页面只把状态读出来,不再另存一份

保存为 details-group.html,使用现代浏览器打开。name分组是相对较新的能力,页面会检测对应属性;不支持分组的环境仍能独立折叠,只是不应承诺同组互斥。

<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>原生折叠区分组实验</title>
<style>
body { max-width: 42rem; margin: 2rem auto; padding: 1rem; font: 16px sans-serif; }
details { border-bottom: 1px solid #888; padding: .8rem 0; }
summary { cursor: pointer; font-weight: bold; }
summary:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
</style>
<p id="support"></p>
<button id="first" type="button">打开第一项</button>
<button id="close-all" type="button">关闭全部</button>
<button id="attribute" type="button">把第三项 open 属性写成 false 字符串</button>
<details id="one" name="help" open>
  <summary>导出文件在哪里下载?</summary>
  <p>在任务列表中找到对应任务,生成完成后使用下载入口。</p>
</details>
<details id="two" name="help">
  <summary>关闭页面以后任务还会继续吗?</summary>
  <p>这取决于服务端任务协议,不由本地折叠区的状态决定。</p>
</details>
<details id="three" name="help">
  <summary>怎样重新查看这份说明?</summary>
  <p>再次展开当前项目即可。内容仍在文档中,没有重新发请求。</p>
  <a href="#status">跳到当前状态</a>
</details>
<p id="status" role="status"></p>
<script>
const panels = [...document.querySelectorAll('details')];
const status = document.querySelector('#status');
document.querySelector('#support').textContent =
  'name' in HTMLDetailsElement.prototype
    ? '此浏览器支持 details name 分组'
    : '此浏览器未检测到 name 分组,各项仍可独立折叠';
function refresh() {
  const opened = panels.filter(panel => panel.open).map(panel => panel.id);
  status.textContent = `当前打开:${opened.join(', ') || '无'}`;
}
for (const panel of panels) panel.addEventListener('toggle', refresh);
document.querySelector('#first').onclick = () => { panels[0].open = true; };
document.querySelector('#close-all').onclick = () => {
  for (const panel of panels) panel.open = false;
};
document.querySelector('#attribute').onclick = () => {
  panels[2].setAttribute('open', 'false');
};
refresh();
</script>
</html>

先用鼠标展开第二项,支持name分组时第一项会关闭;再用Tab聚焦summary,按Enter或Space,检查原生键盘动作。最后点击“关闭全部”,应显示当前打开为空。脚本通过实际open属性生成状态,没有假设用户只能从某一个按钮修改它。

name相同的details形成互斥组,不要求它们在DOM中紧挨着。组件库若给所有折叠区统一写一个固定name,可能让页面远处两个本不相关的区域互相关闭。组名应该来自真实交互关系,而不是只为复用CSS类顺手生成。

分组能力缺失时,独立折叠仍然成立,但互斥规则并没有自动降级实现。产品必须决定接受这个差异,还是提供经过测试的替代行为。

open=false 字符串,仍然代表属性存在

点击第三个实验按钮,第三项会打开。HTML布尔属性按是否存在判断,字符串false没有关闭语义。服务器模板输出 open="false",也会把折叠内容打开。

脚本里应使用 element.open = false 或移除属性,表达关闭;模板则应在条件不成立时不输出该属性。不要把HTML布尔属性与JSON布尔值混成同一份文本规则。

这同样影响初始化。一个分组里直接标记多个open,并不代表浏览器会允许多项一直保持打开;分组算法会维护自身规则。页面需要“全部展开”时,就应该选择不互斥的结构,而不是不断设置属性与浏览器争状态。

toggle在打开状态变化时通知应用,但相近的连续变更可能合并事件。例子每次都重新读取全部面板状态,因此不依赖一开一关必定产生固定两次回调。把toggle事件数当成用户点击次数,会给埋点带来错误解释。

summary 是操作入口,不只是标题样式

summary应明确说明展开后能看到什么。把它写成“更多”,整页重复十几次,键盘与辅助技术用户很难判断当前操作对象。原生可点击并不等于标签已经足够清楚。

示例保留默认交互,仅添加focus-visible轮廓。不要全局outline:none以后只照顾鼠标悬停;展开图标和颜色变化不能告诉键盘用户当前焦点在哪一项。

也不建议把整个标题行塞满互相竞争的按钮和链接,再让summary外层同时承担折叠动作。需要独立动作时,应根据语义安排清晰入口,并测试点击或回车究竟触发哪个动作。简单FAQ用一段明确summary文本通常更稳。

如果设计要求自定义展开图标,可以调整marker样式或添加装饰元素,但不要因此删掉原生语义再重建同样的键盘行为。辅助技术对summary内部复杂标题或嵌套语义的呈现可能有差异,应在实际支持环境核对,不靠视觉截图判断完整可用性。

折叠不会卸载内容,也不停止后台任务

details关闭以后,里面的节点仍在文档中,脚本引用仍然有效。事件监听器、计时器和已经发出的网络请求不会因为一项折叠就自动销毁。若内容包含需要暂停的昂贵组件,应把真正的资源策略接到状态变化上,而不是把display层变化当成生命周期完成。

同理,折叠不是隐藏机密数据的方式。响应正文里已经包含的内容,可以从开发工具或原始HTML读取;需要权限保护的内容应由服务器决定是否交付。

异步加载也要防止迟到结果改变用户意图。用户打开一项触发请求,又立即关闭,数据回来可以更新受控缓存,但不能无条件重新打开面板。内容是否就绪与用户当前是否希望展开,应该各有自己的状态。

关闭全部后,通过脚本按钮打开第一项,再用键盘打开第二项。同组互斥时,第一项应收起。把第三项open写成字符串false,第三项又会打开;要关闭它,应删除属性或将open属性值设为布尔false。

正文到此结束
评论插件初始化中...
Loading...