From 37f646f4f1e8ad06e58aa566058f1910fc66a643 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 15:32:26 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20keyboard=20shor?= =?UTF-8?q?tcuts=20to=20V2=20Exam=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This PR implements a significant micro-UX improvement for the ENARM V2 exam-taking experience by adding keyboard shortcuts and discoverability hints. šŸ’” What: - Added global keyboard listeners for numeric (1-4) and alpha (A-D) keys to select answers. - Mapped 'Enter' key to "Confirmar Respuesta" and "Siguiente/Ver Resumen" actions. - Added visual bracketed hints (e.g., [1], [Enter]) to relevant interactive elements. - Enhanced ARIA labels and title attributes with shortcut information. šŸŽÆ Why: To improve efficiency and reduce friction during high-repetition study sessions, allowing users to stay in "flow state" without relying exclusively on mouse/touch interaction. ♿ Accessibility: - Updated `aria-label` on answer buttons to include shortcut info (e.g., "Opción A (atajo: 1)"). - Updated `aria-label` and `title` on action buttons with shortcut info (e.g., "Confirmar respuesta (atajo: Enter)"). - Decorative icons and hints are marked appropriately to avoid screen reader noise. Tests: - Added 3 new unit tests in `V2Examen.test.jsx` verifying '1', 'a', and 'Enter' shortcuts. - Verified visual changes and keyboard functionality with Playwright automated screenshots. Co-authored-by: godie <227743+godie@users.noreply.github.com> --- src/v2/__tests__/V2Examen.test.jsx | 63 +++++++++++++++-- src/v2/pages/V2Examen.jsx | 104 ++++++++++++++++++++++------- 2 files changed, 138 insertions(+), 29 deletions(-) diff --git a/src/v2/__tests__/V2Examen.test.jsx b/src/v2/__tests__/V2Examen.test.jsx index 68331b8..01a3ee1 100644 --- a/src/v2/__tests__/V2Examen.test.jsx +++ b/src/v2/__tests__/V2Examen.test.jsx @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'; -import { MemoryRouter, useHistory } from 'react-router-dom'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import V2Examen from '../pages/V2Examen'; import ExamService from '../../services/ExamService'; @@ -101,7 +101,7 @@ describe('V2Examen', () => { await screen.findByText('ECG'); // Click on the ECG option (Option A) - aria-label uses lowercase - const ecgButton = await screen.findByRole('button', { name: /Opción A: ECG/i }); + const ecgButton = await screen.findByRole('button', { name: /Opción A/i }); fireEvent.click(ecgButton); // Confirm button should be enabled after selection - aria-label is 'Confirmar respuesta' @@ -109,6 +109,61 @@ describe('V2Examen', () => { expect(confirmBtn).not.toBeDisabled(); }); + it('allows selecting an answer using keyboard shortcut', async () => { + render( + + + + ); + + await screen.findByText('ECG'); + + // Simulate pressing '1' + fireEvent.keyDown(window, { key: '1' }); + + // Confirm button should be enabled + const confirmBtn = await screen.findByRole('button', { name: /Confirmar respuesta/i }); + expect(confirmBtn).not.toBeDisabled(); + }); + + it('allows selecting an answer using alpha keyboard shortcut', async () => { + render( + + + + ); + + await screen.findByText('ECG'); + + // Simulate pressing 'a' + fireEvent.keyDown(window, { key: 'a' }); + + // Confirm button should be enabled + const confirmBtn = await screen.findByRole('button', { name: /Confirmar respuesta/i }); + expect(confirmBtn).not.toBeDisabled(); + }); + + it('allows submitting an answer using Enter key', async () => { + render( + + + + ); + + await screen.findByText('ECG'); + + // Select answer + fireEvent.keyDown(window, { key: '1' }); + + // Simulate pressing 'Enter' + fireEvent.keyDown(window, { key: 'Enter' }); + + // Check feedback appears + await waitFor(() => { + expect(screen.getByText('+50 XP')).toBeDefined(); + }); + }); + it('shows feedback after submitting answer', async () => { render( @@ -120,7 +175,7 @@ describe('V2Examen', () => { await screen.findByText('ECG'); // Select answer - const ecgButton = await screen.findByRole('button', { name: /Opción A: ECG/i }); + const ecgButton = await screen.findByRole('button', { name: /Opción A/i }); fireEvent.click(ecgButton); // Submit - use lowercase in aria-label diff --git a/src/v2/pages/V2Examen.jsx b/src/v2/pages/V2Examen.jsx index c2fb064..df205dd 100644 --- a/src/v2/pages/V2Examen.jsx +++ b/src/v2/pages/V2Examen.jsx @@ -177,6 +177,50 @@ const V2Examen = () => { history.push('/dashboard'); }, [history]); + // Keyboard shortcuts (defined after callbacks to avoid ReferenceError) + useEffect(() => { + const handleKeyDown = (e) => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + if (loading || !caso) return; + + const currentQuestion = caso?.preguntas?.[currentQuestionIndex]; + if (!currentQuestion) return; + + const key = e.key.toLowerCase(); + + // Select answer options (1-4 or A-D) + if (!showFeedback) { + const optionKeys = ['1', '2', '3', '4']; + const alphaKeys = ['a', 'b', 'c', 'd']; + + let index = -1; + if (optionKeys.includes(key)) { + index = optionKeys.indexOf(key); + } else if (alphaKeys.includes(key)) { + index = alphaKeys.indexOf(key); + } + + if (index !== -1 && index < currentQuestion.respuestas.length) { + handleSelectAnswer(currentQuestionIndex, index); + } + } + + // Submit or Next (Enter) + if (key === 'enter') { + if (!showFeedback) { + if (selectedAnswers[currentQuestionIndex] !== undefined) { + handleSubmitAnswer(); + } + } else { + handleNext(); + } + } + }; + + window.addEventListener('keydown', handleKeyDown, { passive: true }); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [loading, caso, showFeedback, currentQuestionIndex, selectedAnswers, handleSelectAnswer, handleSubmitAnswer, handleNext]); + if (loading) { return (
@@ -203,8 +247,8 @@ const V2Examen = () => { ); } - const currentQuestion = caso.preguntas[currentQuestionIndex]; - + const currentQuestion = caso?.preguntas?.[currentQuestionIndex]; + // Guard: if no question at current index, show error if (!currentQuestion) { return ( @@ -347,13 +391,15 @@ const V2Examen = () => { iconColor = 'var(--md-sys-color-on-primary)'; } + const shortcut = rIdx + 1; return ( ); @@ -398,9 +449,10 @@ const V2Examen = () => { style={{ padding: '0 40px' }} onClick={handleSubmitAnswer} disabled={selectedAnswer === undefined} - aria-label='Confirmar respuesta' + aria-label='Confirmar respuesta (atajo: Enter)' + title='Atajo: Enter' > - Confirmar Respuesta + Confirmar Respuesta [Enter] {selectedAnswer === undefined && ( @@ -430,9 +482,10 @@ const V2Examen = () => { className='v2-btn-filled v2-btn-h-56' style={{ padding: '0 40px' }} onClick={handleNext} - aria-label='Siguiente pregunta' + aria-label='Siguiente pregunta (atajo: Enter)' + title='Atajo: Enter' > - Siguiente + Siguiente [Enter] )} @@ -441,9 +494,10 @@ const V2Examen = () => { className='v2-btn-filled v2-btn-h-56' style={{ padding: '0 40px' }} onClick={handleNext} - aria-label='Ver resumen de sesión' + aria-label='Ver resumen de sesión (atajo: Enter)' + title='Atajo: Enter' > - Ver Resumen + Ver Resumen [Enter] )}