restore grid keyboard focus on load and cell click

This commit is contained in:
2026-08-25 15:05:13 +03:00
parent a9abf582f5
commit b425e7f3b6
+40 -11
View File
@@ -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<SVGSVGElement>(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
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 (
<div
@@ -178,7 +201,13 @@ export default function Grid() {
id={`cell-${r}-${c}`}
data-cell={`${r},${c}`}
onClick={() => {
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)
}}