Componentes web accesibles

Un límite de shadow cambia el funcionamiento de la semántica y de ARIA. Hay dos fallas que predominan: la falta de semántica dentro del árbol de sombra y las referencias de ARIA que se rompen de forma silenciosa porque no pueden cruzar las raíces de sombra.

Comienza con el lenguaje de marcado semántico: usa <button>, <nav> y <ul> reales dentro de la plantilla para que la mayoría de la semántica se incluya de forma gratuita y reserva ElementInternals/ARIA para los espacios vacíos.

Semántica predeterminada a través del mixin de ARIA de ElementInternals

Establece el rol y el estado de ARIA de un elemento personalizado en JS a través de ElementInternals, en lugar de escribir atributos de ARIA en el host. Esto mantiene la semántica como valores predeterminados que posee el componente, a la vez que permite que un consumidor los anule con atributos en el DOM de luz.

class ToggleSwitch extends HTMLElement {
  #internals;
  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.#internals.role = 'switch';          // default role, no host attribute needed
    this.#internals.ariaChecked = 'false';    // exposed to the accessibility tree
    this.addEventListener('click', () => this.toggle());
    this.addEventListener('keydown', (e) => {
      if (e.key === ' ') { e.preventDefault(); this.toggle(); }
    });
  }
  connectedCallback() {
    // Put the host in the tab order on connect (setting tabIndex in the constructor throws on createElement).
    if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
  }
  toggle() {
    const on = this.#internals.ariaChecked !== 'true';
    this.#internals.ariaChecked = String(on);
  }
}
customElements.define('toggle-switch', ToggleSwitch);

Esto evita que se contamine el DOM con atributos de ARIA y que los consumidores los destruyan accidentalmente, mientras que el árbol de accesibilidad sigue viendo el rol y el estado correctos.

Control del foco y delegatesFocus

  • Cuando el elemento host en sí es el control interactivo y lleva el rol y el estado a través de ElementInternals (como ToggleSwitch más arriba), coloca el host en el orden de tabulación dentro de connectedCallback (if (!this.hasAttribute('tabindex')) this.tabIndex = 0, nunca en el constructor, que arroja un error en document.createElement) para que el enfoque se dirija al host y la tecnología de accesibilidad anuncie su rol y estado. No configures tabindex en el host para componentes estáticos no interactivos (como un texto destacado o una tarjeta).
  • Adjunta la raíz de sombra con delegatesFocus: true cuando el componente unifica un control enfocable interno que posee la semántica (o en un elemento personalizado asociado a un formulario con static formAssociated = true). Enfocar el host (y hacer clic en un <label> asociado para los elementos asociados a un formulario) reenvía el enfoque al primer elemento secundario enfocable, y :focus coincide con el host. No combines delegatesFocus: true con un rol o estado en el host, o el enfoque se dirigirá al elemento interno y omitirá el estado del host.
  • Para el enfoque móvil o el enfoque en movimiento en una región recién revelada, asigna tabindex="-1" al objetivo y llama a .focus(). Haz que los objetivos que se pueden enfocar de forma programática sean explícitos en lugar de depender del orden de las pestañas.

Relaciones de ARIA entre raíces de sombra

  • OBLIGATORIO: Un id al que hace referencia aria-labelledby, aria-describedby, aria-controls, etc., se resuelve solo dentro del propio árbol del elemento de referencia. Un atributo basado en id en el DOM de luz no puede apuntar a un id dentro de un árbol de sombra y viceversa; la referencia se ignora de forma silenciosa.
  • Para cruzar el límite, usa las propiedades de elementos de ARIA reflejadas (p. ej., ariaLabelledByElements, ariaDescribedByElements, etc.), que toman referencias directas de elementos de JS en lugar de IDs. Se resuelven hacia afuera: un elemento dentro de un árbol de sombra puede apuntar a un objetivo en el DOM de luz o en cualquier árbol de ancestros (incluido el contenido con ranuras), por lo que puedes etiquetar un control interno de sombra desde el contenido del DOM de luz, algo que las referencias de id no pueden expresar.
  • No se resuelven hacia adentro: un elemento no puede hacer referencia dentro de un árbol de sombra descendiente (p.ej., un host no puede ser etiquetado por su propio contenido de sombra). Para esa dirección, el mecanismo emergente de destino de referencia es la dirección de los estándares, pero aún no está disponible de forma general.
  • Cuando una relación no se puede expresar de ninguna de las dos maneras, mantenla dentro de un árbol (renderiza la etiqueta en el mismo árbol) o usa texto aria-label en lugar de una referencia id.

Estrategias de resguardo

Cuando el mixin de ARIA ElementInternals no esté disponible, establece el rol y el estado de ARIA como atributos en el host (this.setAttribute('role', 'switch')). Esto funciona en todas partes, pero tiene el costo de colocar atributos en el DOM que un consumidor podría anular.

Cuando las propiedades de element de ARIA reflejadas no estén disponibles, usa un atributo aria-* basado en id para las relaciones del mismo árbol. Una etiqueta que cruza límites no tiene un equivalente basado en id, por lo que se recurre al texto de aria-label.