diff --git a/src/components/table/PlainTable.tsx b/src/components/table/PlainTable.tsx index 8ec8988..0a7fc22 100644 --- a/src/components/table/PlainTable.tsx +++ b/src/components/table/PlainTable.tsx @@ -649,6 +649,7 @@ export function PlainTable({ }, [onRowClick], ), + tableRef, ); // ── Row click handler ───────────────────────── diff --git a/src/components/table/useKeyboardNav.ts b/src/components/table/useKeyboardNav.ts index 3e620c3..3f30eff 100644 --- a/src/components/table/useKeyboardNav.ts +++ b/src/components/table/useKeyboardNav.ts @@ -12,15 +12,27 @@ import type { TableRow, SelectionState } from './types'; /** Number of rows to jump with PageUp / PageDown. */ const PAGE_SIZE = 10; +/** + * Elements that consume keystrokes themselves. The nav binds bare letters (j/k), Space, Enter, + * Home and End, so any of these inside a cell would otherwise be unusable from the keyboard. + */ +const INTERACTIVE_CONTENT = + 'a[href], button, input, select, textarea, [contenteditable]:not([contenteditable="false"])'; + /** * Scroll the active row into view within the table container. + * + * Scoped to the grid that owns the event when a container is supplied. Row numbers restart at 0 in + * every grid, so a document-wide lookup finds the first match on the page rather than this grid's. */ -function scrollActiveRowIntoView(rowNum: number) { +function scrollActiveRowIntoView( + rowNum: number, + container?: HTMLElement | null, +) { // Defer to allow React to render the new selection state first requestAnimationFrame(() => { - const el = document.querySelector( - `[data-row-num="${rowNum}"]`, - ); + const root: ParentNode = container ?? document; + const el = root.querySelector(`[data-row-num="${rowNum}"]`); el?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }); } @@ -30,11 +42,18 @@ export function useKeyboardNav( selection: SelectionState, onSelectionChange?: (selection: SelectionState) => void, onRowClick?: (row: TableRow, event: React.KeyboardEvent) => void, + containerRef?: React.RefObject, ) { const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { if (rows.length === 0) return; + // Let interactive cell content handle its own keys. Without this, Enter on an in-cell link + // activates the row instead of following the link, and typing "j" in an in-cell input moves + // the selection instead of typing a letter. + const target = event.target as HTMLElement | null; + if (target?.closest?.(INTERACTIVE_CONTENT)) return; + const { activeRow } = selection; let nextRow: number | null = null; @@ -126,10 +145,10 @@ export function useKeyboardNav( : new Set([nextRow]), }; onSelectionChange?.(newSelection); - scrollActiveRowIntoView(nextRow); + scrollActiveRowIntoView(nextRow, containerRef?.current); } }, - [rows, selection, onSelectionChange, onRowClick], + [rows, selection, onSelectionChange, onRowClick, containerRef], ); return { handleKeyDown };