mirror of
https://github.com/docmost/docmost.git
synced 2026-08-30 18:36:26 +08:00
feat(base): keyboard navigation controller and grid wiring
This commit is contained in:
@@ -7,8 +7,23 @@ import {
|
|||||||
windowScroll,
|
windowScroll,
|
||||||
} from "@tanstack/react-virtual";
|
} from "@tanstack/react-virtual";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { IBaseRow, IBaseProperty, EditingCell } from "@/ee/base/types/base.types";
|
import {
|
||||||
import { editingCellAtomFamily } from "@/ee/base/atoms/base-atoms";
|
IBaseRow,
|
||||||
|
IBaseProperty,
|
||||||
|
EditingCell,
|
||||||
|
FocusedCell,
|
||||||
|
CellCoord,
|
||||||
|
} from "@/ee/base/types/base.types";
|
||||||
|
import {
|
||||||
|
editingCellAtomFamily,
|
||||||
|
focusedCellAtomFamily,
|
||||||
|
activeFormulaEditorAtomFamily,
|
||||||
|
pendingTypeInsertAtom,
|
||||||
|
type FormulaEditorTarget,
|
||||||
|
type PendingTypeInsert,
|
||||||
|
} from "@/ee/base/atoms/base-atoms";
|
||||||
|
import { isSystemPropertyType } from "@/ee/base/property-types/property-type.registry";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useColumnResize } from "@/ee/base/hooks/use-column-resize";
|
import { useColumnResize } from "@/ee/base/hooks/use-column-resize";
|
||||||
import { useGridKeyboardNav } from "@/ee/base/hooks/use-grid-keyboard-nav";
|
import { useGridKeyboardNav } from "@/ee/base/hooks/use-grid-keyboard-nav";
|
||||||
import { useRowAutoScroll } from "@/ee/base/hooks/use-row-autoscroll";
|
import { useRowAutoScroll } from "@/ee/base/hooks/use-row-autoscroll";
|
||||||
@@ -113,6 +128,14 @@ export function GridContainer({
|
|||||||
const { selectionCount, clear: clearSelection } = useRowSelection(pageId);
|
const { selectionCount, clear: clearSelection } = useRowSelection(pageId);
|
||||||
const { deleteSelected } = useDeleteSelectedRows(pageId);
|
const { deleteSelected } = useDeleteSelectedRows(pageId);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [focusedCell, setFocusedCell] = useAtom(focusedCellAtomFamily(pageId)) as unknown as [FocusedCell, (val: FocusedCell) => void];
|
||||||
|
const focusedCellRef = useRef(focusedCell);
|
||||||
|
focusedCellRef.current = focusedCell;
|
||||||
|
const [, setActiveFormulaEditor] = useAtom(activeFormulaEditorAtomFamily(pageId)) as unknown as [FormulaEditorTarget, (val: FormulaEditorTarget) => void];
|
||||||
|
const [, setPendingTypeInsert] = useAtom(pendingTypeInsertAtom) as unknown as [PendingTypeInsert, (val: PendingTypeInsert) => void];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
// Only act while an inline cell editor is open. Popover-based cells
|
// Only act while an inline cell editor is open. Popover-based cells
|
||||||
@@ -141,12 +164,6 @@ export function GridContainer({
|
|||||||
|
|
||||||
useColumnResize(table, onResizeEnd ?? (() => {}));
|
useColumnResize(table, onResizeEnd ?? (() => {}));
|
||||||
|
|
||||||
useGridKeyboardNav({
|
|
||||||
table,
|
|
||||||
editingCell,
|
|
||||||
setEditingCell,
|
|
||||||
containerRef: bodyRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
// When the scroll container is the window (inline embed mode), the default
|
// When the scroll container is the window (inline embed mode), the default
|
||||||
// Element-mode observers read scrollTop/scrollLeft, which Window does not
|
// Element-mode observers read scrollTop/scrollLeft, which Window does not
|
||||||
@@ -226,6 +243,143 @@ export function GridContainer({
|
|||||||
|
|
||||||
const virtualItems = virtualizer.getVirtualItems();
|
const virtualItems = virtualizer.getVirtualItems();
|
||||||
|
|
||||||
|
const pinnedLeftWidth = useCallback(
|
||||||
|
() =>
|
||||||
|
table
|
||||||
|
.getVisibleLeafColumns()
|
||||||
|
.filter((c) => c.getIsPinned() === "left")
|
||||||
|
.reduce((sum, c) => sum + c.getSize(), 0),
|
||||||
|
[table],
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollCellIntoView = useCallback(
|
||||||
|
(coord: CellCoord, rowIndex: number) => {
|
||||||
|
if (rowIndex >= 0) virtualizer.scrollToIndex(rowIndex, { align: "auto" });
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const scroller = bodyRef.current;
|
||||||
|
const el = document.getElementById(
|
||||||
|
`base-cell-${coord.rowId}-${coord.propertyId}`,
|
||||||
|
);
|
||||||
|
if (!scroller || !el) return;
|
||||||
|
const cellRect = el.getBoundingClientRect();
|
||||||
|
const scRect = scroller.getBoundingClientRect();
|
||||||
|
const pinned = pinnedLeftWidth();
|
||||||
|
if (cellRect.left < scRect.left + pinned) {
|
||||||
|
scroller.scrollLeft -= scRect.left + pinned - cellRect.left;
|
||||||
|
} else if (cellRect.right > scRect.right) {
|
||||||
|
scroller.scrollLeft += cellRect.right - scRect.right;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[virtualizer, pinnedLeftWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openEditor = useCallback(
|
||||||
|
(coord: CellCoord) => {
|
||||||
|
const prop = properties.find((p) => p.id === coord.propertyId);
|
||||||
|
if (!prop) return;
|
||||||
|
if (prop.type === "checkbox") {
|
||||||
|
if (!editable) return;
|
||||||
|
const current = table.getRow(coord.rowId, true)?.getValue(coord.propertyId);
|
||||||
|
onCellUpdate(coord.rowId, coord.propertyId, !current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!editable) {
|
||||||
|
if (prop.type === "file") setEditingCell(coord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prop.type === "formula") {
|
||||||
|
setActiveFormulaEditor({ propertyId: coord.propertyId, rowId: coord.rowId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSystemPropertyType(prop.type)) return;
|
||||||
|
setEditingCell(coord);
|
||||||
|
},
|
||||||
|
[properties, editable, table, onCellUpdate, setEditingCell, setActiveFormulaEditor],
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearCell = useCallback(
|
||||||
|
(coord: CellCoord) => {
|
||||||
|
if (!editable) return;
|
||||||
|
const prop = properties.find((p) => p.id === coord.propertyId);
|
||||||
|
if (!prop || isSystemPropertyType(prop.type)) return;
|
||||||
|
onCellUpdate(coord.rowId, coord.propertyId, null);
|
||||||
|
},
|
||||||
|
[editable, properties, onCellUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const beginTypeToEdit = useCallback(
|
||||||
|
(coord: CellCoord, char: string) => {
|
||||||
|
if (!editable) return;
|
||||||
|
const prop = properties.find((p) => p.id === coord.propertyId);
|
||||||
|
if (!prop || isSystemPropertyType(prop.type) || prop.type === "checkbox") return;
|
||||||
|
if (["text", "number", "url", "email"].includes(prop.type)) {
|
||||||
|
setPendingTypeInsert({ rowId: coord.rowId, propertyId: coord.propertyId, char });
|
||||||
|
setEditingCell(coord);
|
||||||
|
} else {
|
||||||
|
openEditor(coord);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[editable, properties, setPendingTypeInsert, setEditingCell, openEditor],
|
||||||
|
);
|
||||||
|
|
||||||
|
const prevEditingRef = useRef(editingCell);
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevEditingRef.current;
|
||||||
|
prevEditingRef.current = editingCell;
|
||||||
|
if (prev && !editingCell) {
|
||||||
|
if (!focusedCellRef.current) setFocusedCell(prev);
|
||||||
|
const grid = bodyRef.current;
|
||||||
|
const active = document.activeElement;
|
||||||
|
if (grid && active && !grid.contains(active)) {
|
||||||
|
grid.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editingCell, setFocusedCell]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fc = focusedCellRef.current;
|
||||||
|
if (!fc) return;
|
||||||
|
const rowOk = rowIds.includes(fc.rowId);
|
||||||
|
const colOk = table.getVisibleLeafColumns().some((c) => c.id === fc.propertyId);
|
||||||
|
if (!rowOk || !colOk) setFocusedCell(null);
|
||||||
|
}, [rowIds, table.getState().columnVisibility, table.getState().columnOrder, setFocusedCell]);
|
||||||
|
|
||||||
|
const handleGridFocus = useCallback(
|
||||||
|
(e: React.FocusEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target !== e.currentTarget) return;
|
||||||
|
if (editingCellRef.current || focusedCellRef.current) return;
|
||||||
|
const firstRow = rowIdsRef.current[0];
|
||||||
|
const firstCol = table
|
||||||
|
.getVisibleLeafColumns()
|
||||||
|
.find((c) => c.id !== "__row_number")?.id;
|
||||||
|
if (firstRow && firstCol) setFocusedCell({ rowId: firstRow, propertyId: firstCol });
|
||||||
|
},
|
||||||
|
[table, setFocusedCell],
|
||||||
|
);
|
||||||
|
|
||||||
|
useGridKeyboardNav({
|
||||||
|
table,
|
||||||
|
properties,
|
||||||
|
containerRef: bodyRef,
|
||||||
|
focusedCell,
|
||||||
|
setFocusedCell,
|
||||||
|
editingCell,
|
||||||
|
setEditingCell,
|
||||||
|
openEditor,
|
||||||
|
clearCell,
|
||||||
|
beginTypeToEdit,
|
||||||
|
scrollCellIntoView,
|
||||||
|
selectionCount,
|
||||||
|
clearSelection,
|
||||||
|
deleteSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeCell = editingCell ?? focusedCell;
|
||||||
|
const activeDescendantId = activeCell
|
||||||
|
? `base-cell-${activeCell.rowId}-${activeCell.propertyId}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasNextPage || isFetchingNextPage || !onFetchNextPage) return;
|
if (!hasNextPage || isFetchingNextPage || !onFetchNextPage) return;
|
||||||
const lastItem = virtualItems[virtualItems.length - 1];
|
const lastItem = virtualItems[virtualItems.length - 1];
|
||||||
@@ -244,29 +398,6 @@ export function GridContainer({
|
|||||||
}
|
}
|
||||||
}, [rows.length]);
|
}, [rows.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = bodyRef.current;
|
|
||||||
if (!el || !pageId) return;
|
|
||||||
const handler = (e: KeyboardEvent) => {
|
|
||||||
if (editingCell) return;
|
|
||||||
const active = document.activeElement as HTMLElement | null;
|
|
||||||
if (!active || !el.contains(active)) return;
|
|
||||||
const tag = active.tagName;
|
|
||||||
if (tag === "INPUT" || tag === "TEXTAREA" || active.isContentEditable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Escape" && selectionCount > 0) {
|
|
||||||
clearSelection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((e.key === "Delete" || e.key === "Backspace") && selectionCount > 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
void deleteSelected();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
el.addEventListener("keydown", handler);
|
|
||||||
return () => el.removeEventListener("keydown", handler);
|
|
||||||
}, [editingCell, selectionCount, clearSelection, deleteSelected, pageId]);
|
|
||||||
|
|
||||||
const gridTemplateColumns = useMemo(() => {
|
const gridTemplateColumns = useMemo(() => {
|
||||||
const visibleColumns = table.getVisibleLeafColumns();
|
const visibleColumns = table.getVisibleLeafColumns();
|
||||||
@@ -314,7 +445,7 @@ export function GridContainer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="grid" style={GRID_ROOT_STYLE}>
|
<div style={GRID_ROOT_STYLE}>
|
||||||
{aboveBand}
|
{aboveBand}
|
||||||
<div className={classes.stickyBand}>
|
<div className={classes.stickyBand}>
|
||||||
<div
|
<div
|
||||||
@@ -341,6 +472,13 @@ export function GridContainer({
|
|||||||
className={classes.bodyGrid}
|
className={classes.bodyGrid}
|
||||||
ref={bodyRef}
|
ref={bodyRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
role="grid"
|
||||||
|
aria-label={t("Base table")}
|
||||||
|
aria-rowcount={rows.length}
|
||||||
|
aria-colcount={table.getVisibleLeafColumns().length}
|
||||||
|
aria-multiselectable
|
||||||
|
aria-activedescendant={activeDescendantId}
|
||||||
|
onFocus={handleGridFocus}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--base-grid-cols": bodyGridTemplateColumns,
|
"--base-grid-cols": bodyGridTemplateColumns,
|
||||||
|
|||||||
@@ -1,120 +1,272 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { Table } from "@tanstack/react-table";
|
import { Table } from "@tanstack/react-table";
|
||||||
import { IBaseRow, EditingCell } from "@/ee/base/types/base.types";
|
import {
|
||||||
|
IBaseRow,
|
||||||
|
IBaseProperty,
|
||||||
|
EditingCell,
|
||||||
|
FocusedCell,
|
||||||
|
CellCoord,
|
||||||
|
} from "@/ee/base/types/base.types";
|
||||||
|
import { computeNextCell } from "@/ee/base/utils/grid-cell-nav";
|
||||||
|
|
||||||
type UseGridKeyboardNavOptions = {
|
type UseGridKeyboardNavOptions = {
|
||||||
table: Table<IBaseRow>;
|
table: Table<IBaseRow>;
|
||||||
|
properties: IBaseProperty[];
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
focusedCell: FocusedCell;
|
||||||
|
setFocusedCell: (cell: FocusedCell) => void;
|
||||||
editingCell: EditingCell;
|
editingCell: EditingCell;
|
||||||
setEditingCell: (cell: EditingCell) => void;
|
setEditingCell: (cell: EditingCell) => void;
|
||||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
openEditor: (coord: CellCoord) => void;
|
||||||
|
clearCell: (coord: CellCoord) => void;
|
||||||
|
beginTypeToEdit: (coord: CellCoord, char: string) => void;
|
||||||
|
scrollCellIntoView: (coord: CellCoord, rowIndex: number) => void;
|
||||||
|
selectionCount: number;
|
||||||
|
clearSelection: () => void;
|
||||||
|
deleteSelected: () => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPrintableKey = (e: KeyboardEvent) =>
|
||||||
|
e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
|
||||||
|
|
||||||
|
const isTextEntry = (el: Element | null) =>
|
||||||
|
!!el &&
|
||||||
|
(el.tagName === "INPUT" ||
|
||||||
|
el.tagName === "TEXTAREA" ||
|
||||||
|
(el as HTMLElement).isContentEditable);
|
||||||
|
|
||||||
export function useGridKeyboardNav({
|
export function useGridKeyboardNav({
|
||||||
table,
|
table,
|
||||||
|
properties,
|
||||||
|
containerRef,
|
||||||
|
focusedCell,
|
||||||
|
setFocusedCell,
|
||||||
editingCell,
|
editingCell,
|
||||||
setEditingCell,
|
setEditingCell,
|
||||||
containerRef,
|
openEditor,
|
||||||
|
clearCell,
|
||||||
|
beginTypeToEdit,
|
||||||
|
scrollCellIntoView,
|
||||||
|
selectionCount,
|
||||||
|
clearSelection,
|
||||||
|
deleteSelected,
|
||||||
}: UseGridKeyboardNavOptions) {
|
}: UseGridKeyboardNavOptions) {
|
||||||
const getNavigableColumns = useCallback(() => {
|
const getColIds = useCallback(
|
||||||
return table
|
() =>
|
||||||
.getVisibleLeafColumns()
|
table
|
||||||
.filter((col) => col.id !== "__row_number")
|
.getVisibleLeafColumns()
|
||||||
.map((col) => col.id);
|
.filter((col) => col.id !== "__row_number")
|
||||||
}, [table]);
|
.map((col) => col.id),
|
||||||
|
[table],
|
||||||
|
);
|
||||||
|
|
||||||
const getRowIds = useCallback(() => {
|
const getRowIds = useCallback(
|
||||||
return table.getRowModel().rows.map((row) => row.id);
|
() => table.getRowModel().rows.map((row) => row.id),
|
||||||
}, [table]);
|
[table],
|
||||||
|
);
|
||||||
|
|
||||||
const navigate = useCallback(
|
const propertyType = useCallback(
|
||||||
(rowDelta: number, colDelta: number) => {
|
(propertyId: string) => properties.find((p) => p.id === propertyId)?.type,
|
||||||
if (!editingCell) return;
|
[properties],
|
||||||
|
);
|
||||||
|
|
||||||
const columns = getNavigableColumns();
|
const goEditing = useCallback(
|
||||||
const rowIds = getRowIds();
|
(next: CellCoord) => {
|
||||||
|
|
||||||
const currentColIndex = columns.indexOf(editingCell.propertyId);
|
|
||||||
const currentRowIndex = rowIds.indexOf(editingCell.rowId);
|
|
||||||
|
|
||||||
if (currentColIndex === -1 || currentRowIndex === -1) return;
|
|
||||||
|
|
||||||
let nextColIndex = currentColIndex + colDelta;
|
|
||||||
let nextRowIndex = currentRowIndex + rowDelta;
|
|
||||||
|
|
||||||
if (nextColIndex < 0) {
|
|
||||||
nextColIndex = columns.length - 1;
|
|
||||||
nextRowIndex -= 1;
|
|
||||||
} else if (nextColIndex >= columns.length) {
|
|
||||||
nextColIndex = 0;
|
|
||||||
nextRowIndex += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextRowIndex < 0 || nextRowIndex >= rowIds.length) return;
|
|
||||||
|
|
||||||
// Blur fires onBlur->commit before React unmounts the input
|
|
||||||
(document.activeElement as HTMLElement | null)?.blur();
|
(document.activeElement as HTMLElement | null)?.blur();
|
||||||
|
setEditingCell(next);
|
||||||
setEditingCell({
|
setFocusedCell(next);
|
||||||
rowId: rowIds[nextRowIndex],
|
scrollCellIntoView(next, getRowIds().indexOf(next.rowId));
|
||||||
propertyId: columns[nextColIndex],
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[editingCell, getNavigableColumns, getRowIds, setEditingCell],
|
[setEditingCell, setFocusedCell, scrollCellIntoView, getRowIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const goFocused = useCallback(
|
||||||
|
(next: CellCoord) => {
|
||||||
|
setFocusedCell(next);
|
||||||
|
scrollCellIntoView(next, getRowIds().indexOf(next.rowId));
|
||||||
|
},
|
||||||
|
[setFocusedCell, scrollCellIntoView, getRowIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (!editingCell) return;
|
if (editingCell) {
|
||||||
|
const inInput = isTextEntry(e.target as Element);
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowUp":
|
||||||
|
case "ArrowDown":
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "ArrowRight": {
|
||||||
|
if (inInput) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const d =
|
||||||
|
e.key === "ArrowUp"
|
||||||
|
? [-1, 0]
|
||||||
|
: e.key === "ArrowDown"
|
||||||
|
? [1, 0]
|
||||||
|
: e.key === "ArrowLeft"
|
||||||
|
? [0, -1]
|
||||||
|
: [0, 1];
|
||||||
|
const next = computeNextCell(
|
||||||
|
getRowIds(),
|
||||||
|
getColIds(),
|
||||||
|
editingCell,
|
||||||
|
d[0],
|
||||||
|
d[1],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (next) goEditing(next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Tab": {
|
||||||
|
e.preventDefault();
|
||||||
|
const next = computeNextCell(
|
||||||
|
getRowIds(),
|
||||||
|
getColIds(),
|
||||||
|
editingCell,
|
||||||
|
0,
|
||||||
|
e.shiftKey ? -1 : 1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (next) goEditing(next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Enter": {
|
||||||
|
e.preventDefault();
|
||||||
|
const next = computeNextCell(
|
||||||
|
getRowIds(),
|
||||||
|
getColIds(),
|
||||||
|
editingCell,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
(document.activeElement as HTMLElement | null)?.blur();
|
||||||
|
setEditingCell(null);
|
||||||
|
if (next) goFocused(next);
|
||||||
|
else setFocusedCell(editingCell);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Escape": {
|
||||||
|
e.preventDefault();
|
||||||
|
(document.activeElement as HTMLElement | null)?.blur();
|
||||||
|
setEditingCell(null);
|
||||||
|
setFocusedCell(editingCell);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const target = e.target as HTMLElement;
|
if (isTextEntry(document.activeElement)) return;
|
||||||
const isInputActive =
|
|
||||||
target.tagName === "INPUT" ||
|
if (e.key === "Escape") {
|
||||||
target.tagName === "TEXTAREA" ||
|
if (selectionCount > 0) {
|
||||||
target.isContentEditable;
|
e.preventDefault();
|
||||||
|
clearSelection();
|
||||||
|
} else if (focusedCell) {
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusedCell(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Delete" || e.key === "Backspace") {
|
||||||
|
if (selectionCount > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
void deleteSelected();
|
||||||
|
} else if (focusedCell) {
|
||||||
|
e.preventDefault();
|
||||||
|
clearCell(focusedCell);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!focusedCell) return;
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
if (!isInputActive) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
{
|
||||||
navigate(-1, 0);
|
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, -1, 0, false);
|
||||||
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
if (!isInputActive) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
{
|
||||||
navigate(1, 0);
|
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 1, 0, false);
|
||||||
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
if (!isInputActive) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
{
|
||||||
navigate(0, -1);
|
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 0, -1, false);
|
||||||
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
if (!isInputActive) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
{
|
||||||
navigate(0, 1);
|
const next = computeNextCell(getRowIds(), getColIds(), focusedCell, 0, 1, false);
|
||||||
|
if (next) goFocused(next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Tab":
|
case "Tab": {
|
||||||
e.preventDefault();
|
const next = computeNextCell(
|
||||||
navigate(0, e.shiftKey ? -1 : 1);
|
getRowIds(),
|
||||||
|
getColIds(),
|
||||||
|
focusedCell,
|
||||||
|
0,
|
||||||
|
e.shiftKey ? -1 : 1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (next) {
|
||||||
|
e.preventDefault();
|
||||||
|
goFocused(next);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "Escape":
|
}
|
||||||
|
case "Enter":
|
||||||
|
case "F2":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setEditingCell(null);
|
openEditor(focusedCell);
|
||||||
break;
|
break;
|
||||||
|
default: {
|
||||||
|
if (e.key === " " && propertyType(focusedCell.propertyId) === "checkbox") {
|
||||||
|
e.preventDefault();
|
||||||
|
openEditor(focusedCell);
|
||||||
|
} else if (isPrintableKey(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
beginTypeToEdit(focusedCell, e.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[editingCell, navigate, setEditingCell],
|
[
|
||||||
|
editingCell,
|
||||||
|
focusedCell,
|
||||||
|
getRowIds,
|
||||||
|
getColIds,
|
||||||
|
goEditing,
|
||||||
|
goFocused,
|
||||||
|
setEditingCell,
|
||||||
|
setFocusedCell,
|
||||||
|
openEditor,
|
||||||
|
clearCell,
|
||||||
|
beginTypeToEdit,
|
||||||
|
propertyType,
|
||||||
|
selectionCount,
|
||||||
|
clearSelection,
|
||||||
|
deleteSelected,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const el = containerRef.current;
|
||||||
if (!container) return;
|
if (!el) return;
|
||||||
|
el.addEventListener("keydown", handleKeyDown);
|
||||||
container.addEventListener("keydown", handleKeyDown);
|
return () => el.removeEventListener("keydown", handleKeyDown);
|
||||||
return () => container.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [containerRef, handleKeyDown]);
|
}, [containerRef, handleKeyDown]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user