useGridFocus@astryxdesign/core v0.5.2 · useGridFocus

Usage

Manages keyboard navigation within a 2D grid following the WAI-ARIA grid pattern. Supports arrow keys for cell-to-cell navigation, Home/End for row boundaries, Ctrl+Home/Ctrl+End for grid boundaries, and Page Up/Down for custom callbacks (e.g., month navigation in calendars). Boundary navigation callbacks allow cross-grid navigation.

ts
import {useGridFocus} from '@astryxdesign/core/hooks'

Best practices

GuidancePractices
Do

Use for calendar date grids: wire onPageUp/onPageDown to month navigation and onNavigateBefore/onNavigateAfter for cross-month arrow key navigation.

Do

Attach both gridRef and handleKeyDown to the grid container element.

Do

For roving-tabindex grids (e.g. Calendar), set hasRovingTabIndex: true and attach handleFocus to the container onFocus; seed one focus target with tabindex=0 and the hook repairs and moves it.

Don't

Use for simple linear lists; prefer useListFocus for 1D navigation.

Parameters

ParamTypeDescription
optionsrequired

Configuration object for grid focus behavior.

options.columnsrequired
number

Number of columns in the grid. Used for up/down navigation (moves by this many cells).

options.cellSelector
string (default: 'button:not([disabled]), [tabindex]:not([tabindex="-1"])')

Selector for cells within the grid. Should match ALL cell positions in DOM order (including disabled/empty) so grid geometry is preserved.

options.isCellFocusable
(cell: HTMLElement) => boolean

Predicate for whether a matched cell can receive focus. Omit to treat every matched cell as focusable.

options.getFocusTarget
(cell: HTMLElement) => HTMLElement | null

Resolves the element to focus for a cell, e.g. a button inside a role="gridcell" wrapper. Omit to focus the cell itself.

options.onNavigateBefore
(column: number, offset: number) => void

Callback when navigation would go before the first cell. Receives the column index and offset (1 for horizontal, columns for vertical).

options.onNavigateAfter
(column: number, offset: number) => void

Callback when navigation would go after the last cell. Receives the column index and offset.

options.onPageUp
() => void

Callback for Page Up key (e.g., navigate to previous month in calendars).

options.onPageDown
() => void

Callback for Page Down key (e.g., navigate to next month in calendars).

options.isRtl
boolean (default: undefined (auto-detect from the container's computed direction))

Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts. When omitted, auto-detected from the container computed direction on keydown.

options.hasRovingTabIndex
boolean (default: false)

Own a single roving tab stop across the grid: one focusable cell (its resolved focus target) carries tabindex="0", the rest -1. Stamped/repaired on render and moved with arrow navigation. Attach the returned handleFocus to the container onFocus.

Returns

FieldTypeDescription
gridRefReact.RefObject<HTMLElement | null>

Ref to attach to the grid container element.

handleKeyDown(e: React.KeyboardEvent) => void

Key down handler to attach to the grid container.

handleFocus(e: React.FocusEvent) => void

Focus handler for the grid container. Keeps the roving tab stop in sync when hasRovingTabIndex is enabled; a no-op otherwise, so always safe to attach.

focusCell(index: number) => void

Focus a specific cell by index (clamped to valid range).

focusFirst() => void

Focus the first focusable cell in the grid.

focusLast() => void

Focus the last focusable cell in the grid.