Skip to content

Commit b401559

Browse files
authored
feat: make keyboard shortcuts help page (#2116)
1 parent 341c0d8 commit b401559

File tree

16 files changed

+473
-31
lines changed

16 files changed

+473
-31
lines changed

package-lock.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"copy-to-clipboard": "^3.3.3",
3939
"crc-32": "^1.2.2",
4040
"history": "^4.10.1",
41+
"hotkeys-js": "^3.13.9",
4142
"lodash": "^4.17.21",
4243
"monaco-editor": "^0.52.2",
4344
"numeral": "^2.0.6",

src/containers/AsideNavigation/AsideNavigation.scss

+5
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@
1919
width: 300px;
2020
padding: 10px;
2121
}
22+
23+
&__hotkeys-panel-title {
24+
display: flex;
25+
gap: var(--g-spacing-2);
26+
}
2227
}

src/containers/AsideNavigation/AsideNavigation.tsx

+99-20
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ 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} from '@gravity-ui/navigation';
5+
import {AsideHeader, FooterItem, HotkeysPanel} from '@gravity-ui/navigation';
6+
import {Hotkey} from '@gravity-ui/uikit';
67
import type {IconData} from '@gravity-ui/uikit';
8+
import hotkeys from 'hotkeys-js';
79
import {useHistory} from 'react-router-dom';
810

9-
import {settingsManager} from '../../services/settings';
1011
import {cn} from '../../utils/cn';
11-
import {ASIDE_HEADER_COMPACT_KEY, LANGUAGE_KEY} from '../../utils/constants';
12+
import {ASIDE_HEADER_COMPACT_KEY} from '../../utils/constants';
1213
import {useSetting} from '../../utils/hooks';
1314

15+
import {InformationPopup} from './InformationPopup';
16+
import {HOTKEYS, SHORTCUTS_HOTKEY} from './constants';
1417
import i18n from './i18n';
1518

1619
import userSecret from '../../assets/icons/user-secret.svg';
@@ -62,26 +65,86 @@ export interface AsideNavigationProps {
6265

6366
enum Panel {
6467
UserSettings = 'UserSettings',
68+
Information = 'Information',
69+
Hotkeys = 'Hotkeys',
6570
}
6671

67-
function getDocumentationLink() {
68-
// Use saved language from settings if it's present, otherwise use browser language
69-
const lang = settingsManager.readUserSettingsValue(LANGUAGE_KEY, navigator.language);
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]);
7094

71-
if (lang === 'ru') {
72-
return 'https://ydb.tech/docs/ru/';
73-
}
74-
75-
return 'https://ydb.tech/docs/en/';
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+
);
76109
}
77110

78111
export function AsideNavigation(props: AsideNavigationProps) {
79112
const history = useHistory();
80113

81114
const [visiblePanel, setVisiblePanel] = React.useState<Panel>();
82-
115+
const [informationPopupVisible, setInformationPopupVisible] = React.useState(false);
83116
const [compact, setIsCompact] = useSetting<boolean>(ASIDE_HEADER_COMPACT_KEY);
84117

118+
const toggleInformationPopup = () => setInformationPopupVisible((prev) => !prev);
119+
120+
const closeInformationPopup = React.useCallback(() => setInformationPopupVisible(false), []);
121+
122+
const openHotkeysPanel = React.useCallback(() => {
123+
closeInformationPopup();
124+
setVisiblePanel(Panel.Hotkeys);
125+
}, [closeInformationPopup]);
126+
127+
const closePanel = React.useCallback(() => {
128+
setVisiblePanel(undefined);
129+
}, []);
130+
131+
const renderInformationPopup = () => {
132+
return <InformationPopup onKeyboardShortcutsClick={openHotkeysPanel} />;
133+
};
134+
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+
85148
return (
86149
<React.Fragment>
87150
<AsideHeader
@@ -100,13 +163,16 @@ export function AsideNavigation(props: AsideNavigationProps) {
100163
<FooterItem
101164
compact={compact}
102165
item={{
103-
id: 'documentation',
104-
title: i18n('navigation-item.documentation'),
166+
id: 'information',
167+
title: i18n('navigation-item.information'),
105168
icon: CircleQuestion,
106-
onItemClick: () => {
107-
window.open(getDocumentationLink(), '_blank', 'noreferrer');
108-
},
169+
current: informationPopupVisible,
170+
onItemClick: toggleInformationPopup,
109171
}}
172+
enableTooltip={!informationPopupVisible}
173+
popupVisible={informationPopupVisible}
174+
onClosePopup={closeInformationPopup}
175+
renderPopupContent={renderInformationPopup}
110176
/>
111177

112178
<FooterItem
@@ -137,10 +203,23 @@ export function AsideNavigation(props: AsideNavigationProps) {
137203
visible: visiblePanel === Panel.UserSettings,
138204
content: props.settings,
139205
},
206+
{
207+
id: 'information',
208+
visible: visiblePanel === Panel.Information,
209+
},
210+
{
211+
id: 'hotkeys',
212+
visible: visiblePanel === Panel.Hotkeys,
213+
keepMounted: true,
214+
content: (
215+
<HotkeysPanelWrapper
216+
visiblePanel={visiblePanel}
217+
closePanel={closePanel}
218+
/>
219+
),
220+
},
140221
]}
141-
onClosePanel={() => {
142-
setVisiblePanel(undefined);
143-
}}
222+
onClosePanel={closePanel}
144223
/>
145224
</React.Fragment>
146225
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
@import '../../../styles/mixins.scss';
2+
3+
:root {
4+
--information-popup-padding: 16px;
5+
--information-popup-header-padding: 16px;
6+
}
7+
8+
.information-popup {
9+
&__content {
10+
position: relative;
11+
12+
display: flex;
13+
flex-direction: column;
14+
15+
box-sizing: border-box;
16+
width: 280px;
17+
padding: var(--information-popup-header-padding) 0 0 0;
18+
}
19+
20+
&__docs,
21+
&__footer {
22+
display: flex;
23+
flex-direction: column;
24+
flex-shrink: 0;
25+
}
26+
27+
&__docs {
28+
padding-bottom: 8px;
29+
}
30+
31+
&__footer {
32+
position: relative;
33+
34+
padding: 12px 0 8px;
35+
36+
border-top: 1px solid var(--g-color-line-generic);
37+
background-color: var(--g-color-base-generic);
38+
}
39+
40+
&__title {
41+
flex-shrink: 0;
42+
43+
margin-bottom: 4px;
44+
padding: 4px var(--information-popup-padding);
45+
}
46+
47+
&__docs-list-wrap {
48+
display: flex;
49+
flex-direction: column;
50+
flex-shrink: 0;
51+
52+
margin-bottom: 12px;
53+
54+
&:last-child {
55+
margin-bottom: 0;
56+
}
57+
}
58+
59+
&__docs-link,
60+
&__shortcuts-item {
61+
display: flex;
62+
flex-grow: 1;
63+
align-items: center;
64+
65+
box-sizing: border-box;
66+
width: 100%;
67+
height: 100%;
68+
padding: 8px var(--information-popup-padding);
69+
70+
line-height: var(--g-text-body-1-line-height);
71+
cursor: pointer;
72+
73+
&:hover {
74+
background-color: var(--g-color-base-simple-hover);
75+
}
76+
}
77+
78+
&__shortcuts-item {
79+
justify-content: space-between;
80+
}
81+
82+
&__docs-link {
83+
&,
84+
&:hover,
85+
&:active,
86+
&:visited,
87+
&:focus {
88+
text-decoration: none;
89+
90+
color: inherit;
91+
outline: none;
92+
}
93+
}
94+
95+
&__item-icon-wrap {
96+
width: 16px;
97+
height: 16px;
98+
margin-right: 10px;
99+
}
100+
101+
&__shortcuts-content {
102+
display: flex;
103+
align-items: center;
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {Keyboard} from '@gravity-ui/icons';
2+
import {Flex, Hotkey, Icon, Link, List, Text} from '@gravity-ui/uikit';
3+
4+
import {settingsManager} from '../../../services/settings';
5+
import {cn} from '../../../utils/cn';
6+
import {LANGUAGE_KEY} from '../../../utils/constants';
7+
import {SHORTCUTS_HOTKEY} from '../constants';
8+
import i18n from '../i18n';
9+
10+
import './InformationPopup.scss';
11+
12+
const b = cn('information-popup');
13+
14+
export interface InformationPopupProps {
15+
onKeyboardShortcutsClick?: () => void;
16+
}
17+
18+
export function InformationPopup({onKeyboardShortcutsClick}: InformationPopupProps) {
19+
const getDocumentationLink = () => {
20+
const lang = settingsManager.readUserSettingsValue(LANGUAGE_KEY, navigator.language);
21+
return lang === 'ru' ? 'https://ydb.tech/docs/ru/' : 'https://ydb.tech/docs/en/';
22+
};
23+
24+
return (
25+
<div className={b('content', {})}>
26+
<div className={b('docs')}>
27+
<Text variant="subheader-3" color="primary" className={b('title')}>
28+
Documentation
29+
</Text>
30+
<div className={b('docs-list-wrap')}>
31+
<List
32+
items={[
33+
{
34+
text: i18n('help-center.item.documentation'),
35+
url: getDocumentationLink(),
36+
},
37+
]}
38+
filterable={false}
39+
virtualized={false}
40+
renderItem={({text, url}) => (
41+
<Link
42+
className={b('docs-link')}
43+
rel="noopener"
44+
target="_blank"
45+
href={url}
46+
title={typeof text === 'string' ? text : undefined}
47+
>
48+
{text}
49+
</Link>
50+
)}
51+
itemClassName={b('item')}
52+
/>
53+
</div>
54+
</div>
55+
56+
<div className={b('footer')}>
57+
<Flex
58+
justifyContent="space-between"
59+
className={b('shortcuts-item')}
60+
onClick={onKeyboardShortcutsClick}
61+
>
62+
<Flex alignItems="center">
63+
<div className={b('item-icon-wrap')}>
64+
<Icon data={Keyboard} />
65+
</div>
66+
{i18n('help-center.footer.shortcuts')}
67+
</Flex>
68+
<Hotkey value={SHORTCUTS_HOTKEY} />
69+
</Flex>
70+
</div>
71+
</div>
72+
);
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {InformationPopup} from './InformationPopup';

0 commit comments

Comments
 (0)