La anatomía de un componente Tablist en Vanilla JavaScript versus React

Índice
  1. Ejemplos de HTML plano
  2. Ejemplos de JavaScript vainilla

Si sigues el trasfondo de la comunidad de JavaScript, últimamente parece haber una división. Se remonta a más de una década. En realidad, este tipo de conflictos siempre ha existido. Quizás sea la naturaleza humana.

Cada vez que un marco popular gana terreno, inevitablemente verás que la gente lo compara con sus rivales. Supongo que eso es de esperarse. Todo el mundo tiene un favorito en particular.

Últimamente, el marco que todo el mundo ama (¿odia?) es React. A menudo se lo ve comparado con otros en publicaciones de blogs directos y en matrices de comparación de características de documentos técnicos comerciales. Sin embargo, hace unos años, parecía que jQuery sería para siempre el rey de la colina.

Los marcos van y vienen. Para mí, lo que es más interesante es cuando React, o cualquier marco JS, se enfrenta al propio lenguaje de programación. Porque, por supuesto, bajo el capó, todo está construido sobre JS.

Los dos no están inherentemente en desacuerdo. Incluso me atrevería a decir que si no dominas bien los fundamentos de JS, probablemente no obtendrás todos los beneficios de usar React. Aún así puede resultar útil, similar a utilizar un complemento jQuery sin comprender sus componentes internos. Pero siento que React presupone una mayor familiaridad con JS.

HTML es igualmente importante. Existe bastante confusión sobre cómo React afecta la accesibilidad. Creo que esta narrativa es inexacta. De hecho, el complemento ESLint JSX a11y anunciará sobre posibles violaciones de accesibilidad en el archivo console.

Recientemente, se publicó un estudio anual del millón de sitios principales. Muestra que para los sitios que utilizan marcos JS, existe una mayor probabilidad de problemas de accesibilidad. Esto es evaluación, no causalidad.

Esto no significa necesariamente que los marcos causen estos errores, pero sí indica que las páginas de inicio con estos marcos tenían más errores que el promedio.

Por así decirlo, los encantamientos mágicos de React funcionan independientemente de si reconoces las palabras. En última instancia, usted sigue siendo responsable del resultado.

Dejando a un lado las reflexiones filosóficas, creo firmemente en elegir la mejor herramienta para el trabajo. A veces, eso significa crear una aplicación de una sola página con un enfoque Jamstack. O tal vez un proyecto en particular sea más adecuado para descargar la representación HTML al servidor, donde históricamente se ha manejado.

De cualquier manera, inevitablemente surge la necesidad de que JS mejore la experiencia del usuario. En Reaktiv Studios, con ese fin intentó mantener la mayoría de nuestros componentes de React sincronizados con nuestro enfoque de “HTML plano”. También he estado escribiendo funciones de uso común en Vanilla JS. Esto mantiene abiertas nuestras opciones, para que nuestros clientes sean libres de elegir. También nos permite reutilizar el mismo CSS.

Si se me permite, me gustaría compartir cómo construí nuestros componentes Tabsy AccordionReact. También demostraré cómo escribí la misma funcionalidad sin usar un marco.

Con suerte, esta lección se sentirá como si estuviéramos haciendo un pastel en capas. Primero comencemos con el marcado base, luego cubramos el JS vainilla y terminemos con cómo funciona en React.

Tabla de contenido

  1. Ejemplos de HTML plano
  2. Ejemplos de JavaScript vainilla
  3. Ejemplos de reacción
  4. Conclusión

Como referencia, puedes jugar con nuestros ejemplos en vivo:

  • Demostración en vivo de acordeón
  • Demostración en vivo de pestañas

Ejemplos de HTML plano

Dado que necesitamos JavaScript para crear widgets interactivos de cualquier manera, pensé que el enfoque más sencillo (desde el punto de vista de la implementación del lado del servidor) requeriría solo el mínimo de HTML. El resto se puede aumentar con JS.

Los siguientes son ejemplos de marcado para pestañas y componentes de acordeón , que muestran una comparación antes/después de cómo JS afecta el DOM.

He añadido id="TABS_ID"y id="ACCORDION_ID"con finos demostrativos. Esto es para que sea más obvio lo que está sucediendo. Pero el JS que explicaré genera automáticamente ID únicos si no se proporciona nada en el HTML. Funcionaría bien de cualquier manera, con o sin un idespecificado.

Pestañas (sin ARIA)

div  ul    li      Tab 1    /li    !-- .tabs__item --    li      Tab 2    /li    !-- .tabs__item --  /ul  !-- .tabs__list --  div    p      Tab 1 content    /p  /div  !-- .tabs__panel --  div    p      Tab 2 content    /p  /div  !-- .tabs__panel --/div!-- .tabs --

Pestañas (con ARIA)

div  ul role="tablist"    li      aria-controls="tabpanel_TABS_ID_0"      aria-selected="false"                role="tab"      tabindex="0"          Tab 1    /li    !-- .tabs__item --    li      aria-controls="tabpanel_TABS_ID_1"      aria-selected="true"                role="tab"      tabindex="0"          Tab 2    /li    !-- .tabs__item --  /ul  !-- .tabs__list --  div    aria-hidden="true"    aria-labelledby="tab_TABS_ID_0"          role="tabpanel"      p      Tab 1 content    /p  /div  !-- .tabs__panel --  div    aria-hidden="false"    aria-labelledby="tab_TABS_ID_1"          role="tabpanel"      p      Tab 2 content    /p  /div  !-- .tabs__panel --/div!-- .tabs --

Acordeón (sin ARIA)

div  div    Tab 1  /div  !-- .accordion__item --  div    p      Tab 1 content    /p  /div  !-- .accordion__panel --  div    Tab 2  /div  !-- .accordion__item --  div    p      Tab 2 content    /p  /div  !-- .accordion__panel --/div!-- .accordion --

Acordeón (con ARIA)

div  aria-multiselectable="true"    role="tablist"  div    aria-controls="tabpanel_ACCORDION_ID_0"    aria-selected="true"          role="tab"    tabindex="0"      i aria-hidden="true"/i    Tab 1  /div  !-- .accordion__item --  div    aria-hidden="false"    aria-labelledby="tab_ACCORDION_ID_0"          role="tabpanel"      p      Tab 1 content    /p  /div  !-- .accordion__panel --  div    aria-controls="tabpanel_ACCORDION_ID_1"    aria-selected="false"          role="tab"    tabindex="0"      i aria-hidden="true"/i    Tab 2  /div  !-- .accordion__item --  div    aria-hidden="true"    aria-labelledby="tab_ACCORDION_ID_1"          role="tabpanel"      p      Tab 2 content    /p  /div  !-- .accordion__panel --/div!-- .accordion --

Ejemplos de JavaScript vainilla

Bueno. Ahora que hemos visto los ejemplos HTML antes mencionados, veamos cómo llegamos del antes al después.

Primero, quiero cubrir algunas funciones auxiliares. Estos tendrán más sentido en un momento. Creo que es mejor documentarlos primero, para que podamos concentrarnos en el resto del código una vez que profundicemos más.

Archivo:getDomFallback.js

Esta función proporciona propiedades y métodos DOM comunes como no-op, en lugar de tener que realizar muchas typeof foo.getAttributecomprobaciones y todo eso. Podríamos renunciar por completo a ese tipo de confirmaciones.

Dado que los cambios HTML en vivo pueden ser un entorno potencialmente volátil, siempre me siento un poco más seguro al asegurarme de que mi JS no falla y se lleva el resto de la página con él. Así es como se ve esa función. Simplemente devuelve un objeto con los equivalentes DOM de resultados falsos.

/*  Helper to mock DOM methods, for  when an element might not exist.*/const getDomFallback = () = {  return {    // Props.    children: [],    className: '',    classList: {      contains: () = false,    },    id: '',    innerHTML: '',    name: '',    nextSibling: null,    previousSibling: null,    outerHTML: '',    tagName: '',    textContent: '',    // Methods.    appendChild: () = Object.create(null),    blur: () = undefined,    click: () = undefined,    cloneNode: () = Object.create(null),    closest: () = null,    createElement: () = Object.create(null),    focus: () = undefined,    getAttribute: () = null,    hasAttribute: () = false,    insertAdjacentElement: () = Object.create(null),    insertBefore: () = Object.create(null),    querySelector: () = null,    querySelectorAll: () = [],    removeAttribute: () = undefined,    removeChild: () = Object.create(null),    replaceChild: () = Object.create(null),    setAttribute: () = undefined,  };};// Export.export { getDomFallback };

Archivo:unique.js

Esta función es el equivalente al UUID de un pobre.

Genera una cadena única que se puede utilizar para asociar elementos DOM entre sí. Es útil, porque entonces el autor de una página HTML no tiene que asegurarse de que cada pestaña y componente de acordeón tenga una identificación única. En los ejemplos HTML anteriores, aquí es donde TABS_IDy ACCORDION_IDnormalmente contendría las cadenas numéricas generadas aleatoriamente.

// ==========// Constants.// ==========const BEFORE = '0.';const AFTER = '';// ==================// Get unique string.// ==================const unique = () = {  // Get prefix.  let prefix = Math.random();  prefix = String(prefix);  prefix = prefix.replace(BEFORE, AFTER);  // Get suffix.  let suffix = Math.random();  suffix = String(suffix);  suffix = suffix.replace(BEFORE, AFTER);  // Expose string.  return `${prefix}_${suffix}`;};// Export.export { unique };

En proyectos JavaScript más grandes, normalmente usaría npm install uuid. Pero como mantenemos esto simple y no requerimos paridad criptográfica, concatenar dos Math.random()números ligeramente editados será suficiente para nuestras stringnecesidades de unicidad.

Archivo:tablist.js

Este archivo hace la mayor parte del trabajo. Lo bueno de esto, si lo digo yo mismo, es que hay suficientes similitudes entre un componente de pestañas y un acordeón que podemos manejar ambos con el mismo *.jsarchivo. Continúe y desplácese por todo, y luego desglosaremos lo que hace cada función individualmente.

// Helpers.import { getDomFallback } from './getDomFallback';import { unique } from './unique';// ==========// Constants.// ==========// Boolean strings.const TRUE = 'true';const FALSE = 'false';// ARIA strings.const ARIA_CONTROLS = 'aria-controls';const ARIA_LABELLEDBY = 'aria-labelledby';const ARIA_HIDDEN = 'aria-hidden';const ARIA_MULTISELECTABLE = 'aria-multiselectable';const ARIA_ORIENTATION = 'aria-orientation';const ARIA_SELECTED = 'aria-selected';// Attribute strings.const DATA_INDEX = 'data-index';const HORIZONTAL = 'horizontal';const ID = 'id';const ROLE = 'role';const TABINDEX = 'tabindex';const TABLIST = 'tablist';const VERTICAL = 'vertical';// Event strings.const AFTER_BEGIN = 'afterbegin';const ARROW_LEFT = 'arrowleft';const ARROW_RIGHT = 'arrowright';const CLICK = 'click';const KEYDOWN = 'keydown';// Key strings.const ENTER = 'enter';const FUNCTION = 'function';const SPACE = ' ';// Tag strings.const I = 'i';const LI = 'li';// Selector strings.const ACCORDION_ITEM_ICON = 'accordion__item__icon';const ACCORDION_ITEM_ICON_SELECTOR = `.${ACCORDION_ITEM_ICON}`;const TAB = 'tab';const TAB_SELECTOR = `[${ROLE}=${TAB}]`;const TABPANEL = 'tabpanel';const TABPANEL_SELECTOR = `[${ROLE}=${TABPANEL}]`;const ACCORDION = 'accordion';const TABLIST_CLASS_SELECTOR = '.accordion, .tabs';const TAB_CLASS_SELECTOR = '.accordion__item, .tabs__item';const TABPANEL_CLASS_SELECTOR = '.accordion__panel, .tabs__panel';// ===========// Get tab ID.// ===========const getTabId = (id = '', index = 0) = {  return `${TAB}_${id}_${index}`;};// =============// Get panel ID.// =============const getPanelId = (id = '', index = 0) = {  return `${TABPANEL}_${id}_${index}`;};// ==============// Click handler.// ==============const globalClick = (event = {}) = {  // Get target.  const { target = getDomFallback() } = event;  // Get key.  let { key = '' } = event;  key = key.toLowerCase();  // Key events.  const isArrowLeft = key === ARROW_LEFT;  const isArrowRight = key === ARROW_RIGHT;  const isArrowKey = isArrowLeft || isArrowRight;  const isTriggerKey = key === ENTER || key === SPACE;  // Get parent.  const { parentNode = getDomFallback(), tagName = '' } = target;  // Set later.  let wrapper = getDomFallback();  /*    =====    NOTE:    =====    We test for this, because the method does    not exist on `document.documentElement`.  */  if (typeof target.closest === FUNCTION) {    // Get wrapper.    wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback();  }  // Is multi?  const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE;  // Valid target?  const isValidTarget =    target.getAttribute(ROLE) === TAB  parentNode.getAttribute(ROLE) === TABLIST;  // Is `li`?  const isListItem = isValidTarget  tagName.toLowerCase() === LI;  // Valid event?  const isArrowEvent = isListItem  isArrowKey;  const isTriggerEvent = isValidTarget  (!key || isTriggerKey);  const isValidEvent = isArrowEvent || isTriggerEvent;  // Prevent default.  if (isValidEvent) {    event.preventDefault();  }  // ============  // Arrow event?  // ============  if (isArrowEvent) {    // Get index.    let index = target.getAttribute(DATA_INDEX);    index = parseFloat(index);    // Get list.    const list = wrapper.querySelectorAll(TAB_SELECTOR);    // Set later.    let newIndex = null;    let nextItem = null;    // Arrow left?    if (isArrowLeft) {      newIndex = index - 1;      nextItem = list[newIndex];      if (!nextItem) {        newIndex = list.length - 1;        nextItem = list[newIndex];      }    }    // Arrow right?    if (isArrowRight) {      newIndex = index + 1;      nextItem = list[newIndex];      if (!nextItem) {        newIndex = 0;        nextItem = list[newIndex];      }    }    // Fallback?    nextItem = nextItem || getDomFallback();    // Focus new item.    nextItem.click();    nextItem.focus();  }  // ==============  // Trigger event?  // ==============  if (isTriggerEvent) {    // Get panel.    const panelId = target.getAttribute(ARIA_CONTROLS);    const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback();    // Get booleans.    let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE;    let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE;    // List item?    if (isListItem) {      boolPanel = FALSE;      boolTab = TRUE;    }    // [aria-multiselectable="false"]    if (!isMulti) {      // Get tabs  panels.      const childTabs = wrapper.querySelectorAll(TAB_SELECTOR);      const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR);      // Loop through tabs.      childTabs.forEach((tab = getDomFallback()) = {        tab.setAttribute(ARIA_SELECTED, FALSE);        // li[tabindex="-1"]        if (isListItem) {          tab.setAttribute(TABINDEX, -1);        }      });      // Loop through panels.      childPanels.forEach((panel = getDomFallback()) = {        panel.setAttribute(ARIA_HIDDEN, TRUE);      });    }    // Set individual tab.    target.setAttribute(ARIA_SELECTED, boolTab);    // li[tabindex="0"]    if (isListItem) {      target.setAttribute(TABINDEX, 0);    }    // Set individual panel.    panel.setAttribute(ARIA_HIDDEN, boolPanel);  }};// ====================// Add ARIA attributes.// ====================const addAriaAttributes = () = {  // Get elements.  const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR);  // Loop through.  allWrappers.forEach((wrapper = getDomFallback()) = {    // Get attributes.    const { id = '', classList } = wrapper;    const parentId = id || unique();    // Is accordion?    const isAccordion = classList.contains(ACCORDION);    // Get tabs  panels.    const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR);    const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR);    // Add ID?    if (!wrapper.getAttribute(ID)) {      wrapper.setAttribute(ID, parentId);    }    // [aria-multiselectable="true"]    if (isAccordion  wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) {      wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE);    }    // ===========================    // Loop through tabs  panels.    // ===========================    for (let index = 0; index  childTabs.length; index++) {      // Get elements.      const tab = childTabs[index] || getDomFallback();      const panel = childPanels[index] || getDomFallback();      // Get IDs.      const tabId = getTabId(parentId, index);      const panelId = getPanelId(parentId, index);      // ===================      // Add tab attributes.      // ===================      // Tab: add icon?      if (isAccordion) {        // Get icon.        let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR);        // Create icon?        if (!icon) {          icon = document.createElement(I);          icon.className = ACCORDION_ITEM_ICON;          tab.insertAdjacentElement(AFTER_BEGIN, icon);        }        // [aria-hidden="true"]        icon.setAttribute(ARIA_HIDDEN, TRUE);      }      // Tab: add id?      if (!tab.getAttribute(ID)) {        tab.setAttribute(ID, tabId);      }      // Tab: add controls?      if (!tab.getAttribute(ARIA_CONTROLS)) {        tab.setAttribute(ARIA_CONTROLS, panelId);      }      // Tab: add selected?      if (!tab.getAttribute(ARIA_SELECTED)) {        const bool = !isAccordion  index === 0;        tab.setAttribute(ARIA_SELECTED, bool);      }      // Tab: add role?      if (tab.getAttribute(ROLE) !== TAB) {        tab.setAttribute(ROLE, TAB);      }      // Tab: add data index?      if (!tab.getAttribute(DATA_INDEX)) {        tab.setAttribute(DATA_INDEX, index);      }      // Tab: add tabindex?      if (!tab.getAttribute(TABINDEX)) {        if (isAccordion) {          tab.setAttribute(TABINDEX, 0);        } else {          tab.setAttribute(TABINDEX, index === 0 ? 0 : -1);        }      }      // Tab: first item?      if (index === 0) {        // Get parent.        const { parentNode = getDomFallback() } = tab;        /*          We do this here, instead of outside the loop.          The top level item isn't always the `tablist`.          The accordion UI only has `div`, whereas          the tabs UI has both `div` and `ul`.        */        if (parentNode.getAttribute(ROLE) !== TABLIST) {          parentNode.setAttribute(ROLE, TABLIST);        }        // Accordion?        if (isAccordion) {          // [aria-orientation="vertical"]          if (parentNode.getAttribute(ARIA_ORIENTATION) !== VERTICAL) {            parentNode.setAttribute(ARIA_ORIENTATION, VERTICAL);          }          // Tabs?        } else {          // [aria-orientation="horizontal"]          if (parentNode.getAttribute(ARIA_ORIENTATION) !== HORIZONTAL) {            parentNode.setAttribute(ARIA_ORIENTATION, HORIZONTAL);          }        }      }      // =====================      // Add panel attributes.      // =====================      // Panel: add ID?      if (!panel.getAttribute(ID)) {        panel.setAttribute(ID, panelId);      }      // Panel: add hidden?      if (!panel.getAttribute(ARIA_HIDDEN)) {        const bool = isAccordion || index !== 0;        panel.setAttribute(ARIA_HIDDEN, bool);      }      // Panel: add labelled?      if (!panel.getAttribute(ARIA_LABELLEDBY)) {        panel.setAttribute(ARIA_LABELLEDBY, tabId);      }      // Panel: add role?      if (panel.getAttribute(ROLE) !== TABPANEL) {        panel.setAttribute(ROLE, TABPANEL);      }      // Panel: add tabindex?      if (!panel.getAttribute(TABINDEX)) {        panel.setAttribute(TABINDEX, 0);      }    }  });};// =====================// Remove global events.// =====================const unbind = () = {  document.removeEventListener(CLICK, globalClick);  document.removeEventListener(KEYDOWN, globalClick);};// ==================// Add global events.// ==================const init = () = {  // Add attributes.  addAriaAttributes();  // Prevent doubles.  unbind();  document.addEventListener(CLICK, globalClick);  document.addEventListener(KEYDOWN, globalClick);};// ==============// Bundle object.// ==============const tablist = {  init,  unbind,};// =======// Export.// =======export { tablist };

Función: getTabIdygetPanelId

Estas dos funciones se utilizan para crear identificaciones únicas individualmente para elementos en un bucle, basadas en una identificación principal existente (o generada). Esto es útil para garantizar valores coincidentes para atributos como aria-controls="…"y aria-labelledby="…". Piense en ellos como equivalentes de accesibilidad de label for="…", que le indican al navegador qué elementos están relacionados entre sí.

const getTabId = (id = '', index = 0) = {  return `${TAB}_${id}_${index}`;};
const getPanelId = (id = '', index = 0) = {  return `${TABPANEL}_${id}_${index}`;};

Función:globalClick

Este es un controlador de clics que se aplica a documentnivel. Eso significa que no tenemos que agregar manualmente controladores de clics a varios elementos. En su lugar, utilizamos la difusión de eventos para escuchar los clics más abajo en el documento y permitir que se propaguen hasta la parte superior.

Convenientemente, así es también como podemos manejar eventos del teclado como la pulsación de las teclas ArrowLeft, ArrowRight, (o la barra espaciadora). EnterEstos son necesarios para tener una interfaz de usuario accesible.

En la primera parte de la función, desestructuramos targety keydel entrante event. A continuación, desestructuramos el parentNodey tagNamedel target.

Luego, intentamos obtener el elemento contenedor. Este sería el que tiene class="tabs"o class="accordion". Debido a que es posible que en realidad estemos haciendo clic en el elemento ancestro que se encuentra en la parte superior del árbol DOM (que existe pero posiblemente no tenga el *.closest(…)método), hacemos una typeofverificación. Si esa función existe, intentamos obtener el elemento. Aun así, es posible que nos quedemos sin rival. Así que tenemos uno más getDomFallbackpara estar seguros.

// Get target.const { target = getDomFallback() } = event;// Get key.let { key = '' } = event;key = key.toLowerCase();// Key events.const isArrowLeft = key === ARROW_LEFT;const isArrowRight = key === ARROW_RIGHT;const isArrowKey = isArrowLeft || isArrowRight;const isTriggerKey = key === ENTER || key === SPACE;// Get parent.const { parentNode = getDomFallback(), tagName = '' } = target;// Set later.let wrapper = getDomFallback();/*  =====  NOTE:  =====  We test for this, because the method does  not exist on `document.documentElement`.*/if (typeof target.closest === FUNCTION) {  // Get wrapper.  wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback();}

Luego, almacenamos un valor booleano sobre si el elemento contenedor tiene aria-multiselectable="true". Volveré a eso. Del mismo modo, almacenamos si la etiqueta en la que se hizo clic es o no un archivo li. Necesitamos esta información más adelante.

También determinamos si el clic se produjo en un contenido relevante target. Recuerde, estamos utilizando la difusión de eventos, por lo que realmente el usuario podría haber hecho clic en cualquier cosa. También interrogamos un poco el evento para determinar si fue activado cuando el usuario presionó una tecla. Si es así, determinamos si la clave es relevante.

Queremos asegurarnos de que:

  • Tienerole="tab"
  • Tiene un elemento padre conrole="tablist"

Luego agrupamos nuestros otros valores booleanos en dos categorías isArrowEventy isTriggerEvent. Que a su vez se combinan aún más en isValidEvent.

// Is multi?const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE;// Valid target?const isValidTarget =  target.getAttribute(ROLE) === TAB  parentNode.getAttribute(ROLE) === TABLIST;// Is `li`?const isListItem = isValidTarget  tagName.toLowerCase() === LI;// Valid event?const isArrowEvent = isListItem  isArrowKey;const isTriggerEvent = isValidTarget  (!key || isTriggerKey);const isValidEvent = isArrowEvent || isTriggerEvent;// Prevent default.if (isValidEvent) {  event.preventDefault();}

Luego ingresamos un ifcondicional que verifica si se presionaron las teclas de flecha izquierda o derecha. Si es así, entonces queremos cambiar el foco a la pestaña adyacente correspondiente. Si ya estamos al principio de nuestra lista, saltaremos al final. O si ya llegamos al final, saltaremos al principio.

Al desencadenar el clickevento, eso hace que esta misma función se ejecute nuevamente. Luego se evalúa como un evento desencadenante. Esto se trata en el siguiente bloque.

if (isArrowEvent) {  // Get index.  let index = target.getAttribute(DATA_INDEX);  index = parseFloat(index);  // Get list.  const list = wrapper.querySelectorAll(TAB_SELECTOR);  // Set later.  let newIndex = null;  let nextItem = null;  // Arrow left?  if (isArrowLeft) {    newIndex = index - 1;    nextItem = list[newIndex];    if (!nextItem) {      newIndex = list.length - 1;      nextItem = list[newIndex];    }  }  // Arrow right?  if (isArrowRight) {    newIndex = index + 1;    nextItem = list[newIndex];    if (!nextItem) {      newIndex = 0;      nextItem = list[newIndex];    }  }  // Fallback?  nextItem = nextItem || getDomFallback();  // Focus new item.  nextItem.click();  nextItem.focus();}

Suponiendo que el disparador eventsea realmente válido, pasamos nuestra siguiente ifverificación. Ahora, nos preocupa obtener el role="tabpanel"elemento con un idque coincida con el de nuestra pestaña aria-controls="…".

Una vez lo tenemos, comprobamos si el panel está oculto y si la pestaña está seleccionada. Básicamente, primero presuponemos que estamos tratando con un acordeón y volteamos los valores booleanos a sus opuestos.

Aquí es también donde isListItementra en juego nuestro valor booleano anterior. Si el usuario hace clic en un, lientonces sabemos que estamos tratando con pestañas , no con un acordeón . En cuyo caso, queremos marcar nuestro panel como visible (a través de aria-hiddden="false") y nuestra pestaña como seleccionada (a través de aria-selected="true").

Además, queremos asegurarnos de que el contenedor tenga aria-multiselectable="false"o falte por completo aria-multiselectable. Si ese es el caso, recorremos todos los elementos vecinos role="tab"y todos role="tabpanel"los elementos y los configuramos en sus estados inactivos. Finalmente, llegamos a configurar los valores booleanos previamente determinados para el emparejamiento de pestañas individuales y paneles.

if (isTriggerEvent) {  // Get panel.  const panelId = target.getAttribute(ARIA_CONTROLS);  const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback();  // Get booleans.  let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE;  let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE;  // List item?  if (isListItem) {    boolPanel = FALSE;    boolTab = TRUE;  }  // [aria-multiselectable="false"]  if (!isMulti) {    // Get tabs  panels.    const childTabs = wrapper.querySelectorAll(TAB_SELECTOR);    const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR);    // Loop through tabs.    childTabs.forEach((tab = getDomFallback()) = {      tab.setAttribute(ARIA_SELECTED, FALSE);      // li[tabindex="-1"]      if (isListItem) {        tab.setAttribute(TABINDEX, -1);      }    });    // Loop through panels.    childPanels.forEach((panel = getDomFallback()) = {      panel.setAttribute(ARIA_HIDDEN, TRUE);    });  }  // Set individual tab.  target.setAttribute(ARIA_SELECTED, boolTab);  // li[tabindex="0"]  if (isListItem) {    target.setAttribute(TABINDEX, 0);  }  // Set individual panel.  panel.setAttribute(ARIA_HIDDEN, boolPanel);}

Función:addAriaAttributes

El lector astuto podría estar pensando:

Antes dijiste que comenzamos con el marcado más básico posible, pero la globalClickfunción buscaba atributos que no estarían allí. ¿¡Por qué mentirías!?

O tal vez no, porque el lector astuto también habría notado la función denominada addAriaAttributes. De hecho, esta función hace exactamente lo que dice. Da vida a la estructura DOM base, agregando todos los requisitos aria-*y roleatributos.

Esto no sólo hace que la interfaz de usuario sea inherentemente más accesible a las tecnologías de asistencia, sino que también garantiza que la funcionalidad realmente funcione. Prefiero construir cosas Vanilla JS de esta manera, en lugar de girar hacia class="…"la interactividad, porque me obliga a pensar en la totalidad de la experiencia del usuario, más allá de lo que puedo ver visualmente.

En primer lugar, obtenemos todos los elementos de la página que tienen class="tabs"y/o class="accordion". Luego comprobamos si tenemos algo con qué trabajar. De lo contrario, saldríamos de nuestra función aquí. Suponiendo que tenemos una lista, recorremos cada uno de los elementos envolventes y los pasamos al alcance de nuestra función como wrapper.

// Get elements.const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR);// Loop through.allWrappers.forEach((wrapper = getDomFallback()) = {  /*    NOTE: Cut, for brevity.  */});

Dentro del alcance de nuestra función de bucle, desestructuramos idy classListdesde wrapper. Si no hay ningún ID, generamos uno mediante unique(). Colocamos una bandera booleana, para identificar si estamos trabajando con acordeón . Esto se usa más tarde.

También obtenemos descendientes de wrapperpestañas y paneles, a través de sus selectores de nombres de clases.

Pestañas:

  • class="tabs__item"o
  • class="accordion__item"

Paneles:

  • class="tabs__panel"o
  • class="accordion__panel"

Luego configuramos el contenedor idsi aún no tiene uno.

Si estamos ante un acordeón al que le falta aria-multiselectable="false", ponemos su bandera en true. La razón es que, si los desarrolladores están buscando un paradigma de interfaz de usuario de acordeón (y también tienen pestañas disponibles, que son inherentemente mutuamente excluyentes), entonces la suposición más segura es que el acordeón debería admitir la expansión y el colapso de varios paneles.

// Get attributes.const { id = '', classList } = wrapper;const parentId = id || unique();// Is accordion?const isAccordion = classList.contains(ACCORDION);// Get tabs  panels.const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR);const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR);// Add ID?if (!wrapper.getAttribute(ID)) {  wrapper.setAttribute(ID, parentId);}// [aria-multiselectable="true"]if (isAccordion  wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) {  wrapper.setAttribute(A

SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion