Skip to content

♿ ESTÁNDAR AVANZADO DE ACCESIBILIDAD WEB (WCAG 2.1 AA)

🎯 OBJETIVO

Garantizar que todas las interfaces del sistema sean totalmente operables por usuarios que dependen de lectores de pantalla, navegación por teclado o configuraciones de accesibilidad.


🎯 REGLAS INQUEBRANTABLES

[REQUIRED] A11Y-001: Todo elemento interactivo DEBE ser accesible por teclado (Tecla Tab, Enter, Space). Queda prohibido asignar eventos onClick en divs o spans sin role="button" y tabIndex={0}.

Por qué: un onClick en un div no recibe foco de teclado ni se activa con Enter o Espacio: para quien no usa mouse, ese elemento simplemente no existe. Es la misma clase de bug que un enlace roto, solo que invisible para quien prueba con mouse.

[REQUIRED] A11Y-002: Gestión Estricta de Foco en Modales (Focus Trap). Cuando un modal se abre, el foco debe capturarse dentro de él y restaurarse al elemento detonador al cerrarse.

Por qué: sin captura de foco, tabular dentro de un modal abierto saca el foco hacia elementos de la página que quedaron detrás, invisibles pero seleccionables — confuso con mouse y directamente inutilizable con teclado o lector de pantalla.

[REQUIRED] A11Y-003: Contraste de Color >= 4.5:1 para texto normal. Verificación automatizada en CI/CD con @axe-core/playwright.

Por qué: por debajo de 4.5:1 el texto se vuelve difícil de leer con baja visión o en pantallas con reflejo de luz, y es el umbral objetivo de WCAG AA, no una preferencia estética. Verificarlo en CI es lo que impide que un ajuste de paleta lo rompa sin que nadie lo note.


⌨️ 1. GESTIÓN DE FOCO Y FOCUS TRAPPING EN MODALES

tsx
// src/components/Modal.tsx
import { useEffect, useRef } from 'react'

export function Modal({ isOpen, onClose, children }: { isOpen: boolean; onClose: () => void; children: React.ReactNode }) {
  const modalRef = useRef<HTMLDivElement>(null)
  const previousFocusRef = useRef<HTMLElement | null>(null)

  useEffect(() => {
    if (isOpen) {
      previousFocusRef.current = document.activeElement as HTMLElement
      modalRef.current?.focus()
    } else {
      previousFocusRef.current?.focus() // Restaurar foco
    }
  }, [isOpen])

  if (!isOpen) return null

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
      <div
        ref={modalRef}
        tabIndex={-1}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        className="bg-background p-6 rounded-lg max-w-md w-full focus:outline-none"
      >
        {children}
        <button
          onClick={onClose}
          className="mt-4 px-4 py-2 bg-surface border rounded-lg text-sm"
        >
          Cerrar
        </button>
      </div>
    </div>
  )
}

🧪 2. TESTING AUTOMATIZADO CON PLAYWRIGHT Y AXE-CORE

typescript
// e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test('Verificar que el Dashboard cumple con WCAG 2.1 AA', async ({ page }) => {
  await page.goto('/dashboard')

  const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze()

  expect(accessibilityScanResults.violations).toEqual([])
})

📋 CHECKLIST DE ACCESIBILIDAD

  • [ ] Jerarquía de encabezados única por página (<h1> principal, luego <h2>, <h3>).
  • [ ] Atributos alt descriptivos en todas las imágenes.
  • [ ] Navegación completa por teclado con indicador de foco visible (focus-visible).
  • [ ] Captura de foco (Focus Trap) en diálogos y modales.
  • [ ] Pruebas automatizadas con @axe-core/playwright sin violaciones WCAG 2.1 AA.

171 documentos indexados · generado desde INDEX.json