From 4fe386e874258c6ee6c4b9916fd051e8b3de4331 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 15:31:21 +0000
Subject: [PATCH] feat: add keyboard shortcuts and accessibility hints to
flashcard study
- Implement global keydown listener for Space/Enter (flip/rate) and 1-4 (SM-2 ratings)
- Add visual hints [Espacio] and [1-4] to buttons for discoverability
- Update aria-label and title attributes for improved accessibility
- Add comprehensive unit tests for keyboard shortcut functionality
- Ensure hook order prevents initialization ReferenceErrors
Co-authored-by: godie <227743+godie@users.noreply.github.com>
---
.Jules/palette.md | 4 ++
src/v2/pages/V2FlashcardStudy.jsx | 43 ++++++++++++++----
src/v2/pages/V2FlashcardStudy.test.jsx | 62 ++++++++++++++++++++++++++
3 files changed, 101 insertions(+), 8 deletions(-)
diff --git a/.Jules/palette.md b/.Jules/palette.md
index 697428c..692a9e9 100644
--- a/.Jules/palette.md
+++ b/.Jules/palette.md
@@ -80,6 +80,10 @@
**Learning:** Centralizing the visual and semantic treatment of required fields (e.g., a red asterisk `*` and `aria-required="true"`) in base components like `CustomTextInput` ensures a consistent and accessible experience across all forms without duplicating logic. Additionally, meticulous attention to Spanish orthography (e.g., 'Clínico' vs 'Clinico', 'Ocurrió' vs 'Ocurrio') significantly elevates the professional feel of the application for native speakers.
**Action:** Always provide standardized visual cues for mandatory fields in base components and maintain strict linguistic standards for localized interfaces.
+## 2025-06-26 - [Flashcard Keyboard Shortcuts and Accessibility Hints]
+**Learning:** Adding keyboard shortcuts (Space/Enter, 1-4) for high-repetition tasks like flashcard study significantly reduces friction. Including visual hints in brackets (e.g., `[Espacio]`, `[1]`) within button labels ensures discoverability without cluttering the UI. Crucially, when using `useCallback` in global listeners, the `useEffect` must be defined *after* the callbacks to avoid initialization `ReferenceError` due to lack of hoisting.
+**Action:** Always implement keyboard shortcuts for study interfaces and provide clear visual and ARIA hints for discovery. Ensure correct hook ordering to avoid initialization errors.
+
## 2025-06-12 - [Maximizing Clickable Areas and Robust Input Grouping]
**Learning:** In collection-based form inputs (like exam questions), moving padding from the `
` to a full-width block `label` maximizes the interactive hit area, significantly improving the user experience on both desktop and touch devices. Furthermore, using indices (e.g., `questionIndex`) instead of text for input `name` and `id` attributes ensures robust grouping and accessibility even when multiple questions share identical text.
**Action:** Always wrap collection-item inputs in full-width block labels and use stable, unique indices for input grouping.
diff --git a/src/v2/pages/V2FlashcardStudy.jsx b/src/v2/pages/V2FlashcardStudy.jsx
index c21524a..c591822 100644
--- a/src/v2/pages/V2FlashcardStudy.jsx
+++ b/src/v2/pages/V2FlashcardStudy.jsx
@@ -236,10 +236,10 @@ const Flashcard = ({ card, isFlipped, onFlip }) => (
// Quality rating buttons (SM-2 mapped to UI)
const QualityButtons = ({ onRate, disabled }) => {
const buttons = [
- { quality: 1, label: 'Otra vez', sublabel: '< 1 día', color: '#ba1a1a', icon: 'replay' },
- { quality: 3, label: 'Difícil', sublabel: '2-3 días', color: '#9c4247', icon: 'sentiment_dissatisfied' },
- { quality: 4, label: 'Bien', sublabel: '4-6 días', color: '#0fa397', icon: 'sentiment_satisfied' },
- { quality: 5, label: 'Fácil', sublabel: '7+ días', color: '#4a6360', icon: 'sentiment_very_satisfied' }
+ { quality: 1, label: 'Otra vez', sublabel: '< 1 día', color: '#ba1a1a', icon: 'replay', hint: '1' },
+ { quality: 3, label: 'Difícil', sublabel: '2-3 días', color: '#9c4247', icon: 'sentiment_dissatisfied', hint: '2' },
+ { quality: 4, label: 'Bien', sublabel: '4-6 días', color: '#0fa397', icon: 'sentiment_satisfied', hint: '3' },
+ { quality: 5, label: 'Fácil', sublabel: '7+ días', color: '#4a6360', icon: 'sentiment_very_satisfied', hint: '4' }
];
return (
@@ -255,13 +255,14 @@ const QualityButtons = ({ onRate, disabled }) => {
opacity: disabled ? 0.5 : 1,
border: 'none'
}}
- aria-label={`Calificar como ${btn.label}`}
+ aria-label={`Atajo: ${btn.hint}. Calificar como ${btn.label}`}
+ title={`Atajo: ${btn.hint}`}
>
{btn.icon}
- {btn.label}
+ {btn.label} [{btn.hint}]
{btn.sublabel}
@@ -348,7 +349,7 @@ const V2FlashcardStudy = () => {
useEffect(() => {
fetchDueFlashcards();
}, [fetchDueFlashcards]);
-
+
// Handle card flip
const handleFlip = useCallback(() => {
if (!isFlipped) {
@@ -386,6 +387,30 @@ const V2FlashcardStudy = () => {
setIsSubmitting(false);
}
}, [currentCard, currentIndex, flashcards.length, isSubmitting]);
+
+ // Keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ if (!isFlipped) {
+ handleFlip();
+ } else if (!isSubmitting) {
+ handleRate(4); // "Bien" is default
+ }
+ } else if (isFlipped && !isSubmitting) {
+ if (e.key === '1') handleRate(1);
+ if (e.key === '2') handleRate(3);
+ if (e.key === '3') handleRate(4);
+ if (e.key === '4') handleRate(5);
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [isFlipped, isSubmitting, handleFlip, handleRate]);
// Restart session
const handleRestartSession = useCallback(() => {
@@ -501,9 +526,11 @@ const V2FlashcardStudy = () => {
)}
diff --git a/src/v2/pages/V2FlashcardStudy.test.jsx b/src/v2/pages/V2FlashcardStudy.test.jsx
index 75fed30..cde3828 100644
--- a/src/v2/pages/V2FlashcardStudy.test.jsx
+++ b/src/v2/pages/V2FlashcardStudy.test.jsx
@@ -143,4 +143,66 @@ describe('V2FlashcardStudy', () => {
expect(screen.getByText('¡Todo al día!')).toBeTruthy();
});
});
+
+ it('flips the card when pressing Space', async () => {
+ render(
+
+
+
+ );
+
+ await waitFor(() => screen.getByText('¿Cuál es la tríada de Virchow?'));
+
+ fireEvent.keyDown(window, { key: ' ' });
+
+ await waitFor(() => {
+ expect(screen.getByText(/Estasis venosa/)).toBeTruthy();
+ });
+ });
+
+ it('rates the card when pressing numeric keys when flipped', async () => {
+ render(
+
+
+
+ );
+
+ await waitFor(() => screen.getByText('¿Cuál es la tríada de Virchow?'));
+
+ // Flip first
+ fireEvent.keyDown(window, { key: 'Enter' });
+ await waitFor(() => screen.getByText(/Estasis venosa/));
+
+ // Rate with '4' (Fácil)
+ fireEvent.keyDown(window, { key: '4' });
+
+ await waitFor(() => {
+ expect(screen.getByText('Agente causal más común de epiglotitis')).toBeTruthy();
+ });
+
+ expect(FlashcardService.reviewFlashcard).toHaveBeenCalledWith(1, 5); // 4 maps to quality 5
+ });
+
+ it('rates the card as "Bien" when pressing Space when flipped', async () => {
+ render(
+
+
+
+ );
+
+ await waitFor(() => screen.getByText('¿Cuál es la tríada de Virchow?'));
+
+ // Flip first
+ fireEvent.keyDown(window, { key: ' ' });
+ await waitFor(() => screen.getByText(/Estasis venosa/));
+
+ // Rate with Space
+ fireEvent.keyDown(window, { key: ' ' });
+
+ await waitFor(() => {
+ expect(screen.getByText('Agente causal más común de epiglotitis')).toBeTruthy();
+ });
+
+ expect(FlashcardService.reviewFlashcard).toHaveBeenCalledWith(1, 4); // Space maps to quality 4
+ });
});