fmg
fmg

Reputation: 925

Custom label elements associated to form controls via ElementInternals?

I'm writing a custom input element called fancy-input, which can participate in a form via the FormInternals API. In particular, the ElementInternals.labels property gives me a list of the label elements associated, via their for attributes, to an instance of fancy-input.

But now let's say I also want to write a custom label element called fancy-label. It renders a native label element in its shadow DOM with fancy styles, formatting, etc. How do make it known that an instance of fancy-label is the "label" associated with an instance of fancy-input. In particular, I'd like my fancy-label node appear in the NodeList returned by calling ElementInternals.labels on the associated fancy-input element.

One possible approach would be for fancy-label to render a native label into a slot, keeping it in the light DOM and, hence, visible to the form and the fancy-input element. But is there a solution with the native label in the shadow DOM of fancy-label?

Upvotes: 3

Views: 225

Answers (1)

Ferdinand Prantl
Ferdinand Prantl

Reputation: 5729

Today's API related to form-associated elements (FACE) doesn't allow you to create a custom label functioning just like the native label. This topic has been discussed last year, and could be solved by delegating the label or cross-DOM references. However, you can still implement a working custom label fairly easily today. The feasibility will depend on your goal.

For example, instead of this requirement:

"I'd like my fancy-label node appear in the NodeList returned by calling ElementInternals.labels on the associated fancy-input element."

Let's say this:

"I'd like my fancy-label node work with mouse, keyboard and assistive technologies like screen readers (SR) just as the native label does."

This requirement can be implemented with the aria-labelledby attribute. Instead of having the label point to its field, you can have the field point to its label. You will need to assign a unique ID to the label instead of to the field:

<fancy-label id="text-label">
  Name:
  <input name="text" type="text" aria-labelledby="text-label">
</fancy-label>

<fancy-label id="text-label">Name:</fancy-label>
<input name="text" type="text" aria-labelledby="text-label">

You can reference any element with content by aria-labelledby. SR will read its contents it just like the contents of a native label. What remains is to handling clicks on the custom label, so that they'd perform clicks on the field. You'll need a little JavaScript for it:

class FancyLabel extends HTMLElement {
  constructor() {
    super()

    this.attachShadow({ mode: 'open' })
    this.shadowRoot.appendChild(document.createElement('slot'))

    this.addEventListener('click', event => this.#handleClick(event))
  }

  #handleClick(event) {
    const field = document.querySelector(`[aria-labelledby="${this.id}"]`)
    // ignore the click if the field does not exist, or if it is disabled,
    // or if the field is inside the label and this event bubbled from it
    if (!field || field.disabled || event.target === field) return
    field.focus()
    field.click()
  }
}

customElements.define('fancy-label', FancyLabel)

If you wanted to support the for attribute known from the native label element, you could add code for:

  • ensuring a unique id attribute on the custom label
  • watching the value of the for attribute on the custom label
  • setting the aria-labelledby attribute on the target field (pointed to by the for attribute) to the id of the custom label whenever its for attribute changes

You have a look at piwo-label, which implements this.

Upvotes: 0

Related Questions