@@ -2,15 +2,18 @@ import React from 'react';
22
33import { CircleQuestion , Gear , Person } from '@gravity-ui/icons' ;
44import 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' ;
67import type { IconData } from '@gravity-ui/uikit' ;
8+ import hotkeys from 'hotkeys-js' ;
79import { useHistory } from 'react-router-dom' ;
810
9- import { settingsManager } from '../../services/settings' ;
1011import { cn } from '../../utils/cn' ;
11- import { ASIDE_HEADER_COMPACT_KEY , LANGUAGE_KEY } from '../../utils/constants' ;
12+ import { ASIDE_HEADER_COMPACT_KEY } from '../../utils/constants' ;
1213import { useSetting } from '../../utils/hooks' ;
1314
15+ import { InformationPopup } from './InformationPopup' ;
16+ import { HOTKEYS , SHORTCUTS_HOTKEY } from './constants' ;
1417import i18n from './i18n' ;
1518
1619import userSecret from '../../assets/icons/user-secret.svg' ;
@@ -62,26 +65,86 @@ export interface AsideNavigationProps {
6265
6366enum 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
78111export 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 ) ;
0 commit comments