Creando un DatePicker accesible en React conforme a WCAG 2.2 AA
Nuestro equipo de desarrollo comenzó con código de DatePicker generado por Claude basado en el patrón "Date Picker Dialog" de WAI-ARIA APG. El prompt inicial especificaba requisitos de WCAG 2.1/2.2 Nivel AA: input con aria-describedby, botón de activación, diálogo modal con role="dialog" y una tabla de calendario basada en grid. Sin embargo, la versión cruda reveló problemas críticos con el foco, la navegación y la accesibilidad.
La IA implementó navegación básica por teclado (flechas, Home/End, PageUp/Down, Enter/Espacio, Escape), tabindex móvil en elementos gridcell y aria-live="polite" para cambios de mes. Pero las pruebas con VoiceOver y NVDA expusieron problemas: el foco no caía en la fecha seleccionada al abrir, los anuncios aria-live se acumulaban en cola y el diálogo parpadeaba con clics repetidos.
Desviaciones del APG para una accesibilidad real
La especificación APG pide <td role="gridcell"> sin botones anidados, confiando en onClick/onKeyDown manual y tabindex. Las pruebas en el mundo real mostraron claras ventajas de usar <button> dentro de <td>:
- Manejo nativo del foco y soporte para Enter/Espacio sin código extra.
- Semántica automática para lectores de pantalla.
- Estado deshabilitado integrado sin aria-disabled ni preventDefault.
- Eventos de clic fiables desde ratón o teclado.
Esta fue una elección deliberada: el APG es una guía, pero las pruebas en dispositivos tienen prioridad. Aseguramos contraste ≥4.5:1, usamos CSS Modules y APIs nativas Date/Intl para formateo—sin dependencias externas más allá de React/TypeScript.
Solucionando problemas funcionales y de UX
El código crudo no compilaba: faltaba index.html y errores de TypeScript. Tras las correcciones, surgieron más issues:
Bugs funcionales:
- Fechas fuera de minDate/maxDate saltaban a otro mes en vez de mostrar celdas deshabilitadas.
- Sin forma de borrar el input.
Problemas visuales:
- Estilos de foco faltantes en celdas.
- Tamaños desiguales de td.
Accesibilidad:
- Sin foco en fecha seleccionada al abrir—lectores de pantalla callados.
- aria-live="polite" en encabezado de mes causaba colas de anuncios en navegación rápida.
- Blur + clic en trigger provocaba parpadeo del diálogo.
Soluciones:
- Al abrir, el foco va al valor o hoy; el lector de pantalla anuncia contexto (día de semana, fecha, mes, año).
- aria-live="assertive" en región oculta separada para anuncios instantáneos de mes.
- Tras selección, foco regresa al trigger con aria-label actualizado.
- Mostrar mes del valor incluso con días deshabilitados.
- Eliminados OK/Cancel: Enter confirma, Esc cierra.
Estructura del componente y navegación por teclado
Props: value (Date | null), onChange, minDate?, maxDate?, disabledDates?, locale?.
Claves ARIA:
- Input: aria-describedby para formato, readonly.
- Trigger: aria-label dinámico ("Seleccionado: DD/MM/AAAA" o "Elige una fecha").
- Diálogo: role="dialog", aria-modal="true", trampa de foco.
- Grid: table role="grid", td con button role="gridcell", aria-selected en valor, aria-disabled en fechas no disponibles.
- Encabezado de mes: aria-live="assertive" en región educada.
Navegación:
- Flechas: días adyacentes.
- Home/End: primer/último día del mes.
- PageUp/Down: ±1 mes.
- Shift+PageUp/Down: ±1 año.
- Enter/Espacio: seleccionar.
- Escape: cerrar.
Lecciones clave
- La IA acelera el esqueleto (ARIA, navegación), pero las pruebas con lectores de pantalla son esenciales.
- <button> dentro de <td> es más fiable que gridcell puro en navegadores reales + lectores de pantalla.
- aria-live="assertive" evita colas en navegación de meses.
- Gestión de foco: abrir en valor/hoy, regresar a trigger.
- Stack mínimo: React + TS, sin librerías.
El componente está listo para producción como selector de fecha única, con código abierto para extensiones (años, rangos).
— Editorial Team
Aún no hay comentarios.