我正在使用本机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
我们现在生活在未来。标准发生了重大变化,我们现在有了更多强大的自定义元素。
<小时 />博士:
如果与自定义元素关联的构造函数具有值为 true
的 formAssociated
属性,则标签有效。
乍一看,在查看文档时,您可能会得出结论,不允许将自定义元素作为<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>
易于检查元素名称是否包含破折号。要检查自定义元素定义是否包含返回
true
的static formAssociated
属性要困难得多,因为这很可能深埋在外部框架.js
file:
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example
WHATWG规范中确实有一个特殊的附录,它列举了与文档以及表单相关的自定义元素相同的"可标记元素"。规范中有一个例子,今天研究这个问题会在结果中产生这个问答帖子,以及博客文章,如Arthur Evans的"更强大的表单控件"(存档链接),这些文章显示了解决方案是什么。
启用<label>
点击定位自定义元素的解决方案:formAssociated
要启用 <label>
操作,只需将值true
的 formAssociated
属性添加到自定义元素的构造函数中即可。
但是,问题中的代码需要移植到当前的 Web 组件 API。新的formAssociated
属性是新的ElementInternals API(见attachInternals
)的一部分,该API用于更丰富的表单元素控制和可访问性。
如今,自定义元素是用classes
和customElements.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>
。幸运的是,这可以通过将x
和y
传递给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 篇帖子中的更多解释:这里和那里。