19 C
New York

¡Los componentes web HTML facilitan la mejora progresiva y la encapsulación de CSS!

Publicado:

Tengo que agradecer a Jeremy Keith y su artículo maravillosamente revelador de finales del año pasado que me presentó el concepto de componentes web HTML. Este fue el "¡ajá!" momento para mi:

Cuando envuelves algún marcado existente en un elemento personalizado y luego aplicas algún comportamiento nuevo con JavaScript, técnicamente no estás haciendo nada que no hubieras podido hacer antes con un recorrido de DOM y manejo de eventos. Pero es menos frágil hacerlo con un componente web. Es portátil. Obedece al principio de responsabilidad única. Sólo hace una cosa pero lo hace bien.

Hasta entonces, había estado bajo la falsa suposición de que all Los componentes web dependen únicamente de la presencia de JavaScript junto con el sonido bastante aterrador. DOM de sombra. Si bien es posible crear componentes web de esta manera, existe otra forma más. ¿Quizás una mejor manera? Especialmente si usted, como yo, aboga por mejora progresiva. Los componentes web HTML son, después de todo, sólo HTML.

Si bien está fuera del alcance exacto de lo que estamos discutiendo aquí, Any Bell tiene un artículo reciente que ofrece su (excelente) visión de lo que significa la mejora progresiva.

Veamos tres ejemplos específicos que muestran lo que creo que son las características clave de los componentes web HTML (encapsulación de estilo CSS y oportunidades de mejora progresiva) sin verse obligados a depender de JavaScript para funcionar de forma inmediata. Definitivamente usaremos JavaScript, pero los componentes deberían funcionar sin él.

Todos los ejemplos se pueden encontrar en mi Biblioteca de componentes repetitivos de la interfaz de usuario web (construido usando Storybook), junto con el código fuente asociado en GitHub.

Ejemplo 1: <webui-disclosure>

Representación del libro de cuentos de webui-disclosure Web Component.q
Demo en vivo

realmente me gusta como cris ferdinandi enseña construyendo un componente web desde cero, utilizando un patrón de divulgación (mostrar/ocultar) como ejemplo. Este primer ejemplo amplía su demostración.

Comencemos con el ciudadano de primera clase, HTML. Los componentes web nos permiten establecer elementos personalizados con nuestro propio naming, como es el caso de este ejemplo con un <webui-disclosure> etiqueta que estamos usando para sostener un <button> diseñado para mostrar/ocultar un bloque de texto y un <div> que sostiene el <p> de texto que queremos mostrar y ocultar.

<webui-disclosure
  data-bind-escape-key
  data-bind-click-outside
>
  <button
    type="button"
    class="button button--text"
    data-trigger
    hidden
  >
    Show / Hide
  </button>

  <div data-content>
    <p>Content to be shown/hidden.</p>
  </div>
</webui-disclosure>

Si JavaScript está deshabilitado o no se ejecuta (por varios motivos posibles), el botón está oculto de forma predeterminada, gracias a hidden atributo en él, y el contenido dentro del div simplemente se muestra de forma predeterminada.

Lindo. Éste es un ejemplo realmente sencillo de mejora progresiva en funcionamiento. Un visitante puede ver el contenido con o sin el <button>.

Mencioné que este ejemplo amplía la demostración inicial de Chris Ferdinandi. La diferencia clave es que puedes cerrar el elemento haciendo clic en el botón del teclado. ESC o haciendo clic en cualquier lugar fuera del elemento. Eso es lo que los dos [data-attribute]s en el <webui-disclosure la etiqueta es para.

empezamos por definiendo el elemento personalizado para que el navegador sepa qué hacer con nuestro nombre de etiqueta inventado:

customElements.define('webui-disclosure', WebUIDisclosure);

Los elementos personalizados deben denominarse con una identificación discontinua, como <my-pizza> o lo que sea, pero como Notas de Jim Neilsen, por medio de Scott Jehl, eso no significa exactamente que el guión tiene ir entre dos palabras.

Normalmente prefiero usar TypeScript para escribir JavaScript para ayudar a eliminar errores estúpidos y aplicar cierto grado de programación "defensiva". Pero en aras de la simplicidad, la estructura del módulo ES del componente web se ve así en JavaScript simple:

default class WebUIDisclosure extends HTMLElement {
  constructor() {
    super();
    this.trigger = this.querySelector('[data-trigger]');
    this.content = this.querySelector('[data-content]');
    this.bindEscapeKey = this.hasAttribute('data-bind-escape-key');
    this.bindClickOutside = this.hasAttribute('data-bind-click-outside');
    
    if (!this.trigger || !this.content) return;
    
    this.setupA11y();
    this.trigger?.addEventListener('click', this);
  }

  setupA11y() {
    // Add ARIA props/state to button.
  }

  // Handle constructor() event listeners.
  handleEvent(e) {
    // 1. Toggle visibility of content.
    // 2. Toggle ARIA expanded state on button.
  }
  
  // Handle event listeners which are not part of this Web Component.
  connectedCallback() {
    document.addEventListener('keyup', (e) => {
      // Handle ESC key.
    });
  
    document.addEventListener('click', (e) => {
      // Handle clicking outside.
    });
  }

  disconnectedCallback() {
    // Remove event listeners.
  }
}

¿Se pregunta acerca de esos oyentes de eventos? El primero está definido en el constructor() funcionan, mientras que el resto están en connectedCallback() función. Hawk Ticehurst explica el fundamento mucho más elocuentemente de lo que puedo.

Este JavaScript no es necesario para que el componente web "funcione", pero sí incluye algunas funciones interesantes, sin mencionar las consideraciones de accesibilidad, para ayudar con la mejora progresiva que permite la <button> para mostrar y ocultar el contenido. Por ejemplo, JavaScript inyecta el apropiado aria-expanded y aria-controls atributos que permiten a quienes dependen de lectores de pantalla comprender el propósito del botón.

Esa es la pieza de mejora progresiva de este ejemplo.

Para simplificar, no he escrito ningún CSS adicional para este componente. El estilo que ve simplemente se hereda del alcance global existente o de los estilos de componentes (por ejemplo, tipografía y botón).

Sin embargo, el siguiente ejemplo tener algo de CSS de alcance adicional.

Ejemplo 2: <webui-tabs>

Ese primer ejemplo establece los beneficios de mejora progresiva de los componentes web HTML. Otro beneficio que obtenemos es que los estilos CSS son encapsulado, que es una forma elegante de decir que el CSS no se escapa del componente. Los estilos tienen como ámbito exclusivo el componente web y esos estilos no entrarán en conflicto con otros estilos aplicados a la página actual.

Pasemos a un segundo ejemplo, esta vez demostrando el estilo que encapsula los poderes de los componentes web. y cómo apoyan la mejora progresiva de las experiencias de los usuarios. Usaremos un componente con pestañas para organizar el contenido en "paneles" que se revelan cuando se hace clic en la pestaña correspondiente de un panel, el mismo tipo de cosas que encontrará en muchas bibliotecas de componentes.

Representación de libro de cuentos del componente web webui-tabs.
Demo en vivo

Comenzando con la estructura HTML:

<webui-tabs>
  <div data-tablist>
    <a href="#tab1" data-tab>Tab 1</a>
    <a href="#tab2" data-tab>Tab 2</a>
    <a href="#tab3" data-tab>Tab 3</a>
  </div>

  <div id="tab1" data-tabpanel>
    <p>1 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>

  <div id="tab2" data-tabpanel>
    <p>2 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>

  <div id="tab3" data-tabpanel>
    <p>3 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>
</webui-tabs>

Entiendes la idea: tres enlaces diseñados como pestañas que, al hacer clic, abren un panel de pestañas que contiene contenido. Tenga en cuenta que cada [data-tab] en la lista de pestañas apunta a un enlace de anclaje que coincide con un ID del panel de pestañas, por ejemplo, #tab1, #tab2, etc.

Primero veremos el tema de la encapsulación de estilos, ya que no abordamos ese tema en el último ejemplo. Digamos que el CSS está organizado así:

webui-tabs {
  [data-tablist] {
    /* Default styles without JavaScript */
  }
  
  [data-tab] {
    /* Default styles without JavaScript */
  }

  [role='tablist'] {
    /* Style role added by JavaScript */
  }
  
  [role='tab'] {
    /* Style role added by JavaScript */
  }
  
  [role='tabpanel'] {
    /* Style role added by JavaScript */
  }
}

¿Ves lo que está pasando aquí? Tenemos dos reglas de estilo: [data-tablist] y [data-tab] — que contienen los estilos predeterminados del componente web. En otras palabras, estos estilos existen independientemente de si JavaScript se carga o no. Mientras tanto, las otras tres reglas de estilo son selectores que se inyectan en el componente siempre que JavaScript esté habilitado y admitido. Por aquí, Las últimas tres reglas de estilo solo se aplican si JavaScript deja caer el **role** atributo en esos elementos en el HTML. Allí mismo, ya estamos brindando un toque de mejora progresiva al configurar estilos sólo cuando se necesita JavasScript.

Todos estos estilos están completamente encapsulados, o con alcance, al <webui-tabs> componente web. No hay ninguna “filtración”, por así decirlo, que pueda afectar los estilos de otros componentes web, o incluso cualquier otra cosa en la página dentro del alcance global. Incluso podemos optar por renunciar a nombres de clases, selectores complejos y metodologías como BEM a favor de selectores descendientes simples para los hijos del componente, lo que nos permite escribir estilos de forma más declarativa en elementos semánticos.

Rápidamente: DOM “Light” versus DOM Shadow

Para la mayoría de los proyectos web, generalmente prefiero incluir CSS (incluido el componente web parciales Sass) en un único archivo CSS para que los estilos predeterminados del componente estén disponibles en el ámbito global, incluso si JavaScript no se ejecuta.

Sin embargo, es posible importar una hoja de estilo a través de JavaScript que sea solo consumido por este componente web si JavaScript está disponible:

import styles from './styles.css';

class WebUITabs extends HTMLElement {
  constructor() {
    super();
    this.adoptedStyleSheets = [styles];
  }
}

customElements.define('webui-tabs', WebUITabs);

Alternativamente, podríamos inyectar un <style> etiqueta que contiene los estilos del componente en su lugar:

class WebUITabs extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' }); // Required for JavaScript access
    this.shadowRoot.innerHTML = `
      <style> <!-- styles go here --> </style>
      // etc.
    `;
  }
}
    
customElements.define('webui-tabs', WebUITabs);

Cualquiera que sea el método que elija, estos estilos tienen como ámbito directo el componente web, lo que evita que los estilos de los componentes se filtren, pero permite que se hereden los estilos globales.

Consideremos ahora este sencillo ejemplo. Todo lo que escribimos entre las etiquetas de apertura y cierre del componente se considera parte del DOM "Light".

<my-web-component>
  <!-- This is Light DOM -->
  <div>
    <p>Some content... styles are inherited from the global scope</p>
  </div>

  ----------- Shadow DOM Boundary -------------
  | <!-- Anything injected by JavaScript -->  |
  ---------------------------------------------
</my-web-component>

Dave Rupert tiene un excelente artículo. eso hace que sea realmente fácil ver cómo los estilos externos pueden "perforar" el DOM de sombra y seleccionar un elemento en el DOM de luz. Observe cómo el <button> El elemento que está escrito entre las etiquetas del elemento personalizado recibe el button estilos del selector en el CSS global, mientras que el <button> inyectado a través de JavaScript no se modifica.

Si queremos darle estilo al Shadow DOM <button> Tendríamos que hacer eso con estilos internos como los ejemplos anteriores para importar una hoja de estilo o inyectar un archivo en línea. <style> bloquear.

Eso no quiere decir que all Las propiedades de estilo CSS están bloqueadas por Shadow DOM. De hecho, Dave describe 37 propiedades que tienen los componentes web inherit, principalmente en la línea de formato de texto, lista y tabla.

Mejorar progresivamente el componente con pestañas con JavaScript

Aunque este segundo ejemplo trata más sobre la encapsulación de estilos, sigue siendo una buena oportunidad para ver la mejora progresiva que obtenemos prácticamente gratis de los componentes web. Entremos ahora en JavaScript para que podamos ver cómo podemos admitir la mejora progresiva. El código completo es bastante largo, así que lo he abreviado un poco para ayudar a aclarar un poco los puntos.

default class WebUITabs extends HTMLElement {
  constructor() {
    super();
    this.tablist = this.querySelector('[data-tablist]');
    this.tabpanels = this.querySelectorAll('[data-tabpanel]');
    this.tabTriggers = this.querySelectorAll('[data-tab]');

    if (
      !this.tablist ||
      this.tabpanels.length === 0 ||
      this.tabTriggers.length === 0
    ) return;
    
    this.createTabs();
    this.tabTriggers.forEach((tabTrigger, index) => {
      tabTrigger.addEventListener('click', (e) => {
        this.bindClickEvent(e);
      });
      tabTrigger.addEventListener('keydown', (e) => {
        this.bindKeyboardEvent(e, index);
      });
    });
  }

  createTabs() {
    // 1. Hide all tabpanels initially.
    // 2. Add ARIA props/state to tabs & tabpanels.
  }

  bindClickEvent(e) {
    e.preventDefault();
    // Show clicked tab and update ARIA props/state.
  }
  bindKeyboardEvent(e, index) {
    e.preventDefault();
    // Handle keyboard ARROW/HOME/END keys.
  }
}

customElements.define('webui-tabs', WebUITabs);

JavaScript inyecta roles, estados y accesorios ARIA en las pestañas y bloques de contenido para los usuarios de lectores de pantalla, así como enlaces de teclado adicionales para que podamos navegar entre pestañas con el teclado; por ejemplo, el TAB La clave está reservada para acceder a la pestaña activa del componente y a cualquier contenido enfocable dentro de la pestaña activa. tabpanel, y las pestañas se pueden recorrer con el ARROW llaves. Por lo tanto, si JavaScript no se carga, la experiencia predeterminada sigue siendo accesible, donde las pestañas aún enlazan con sus respectivos paneles, y esos paneles se apilan naturalmente verticalmente, uno encima del otro.

¿Y si JavaScript está habilitado y es compatible? Obtenemos una experiencia mejorada, completa con consideraciones de accesibilidad actualizadas.

Ejemplo 3: <webui-ajax-loader>

Representación del libro de cuentos del componente web webui-ajax-loader.
Demo en vivo

Este último ejemplo difiere de los dos anteriores en que es generado enteramente por JavaScripty utiliza el DOM de sombra. Esto se debe a que solo se usa para indicar un estado de "carga" para solicitudes Ajax y, por lo tanto, solo es necesario cuando JavaScript está habilitado.

El marcado HTML son solo las etiquetas de los componentes de apertura y cierre:

<webui-ajax-loader></webui-ajax-loader>

La estructura de JavaScript simplificada:

default class WebUIAjaxLoader extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <svg role="img" part="svg">
        <title>loading</title>
        <circle cx="50" cy="50" r="47" />
      </svg>
    `;
  }
}

customElements.define('webui-ajax-loader',WebUIAjaxLoader);

Observe desde el principio que todo entre el <webui-ajax-loader> tags se inyecta con JavaScript, lo que significa que todo está en Shadow DOM, encapsulado de otros scripts y estilos que no están incluidos directamente con el componente.

Pero también observe el part atributo que se establece en el <svg> elemento. Aquí es donde ampliaremos:

<svg role="img" part="svg">
  <!-- etc. -->
</svg>

Esa es otra forma más que tenemos de diseñar el elemento personalizado: partes nombradas. Ahora podemos diseñar ese SVG desde outside del literal de plantilla que utilizamos para establecer el elemento. Hay una ::part pseudo-selector para que eso suceda:

webui-ajax-loader::part(svg) {
  // Shadow DOM styles for the SVG...
}

Y aquí hay algo interesante: ese selector puede acceder a propiedades personalizadas de CSS, ya sea que tengan un alcance global o local para el elemento.

webui-ajax-loader {
  --fill: orangered;
}

webui-ajax-loader::part(svg) {
  fill: var(--fill);
}

En lo que respecta a la mejora progresiva, JavaScript proporciona todo el HTML. Eso significa que el cargador solo se procesa si JavaScript está habilitado y es compatible. Y cuando es así, se agrega el SVG, completo con un título accesible y todo.

Terminando

¡Eso es todo los ejemplos! Lo que espero es que ahora tengas el mismo tipo de epifanía que tuve cuando leí la publicación de Jeremy Keith: Los componentes web HTML son una característica que da prioridad a HTML.

Por supuesto, JavaScript juega un papel importante, pero sólo tanto como sea necesario. ¿Necesita más encapsulación? ¿Quiere agregar algo de bondad de UX cuando el navegador de un visitante lo admita? Para eso está JavaScript y eso es lo que hace que los componentes web HTML sean una gran adición a la plataforma web: dependen de lenguajes web básicos para hacer aquello para lo que fueron diseñados desde el principio, y sin depender demasiado de uno u otro.

Artículos relacionados

punto_img

Artículos Recientes

punto_img