Hook for keyboard navigation functionality.
The useKeyboardNavigation hook provides keyboard navigation functionality for custom grid implementations. It manages focus state, handles keyboard events, and provides props for cells and containers.
import { useKeyboardNavigation } from '@workspace/active-grid';
const {
focusedCell,
setFocusedCell,
getCellProps,
registerCellRef,
containerProps,
} = useKeyboardNavigation(table, {
enabled: true,
onCellActivate: (position) => {
console.log('Cell activated:', position);
},
});function useKeyboardNavigation<TData>(
table: Table<TData>,
options?: KeyboardNavigationOptions
): KeyboardNavigationResultinterface KeyboardNavigationOptions {
enabled?: boolean;
onCellActivate?: (position: CellPosition) => void;
}booleantrueuseKeyboardNavigation(table, {
enabled: true,
});(position: CellPosition) => voiduseKeyboardNavigation(table, {
onCellActivate: (position) => {
console.log('Activated cell:', position);
},
});interface KeyboardNavigationResult {
focusedCell: CellPosition | null;
setFocusedCell: (position: CellPosition | null) => void;
getCellProps: (rowIndex: number, colIndex: number) => CellProps;
registerCellRef: (rowIndex: number, colIndex: number, element: HTMLElement | null) => void;
containerProps: ContainerProps;
}Current focused cell position:
const { focusedCell } = useKeyboardNavigation(table);
if (focusedCell) {
console.log('Row:', focusedCell.rowIndex);
console.log('Column:', focusedCell.colIndex);
}Type:
type CellPosition = {
rowIndex: number;
colIndex: number;
} | null;Programmatically set focused cell:
const { setFocusedCell } = useKeyboardNavigation(table);
// Focus specific cell
setFocusedCell({ rowIndex: 0, colIndex: 1 });
// Clear focus
setFocusedCell(null);Get props to spread on cell elements:
const { getCellProps } = useKeyboardNavigation(table);
const cellProps = getCellProps(rowIndex, colIndex);
<div {...cellProps}>
Cell content
</div>Returns:
{
tabIndex: number;
onKeyDown: (e: KeyboardEvent) => void;
onFocus: () => void;
'data-focused': boolean;
}Register cell DOM references:
const { registerCellRef } = useKeyboardNavigation(table);
<div ref={(el) => registerCellRef(rowIndex, colIndex, el)}>
Cell content
</div>Props to spread on grid container:
const { containerProps } = useKeyboardNavigation(table);
<div {...containerProps}>
{/* Grid content */}
</div>Returns:
{
onKeyDown: (e: KeyboardEvent) => void;
tabIndex: -1;
}The hook handles these keyboard shortcuts automatically:
CSS approach:
The hook ensures keyboard navigation is accessible:
The hook is optimized for large grids:
The ActiveGrid component uses this hook internally. You only need it when building custom grid implementations.
import { useKeyboardNavigation } from '@workspace/active-grid';
function CustomGrid() {
const table = useActiveGrid<User>();
const {
focusedCell,
getCellProps,
registerCellRef,
containerProps,
} = useKeyboardNavigation(table);
return (
<div {...containerProps} className="grid-container">
{table.getRowModel().rows.map((row, rowIndex) => (
<div key={row.id} className="grid-row">
{row.getVisibleCells().map((cell, colIndex) => (
<div
key={cell.id}
{...getCellProps(rowIndex, colIndex)}
ref={(el) => registerCellRef(rowIndex, colIndex, el)}
className="grid-cell"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
))}
</div>
))}
</div>
);
}const { getCellProps } = useKeyboardNavigation(table, {
onCellActivate: (position) => {
const row = table.getRowModel().rows[position.rowIndex];
const cell = row.getVisibleCells()[position.colIndex];
// Handle activation
console.log('Activated:', cell.column.id);
},
});const { getCellProps } = useKeyboardNavigation(table);
<div
{...getCellProps(rowIndex, colIndex)}
className={cn(
'grid-cell',
focusedCell?.rowIndex === rowIndex &&
focusedCell?.colIndex === colIndex &&
'ring-2 ring-primary'
)}
>
{content}
</div>.grid-cell[data-focused="true"] {
outline: 2px solid var(--ring);
outline-offset: -2px;
}import { useKeyboardNavigation } from '@workspace/active-grid';
import { flexRender } from '@tanstack/react-table';
function CustomDataGrid() {
const table = useActiveGrid<Product>();
const {
focusedCell,
setFocusedCell,
getCellProps,
registerCellRef,
containerProps,
} = useKeyboardNavigation(table, {
enabled: true,
onCellActivate: (position) => {
const row = table.getRowModel().rows[position.rowIndex];
const cell = row.getVisibleCells()[position.colIndex];
// Start editing on activation
if (cell.column.columnDef.meta?.editable) {
startEditing(row.original, cell.column.id);
}
},
});
const handleCellClick = (rowIndex: number, colIndex: number) => {
setFocusedCell({ rowIndex, colIndex });
};
return (
<div {...containerProps} className="grid-container">
{/* Header */}
<div className="grid-header-row">
{table.getFlatHeaders().map((header, colIndex) => (
<div key={header.id} className="grid-header-cell">
{flexRender(header.column.columnDef.header, header.getContext())}
</div>
))}
</div>
{/* Body */}
<div className="grid-body">
{table.getRowModel().rows.map((row, rowIndex) => (
<div key={row.id} className="grid-row">
{row.getVisibleCells().map((cell, colIndex) => {
const cellProps = getCellProps(rowIndex, colIndex);
const isFocused =
focusedCell?.rowIndex === rowIndex &&
focusedCell?.colIndex === colIndex;
return (
<div
key={cell.id}
{...cellProps}
ref={(el) => registerCellRef(rowIndex, colIndex, el)}
onClick={() => handleCellClick(rowIndex, colIndex)}
className={cn(
'grid-cell',
isFocused && 'ring-2 ring-primary'
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
);
})}
</div>
))}
</div>
</div>
);
}function EditableGrid() {
const table = useActiveGrid<User>();
const [editingCell, setEditingCell] = useState<CellPosition | null>(null);
const { getCellProps, registerCellRef, containerProps } = useKeyboardNavigation(
table,
{
onCellActivate: (position) => {
setEditingCell(position);
},
}
);
const handleSave = () => {
setEditingCell(null);
};
return (
<div {...containerProps}>
{table.getRowModel().rows.map((row, rowIndex) => (
<div key={row.id}>
{row.getVisibleCells().map((cell, colIndex) => {
const isEditing =
editingCell?.rowIndex === rowIndex &&
editingCell?.colIndex === colIndex;
if (isEditing) {
return (
<input
key={cell.id}
defaultValue={cell.getValue() as string}
onBlur={handleSave}
autoFocus
/>
);
}
return (
<div
key={cell.id}
{...getCellProps(rowIndex, colIndex)}
ref={(el) => registerCellRef(rowIndex, colIndex, el)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
);
})}
</div>
))}
</div>
);
}// Disable for readonly grids
const navigation = useKeyboardNavigation(table, {
enabled: false,
});
// Conditionally enable
const [isNavigationEnabled, setNavigationEnabled] = useState(true);
const navigation = useKeyboardNavigation(table, {
enabled: isNavigationEnabled,
});import { Table } from '@tanstack/react-table';
import { useKeyboardNavigation, CellPosition } from '@workspace/active-grid';
type User = {
id: string;
name: string;
};
function MyGrid() {
const table: Table<User> = useActiveGrid<User>();
const {
focusedCell,
setFocusedCell,
getCellProps,
registerCellRef,
containerProps,
} = useKeyboardNavigation(table, {
enabled: true,
onCellActivate: (position: CellPosition) => {
console.log(position);
},
});
return <div {...containerProps}>{/* ... */}</div>;
}