material-react-table
Version:
A fully featured Material UI V6 implementation of TanStack React Table V8, written from the ground up in TypeScript.
235 lines (221 loc) • 6.76 kB
text/typescript
import {
MRT_Header,
type MRT_Cell,
type MRT_RowData,
type MRT_TableInstance,
} from '../types';
import {
getMRT_RowSelectionHandler,
getMRT_SelectAllHandler,
} from './row.utils';
import { parseFromValuesOrFunc } from './utils';
const isWinCtrlMacMeta = (event: React.KeyboardEvent<HTMLTableCellElement>) => {
return (
(event.ctrlKey && navigator.platform.toLowerCase().includes('win')) ||
(event.metaKey && navigator.platform.toLowerCase().includes('mac'))
);
};
export const isCellEditable = <TData extends MRT_RowData>({
cell,
table,
}: {
cell: MRT_Cell<TData>;
table: MRT_TableInstance<TData>;
}) => {
const { enableEditing } = table.options;
const {
column: { columnDef },
row,
} = cell;
return (
!cell.getIsPlaceholder() &&
parseFromValuesOrFunc(enableEditing, row) &&
parseFromValuesOrFunc(columnDef.enableEditing, row) !== false
);
};
export const openEditingCell = <TData extends MRT_RowData>({
cell,
table,
}: {
cell: MRT_Cell<TData>;
table: MRT_TableInstance<TData>;
}) => {
const {
options: { editDisplayMode },
refs: { editInputRefs },
} = table;
const { column } = cell;
if (isCellEditable({ cell, table }) && editDisplayMode === 'cell') {
table.setEditingCell(cell);
queueMicrotask(() => {
const textField = editInputRefs.current?.[column.id];
if (textField) {
textField.focus();
textField.select?.();
}
});
}
};
export const cellKeyboardShortcuts = <TData extends MRT_RowData = MRT_RowData>({
cell,
cellElements,
cellValue,
containerElement,
event,
header,
parentElement,
table,
}: {
cell?: MRT_Cell<TData>;
header?: MRT_Header<TData>;
cellElements?: Array<HTMLTableCellElement>;
cellValue?: string;
containerElement?: HTMLTableElement;
event: React.KeyboardEvent<HTMLTableCellElement>;
parentElement?: HTMLTableRowElement;
table: MRT_TableInstance<TData>;
}) => {
if (!table.options.enableKeyboardShortcuts) return;
if (event.isPropagationStopped()) return;
const currentCell = event.currentTarget;
if (cellValue && isWinCtrlMacMeta(event) && event.key === 'c') {
navigator.clipboard.writeText(cellValue);
} else if (['Enter', ' '].includes(event.key)) {
if (cell?.column?.id === 'mrt-row-select') {
event.preventDefault();
getMRT_RowSelectionHandler({
row: cell.row,
table,
//@ts-expect-error
staticRowIndex: +event.target.getAttribute('data-index'),
})(event as any);
} else if (
header?.column?.id === 'mrt-row-select' &&
table.options.enableSelectAll
) {
event.preventDefault();
getMRT_SelectAllHandler({
table,
})(event as any);
} else if (
cell?.column?.id === 'mrt-row-expand' &&
(cell.row.getCanExpand() ||
table.options.renderDetailPanel?.({ row: cell.row, table }))
) {
event.preventDefault();
cell.row.toggleExpanded();
} else if (
header?.column?.id === 'mrt-row-expand' &&
table.options.enableExpandAll
) {
event.preventDefault();
table.toggleAllRowsExpanded();
} else if (cell?.column.id === 'mrt-row-pin') {
event.preventDefault();
cell.row.getIsPinned()
? cell.row.pin(false)
: cell.row.pin(
table.options.rowPinningDisplayMode?.includes('bottom')
? 'bottom'
: 'top',
);
} else if (header && isWinCtrlMacMeta(event)) {
const actionsButton = currentCell.querySelector(
`button[aria-label="${table.options.localization.columnActions}"]`,
);
if (actionsButton) {
(actionsButton as HTMLButtonElement).click();
}
} else if (header?.column?.getCanSort()) {
event.preventDefault();
header.column.toggleSorting();
}
} else if (
[
'ArrowRight',
'ArrowLeft',
'ArrowUp',
'ArrowDown',
'Home',
'End',
'PageUp',
'PageDown',
].includes(event.key)
) {
event.preventDefault();
const currentRow = parentElement || currentCell.closest('tr');
const tableElement = containerElement || currentCell.closest('table');
const allCells =
cellElements ||
Array.from(tableElement?.querySelectorAll('th, td') || []);
const currentCellIndex = allCells.indexOf(currentCell);
const currentIndex = parseInt(
currentCell.getAttribute('data-index') || '0',
);
let nextCell: HTMLElement | undefined = undefined;
//home/end first or last cell in row
const findEdgeCell = (rowIndex: 'c' | 'f' | 'l', edge: 'f' | 'l') => {
const row =
rowIndex === 'c'
? currentRow
: rowIndex === 'f'
? tableElement?.querySelector('tr')
: tableElement?.lastElementChild?.lastElementChild;
const rowCells = Array.from(row?.children || []);
const targetCell =
edge === 'f' ? rowCells[0] : rowCells[rowCells.length - 1];
return targetCell as HTMLElement;
};
//page up/down first or last cell in column
const findBottomTopCell = (columnIndex: number, edge: 'b' | 't') => {
const row =
edge === 't'
? tableElement?.querySelector('tr')
: tableElement?.lastElementChild?.lastElementChild;
const rowCells = Array.from(row?.children || []);
const targetCell = rowCells[columnIndex];
return targetCell as HTMLElement;
};
const findAdjacentCell = (
columnIndex: number,
searchDirection: 'f' | 'b',
) => {
const searchArray =
searchDirection === 'f'
? allCells.slice(currentCellIndex + 1)
: allCells.slice(0, currentCellIndex).reverse();
return searchArray.find((cell) =>
cell.matches(`[data-index="${columnIndex}"]`),
) as HTMLElement | undefined;
};
switch (event.key) {
case 'ArrowRight':
nextCell = findAdjacentCell(currentIndex + 1, 'f');
break;
case 'ArrowLeft':
nextCell = findAdjacentCell(currentIndex - 1, 'b');
break;
case 'ArrowUp':
nextCell = findAdjacentCell(currentIndex, 'b');
break;
case 'ArrowDown':
nextCell = findAdjacentCell(currentIndex, 'f');
break;
case 'Home':
nextCell = findEdgeCell(isWinCtrlMacMeta(event) ? 'f' : 'c', 'f');
break;
case 'End':
nextCell = findEdgeCell(isWinCtrlMacMeta(event) ? 'l' : 'c', 'l');
break;
case 'PageUp':
nextCell = findBottomTopCell(currentIndex, 't');
break;
case 'PageDown':
nextCell = findBottomTopCell(currentIndex, 'b');
break;
}
if (nextCell) {
nextCell.focus();
}
}
};