# Accesibilidad A11y — Ejemplos Completos y Checklist

Referencia extendida con todos los ejemplos de código y checklist detallado.

---

## Elementos Semánticos HTML5

```html
<!-- MAL: divs para todo, sin semántica -->
<div class="header">
  <div class="nav">...</div>
</div>
<div class="content">
  <div class="article">...</div>
  <div class="sidebar">...</div>
</div>
<div class="footer">...</div>

<!-- BIEN: elementos semánticos correctos -->
<header>
  <nav aria-label="Navegación principal">...</nav>
</header>
<main>
  <article>...</article>
  <aside aria-label="Información complementaria">...</aside>
</main>
<footer>...</footer>
```

## Landmarks y Skip Link

```html
<!-- Cada página DEBE tener exactamente un <main> -->
<main id="contenido-principal">
  <!-- Contenido primario -->
</main>

<!-- Skip link OBLIGATORIO — primer elemento enfocable -->
<a href="#contenido-principal" class="skip-link">
  Saltar al contenido principal
</a>

<!-- CSS para el skip link -->
<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #005fcc;
  color: white;
  padding: 8px;
  z-index: 100;
  transition: top 0.2s;
}
.skip-link:focus {
  top: 0;   /* Visible solo al enfocar con teclado */
}
</style>
```

---

## Atributos ARIA — Ejemplos Extendidos

### aria-label vs aria-labelledby vs aria-describedby

```html
<!-- aria-label: cuando no hay texto visible -->
<button aria-label="Cerrar diálogo de confirmación">
  <svg aria-hidden="true"><!-- ícono X --></svg>
</button>

<!-- aria-labelledby: referenciar texto existente en el DOM -->
<h2 id="titulo-modal">Confirmar eliminación</h2>
<dialog aria-labelledby="titulo-modal">
  <!-- El título del diálogo describe el modal -->
</dialog>

<!-- aria-describedby: información adicional (no el nombre) -->
<input
  id="email"
  type="email"
  aria-describedby="email-hint email-error"
/>
<span id="email-hint">Usa tu correo corporativo @empresa.com.mx</span>
<span id="email-error" role="alert" aria-live="polite"></span>
```

### Estados y propiedades ARIA comunes

```html
<!-- Menú expandible -->
<button
  aria-expanded="false"
  aria-controls="menu-opciones"
  id="btn-menu"
>
  Opciones
</button>
<ul id="menu-opciones" aria-labelledby="btn-menu" hidden>
  <li><a href="/perfil">Mi perfil</a></li>
  <li><a href="/salir">Cerrar sesión</a></li>
</ul>

<!-- Pestañas (tabs) -->
<div role="tablist" aria-label="Secciones del expediente">
  <button
    role="tab"
    aria-selected="true"
    aria-controls="panel-datos"
    id="tab-datos"
  >Datos generales</button>
  <button
    role="tab"
    aria-selected="false"
    aria-controls="panel-documentos"
    id="tab-docs"
    tabindex="-1"
  >Documentos</button>
</div>
<div role="tabpanel" id="panel-datos" aria-labelledby="tab-datos">...</div>
<div role="tabpanel" id="panel-documentos" aria-labelledby="tab-docs" hidden>...</div>

<!-- Notificaciones en vivo -->
<div aria-live="polite" aria-atomic="true" class="sr-only" id="notificacion">
  <!-- El lector anuncia cambios en este div automáticamente -->
</div>
```

---

## Contraste de Color — Ejemplos CSS

```css
/* MAL: contraste insuficiente */
.texto-secundario {
  color: #999999;      /* sobre #ffffff = ratio 2.85:1 — FALLA */
  background: #ffffff;
}

/* BIEN: contraste suficiente */
.texto-secundario {
  color: #767676;      /* sobre #ffffff = ratio 4.54:1 — PASA AA */
  background: #ffffff;
}

/* MAL: información solo por color (daltonismo) */
.error { color: red; }
.exito { color: green; }

/* BIEN: color + ícono/texto descriptivo */
.error {
  color: #d32f2f;
  background: #fdecea;
  border-left: 4px solid #d32f2f;
}
.error::before {
  content: "✗ Error: ";  /* texto descriptivo adicional */
}
```

---

## Navegación por Teclado — Ejemplos Completos

### Orden de foco

```html
<!-- MAL: tabindex positivos crean orden artificial y frágil -->
<input tabindex="3" name="apellido" />
<input tabindex="1" name="nombre" />
<input tabindex="2" name="email" />

<!-- BIEN: orden natural del DOM + tabindex="0" solo si necesario -->
<input name="nombre" />
<input name="apellido" />
<input name="email" />

<!-- tabindex="-1": enfocable por JavaScript, no por teclado -->
<div id="mensaje-error" tabindex="-1">Error al guardar</div>
<script>
  document.getElementById('mensaje-error').focus();
</script>
```

### Gestión de foco en modales

```javascript
// Trampa de foco — OBLIGATORIA en diálogos modales
function trapFocus(modal) {
  const focusables = modal.querySelectorAll(
    'a[href], button:not([disabled]), input:not([disabled]), ' +
    'select:not([disabled]), textarea:not([disabled]), [tabindex="0"]'
  );
  const first = focusables[0];
  const last = focusables[focusables.length - 1];

  modal.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  });

  first.focus();
}

let triggerAnterior;
function abrirModal(modal, trigger) {
  triggerAnterior = trigger;
  modal.removeAttribute('hidden');
  trapFocus(modal);
}

function cerrarModal(modal) {
  modal.setAttribute('hidden', '');
  triggerAnterior?.focus();
}
```

---

## Formularios Accesibles — Ejemplos Completos

### Mensajes de error accesibles

```html
<!-- BIEN: error asociado al input + role="alert" -->
<div>
  <label for="correo">Correo electrónico</label>
  <input
    type="email"
    id="correo"
    name="correo"
    aria-invalid="true"
    aria-describedby="correo-error"
    aria-required="true"
  />
  <span id="correo-error" role="alert">
    El formato del correo no es válido. Ejemplo: usuario@empresa.com.mx
  </span>
</div>
```

### Grupos de controles relacionados

```html
<fieldset>
  <legend>Tipo de contrato</legend>
  <label>
    <input type="radio" name="contrato" value="base" /> Base
  </label>
  <label>
    <input type="radio" name="contrato" value="confianza" /> Confianza
  </label>
  <label>
    <input type="radio" name="contrato" value="honorarios" /> Honorarios
  </label>
</fieldset>
```

---

## Tablas Accesibles

```html
<!-- BIEN: tabla de datos completa -->
<table>
  <caption>
    Nómina quincenal — Enero 2026
    <details>
      <summary>Descripción de la tabla</summary>
      Lista de empleados con su sueldo bruto para la quincena del 1 al 15 de enero.
    </details>
  </caption>
  <thead>
    <tr>
      <th scope="col">Nombre</th>
      <th scope="col">Apellido</th>
      <th scope="col">Sueldo bruto</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Juan</td>
      <td>Pérez</td>
      <td>$50,000.00</td>
    </tr>
  </tbody>
</table>

<!-- Tablas complejas con headers múltiples -->
<table>
  <tr>
    <th id="nombre" scope="col">Nombre</th>
    <th id="q1" scope="col">Q1</th>
    <th id="q2" scope="col">Q2</th>
  </tr>
  <tr>
    <td headers="nombre">Saúl</td>
    <td headers="nombre q1">$10,000</td>
    <td headers="nombre q2">$12,000</td>
  </tr>
</table>
```

---

## Imágenes y Multimedia

```html
<!-- BIEN: alt descriptivo para imágenes informativas -->
<img
  src="grafica-ventas.png"
  alt="Gráfica de barras: ventas Q1=$500K, Q2=$750K, Q3=$600K, Q4=$900K"
/>

<!-- BIEN: alt vacío para imágenes decorativas -->
<img src="separador-decorativo.png" alt="" role="presentation" />

<!-- BIEN: SVG accesible -->
<svg role="img" aria-labelledby="svg-titulo svg-desc">
  <title id="svg-titulo">Tendencia de crecimiento</title>
  <desc id="svg-desc">Línea ascendente del 20% al 80% entre 2024 y 2026</desc>
</svg>

<!-- Video con subtítulos y audiodescripción -->
<video controls>
  <source src="tutorial.mp4" type="video/mp4" />
  <track kind="subtitles" src="subtitulos-es.vtt" srclang="es" label="Español" default />
  <track kind="descriptions" src="audiodesc-es.vtt" srclang="es" label="Audiodescripción" />
  <p>Tu navegador no soporta video. <a href="tutorial.mp4">Descargar tutorial</a></p>
</video>
```

---

## Lectores de Pantalla — Clase sr-only

```css
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.sr-only-focusable:focus {
  position: static;
  width: auto;
  height: auto;
  overflow: visible;
  clip: auto;
  white-space: normal;
}
```

```html
<button>
  Eliminar
  <span class="sr-only">expediente de Juan Pérez López</span>
</button>

<a href="/descargar/reporte.pdf">
  <svg aria-hidden="true"><!-- ícono PDF --></svg>
  <span class="sr-only">Descargar reporte de nómina en formato PDF</span>
</a>
```

---

## Checklist WCAG 2.1 AA Completo

### Perceptible
- [ ] Toda imagen tiene `alt` descriptivo o `alt=""` si es decorativa
- [ ] Videos tienen subtítulos en español
- [ ] No se transmite información solo por color
- [ ] Contraste de texto normal >= 4.5:1
- [ ] Contraste de texto grande >= 3:1
- [ ] Contraste de UI (botones, inputs) >= 3:1
- [ ] El contenido no desaparece al hacer zoom al 200%

### Operable
- [ ] Todo lo clickeable es accesible por teclado (Tab, Enter, Escape, flechas)
- [ ] El foco es siempre visible
- [ ] No hay trampas de teclado (excepto modales intencionales)
- [ ] Hay skip link al contenido principal
- [ ] Los modales atrapan foco y regresan al trigger al cerrar
- [ ] No hay contenido que destelle más de 3 veces por segundo

### Comprensible
- [ ] El idioma del documento está declarado (`<html lang="es">`)
- [ ] Los mensajes de error son descriptivos (no solo "Error")
- [ ] Los labels están asociados a sus inputs
- [ ] Los campos requeridos están marcados con `aria-required="true"`
- [ ] Los errores de validación se anuncian con `role="alert"` o `aria-live`

### Robusto
- [ ] El HTML valida sin errores críticos
- [ ] Los elementos interactivos tienen nombres accesibles
- [ ] Los estados (expandido, seleccionado, inválido) se comunican via ARIA
- [ ] La página funciona con JS deshabilitado en funciones críticas

---

## Herramientas de Prueba

```bash
# axe-core en CLI
npm install -g @axe-core/cli
axe https://miapp.com --tags wcag2a,wcag2aa

# Lighthouse (accesibilidad)
lighthouse https://miapp.com --only-categories=accessibility

# Pa11y
npm install -g pa11y
pa11y https://miapp.com --standard WCAG2AA
```

### Pruebas manuales obligatorias

1. **Solo teclado**: navega toda la página sin ratón. Tab, Shift+Tab, Enter, Escape, flechas.
2. **NVDA + Firefox** o **VoiceOver + Safari**: escucha la página como lo haría un usuario ciego.
3. **Zoom 200%**: verifica que el layout no se rompe.
4. **Windows High Contrast Mode**: verifica visibilidad de controles.
5. **Desactivar CSS**: verifica que el orden del DOM sea lógico.
