Skip to content

Commit 08b39d1

Browse files
authored
fix: useHotkeysPanel hook (#2151)
1 parent c5bd2ac commit 08b39d1

File tree

7 files changed

+154
-101
lines changed

7 files changed

+154
-101
lines changed

src/containers/AsideNavigation/AsideNavigation.tsx

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ import React from 'react';
22

33
import {CircleQuestion, Gear, Person} from '@gravity-ui/icons';
44
import type {MenuItem} from '@gravity-ui/navigation';
5-
import {AsideHeader, FooterItem, HotkeysPanel} from '@gravity-ui/navigation';
6-
import {Hotkey} from '@gravity-ui/uikit';
5+
import {AsideHeader, FooterItem} from '@gravity-ui/navigation';
76
import type {IconData} from '@gravity-ui/uikit';
8-
import hotkeys from 'hotkeys-js';
97
import {useHistory} from 'react-router-dom';
108

119
import {cn} from '../../utils/cn';
1210
import {ASIDE_HEADER_COMPACT_KEY} from '../../utils/constants';
1311
import {useSetting} from '../../utils/hooks';
1412

1513
import {InformationPopup} from './InformationPopup';
16-
import {HOTKEYS, SHORTCUTS_HOTKEY} from './constants';
14+
import {useHotkeysPanel} from './hooks/useHotkeysPanel';
1715
import i18n from './i18n';
1816

1917
import userSecret from '../../assets/icons/user-secret.svg';
@@ -69,45 +67,6 @@ enum Panel {
6967
Hotkeys = 'Hotkeys',
7068
}
7169

72-
/**
73-
* HotkeysPanelWrapper creates a render cycle separation between mounting and visibility change.
74-
* This is necessary for smooth animations as HotkeysPanel uses CSSTransition internally.
75-
*
76-
* When a component is both mounted and set to visible at once, CSSTransition can't
77-
* properly sequence its transition classes (panel → panel-active) because it's already active when mounted
78-
* and counts transition as it has already happened.
79-
* This wrapper ensures the component mounts first, then sets visible=true in a subsequent render cycle
80-
* to make transition actually happen.
81-
*/
82-
function HotkeysPanelWrapper({
83-
visiblePanel,
84-
closePanel,
85-
}: {
86-
visiblePanel?: Panel;
87-
closePanel: () => void;
88-
}) {
89-
const [visible, setVisible] = React.useState(false);
90-
91-
React.useEffect(() => {
92-
setVisible(visiblePanel === Panel.Hotkeys);
93-
}, [visiblePanel]);
94-
95-
return (
96-
<HotkeysPanel
97-
visible={visible}
98-
hotkeys={HOTKEYS}
99-
className={b('hotkeys-panel')}
100-
title={
101-
<div className={b('hotkeys-panel-title')}>
102-
{i18n('help-center.footer.shortcuts')}
103-
<Hotkey value={SHORTCUTS_HOTKEY} />
104-
</div>
105-
}
106-
onClose={closePanel}
107-
/>
108-
);
109-
}
110-
11170
export function AsideNavigation(props: AsideNavigationProps) {
11271
const history = useHistory();
11372

@@ -128,23 +87,16 @@ export function AsideNavigation(props: AsideNavigationProps) {
12887
setVisiblePanel(undefined);
12988
}, []);
13089

90+
const {renderPanel: renderHotkeysPanel} = useHotkeysPanel({
91+
isPanelVisible: visiblePanel === Panel.Hotkeys,
92+
closePanel,
93+
openPanel: openHotkeysPanel,
94+
});
95+
13196
const renderInformationPopup = () => {
13297
return <InformationPopup onKeyboardShortcutsClick={openHotkeysPanel} />;
13398
};
13499

135-
React.useEffect(() => {
136-
// Register hotkey for keyboard shortcuts
137-
hotkeys(SHORTCUTS_HOTKEY, openHotkeysPanel);
138-
139-
// Add listener for custom event from Monaco editor
140-
window.addEventListener('openKeyboardShortcutsPanel', openHotkeysPanel);
141-
142-
return () => {
143-
hotkeys.unbind(SHORTCUTS_HOTKEY);
144-
window.removeEventListener('openKeyboardShortcutsPanel', openHotkeysPanel);
145-
};
146-
}, [openHotkeysPanel]);
147-
148100
return (
149101
<React.Fragment>
150102
<AsideHeader
@@ -211,12 +163,7 @@ export function AsideNavigation(props: AsideNavigationProps) {
211163
id: 'hotkeys',
212164
visible: visiblePanel === Panel.Hotkeys,
213165
keepMounted: true,
214-
content: (
215-
<HotkeysPanelWrapper
216-
visiblePanel={visiblePanel}
217-
closePanel={closePanel}
218-
/>
219-
),
166+
content: renderHotkeysPanel(),
220167
},
221168
]}
222169
onClosePanel={closePanel}

src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Flex, Hotkey, Icon, Link, List, Text} from '@gravity-ui/uikit';
44
import {settingsManager} from '../../../services/settings';
55
import {cn} from '../../../utils/cn';
66
import {LANGUAGE_KEY} from '../../../utils/constants';
7-
import {SHORTCUTS_HOTKEY} from '../constants';
7+
import {SHORTCUTS_HOTKEY} from '../hooks/useHotkeysPanel';
88
import i18n from '../i18n';
99

1010
import './InformationPopup.scss';

src/containers/AsideNavigation/constants.tsx

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react';
2+
3+
import {HotkeysPanel as UIKitHotkeysPanel} from '@gravity-ui/navigation';
4+
import {Hotkey} from '@gravity-ui/uikit';
5+
import hotkeys from 'hotkeys-js';
6+
7+
import {cn} from '../../../utils/cn';
8+
import i18n from '../i18n';
9+
10+
const b = cn('kv-navigation');
11+
12+
export const isMac = () => navigator.platform.toUpperCase().includes('MAC');
13+
14+
export const SHORTCUTS_HOTKEY = isMac() ? 'cmd+K' : 'ctrl+K';
15+
16+
export const HOTKEYS = [
17+
{
18+
title: 'Query Editor',
19+
items: [
20+
{
21+
title: i18n('hotkeys.execute-query'),
22+
value: isMac() ? 'cmd+enter' : 'ctrl+enter',
23+
},
24+
{
25+
title: i18n('hotkeys.execute-selected-query'),
26+
value: isMac() ? 'cmd+shift+enter' : 'ctrl+shift+enter',
27+
},
28+
{
29+
title: i18n('hotkeys.previous-query'),
30+
value: isMac() ? 'cmd+arrowUp' : 'ctrl+arrowUp',
31+
},
32+
{
33+
title: i18n('hotkeys.next-query'),
34+
value: isMac() ? 'cmd+arrowDown' : 'ctrl+arrowDown',
35+
},
36+
{
37+
title: i18n('hotkeys.save-query'),
38+
value: isMac() ? 'cmd+s' : 'ctrl+s',
39+
},
40+
{
41+
title: i18n('hotkeys.save-selected-query'),
42+
value: isMac() ? 'cmd+shift+s' : 'ctrl+shift+s',
43+
},
44+
],
45+
},
46+
];
47+
48+
export interface HotkeysPanelProps {
49+
visible: boolean;
50+
closePanel: () => void;
51+
}
52+
53+
/**
54+
* HotkeysPanelWrapper creates a render cycle separation between mounting and visibility change.
55+
* This is necessary for smooth animations as HotkeysPanel uses CSSTransition internally.
56+
*
57+
* When a component is both mounted and set to visible at once, CSSTransition can't
58+
* properly sequence its transition classes (panel → panel-active) because it's already active when mounted
59+
* and counts transition as it has already happened.
60+
* This wrapper ensures the component mounts first, then sets visible=true in a subsequent render cycle
61+
* to make transition actually happen.
62+
*/
63+
export const HotkeysPanelWrapper = ({visible: propsVisible, closePanel}: HotkeysPanelProps) => {
64+
const [visible, setVisible] = React.useState(false);
65+
66+
React.useEffect(() => {
67+
setVisible(propsVisible);
68+
}, [propsVisible]);
69+
70+
return (
71+
<UIKitHotkeysPanel
72+
visible={visible}
73+
hotkeys={HOTKEYS}
74+
className={b('hotkeys-panel')}
75+
title={
76+
<div className={b('hotkeys-panel-title')}>
77+
{i18n('hotkeys.title')}
78+
<Hotkey value={SHORTCUTS_HOTKEY} />
79+
</div>
80+
}
81+
onClose={closePanel}
82+
/>
83+
);
84+
};
85+
86+
interface UseHotkeysPanel {
87+
isPanelVisible: boolean;
88+
openPanel: () => void;
89+
closePanel: () => void;
90+
}
91+
92+
export const useHotkeysPanel = ({isPanelVisible, openPanel, closePanel}: UseHotkeysPanel) => {
93+
React.useEffect(() => {
94+
hotkeys(SHORTCUTS_HOTKEY, openPanel);
95+
96+
window.addEventListener('openKeyboardShortcutsPanel', openPanel);
97+
98+
return () => {
99+
hotkeys.unbind(SHORTCUTS_HOTKEY);
100+
window.removeEventListener('openKeyboardShortcutsPanel', openPanel);
101+
};
102+
}, [openPanel]);
103+
104+
const renderPanel = React.useCallback(
105+
() => <HotkeysPanelWrapper visible={isPanelVisible} closePanel={closePanel} />,
106+
[isPanelVisible, closePanel],
107+
);
108+
109+
return {
110+
renderPanel,
111+
};
112+
};

src/containers/AsideNavigation/i18n/en.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"navigation-item.information": "information",
2+
"navigation-item.information": "Information",
33
"navigation-item.settings": "Settings",
44
"navigation-item.account": "Account",
55

@@ -10,5 +10,13 @@
1010
"account.user": "YDB User",
1111

1212
"account.login": "Login",
13-
"account.logout": "Logout"
13+
"account.logout": "Logout",
14+
15+
"hotkeys.title": "Keyboard shortcuts",
16+
"hotkeys.execute-query": "Execute query",
17+
"hotkeys.execute-selected-query": "Execute selected query",
18+
"hotkeys.previous-query": "Previous query",
19+
"hotkeys.next-query": "Next query",
20+
"hotkeys.save-query": "Save query",
21+
"hotkeys.save-selected-query": "Save selected query"
1422
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"navigation-item.information": "Информация",
3+
"navigation-item.settings": "Настройки",
4+
"navigation-item.account": "Аккаунт",
5+
6+
"help-center.header.title": "Документация",
7+
"help-center.item.documentation": "Посмотреть документацию",
8+
"help-center.footer.shortcuts": "Горячие клавиши",
9+
10+
"account.user": "Пользователь YDB",
11+
12+
"account.login": "Войти",
13+
"account.logout": "Выйти",
14+
15+
"hotkeys.title": "Быстрые клавиши",
16+
"hotkeys.execute-query": "Выполнить запрос",
17+
"hotkeys.execute-selected-query": "Выполнить выбранный запрос",
18+
"hotkeys.previous-query": "Предыдущий запрос",
19+
"hotkeys.next-query": "Следующий запрос",
20+
"hotkeys.save-query": "Сохранить запрос",
21+
"hotkeys.save-selected-query": "Сохранить выбранный запрос"
22+
}

src/containers/AsideNavigation/utils.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)