原创

HTML inert 实战:整块交互隔离为何不等于隐藏或禁用表单

给一块编辑区盖上半透明遮罩,鼠标点不进去,Tab 却还能继续进入下面的输入框。pointer-events: none 只改变指针命中,不会顺手把后代从键盘焦点、编辑与辅助技术交互里隔开。

inert 适合表达“这整个子树暂时不参与交互”。它比逐个给按钮加 disabled 更接近区块级状态,但它不是隐藏,也不等于所有表单控件都被禁用。界面仍然看得见,表单数据也可能照样被收集。

先用一个预览模式实验把这些差别分开,再决定它适不适合放进抽屉、加载遮罩或多步骤编辑器。需要支持原生 inert 的现代浏览器,旧环境应检测能力并选择明确降级,不要只给属性加上名字就认定行为已经存在。

一个能看见、却暂时不能操作的编辑区

保存为 inert-preview.html,直接用浏览器打开:

<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>inert 编辑区实验</title>
<style>
body { max-width: 42rem; margin: 2rem auto; padding: 1rem; font: 16px sans-serif; }
#editor { border: 1px solid #777; padding: 1rem; }
#editor[inert] { opacity: .55; border-style: dashed; }
label { display: block; margin: .75rem 0; }
button { margin: .25rem; }
</style>
<p id="mode" role="status">编辑模式</p>
<button id="toggle" type="button" aria-pressed="false">进入预览</button>
<button id="collect" type="button">读取当前表单数据</button>
<form id="settings">
  <section id="editor" aria-labelledby="editor-title">
    <h2 id="editor-title">通知设置</h2>
    <label>显示名称 <input name="displayName" value="编辑者"></label>
    <label><input name="emailEnabled" type="checkbox" value="yes" checked>邮件通知</label>
    <p><a href="#outside">区块内链接</a></p>
    <button id="inside" type="button">区块内动作</button>
  </section>
</form>
<p id="outside" tabindex="-1">编辑区之外仍然可以操作。</p>
<p id="action" role="status"></p>
<pre id="data"></pre>
<script>
const editor = document.querySelector('#editor');
const toggle = document.querySelector('#toggle');
const mode = document.querySelector('#mode');
const action = document.querySelector('#action');
let clicks = 0;
if (!('inert' in HTMLElement.prototype)) {
  toggle.disabled = true;
  mode.textContent = '此浏览器缺少原生 inert,本页不启用预览隔离';
}
toggle.addEventListener('click', () => {
  const preview = !editor.inert;
  // 若从其他入口切换,应先把焦点移到仍可操作的位置。
  if (preview && editor.contains(document.activeElement)) toggle.focus();
  editor.inert = preview;
  toggle.setAttribute('aria-pressed', String(preview));
  toggle.textContent = preview ? '返回编辑' : '进入预览';
  mode.textContent = preview ? '预览模式:编辑区暂不参与交互' : '编辑模式';
});
document.querySelector('#inside').addEventListener('click', () => {
  clicks += 1;
  action.textContent = `区块内动作执行 ${clicks} 次`;
});
document.querySelector('#collect').addEventListener('click', () => {
  const entries = [...new FormData(document.querySelector('#settings')).entries()];
  document.querySelector('#data').textContent = JSON.stringify(entries, null, 2);
});
</script>
</html>

编辑模式下,输入框、复选框、链接和内部按钮均可操作。切到预览后,内容仍显示,但点击内部按钮不应再产生用户动作,Tab 也应越过整个编辑子树。外部的“返回编辑”和“读取当前表单数据”仍然可用。

样式中的透明度只是在告诉使用者当前状态,不承担交互隔离。删掉这条 CSS,inert 行为仍应成立;删掉 inert 只留灰色,则控件仍可操作。视觉与行为分开验证,能发现大量“看上去已经禁用”的假状态。

收集表单数据时,它并没有变成 disabled

在预览模式点“读取当前表单数据”,仍会得到名称和勾选的邮件字段。这不是浏览器漏了限制,而是 inert 没有把字段的 disabled 状态改成真。它约束交互子树,不负责改写表单条目的构造协议。

所以,预览页面是否允许提交已有值、哪些字段应该提交,必须单独决定。若把“不让用户编辑”直接等同于“不要向服务器发送”,就容易在换控件状态时意外丢字段。反过来,后台权限也不能靠 inert 保证,脚本或自行构造的 HTTP 请求都可以提交其他值。

控件自己的 readonly、disabled 与区块 inert 各有用途。readonly 主要针对支持该属性的输入控件;disabled 影响控件参与交互和表单流程;inert 对整棵后代树起作用,适合临时整体隔离。不是范围更大就一定更适合,每个选择都会改变不同合同。

当前焦点要有去处,不能等属性替你设计

示例保留了一条检查:如果切换前焦点在编辑区里,先移到仍可操作的切换按钮。鼠标点击外部按钮时焦点通常已经在那里,这段逻辑仍能覆盖以后从快捷键、定时状态或其他入口调用的情况。

不应该把“加上 inert 以后浏览器总会把焦点放到我想要的位置”作为组件合同。它会让子树无法被正常聚焦,但下一步焦点落在哪里、恢复编辑时应该回到哪个控件,都属于界面自己的状态决策。

如果做的是抽屉或模态层,需要先确认哪些区域才是背景。对普通自绘弹层,把 inert 加到包含弹层本身的祖先上,会把自己的关闭按钮也隔离。通过 showModal() 打开的原生模态 dialog 存在专门的逃逸规则,不能把普通子树的结论直接套给它。通常应让交互层与背景成为可清楚分组的兄弟区域,或直接采用能表达模态行为的原生 dialog。

inert 也不自动实现完整焦点循环、Escape 关闭、初始焦点与关闭回焦。仅凭背景不能点,就给一个普通 aside 声明 aria-modal="true",仍然可能向辅助技术承诺了没有实现的行为。优先使用原生模态能力,手动组合时逐项验证真正需要的交互。

它与 hidden、aria-hidden 解决的事不同

hidden 表示内容不应继续呈现,inert 则允许内容保持可见。预览模式需要用户继续核对文字,因此可以保留视觉内容;后台标签页的非活动面板若根本不该显示,就更适合正常隐藏,而不是把所有内容堆着再统一 inert。

aria-hidden 影响可访问树暴露,不会自动阻止鼠标和键盘进入。把一个还能聚焦的按钮放进 aria-hidden 子树,会让视觉键盘用户与辅助技术得到不一致状态。不能用它冒充交互禁用开关。

inert 子树还会影响辅助技术访问和文本查找、选择等行为。对只想禁止编辑、却仍希望读屏继续阅读的内容,整块 inert 可能过度;只读文本展示或合适的 readonly 控件更容易表达真实需求。背景完全暂时不可用,与内容仍应可读,是两种不同产品选择。

恢复状态时也要考虑嵌套。内层组件原本就有自己的 inert 属性,外层解除隔离不应擅自清掉内层的属性。组件应该只管理自己拥有的那一层状态,避免多个面板互相覆盖。

验证时至少同时操作鼠标、Tab 与外部切换按钮,再确认表单采集结果。若只用鼠标点一次,遮罩、pointer-events 和 inert 都可能表现相似;真正能区分它们的,是整条交互与数据路径是否符合预期。

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