单击标签不聚焦自定义元素(Web 组件)



我正在使用本机DOM(无聚合物)编写自定义<select>元素。

我正在尝试将我的元素与<label>元素一起使用,并在单击<label>时正确触发对我的元素的单击事件,即:

<label>
  My Select:
  <my-select placeholder="Please select one">...</my-select>
</label>

<label for='mySelect1'>My Select:</label>
<my-select id='mySelect1' placeholder="Please select one">...</my-select>

但是,即使我添加了一个tabindex以使其可聚焦,这种行为似乎也不是开箱即用的。

下面是代码的精简版本和一个带有一些基本调试的 JSFiddle:

var MySelectOptionProto = Object.create(HTMLElement.prototype);
document.registerElement('my-select-option', {
  prototype: MySelectOptionProto
});
var MySelectProto = Object.create(HTMLElement.prototype);
MySelectProto.createdCallback = function() {
  if (!this.getAttribute('tabindex')) {
    this.setAttribute('tabindex', 0);
  }
  
  this.placeholder = document.createElement('span');
  this.placeholder.className = 'my-select-placeholder';
  this.appendChild(this.placeholder);
  var selected = this.querySelector('my-select-option[selected]');
  
  this.placeholder.textContent = selected
    ? selected.textContent
    : (this.getAttribute('placeholder') || '');
};
document.registerElement('my-select', {
  prototype: MySelectProto
});

但是标准还没有完成,也许将来您也可以将内置语义与自主自定义元素一起使用。

— 超夏普, 七月 15, 2016 在 16:05

我们现在生活在未来。标准发生了重大变化,我们现在有了更多强大的自定义元素。

<小时 />

博士:

如果与自定义元素关联的构造函数具有值为 trueformAssociated 属性,则标签有效。

<小时 />

乍一看,在查看文档时,您可能会得出结论,不允许将自定义元素作为<label>目标。文档说:

可以与<label>元素关联的元素包括<button><input>type="hidden"除外)、<meter><output><progress><select><textarea>

但是没有提到自定义元素。无论如何,您都会碰碰运气,并尝试在HTML验证器中输入此HTML片段:

<label>Label: <my-custom-element></my-custom-element></label>

或:

<label for="my-id">Label: </label><my-custom-element id="my-id"></my-custom-element>

这两个都是有效的 HTML 片段!

但是,尝试此 HTML 片段:

<label for="my-id">Label: </label><span id="my-id"></span>

显示此错误消息:

错误:label元素的 for 属性的值必须是非隐藏窗体控件的 ID。

但是,为什么随机<my-custom-element>被视为"非隐藏表单控件"?在验证器的 GitHub 问题中,您可以找到已经修复的问题 #963:

label for不允许自定义元素作为目标

该规范允许与表单关联的自定义元素作为目标for
https://html.spec.whatwg.org/multipage/forms.html#attr-label-for

这当前会触发错误:

<label for="mat-select-0">Select:</label>
<mat-select role="listbox" id="mat-select-0"></mat-select>

易于检查元素名称是否包含破折号。要检查自定义元素定义是否包含返回truestatic formAssociated属性要困难得多,因为这很可能深埋在外部框架.js file:
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example

WHATWG规范中确实有一个特殊的附录,它列举了与文档以及表单相关的自定义元素相同的"可标记元素"。规范中有一个例子,今天研究这个问题会在结果中产生这个问答帖子,以及博客文章,如Arthur Evans的"更强大的表单控件"(存档链接),这些文章显示了解决方案是什么。

启用<label>点击定位自定义元素的解决方案:formAssociated

要启用 <label> 操作,只需将值trueformAssociated 属性添加到自定义元素的构造函数中即可。

但是,问题中的代码需要移植到当前的 Web 组件 API。新的formAssociated属性是新的ElementInternals API(见attachInternals)的一部分,该API用于更丰富的表单元素控制和可访问性。

如今,自定义元素是用classescustomElements.define定义的,并且使用ShadowRoots。然后,可以将static formAssociated = true;字段添加到您认为属于"与表单关联的自定义元素"的类中。具有此属性会使自定义元素在单击<label>时调度click事件;现在我们到了某个地方!

为了将此click转换为focus操作,我们在attachShadow调用中还需要一件事:属性delegatesFocus设置为 true

// Rough translation of your original code to modern code, minus the `tabIndex` experimentation.
class MySelectOptionProto extends HTMLElement{}
customElements.define("my-select-option", MySelectOptionProto);
class MySelectProto extends HTMLElement{
  #shadowRoot;
  #internals = this.attachInternals();
  static formAssociated = true;
  constructor(){
    super();
    this.#shadowRoot = this.attachShadow({
      mode: "open",
      delegatesFocus: true
    });
    this.#shadowRoot.replaceChildren(Object.assign(document.createElement("span"), {
      classList: "my-select-placeholder"
    }));
    this.#shadowRoot.firstElementChild.textContent = this.querySelector("my-select-option[selected]")?.textContent ?? (this.getAttribute("placeholder") || "");
  }
}
customElements.define("my-select", MySelectProto);
<label>My label:
  <my-select placeholder="Hello">
    <my-select-option>Goodbye</my-select-option>
    <my-select-option>world</my-select-option>
  </my-select>
</label>
<label for="my-id">My label:</label>
<my-select id="my-id">
  <my-select-option>Never gonna give you</my-select-option>
  <my-select-option selected>up</my-select-option>
</my-select>
<label for="my-id">My other label</label>

delegatesFocus 单击自定义元素的某些不可聚焦部分时,将聚焦阴影根目录的第一个可聚焦子项。

如果需要更多控制,可以删除该属性,并将事件侦听器添加到自定义元素实例。为了确保点击来自<label>,您可以检查事件的target是否是您的自定义元素,即 this,并且屏幕上 x 和 y 位置的元素是针对自定义元素的<label>。幸运的是,这可以通过将xy传递给document.elementFromPoint来非常可靠地完成。然后,只需在所需元素上调用focus即可完成这项工作。

以自定义元素为目标的标签列表是 ElementInternals API 通过 labels 属性提供的另一件事。

class MySelectProto extends HTMLElement{
  // …
  constructor(){
    // …
    this.addEventListener("click", ({ target, x, y }) => {
      const relatedTarget = document.elementFromPoint(x, y);
      
      if(target === this && new Set(this.#internals.labels).has(relatedTarget)){
        console.log("Label", relatedTarget, "has been clicked and", this, "can now become focused.");
        // this.focus(); // Or, more likely, some target within `this.#shadowRoot`.
      }
    });
  }
}

只有措辞内容元素可以由 <label> 定位。

因此,如果要

使用非标准(自主自定义)元素,则必须自己管理焦点操作。

相反,您可以选择定义一个自定义内置元素,该元素将扩展<select>元素,如以下示例所示:https://jsfiddle.net/h56692ee/4/

 var MySelectProto = Object.create( HTMLSelectElement.prototype )
 //...
 document.registerElement('my-select', { prototype: MySelectProto, extends: "select" } )

您需要对 HTML 使用 is 属性表示法:

<label>
   My Select:
   <select is="my-select" placeholder="Please select one">
       <option>...</option>
   </select>
</label> 

更新 这 2 篇帖子中的更多解释:这里和那里。

相关内容

  • 没有找到相关文章