Addaw, ir a inicio Donar

Lección 10 de 10 · Caso práctico · 25 min · Revisado el 26/10/2026

Caso práctico, ejercicios y lista de comprobación

Es el momento de aplicar el curso completo a un proyecto real. Vas a revisar el código de una aplicación de cita previa, corregir sus fallos de CSS, JavaScript y framework, y dejar pruebas automáticas para que no vuelvan. Después tienes ejercicios de autoevaluación con su solución y una lista de comprobación para tus propios desarrollos.

Caso práctico: la cita previa del Ayuntamiento de Vega Alta

El Ayuntamiento de Vega Alta (municipio ficticio) ha estrenado una aplicación de cita previa hecha en React e incrustada en su sede electrónica. Antes de la revisión periódica que exige el RD 1112/2018 (PDF, se abre en una ventana nueva), el equipo de desarrollo te pide que la revises. El flujo tiene cuatro pantallas: elegir trámite, elegir oficina, elegir día y hora, y confirmar con los datos personales. La revisión con teclado, emulación de preferencias y lector de pantalla arroja estos hallazgos.

Paso 1. Foco y estilos de interacción

La hoja de estilos global contiene *:focus { outline: none; } y los botones muestran el foco con un box-shadow azul claro. La cabecera de la sede es sticky y mide 96 píxeles, y un banner de cookies fijo ocupa la parte inferior: al volver hacia atrás con Mayús + Tab, los botones de las horas quedan debajo de la cabecera. Los botones de los días del calendario miden 20 por 20 píxeles y están pegados.

Corrección: se elimina la regla global y se define un :focus-visible con outline oscuro de 3 píxeles y halo claro (2.4.7, 1.4.11); se añade scroll-padding-top y scroll-padding-bottom al html con la altura de la cabecera y del banner, y la cabecera deja de ser fija con max-height: 30em (2.4.11, 1.4.10); los días pasan a min-width y min-height de 2.75rem (2.5.8, y de paso los 44 píxeles de 2.5.5).

Paso 2. Colores forzados y modo oscuro

Con un tema de contraste de Windows, los botones «Siguiente» se convierten en texto suelto y las horas disponibles (fondo verde) no se distinguen de las ocupadas (fondo gris). En modo oscuro, el logotipo del ayuntamiento desaparece.

Corrección: border: 2px solid transparent en los botones; las horas ocupadas pasan a ser <button disabled> con el texto «ocupada» (que además resuelve 1.4.1 Uso del color) y, en forced-colors: active, se marcan con GrayText; la hora seleccionada lleva un borde de 3 píxeles en Highlight. Para el logotipo, una versión clara en un <picture> con prefers-color-scheme: dark.

Paso 3. Comportamiento en JavaScript

El selector de oficina es un <select> que navega a la pantalla de horarios en el evento change: con las flechas, cada oficina recorrida dispara la navegación (3.2.2). Los botones de las horas reservan en mousedown (2.5.2). La sesión caduca a los 10 minutos sin aviso y se pierden los datos (2.2.1).

Corrección: el select se acompaña de un botón «Ver horarios»; las horas usan click; dos minutos antes de caducar aparece un diálogo modal con el foco en «Ampliar sesión», y el formulario guarda un borrador para no perder los datos.

Paso 4. Rutas, componentes y animación

Al pasar de una pantalla a otra, el título de la pestaña sigue siendo «Cita previa», el foco se queda en el botón que ya no existe y el lector no anuncia nada. El componente <CampoTexto> aplica aria-describedby y aria-invalid a su div exterior, así que los errores no se anuncian. En la portada, un carrusel de avisos rota cada 4 segundos sin botón de pausa.

Corrección: un componente Pagina que actualiza document.title («Elegir oficina - Paso 2 de 4 - Cita previa»), hace scroll arriba, enfoca el <h1> y anuncia la carga en una región de estado permanente (2.4.2, 2.4.3, 4.1.3); CampoTexto extiende el resto de propiedades sobre el <input> y genera sus id con useId(); el carrusel se detiene con un botón «Pausar avisos» y no rota si prefers-reduced-motion: reduce está activo (2.2.2).

Paso 5. Evitar que vuelva

Se añade eslint-plugin-jsx-a11y en modo strict, pruebas de componente con vitest-axe para cada estado de CampoTexto y del calendario, y una prueba de Playwright que recorre las cuatro pantallas con @axe-core/playwright, comprueba que tras cada navegación el foco está en el <h1> y repite el flujo con page.emulateMedia({ forcedColors: 'active', reducedMotion: 'reduce' }). Como había 40 errores heredados en la sede, se fija ese número como umbral máximo en la integración continua y se reduce en cada iteración.

Ejercicios de autoevaluación

Ejercicio 1. Un botón de cerrar mide 16 por 16 píxeles CSS y no tiene ningún otro objetivo a menos de 30 píxeles. ¿Incumple 2.5.8?

Ver solución

No necesariamente. 2.5.8 admite objetivos menores de 24 por 24 si tienen espacio suficiente: un círculo de 24 píxeles de diámetro centrado en el botón no corta a ningún otro objetivo ni a su círculo. Aun así, conviene ampliar la zona activa.

Ejercicio 2. En modo de colores forzados, el indicador de foco de una web ha desaparecido por completo. El CSS usa outline: none; box-shadow: 0 0 0 3px #ffbf47. ¿Qué cambias?

Ver solución

Sustituir outline: none por outline: 3px solid transparent. En modo normal el contorno transparente no se ve y se ve la sombra; en colores forzados la sombra se elimina y el sistema pinta el contorno con su color.

Ejercicio 3. Una lista de documentos permite borrar cada uno con un botón «Eliminar». Tras borrar, el lector de pantalla empieza a leer desde el principio de la página. ¿Qué ocurre y cómo se corrige?

Ver solución

El botón que tenía el foco se ha eliminado del DOM y el foco ha caído al body. Antes de eliminar la fila, mueve el foco al botón «Eliminar» de la fila siguiente (o de la anterior si era la última, o al encabezado de la lista si queda vacía) y anuncia «Documento eliminado» en una región de estado.

Ejercicio 4. Un Web Component <campo-fecha> contiene su <input> en un shadow root. Fuera, el autor escribe <label for="nacimiento"> y le da id="nacimiento" al componente. El campo no tiene nombre. ¿Por qué y qué opciones hay?

Ver solución

Las referencias por id no cruzan la frontera del Shadow DOM, y el anfitrión no es un elemento etiquetable. Opciones: que el componente reciba la etiqueta como atributo y la pinte dentro de su propio árbol junto al <input>; convertirlo en elemento asociado a formulario (formAssociated con ElementInternals) para que la etiqueta externa funcione; o, como último recurso, aria-label.

Ejercicio 5. Un buscador filtra resultados mientras se escribe y, cuando solo queda un resultado, navega automáticamente a su ficha. ¿Qué criterio incumple y qué parte del comportamiento sí es correcta?

Ver solución

La navegación automática es un cambio de contexto provocado por una entrada sin aviso previo: incumple 3.2.2 Al recibir entradas. Filtrar la lista mientras se escribe es un cambio de contenido y es correcto, siempre que el número de resultados se anuncie en una región de estado (4.1.3) tras una breve pausa en la escritura.

Lista de comprobación final

  • Ninguna regla elimina el foco sin sustituto en :focus-visible.
  • El indicador de foco usa outline y contrasta al menos 3:1.
  • Ninguna cabecera, barra o banner fijo tapa el elemento enfocado.
  • Los objetivos de puntero miden 24 por 24 píxeles CSS o tienen espacio equivalente.
  • El contenido con hover o foco se cierra con Escape y se puede recorrer con el puntero.
  • La interfaz funciona con un tema de contraste de Windows y en modo oscuro.
  • Las animaciones respetan prefers-reduced-motion y el movimiento automático tiene botón de pausa.
  • Las acciones se ejecutan con click, no al presionar.
  • Ningún foco ni cambio de valor provoca un cambio de contexto sin aviso.
  • Las sesiones avisan antes de caducar y permiten ampliar.
  • El foco nunca cae al body al insertar o eliminar contenido.
  • Cada cambio de ruta actualiza el título, mueve el foco y hace scroll arriba.
  • Los componentes exigen su nombre accesible y aplican los atributos al control real.
  • Linter, pruebas de componente y pruebas e2e con axe se ejecutan en cada cambio.

Para seguir aprendiendo

Comprueba lo que has aprendido

Esta lección se puede leer libremente. Para hacer su test de 3 preguntas, los ejercicios prácticos con páginas reales, guardar tu avance y optar al examen y al certificado, matricúlate en el curso.