最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Clicking label not focusing custom element (Web Components) - Stack Overflow

programmeradmin3浏览0评论

I’m writing a custom <select> element using native DOM (no Polymer).

I’m trying to use my element with a <label> element and correctly trigger click events to my element when the <label> is clicked, i.e.:

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

or

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

However, this behavior doesn’t seem to work out of the box, even if I add a tabindex to make it focusable.

Here’s a stripped down version of the code and a JSFiddle with some basic debugging:

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
});

I’m writing a custom <select> element using native DOM (no Polymer).

I’m trying to use my element with a <label> element and correctly trigger click events to my element when the <label> is clicked, i.e.:

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

or

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

However, this behavior doesn’t seem to work out of the box, even if I add a tabindex to make it focusable.

Here’s a stripped down version of the code and a JSFiddle with some basic debugging:

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
});
Share Improve this question edited Oct 21, 2022 at 0:32 Sebastian Simon 19.6k8 gold badges61 silver badges84 bronze badges asked Jul 12, 2016 at 4:52 DogokuDogoku 4,6754 gold badges26 silver badges35 bronze badges
Add a ment  | 

2 Answers 2

Reset to default 8

But the standard is not finished, and maybe in the future you’ll be able to use the built-in semantic with autonomous custom elements, too.

— Supersharp, Jul 15, 2016 at 16:05

We live in the future now. The standard has significantly changed and we have more capable custom elements now.


Tl;dr:

Labels work iff the constructor associated with your custom element has a formAssociated property with the value true.


When looking at the documentation, at first sight, you might conclude that custom elements are not allowed as <label> targets. The docs say:

Elements that can be associated with a <label> element include <button>, <input> (except for type="hidden"), <meter>, <output>, <progress>, <select> and <textarea>.

But custom elements are not mentioned. You try your luck anyway and try to enter this HTML fragment in an HTML validator:

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

or:

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

and both of these are valid HTML snippets!

However, trying this HTML fragment:

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

shows this error message:

Error: The value of the for attribute of the label element must be the ID of a non-hidden form control.

But why is a random <my-custom-element> considered a “non-hidden form control”? In the validator’s GitHub issues you then find the already fixed issue #963:

label for doesn’t allow custom elements as targets

The spec allows form-associated custom elements as for targets
https://html.spec.whatwg/multipage/forms.html#attr-label-for

This currently triggers an error:

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

Easy to check if element name contains a dash. Much harder to check that custom element definition includes a static formAssociated property returning true, since this is likely buried deep in an external framework .js file:
https://html.spec.whatwg/multipage/custom-elements.html#custom-elements-face-example

There is indeed one peculiar addendum in the WHATWG specification which enumerates the same “labelable elements” as the documentation plus form-associated custom elements. There is an example in the specification, and researching this problem today yields this Q&A post in the results as well as blog articles such as “More capable form controls” by Arthur Evans (archived link), which show what the solution is.

The solution to enable <label> clicking targeting custom elements: formAssociated

All that needs to be done to enable the <label> action is to add a formAssociated property with the value true to the constructor of the custom element.

However, the code in the question needs to be ported to the current Web Components API. The new formAssociated property is part of the new ElementInternals API (see attachInternals) which is used for richer form element control and accessibility.

Nowadays, custom elements are defined with classes and customElements.define, and ShadowRoots are used. The static formAssociated = true; field can then be added to the class which you consider to belong to the “form-associated custom elements”. Having this property makes your custom element dispatch click events when clicking the <label>; now we’re getting somewhere!

In order to turn this click into a focus action, there’s one more thing we need in the attachShadow call: the property delegatesFocus set to 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 will focus the first focusable child of the shadow root, when some non-focusable part of the custom element has been clicked.

If you need more control, you can remove the property, and add an event listener to the custom element instance. In order to be sure that the click came from the <label>, you can check if the event’s target is your custom element, i.e. this, and that the element at the x and y position on the screen was a <label> targeting your custom element. Fortunately, this can be done quite reliably by passing x and y to document.elementFromPoint. Then, simply calling focus on your desired element will do the job.

The list of labels that target a custom element is another thing that the ElementInternals API provides via the labels property.

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 bee focused.");
        // this.focus(); // Or, more likely, some target within `this.#shadowRoot`.
      }
    });
  }
}

Only the phrasing content elements can be targeted by <label>.

So you'll have to manage the focus action by yourself if you want to use a non-standard (autonomous custom) element.

Instead you can choose to define a Customized built-in element that will extend the <select> element, as in the following example: https://jsfiddle/h56692ee/4/

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

You'll need to use the is attribute notation for HTML:

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

Update More explanations in these 2 posts: here and there.

发布评论

评论列表(0)

  1. 暂无评论