`${f.etiqueta}: ${entero(f.valor)}`)
- .join(". ")}.`}
- className="grid gap-2.5"
- >
+ {/*
+ Se oculta al árbol de accesibilidad: la tabla equivalente de abajo lleva
+ los datos. `role="img"` sobre un `
` es inválido y, además, duplicaba
+ el anuncio con la tabla.
+ */}
+
{filas.map((f) => {
const anchoTotal = Math.max(f.valor > 0 ? 1.5 : 0, (f.valor / max) * 100);
const dest = f.destacado ?? 0;
diff --git a/src/components/admin/graficas/Embudo.tsx b/src/components/admin/graficas/Embudo.tsx
index 063bcce..4083d8f 100644
--- a/src/components/admin/graficas/Embudo.tsx
+++ b/src/components/admin/graficas/Embudo.tsx
@@ -32,11 +32,16 @@ export function Embudo({ embudo }: { embudo: TramoEmbudo[] }) {
return (
+ {/*
+ La representación visual se oculta al árbol de accesibilidad y la tabla
+ equivalente de más abajo es la que lleva los datos.
+
+ Antes esta lista declaraba `role="img"`, que es inválido sobre un ``
+ —el rol contradice la semántica de lista— y además duplicaba el
+ anuncio: se leía el resumen completo y después la tabla entera.
+ */}
`${ETAPA_ETIQUETA[t.etapa]}: ${entero(t.cantidad)}`)
- .join(". ")}.`}
+ aria-hidden="true"
className="grid gap-[2px]"
style={{ backgroundColor: COLOR_SUPERFICIE }}
>
diff --git a/src/components/marca/RielClases.tsx b/src/components/marca/RielClases.tsx
index b34f026..f656b20 100644
--- a/src/components/marca/RielClases.tsx
+++ b/src/components/marca/RielClases.tsx
@@ -53,7 +53,17 @@ export function RielClases({
) : null}
-
` es una combinación inválida: el rol
+ contradice la semántica implícita de lista y las herramientas de
+ auditoría lo marcan. La intención sí 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.
+ */}
+
{
const on = activas.includes(clase);
return (
-
{clase}
-
+
);
})}
-
+
);
}
@@ -120,6 +130,9 @@ export function RielClasesSelector({
disabled={disabled}
onClick={() => alternar(clase)}
aria-pressed={on}
+ // El nombre accesible se declara aquí, no con un texto oculto junto
+ // a la letra visible: eso hacía que se anunciara "A Clase A".
+ aria-label={`Clase ${clase}`}
title={`Clase ${clase}`}
className={[
d.celda,
@@ -132,7 +145,6 @@ export function RielClasesSelector({
].join(" ")}
>
{clase}
- Clase {clase}
);
})}
diff --git a/tests/05-accesibilidad.spec.ts b/tests/05-accesibilidad.spec.ts
index e7a0e16..9885654 100644
--- a/tests/05-accesibilidad.spec.ts
+++ b/tests/05-accesibilidad.spec.ts
@@ -78,6 +78,33 @@ for (const ruta of RUTAS_MUESTRA) {
}
}
+ // --- roles que contradicen la semántica del elemento ---
+ //
+ // Una auditoría marcó `
` 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 = {
+ 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)) {