import React from 'react'; import type {HotkeysGroup} from '@gravity-ui/navigation'; import {HotkeysPanel as UIKitHotkeysPanel} from '@gravity-ui/navigation'; import {Hotkey} from '@gravity-ui/uikit'; import hotkeys from 'hotkeys-js'; import {HOTKEY_LABELS} from '../../../containers/Tenant/Query/QueryEditor/constants'; import {cn} from '../../../utils/cn'; import {isMacOS} from '../../../utils/platform'; import i18n from '../i18n'; const b = cn('kv-navigation'); export const SHORTCUTS_HOTKEY = isMacOS() ? 'cmd+K' : 'ctrl+K'; export const DEFAULT_HOTKEY_GROUPS: HotkeysGroup[] = [ { title: i18n('hotkeys.query-editor-group-title'), items: [ { title: i18n('hotkeys.execute-query'), value: isMacOS() ? 'cmd+enter' : 'ctrl+enter', }, { title: i18n('hotkeys.execute-selected-query'), value: isMacOS() ? 'cmd+shift+enter' : 'ctrl+shift+enter', }, { title: i18n('hotkeys.save-query'), value: isMacOS() ? 'cmd+s' : 'ctrl+s', }, { title: i18n('hotkeys.save-selected-query'), value: isMacOS() ? 'cmd+shift+s' : 'ctrl+shift+s', }, ], }, ]; export const EDITOR_TABS_HOTKEY_GROUP: HotkeysGroup = { title: i18n('hotkeys.editor-tabs-group-title'), items: [ {title: i18n('hotkeys.new-tab'), value: HOTKEY_LABELS.newTab}, {title: i18n('hotkeys.close-tab'), value: HOTKEY_LABELS.closeTab}, {title: i18n('hotkeys.rename-tab'), value: HOTKEY_LABELS.renameTab}, {title: i18n('hotkeys.next-tab'), value: HOTKEY_LABELS.nextTab}, {title: i18n('hotkeys.previous-tab'), value: HOTKEY_LABELS.previousTab}, {title: i18n('hotkeys.close-other-tabs'), value: HOTKEY_LABELS.closeOtherTabs}, {title: i18n('hotkeys.close-all-tabs'), value: HOTKEY_LABELS.closeAllTabs}, {title: i18n('hotkeys.save-query-as'), value: HOTKEY_LABELS.saveQueryAs}, ], }; export interface HotkeysPanelProps { visible: boolean; closePanel: () => void; hotkeyGroups?: HotkeysGroup[]; } /** * HotkeysPanelWrapper creates a render cycle separation between mounting and visibility change. * This is necessary for smooth animations as HotkeysPanel uses CSSTransition internally. * * When a component is both mounted and set to visible at once, CSSTransition can't * properly sequence its transition classes (panel → panel-active) because it's already active when mounted * and counts transition as it has already happened. * This wrapper ensures the component mounts first, then sets visible=true in a subsequent render cycle * to make transition actually happen. */ export const HotkeysPanelWrapper = ({ visible: propsVisible, closePanel, hotkeyGroups = DEFAULT_HOTKEY_GROUPS, }: HotkeysPanelProps) => { const [visible, setVisible] = React.useState(false); React.useEffect(() => { setVisible(propsVisible); }, [propsVisible]); return ( {i18n('hotkeys.title')} } onClose={closePanel} /> ); }; interface UseHotkeysPanel { isPanelVisible: boolean; openPanel: () => void; closePanel: () => void; hotkeyGroups?: HotkeysGroup[]; } export const useHotkeysPanel = ({ isPanelVisible, openPanel, closePanel, hotkeyGroups = DEFAULT_HOTKEY_GROUPS, }: UseHotkeysPanel) => { React.useEffect(() => { hotkeys(SHORTCUTS_HOTKEY, openPanel); window.addEventListener('openKeyboardShortcutsPanel', openPanel); return () => { hotkeys.unbind(SHORTCUTS_HOTKEY); window.removeEventListener('openKeyboardShortcutsPanel', openPanel); }; }, [openPanel]); const renderPanel = React.useCallback( () => ( ), [isPanelVisible, closePanel, hotkeyGroups], ); return { renderPanel, }; };