diff --git a/package.json b/package.json
index 53fb464e..4fac6672 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,6 @@
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
- "@mantine/hooks": "^5.10.0",
"@mdi/js": "^6.6.96",
"@monaco-editor/react": "^4.4.4",
"@mui/icons-material": "^5.10.16",
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index 0b772a2b..76d504b5 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -30,12 +30,14 @@ import {
tableNextPageAtom,
tablePageAtom,
updateColumnAtom,
+ selectedCellAtom,
} from "@src/atoms/tableScope";
+import { useMenuAction } from "@src/contexts/TableKbShortcutContext";
import { getFieldType, getFieldProp } from "@src/components/fields";
import { useKeyboardNavigation } from "./useKeyboardNavigation";
import { useSaveColumnSizing } from "./useSaveColumnSizing";
+import useHotKeys from "./useHotKeys";
import type { TableRow, ColumnConfig } from "@src/types/table";
-import { TableKbShortcutProvider } from "@src/contexts/TableKbShortcutContext";
export const DEFAULT_ROW_HEIGHT = 41;
export const DEFAULT_COL_WIDTH = 150;
@@ -182,6 +184,13 @@ export default function Table({
tableRows,
leafColumns,
});
+ const [selectedCell] = useAtom(selectedCellAtom, tableScope);
+ const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell);
+ const { handler } = useHotKeys([
+ ["mod+C", handleCopy],
+ ["mod+X", handleCut],
+ ["mod+V", handlePaste],
+ ]);
// Handle prompt to save local column sizes if user `canEditColumns`
useSaveColumnSizing(columnSizing, canEditColumns);
@@ -243,7 +252,10 @@ export default function Table({
"--row-height": `${tableSchema.rowHeight || DEFAULT_ROW_HEIGHT}px`,
} as any
}
- onKeyDown={handleKeyDown}
+ onKeyDown={(e) => {
+ handleKeyDown(e);
+ handler(e);
+ }}
>
) : (
-
-
-
+
)}
diff --git a/src/components/Table/useHotKeys.tsx b/src/components/Table/useHotKeys.tsx
new file mode 100644
index 00000000..2791dc8f
--- /dev/null
+++ b/src/components/Table/useHotKeys.tsx
@@ -0,0 +1,99 @@
+import { useCallback } from "react";
+
+type HotKeysAction = [
+ string,
+ (event: React.KeyboardEvent | KeyboardEvent) => void
+];
+
+export default function useHotKeys(actions: HotKeysAction[]) {
+ // master event handler
+ const handler = useCallback(
+ (event: React.KeyboardEvent) => {
+ const event_ = "nativeEvent" in event ? event.nativeEvent : event;
+ actions.forEach(([hotkey, handler_]) => {
+ if (getHotkeyMatcher(hotkey)(event_)) {
+ event.preventDefault();
+ handler_(event_);
+ }
+ });
+ },
+ [actions]
+ );
+
+ return { handler };
+}
+
+type KeyboardModifiers = {
+ alt: boolean;
+ ctrl: boolean;
+ meta: boolean;
+ mod: boolean;
+ shift: boolean;
+};
+
+export type Hotkey = KeyboardModifiers & {
+ key?: string;
+};
+function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent): boolean {
+ const { alt, ctrl, meta, mod, shift, key } = hotkey;
+ const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event;
+
+ if (alt !== altKey) {
+ return false;
+ }
+
+ if (mod) {
+ if (!ctrlKey && !metaKey) {
+ return false;
+ }
+ } else {
+ if (ctrl !== ctrlKey) {
+ return false;
+ }
+ if (meta !== metaKey) {
+ return false;
+ }
+ }
+ if (shift !== shiftKey) {
+ return false;
+ }
+
+ if (
+ key &&
+ (pressedKey.toLowerCase() === key.toLowerCase() ||
+ event.code.replace("Key", "").toLowerCase() === key.toLowerCase())
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+type CheckHotkeyMatch = (event: KeyboardEvent) => boolean;
+export function getHotkeyMatcher(hotkey: string): CheckHotkeyMatch {
+ return (event) => isExactHotkey(parseHotkey(hotkey), event);
+}
+
+function parseHotkey(hotkey: string): Hotkey {
+ const keys = hotkey
+ .toLowerCase()
+ .split("+")
+ .map((part) => part.trim());
+
+ const modifiers: KeyboardModifiers = {
+ alt: keys.includes("alt"),
+ ctrl: keys.includes("ctrl"),
+ meta: keys.includes("meta"),
+ mod: keys.includes("mod"),
+ shift: keys.includes("shift"),
+ };
+
+ const reservedKeys = ["alt", "ctrl", "meta", "shift", "mod"];
+
+ const freeKey = keys.find((key) => !reservedKeys.includes(key));
+
+ return {
+ ...modifiers,
+ key: freeKey,
+ };
+}
diff --git a/src/contexts/TableKbShortcutContext.tsx b/src/contexts/TableKbShortcutContext.tsx
index 1576bd00..516124d3 100644
--- a/src/contexts/TableKbShortcutContext.tsx
+++ b/src/contexts/TableKbShortcutContext.tsx
@@ -1,21 +1,13 @@
-import {
- createContext,
- useContext,
- useCallback,
- useState,
- useEffect,
-} from "react";
+import { useCallback, useState, useEffect } from "react";
import { useAtom, useSetAtom } from "jotai";
import { useSnackbar } from "notistack";
import { get, find } from "lodash-es";
-import { useHotkeys } from "@mantine/hooks";
import {
tableScope,
tableSchemaAtom,
tableRowsAtom,
updateFieldAtom,
- selectedCellAtom,
SelectedCell,
} from "@src/atoms/tableScope";
import { getFieldProp, getFieldType } from "@src/components/fields";
@@ -45,7 +37,6 @@ export function useMenuAction(
const updateField = useSetAtom(updateFieldAtom, tableScope);
const [cellValue, setCellValue] = useState();
const [selectedCol, setSelectedCol] = useState();
- const [enableAction, setEnableAction] = useState(false);
const handleCopy = useCallback(async () => {
try {
@@ -134,10 +125,6 @@ export function useMenuAction(
if (handleClose) handleClose();
}, [selectedCell, selectedCol, updateField, enqueueSnackbar, handleClose]);
- useEffect(() => {
- setEnableAction(SUPPORTED_TYPES.has(selectedCol?.type));
- }, [selectedCol]);
-
useEffect(() => {
if (!selectedCell) return setCellValue("");
const selectedCol = tableSchema.columns?.[selectedCell.columnKey];
@@ -147,20 +134,23 @@ export function useMenuAction(
setCellValue(get(selectedRow, selectedCol.fieldName));
}, [selectedCell, tableSchema, tableRows]);
- const checkEnabled = (func: Function) => {
- return function () {
- if (enableAction) {
- return func();
- } else {
- enqueueSnackbar(
- `${selectedCol?.type} field cannot be copied using keyboard shortcut`,
- {
- variant: "info",
- }
- );
- }
- };
- };
+ const checkEnabled = useCallback(
+ (func: Function) => {
+ return function () {
+ if (SUPPORTED_TYPES.has(selectedCol?.type)) {
+ return func();
+ } else {
+ enqueueSnackbar(
+ `${selectedCol?.type} field cannot be copied using keyboard shortcut`,
+ {
+ variant: "info",
+ }
+ );
+ }
+ };
+ },
+ [selectedCol]
+ );
return {
handleCopy: checkEnabled(handleCopy),
@@ -169,26 +159,3 @@ export function useMenuAction(
cellValue,
};
}
-
-const TableKbShortcutContext = createContext(null);
-
-export function useTableKbShortcut() {
- return useContext(TableKbShortcutContext);
-}
-
-export function TableKbShortcutProvider(props: { children: any }) {
- const [selectedCell] = useAtom(selectedCellAtom, tableScope);
- const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell);
-
- useHotkeys([
- ["mod+C", handleCopy],
- ["mod+X", handleCut],
- ["mod+V", handlePaste],
- ]);
-
- return (
-
- {props.children}
-
- );
-}
diff --git a/yarn.lock b/yarn.lock
index 61ea6da5..003c37e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2217,11 +2217,6 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
-"@mantine/hooks@^5.10.0":
- version "5.10.0"
- resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-5.10.0.tgz#e7886025a11dfa25f99c8c7fb7186d6a065c9d5c"
- integrity sha512-dAefxpvqjFtXNeKse+awkIa4U1XGnMMOqWg1+07Y2Ino2G6EiT8AEnYqQyTXgcPoNaWwG9533Q/DDadmyweqaQ==
-
"@mark.probst/unicode-properties@~1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@mark.probst/unicode-properties/-/unicode-properties-1.1.0.tgz#5caafeab4737df93163d6d288007df33f9939b80"