diff --git a/frontend/src/components/Grid.tsx b/frontend/src/components/Grid.tsx index 8041688..359644c 100644 --- a/frontend/src/components/Grid.tsx +++ b/frontend/src/components/Grid.tsx @@ -14,6 +14,15 @@ function getCellSize(gridSize: number): number { return 36 } +function firstWhiteIdx(cells: string[][], width: number): number { + for (let r = 0; r < cells.length; r++) { + for (let c = 0; c < cells[r].length; c++) { + if (cells[r][c] !== 'black') return r * width + c + } + } + return 0 +} + export default function Grid() { const { t } = useTranslation() const svgRef = useRef(null) @@ -42,6 +51,22 @@ export default function Grid() { cellRefs.current[idx]?.focus() }, [selectedCell, width]) + // land focus on the starting cell when a puzzle loads and nothing else has + // it, so typing works without clicking into the grid first + useEffect(() => { + if (!puzzle) return + const t = setTimeout(() => { + const active = document.activeElement + if (active !== document.body && active !== null) return + const st = useStore.getState() + const idx = st.selectedCell + ? st.selectedCell.row * width + st.selectedCell.col + : firstWhiteIdx(puzzle.cells, width) + cellRefs.current[idx]?.focus({ preventScroll: true }) + }, 0) + return () => clearTimeout(t) + }, [puzzle, width]) + if (!puzzle) return null const CELL = getCellSize(Math.max(width, height)) @@ -110,25 +135,23 @@ export default function Grid() { } const focusTypingField = (r: number, c: number) => { - if (!window.matchMedia('(pointer: coarse)').matches) return - const field = typingRef.current - if (field) { - field.setAttribute('aria-label', buildCellLabel(r, c)) - field.focus({ preventScroll: true }) + if (window.matchMedia('(pointer: coarse)').matches) { + const field = typingRef.current + if (field) { + field.setAttribute('aria-label', buildCellLabel(r, c)) + field.focus({ preventScroll: true }) + } + } else { + // desktop: park focus on the matching grid cell so keystrokes reach it + cellRefs.current[r * width + c]?.focus() } } // Exactly one cell is a Tab stop so the grid can receive keyboard focus even // before a cell has been selected. - const focusableIdx = (() => { - if (selectedCell) return selectedCell.row * width + selectedCell.col - for (let r = 0; r < height; r++) { - for (let c = 0; c < width; c++) { - if (puzzle.cells[r][c] !== 'black') return r * width + c - } - } - return 0 - })() + const focusableIdx = selectedCell + ? selectedCell.row * width + selectedCell.col + : firstWhiteIdx(puzzle.cells, width) return (
{ - if (isBlack) return + if (isBlack) { + // keep keyboard focus inside the grid after tapping a black square + if (!window.matchMedia('(pointer: coarse)').matches) { + cellRefs.current[focusableIdx]?.focus() + } + return + } selectCell(r, c) focusTypingField(r, c) }}