fix(a11y): roles ARIA que contradecían la semántica del elemento

Una auditoría marcó `<ul role="img">` en el riel de clases de fuego. El rol
contradice la semántica implícita de lista, así que la combinación es inválida
y el árbol de accesibilidad queda mal formado.

La intención original era correcta: el riel debe anunciarse como una sola
unidad con su resumen, no como "lista de cinco elementos: A, B, C, D, K", que
pierde justo lo que importa, cuáles están encendidas. Lo que estaba mal era el
elemento, no el rol. El riel pasa a un contenedor genérico y conserva
role="img" con su etiqueta.

Al revisar apareció el mismo error en dos gráficas del panel, que además
duplicaban el anuncio: leían el resumen completo y después la tabla accesible
entera. Ahí la representación visual se oculta al árbol y la tabla equivalente
queda como única fuente.

También se corrigió el botón del selector de clases, que se anunciaba "A Clase
A" por tener la letra visible junto a un texto oculto con el nombre completo.

Se agrega una comprobación de regresión que valida el rol contra la semántica
de ul, ol, li, table, nav, main, header y footer, para que no vuelva a
colarse. Suite completa en verde.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Uriel Jareth
2026-08-28 13:20:08 -06:00
co-authored by Claude Opus 5
parent 15d31b5d6b
commit ab9f3ef6d3
4 changed files with 59 additions and 16 deletions
+27
View File
@@ -78,6 +78,33 @@ for (const ruta of RUTAS_MUESTRA) {
}
}
// --- roles que contradicen la semántica del elemento ---
//
// Una auditoría marcó `<ul role="img">` en el riel de clases de fuego:
// el rol contradice la semántica implícita de lista. El mismo error
// estaba en dos gráficas del panel. Los elementos de abajo tienen
// semántica fuerte y no deben recibir un rol arbitrario; lo único
// razonable es quitarles la semántica con presentation/none, o repetir
// la que ya tienen.
const PERMITIDOS: Record<string, string[]> = {
ul: ["list", "presentation", "none", "menu", "menubar", "listbox", "group", "tablist"],
ol: ["list", "presentation", "none", "menu", "menubar", "listbox", "group", "tablist"],
li: ["listitem", "presentation", "none", "menuitem", "option", "tab", "treeitem"],
table: ["table", "grid", "treegrid", "presentation", "none"],
nav: ["navigation", "presentation", "none"],
main: ["main", "presentation", "none"],
header: ["banner", "group", "presentation", "none"],
footer: ["contentinfo", "group", "presentation", "none"],
};
for (const [etiqueta, roles] of Object.entries(PERMITIDOS)) {
for (const el of Array.from(document.querySelectorAll(etiqueta))) {
const rol = el.getAttribute("role");
if (rol && !roles.includes(rol)) {
fallas.push(`rol inapropiado para el elemento: <${etiqueta} role="${rol}">`);
}
}
}
// --- listas mal formadas ---
for (const ul of Array.from(document.querySelectorAll("ul, ol"))) {
for (const hijo of Array.from(ul.children)) {