diff --git a/apps/studio/components/grid/components/common/Hooks.tsx b/apps/studio/components/grid/components/common/Hooks.tsx deleted file mode 100644 index ef2fe369e31..00000000000 --- a/apps/studio/components/grid/components/common/Hooks.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import * as React from 'react' - -function includes(array: string[], element: string) { - return array.indexOf(element) >= 0 -} - -/** - * Hook for listening on key events. - * - * @deprecated Use `useShortcut` from `state/shortcuts/useShortcut` instead. - * The new hook reads from a central registry, respects user preferences, and - * can surface shortcuts in the Cmd+P command menu. - * - * @param {Object|Map} keyMap Key names mapped to event handlers. If a key name exists, its - * default behavior will be suppressed. - * @param {Array} whitelistNodes If target element is in the whitelist nodes array, will not - * trigger shortcut event - * @param {Array} whitelistClasses If target class is in the whitelist classes array, will not - * trigger shortcut event - */ -export function useKeyboardShortcuts( - keyMap: { [id: KeyboardEvent['key']]: (event: KeyboardEvent) => void }, - whitelistNodes: string[] = [], - whitelistClasses: string[] = [] -) { - const [lastKeydown, setLastKeydown] = React.useState() - - const handleKeydown = (event: any) => { - if ( - !keyMap || - includes(whitelistNodes, event.target.nodeName) || - includes(whitelistClasses, event.target.className) - ) { - return - } - - let keyPressed = getKeyPresses(event) - - if (keyMap[keyPressed]) { - /** - * combined keymap will trigger action on KeyDown event - * while single keymap will trigger action on KeyUp event - */ - if (keyPressed.includes('+')) { - event.preventDefault() - keyMap[keyPressed](event) - setLastKeydown(null) - } else { - setLastKeydown(event.key) - event.preventDefault() - } - } - } - - const handleKeyup = (event: any) => { - if (!keyMap) return - - if (keyMap[event.key] && lastKeydown === event.key) { - event.preventDefault() - keyMap[event.key](event) - setLastKeydown(null) - } - } - - function getKeyPresses(event: KeyboardEvent) { - return event.metaKey && event.shiftKey - ? `Command+Shift+${event.key}` - : event.metaKey - ? `Command+${event.key}` - : event.shiftKey && event.key === 'Enter' - ? `Shift+${event.key}` - : event.ctrlKey && event.key - ? `Control+${event.key}` - : event.key - } - - React.useEffect(() => { - window.addEventListener('keydown', handleKeydown) - window.addEventListener('keyup', handleKeyup) - return () => { - window.removeEventListener('keydown', handleKeydown) - window.removeEventListener('keyup', handleKeyup) - } - }) -} diff --git a/apps/studio/components/grid/components/common/Shortcuts.tsx b/apps/studio/components/grid/components/common/Shortcuts.tsx index 958f0afcac9..8e1f7dca488 100644 --- a/apps/studio/components/grid/components/common/Shortcuts.tsx +++ b/apps/studio/components/grid/components/common/Shortcuts.tsx @@ -1,8 +1,9 @@ -import { RefObject, useMemo } from 'react' +import { RefObject } from 'react' import type { DataGridHandle } from 'react-data-grid' -import { useKeyboardShortcuts } from './Hooks' import { SupaRow } from '@/components/grid/types' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table' type ShortcutsProps = { @@ -13,62 +14,42 @@ type ShortcutsProps = { export function Shortcuts({ gridRef, rows }: ShortcutsProps) { const snap = useTableEditorTableStateSnapshot() - const metaKey = useMemo(() => { - function getClientOS() { - return navigator?.appVersion.indexOf('Win') !== -1 - ? 'windows' - : navigator?.appVersion.indexOf('Mac') !== -1 - ? 'macos' - : 'unknown' + useShortcut(SHORTCUT_IDS.TABLE_EDITOR_JUMP_FIRST_ROW, () => { + if (snap.selectedCellPosition) { + gridRef.current!.selectCell({ + idx: snap.selectedCellPosition?.idx ?? 0, + rowIdx: 0, + }) + } else { + gridRef.current!.scrollToCell({ rowIdx: 0 }) } - return getClientOS() === 'windows' ? 'Control' : 'Command' - }, []) + }) - useKeyboardShortcuts( - { - [`${metaKey}+ArrowUp`]: (event) => { - event.stopPropagation() - if (snap.selectedCellPosition) { - const position = { - idx: snap.selectedCellPosition?.idx ?? 0, - rowIdx: 0, - } - gridRef.current!.selectCell(position) - } else { - gridRef.current!.scrollToCell({ rowIdx: Number(0) }) - } - }, - [`${metaKey}+ArrowDown`]: (event) => { - event.stopPropagation() - if (snap.selectedCellPosition) { - const position = { - idx: snap.selectedCellPosition?.idx ?? 0, - rowIdx: rows.length > 1 ? rows.length - 1 : 0, - } - gridRef.current!.selectCell(position) - } else { - gridRef.current!.scrollToCell({ rowIdx: Number(rows.length) }) - } - }, - [`${metaKey}+ArrowLeft`]: (event) => { - event.stopPropagation() - const fronzenColumns = snap.gridColumns.filter((x) => x.frozen) - const position = { - idx: fronzenColumns.length, - rowIdx: snap.selectedCellPosition?.rowIdx ?? 0, - } - gridRef.current!.selectCell(position) - }, - [`${metaKey}+ArrowRight`]: (event) => { - event.stopPropagation() - gridRef.current?.selectCell({ - idx: snap.gridColumns.length - 2, // -2 because we don't want to select the end extra col - rowIdx: snap.selectedCellPosition?.rowIdx ?? 0, - }) - }, - }, - ['INPUT', 'TEXTAREA', 'SELECT'] - ) + useShortcut(SHORTCUT_IDS.TABLE_EDITOR_JUMP_LAST_ROW, () => { + if (snap.selectedCellPosition) { + gridRef.current!.selectCell({ + idx: snap.selectedCellPosition?.idx ?? 0, + rowIdx: rows.length > 1 ? rows.length - 1 : 0, + }) + } else { + gridRef.current!.scrollToCell({ rowIdx: rows.length }) + } + }) + + useShortcut(SHORTCUT_IDS.TABLE_EDITOR_JUMP_FIRST_COL, () => { + const frozenColumns = snap.gridColumns.filter((x) => x.frozen) + gridRef.current!.selectCell({ + idx: frozenColumns.length, + rowIdx: snap.selectedCellPosition?.rowIdx ?? 0, + }) + }) + + useShortcut(SHORTCUT_IDS.TABLE_EDITOR_JUMP_LAST_COL, () => { + gridRef.current?.selectCell({ + idx: snap.gridColumns.length - 2, + rowIdx: snap.selectedCellPosition?.rowIdx ?? 0, + }) + }) return null } diff --git a/apps/studio/state/shortcuts/registry.ts b/apps/studio/state/shortcuts/registry.ts index 3d33e4dd102..c5576932f7e 100644 --- a/apps/studio/state/shortcuts/registry.ts +++ b/apps/studio/state/shortcuts/registry.ts @@ -16,6 +16,10 @@ export const SHORTCUT_IDS = { RESULTS_COPY_JSON: 'results.copy-json', RESULTS_COPY_CSV: 'results.copy-csv', RESULTS_DOWNLOAD_CSV: 'results.download-csv', + TABLE_EDITOR_JUMP_FIRST_ROW: 'table-editor.jump-first-row', + TABLE_EDITOR_JUMP_LAST_ROW: 'table-editor.jump-last-row', + TABLE_EDITOR_JUMP_FIRST_COL: 'table-editor.jump-first-col', + TABLE_EDITOR_JUMP_LAST_COL: 'table-editor.jump-last-col', DATA_TABLE_TOGGLE_FILTERS: 'data-table.toggle-filters', DATA_TABLE_RESET_FILTERS: 'data-table.reset-filters', DATA_TABLE_RESET_COLUMNS: 'data-table.reset-columns', @@ -89,6 +93,34 @@ export const SHORTCUT_DEFINITIONS: Record = { label: 'Download results as CSV', sequence: ['Mod+Shift+D'], }, + [SHORTCUT_IDS.TABLE_EDITOR_JUMP_FIRST_ROW]: { + id: SHORTCUT_IDS.TABLE_EDITOR_JUMP_FIRST_ROW, + label: 'Jump to first row', + sequence: ['Mod+ArrowUp'], + showInSettings: false, + options: { ignoreInputs: true }, + }, + [SHORTCUT_IDS.TABLE_EDITOR_JUMP_LAST_ROW]: { + id: SHORTCUT_IDS.TABLE_EDITOR_JUMP_LAST_ROW, + label: 'Jump to last row', + sequence: ['Mod+ArrowDown'], + showInSettings: false, + options: { ignoreInputs: true }, + }, + [SHORTCUT_IDS.TABLE_EDITOR_JUMP_FIRST_COL]: { + id: SHORTCUT_IDS.TABLE_EDITOR_JUMP_FIRST_COL, + label: 'Jump to first column', + sequence: ['Mod+ArrowLeft'], + showInSettings: false, + options: { ignoreInputs: true }, + }, + [SHORTCUT_IDS.TABLE_EDITOR_JUMP_LAST_COL]: { + id: SHORTCUT_IDS.TABLE_EDITOR_JUMP_LAST_COL, + label: 'Jump to last column', + sequence: ['Mod+ArrowRight'], + showInSettings: false, + options: { ignoreInputs: true }, + }, [SHORTCUT_IDS.AI_ASSISTANT_CANCEL_EDIT]: { id: SHORTCUT_IDS.AI_ASSISTANT_CANCEL_EDIT, label: 'Cancel AI Assistant edit', diff --git a/apps/studio/state/shortcuts/types.ts b/apps/studio/state/shortcuts/types.ts index 76b5d46db90..da5f9502659 100644 --- a/apps/studio/state/shortcuts/types.ts +++ b/apps/studio/state/shortcuts/types.ts @@ -34,6 +34,17 @@ export interface ShortcutOptions { */ timeout?: number + /** + * When `true`, suppresses the shortcut while focus is inside an input-like + * element (text input, textarea, select, contenteditable). Button-type + * inputs (type=button/submit/reset) are not ignored. + * + * Defaults to `undefined`, which falls through to TanStack's per-hotkey + * default: `true` for single keys and Shift/Alt combos, `false` for + * Ctrl/Meta/Mod shortcuts and Escape. Set explicitly to override. + */ + ignoreInputs?: boolean + /** * When `true`, the shortcut also appears as an entry in the Cmd+P command * menu (under the "Shortcuts" section) for as long as the hook is mounted. diff --git a/apps/studio/state/shortcuts/useShortcut.tsx b/apps/studio/state/shortcuts/useShortcut.tsx index c5a9faab23e..8b29baaf8fe 100644 --- a/apps/studio/state/shortcuts/useShortcut.tsx +++ b/apps/studio/state/shortcuts/useShortcut.tsx @@ -58,8 +58,9 @@ export function useShortcut(id: ShortcutId, callback: () => void, options?: Shor const callerEnabled = options?.enabled ?? def.options?.enabled ?? true const enabled = globallyEnabled && callerEnabled const timeout = options?.timeout ?? def.options?.timeout ?? undefined + const ignoreInputs = options?.ignoreInputs ?? def.options?.ignoreInputs - useHotkeySequence(def.sequence, callback, { enabled, timeout }) + useHotkeySequence(def.sequence, callback, { enabled, timeout, ignoreInputs }) // Handle overrides for command menu const enabledInCommandMenu = enabled && (options?.registerInCommandMenu ?? false)