Gallery has no keyboard navigation — user who can't use a mouse (or prefers keyboard) has no way to move between design cards, no Tab/Enter pattern, no Arrow keys #240

Closed
opened 2026-10-04 02:35:16 +00:00 by Mark · 1 comment
Owner

Finding

src/pages/Gallery.tsx has no useEffect with a keydown listener and no onKeyDown on the design cards. grep -n 'Arrow' src/pages/Gallery.tsx returns 0 hits.

A user with a keyboard-only setup (or screen reader) opens the Gallery:

  • Tabs to the search box
  • Types "logo"
  • Now wants to navigate the 4 matching cards
  • Tabs to the first card's actions (favorites, edit, delete, etc.)
  • The cards are 4 separate focus stops with no spatial organization
  • No way to move "right" or "down" between cards in the grid

The Gallery is a grid of cards. Standard grid keyboard navigation is:

  • Arrow Right/Down → next card
  • Arrow Left/Up → previous card
  • Enter → open the design (or its action menu)
  • Space → toggle favorite

Where

  • Gallery: src/pages/Gallery.tsx (no keyboard listener)
  • DesignCard: src/components/gallery/DesignCard.tsx (no onKeyDown on the card itself)
  • No roving tabindex: grep -n 'tabIndex' src/components/gallery/DesignCard.tsx returns 0 hits

Suggested fix shape

Add a keyboard listener to the Gallery's grid container:

useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
    
    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
      e.preventDefault();
      moveSelection(1);
    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
      e.preventDefault();
      moveSelection(-1);
    } else if (e.key === 'Enter') {
      e.preventDefault();
      openSelected();
    } else if (e.key === ' ') {
      e.preventDefault();
      toggleFavoriteSelected();
    }
  };
  
  window.addEventListener('keydown', handleKeyDown);
  return () => window.removeEventListener('keydown', handleKeyDown);
}, [filteredDesigns, selectedCardIndex]);

const moveSelection = (delta: number) => {
  const newIndex = (selectedCardIndex + delta + filteredDesigns.length) % filteredDesigns.length;
  setSelectedCardIndex(newIndex);
  // Scroll the card into view
  document.querySelector(`[data-card-index="${newIndex}"]`)?.scrollIntoView({ block: 'nearest' });
};

Add tabIndex={0} and data-card-index={i} to the design card:

<div
  data-card-index={i}
  tabIndex={i === selectedCardIndex ? 0 : -1}
  onKeyDown={(e) => { /* handled by the global listener */ }}
>
  ...
</div>

The card's focus is managed by the global listener; only the active card has tabIndex={0}, the rest are -1. This is the "roving tabindex" pattern, used in most grid UIs (file managers, photo galleries, etc.).

Severity

Medium. Keyboard-only users can't use the Gallery effectively. Power users with trackpad issues notice.

Discovered by

Gap analysis (2026-10-02) of gallery accessibility.

  • The Gallery's folder nav has clickable folder chips but no keyboard navigation either.
  • The Editor has comprehensive keyboard shortcuts (per the kanban landings).
  • The Workspace has the Workspace's canvas keyboard but no gallery-level shortcuts.
  • Most modern web apps support roving-tabindex grids; this is a standard pattern.
## Finding `src/pages/Gallery.tsx` has no `useEffect` with a `keydown` listener and no `onKeyDown` on the design cards. `grep -n 'Arrow' src/pages/Gallery.tsx` returns 0 hits. A user with a keyboard-only setup (or screen reader) opens the Gallery: - Tabs to the search box - Types "logo" - Now wants to navigate the 4 matching cards - Tabs to the first card's actions (favorites, edit, delete, etc.) - The cards are 4 separate focus stops with no spatial organization - No way to move "right" or "down" between cards in the grid The Gallery is a grid of cards. Standard grid keyboard navigation is: - Arrow Right/Down → next card - Arrow Left/Up → previous card - Enter → open the design (or its action menu) - Space → toggle favorite ## Where - Gallery: `src/pages/Gallery.tsx` (no keyboard listener) - DesignCard: `src/components/gallery/DesignCard.tsx` (no `onKeyDown` on the card itself) - No roving tabindex: `grep -n 'tabIndex' src/components/gallery/DesignCard.tsx` returns 0 hits ## Suggested fix shape Add a keyboard listener to the Gallery's grid container: ```tsx useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); moveSelection(1); } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); moveSelection(-1); } else if (e.key === 'Enter') { e.preventDefault(); openSelected(); } else if (e.key === ' ') { e.preventDefault(); toggleFavoriteSelected(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [filteredDesigns, selectedCardIndex]); const moveSelection = (delta: number) => { const newIndex = (selectedCardIndex + delta + filteredDesigns.length) % filteredDesigns.length; setSelectedCardIndex(newIndex); // Scroll the card into view document.querySelector(`[data-card-index="${newIndex}"]`)?.scrollIntoView({ block: 'nearest' }); }; ``` Add `tabIndex={0}` and `data-card-index={i}` to the design card: ```tsx <div data-card-index={i} tabIndex={i === selectedCardIndex ? 0 : -1} onKeyDown={(e) => { /* handled by the global listener */ }} > ... </div> ``` The card's focus is managed by the global listener; only the active card has `tabIndex={0}`, the rest are -1. This is the "roving tabindex" pattern, used in most grid UIs (file managers, photo galleries, etc.). ## Severity Medium. Keyboard-only users can't use the Gallery effectively. Power users with trackpad issues notice. ## Discovered by Gap analysis (2026-10-02) of gallery accessibility. ## Related - The Gallery's folder nav has clickable folder chips but no keyboard navigation either. - The Editor has comprehensive keyboard shortcuts (per the kanban landings). - The Workspace has the Workspace's canvas keyboard but no gallery-level shortcuts. - Most modern web apps support roving-tabindex grids; this is a standard pattern.
Author
Owner

Closed in commit 6dc99c2 — feat(#240): Gallery keyboard navigation.

Behavior added:

  • Arrow keys (←/↑/→/↓) move focus between cards; selection wraps at both ends.
  • Enter opens the focused card in the Workspace (when a snapshot is present; otherwise toasts).
  • Space toggles the focused card’s favorite status.
  • Roving tabindex: only the active card has tabIndex={0}; the rest are -1.
  • ARIA: grid/list container is role="listbox", each card is role="option" with aria-selected, listbox has aria-activedescendant pointing at the active card so screen readers announce selection changes.
  • Modifier keys (Cmd/Ctrl/Alt/Shift) are NOT swallowed — browser/system chords win.
  • Selection-mode and the new-folder modal disable card-nav so neither steals focus.
  • When the filtered set shrinks below the selected index, selection clamps to the last available card.

Files: src/pages/Gallery.tsx, src/components/gallery/DesignCard.tsx, new tests in src/pages/issue-240.test.tsx. Full suite: 1234 passing.

Closed in commit 6dc99c2 — `feat(#240): Gallery keyboard navigation`. Behavior added: - Arrow keys (←/↑/→/↓) move focus between cards; selection wraps at both ends. - Enter opens the focused card in the Workspace (when a snapshot is present; otherwise toasts). - Space toggles the focused card’s favorite status. - Roving tabindex: only the active card has `tabIndex={0}`; the rest are -1. - ARIA: grid/list container is `role="listbox"`, each card is `role="option"` with `aria-selected`, listbox has `aria-activedescendant` pointing at the active card so screen readers announce selection changes. - Modifier keys (Cmd/Ctrl/Alt/Shift) are NOT swallowed — browser/system chords win. - Selection-mode and the new-folder modal disable card-nav so neither steals focus. - When the filtered set shrinks below the selected index, selection clamps to the last available card. Files: `src/pages/Gallery.tsx`, `src/components/gallery/DesignCard.tsx`, new tests in `src/pages/issue-240.test.tsx`. Full suite: 1234 passing.
Mark closed this issue 2026-10-04 08:23:34 +00:00
Sign in to join this conversation.
No description provided.