登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

template shadowrootmode 怎么在闭合阴影根中保留焦点委托

来源:17golang原创

时间:2026-10-13 10:58:30 120浏览 收藏

在声明式闭合阴影根中保留焦点委托,直接把 shadowrootmode="closed" 和布尔属性 shadowrootdelegatesfocus 写在同一个 template 上。根内部还要有可聚焦控件。closed 不会取消委托,但 host.shadowRoot 返回 null,所以不要靠外部读取这个属性来判断委托是否生效。

我更愿意把这件事拆成两个选择:组件内部结构是否公开,以及用户怎样进入输入框。以一个只有姓名输入框的组件为例,前者可以保持闭合,后者仍然交给浏览器原生焦点机制。下面是可自行复现的设计示例,不是已执行的浏览器测试记录。

closed 和 delegatesFocus 是两项独立配置

从脚本创建根迁移到声明式 Shadow DOM,变化是把根的创建配置放入初始 HTML,而不是等 JavaScript 执行 attachShadow。这个能力不要求把模式改成 open。

配置负责什么不负责什么
shadowrootmode="closed"闭合根,宿主 shadowRoot 不暴露该根不禁止内部控件接收焦点
shadowrootdelegatesfocus为创建的根启用焦点委托不自动补充控件名称与键盘操作
内部原生 input提供实际可输入、可聚焦对象不依赖外部 querySelector 穿过根

我不会把 closed 当成安全隔离工具,也不会为排查方便就直接改为 open。这里要解决的是焦点入口,优先保留原本的封装约定。MDN 对 template 的说明区分了这两项属性;具体焦点目标的选择规则由 HTML 标准定义。

声明式闭合根、焦点委托配置和内部输入框的静态组成关系
图1:原创结构说明图。模板配置分别关联根模式与委托能力;闭合根内仍包含有标签的输入框。连线只表达配置和包含关系,不表示执行顺序。

把委托开关放进声明式模板

我会先使用只有一个输入框的片段,避免多个按钮的顺序掩盖问题。将下面内容保存为 HTML,由目标浏览器正常打开,让初始文档解析器处理模板。不要把它作为字符串赋给 innerHTML 后,期待获得同样的声明式根。


闭合根焦点入口示例
  

输入框与 label 在同一个根内,用 for 和 id 建立名称关系。宿主的 tabindex="0" 表示参与顺序导航的意图,未使用正数抢占页面顺序。示例没有自动聚焦,不会在页面载入时主动把用户拉离前面的内容。

特别容易写错的是 shadowrootdelegatesfocus="false"。它是布尔属性,字符串 false 并不是关闭开关;要关闭就删除该属性。开关应在根创建之前出现在模板中,不能在模板已被解析后再补属性来修改现有根。

Tab 入口不等于 host.focus()

对我来说,最值得单独讨论的是两种“可聚焦”不是同一件事。调用 host.focus() 是程序化焦点请求;Tab 和 Shift+Tab 则使用顺序焦点导航规则。委托宿主在顺序导航中的处理与直接调用 focus() 不同,不应承诺所有触发方式都永远选中同一个内部节点。

上面的单控件片段把这种差异压到最小:内部只有一个正常可聚焦的 input,使用原生顺序即可。若将来在输入框之前放置关闭按钮,默认委托目标可能改变;即便某个节点的 tabindex="-1" 将其排除在 Tab 顺序之外,它仍可能成为程序化委托的候选。不要把“排除 Tab”理解为“无法聚焦”。

HTML 标准还规定:如果焦点已经位于该宿主内部,再请求聚焦宿主可以保留当前内部目标。因此,“host.focus() 每次都会跳到第一项”也不是可靠接口约定。内部有多个控件时,应明确组件的聚焦语义,而不是让外部猜 DOM 顺序。

有些情形可以用内部 autofocus 影响初始目标,但它可能造成页面载入时跳过重要内容。这个例子刻意不使用它。我更倾向让单输入组件保持原生默认,让复杂组件提供明确的聚焦方法,并另行设计导航规则。

宿主聚焦请求、内部焦点与外部观察边界的静态关系
图2:原创静态关系说明图。host.focus()、内部 input 与宿主焦点样式属于交互关系;document.activeElement 和 host.shadowRoot 属于外部观察接口,不是内部节点的完整公开窗口。

迁移时别把闭合根当成没创建

旧组件若以 if (!host.shadowRoot) 为条件执行 attachShadow,在 open 模式下看起来合理,迁移到 closed 声明式根后却不能证明宿主没有根:闭合根本来就会返回 null。别因此再次创建根或重填模板,导致现有内容被破坏。这个例子使用普通 div 作为宿主,不再混入自定义元素的初始化与水合逻辑。

另一个常见差异是注入方式。普通 innerHTML 不会把其中的声明式模板实例化为 ShadowRoot。若你看到隐藏的 template 而非输入框,应先检查是不是走了字符串片段注入,而不是马上归咎于 closed。不支持该特性的浏览器也可能只得到普通模板,业务页面需要单独安排可用的普通 HTML 降级内容。

我不会把 ShadowRoot.prototype 上存在 delegatesFocus,或者 HTMLTemplateElement 上存在某个属性,当成整个组件在目标环境可用的证明。根创建能力、属性识别和键盘体验需要分别考虑。原生声明式根支持范围与早已存在的命令式委托能力也不能混用版本结论。

用可见焦点判断体验而不是猜内部节点

闭合根的外部观察是有意受限的:当内部控件获得焦点,document.activeElement 通常指向宿主,而非内部 input。外部代码可以判断“焦点在这个组件”,却不能据此断言当前是哪个内部控件。host.shadowRoot 为 null 也不能证明内部没有焦点。

请在自己的目标浏览器与平台中检查以下体验,这里不提供虚构的跨浏览器运行结果:

  • 从前置按钮使用 Tab 进入组件,姓名输入框能编辑,焦点轮廓可见。
  • 继续 Tab 能离开组件到“下一项”;Shift+Tab 能返回,不出现意外困住焦点的情况。
  • 点击“聚焦姓名输入框”,在未处于内部焦点时,输入框能接收文字。
  • 闭合根保持不公开,外部读取 shadowRoot 不能当成内部输入框的读取方式。
  • 用屏幕阅读器确认输入框名称为“姓名”;不要假定焦点委托本身解决所有可访问性问题。

系统键盘导航设置可能改变某些控件的 Tab 行为,应连同浏览器版本一起记录。若增加 disabled、hidden 或 inert 状态,原先的聚焦目标还可能不再可用,需要把这些状态列入组件自己的交互测试。

我的取舍是:单控件、静态首屏结构适合直接用 closed 加 shadowrootdelegatesfocus;多控件、特殊初始目标或复杂键盘导航则需要组件内的明确焦点接口。封装可以保留,交互语义不能省略。

一定要给宿主加 tabindex 吗?

它不是声明焦点委托的开关。本文给宿主 tabindex="0" 是为了明确键盘入口设计;真正接收输入的仍是内部控件。不要为每一层容器都添加 tabindex,更不要用正数拼接页面导航顺序。

委托后外部 CSS 能选择内部 input 吗?

不能因为委托就穿透阴影边界。把内部焦点提示写在根内,宿主整体提示可以用 :focus-within;别用取消所有 outline 的方式隐藏原生焦点反馈。

能在根创建后修改 delegatesFocus 吗?

ShadowRoot.delegatesFocus 是只读属性。创建阶段配置好,而不是把布尔属性补到已经失去创建作用的模板上。

规范与文档

HTML 焦点处理规范:https://html.spec.whatwg.org/multipage/interaction.html#focus

MDN 声明式模板:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template

MDN 焦点委托:https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus

MDN 闭合根读取:https://developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot

MDN HTML 注入说明:https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML

MDN 活动元素:https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>