@@ -29,6 +29,14 @@ const UI = {
2929 connected : false ,
3030 desktopName : "" ,
3131
32+ // Modifier key configuration
33+ _modifierKeys : {
34+ shift : { keysym : KeyTable . XK_Shift_L , code : "ShiftLeft" , buttonId : 'noVNC_toggle_shift_button' } ,
35+ ctrl : { keysym : KeyTable . XK_Control_L , code : "ControlLeft" , buttonId : 'noVNC_toggle_ctrl_button' } ,
36+ alt : { keysym : KeyTable . XK_Alt_L , code : "AltLeft" , buttonId : 'noVNC_toggle_alt_button' } ,
37+ windows : { keysym : KeyTable . XK_Super_L , code : "MetaLeft" , buttonId : 'noVNC_toggle_windows_button' }
38+ } ,
39+
3240 statusTimeout : null ,
3341 hideKeyboardTimeout : null ,
3442 idleControlbarTimeout : null ,
@@ -125,6 +133,11 @@ const UI = {
125133 document . getElementById ( "noVNC_status" )
126134 . addEventListener ( 'click' , UI . hideStatus ) ;
127135
136+ // Handle tab/window close to release modifier keys
137+ // This is critical for VMware VMs using websocket reverse proxy
138+ window . addEventListener ( 'beforeunload' , UI . handleBeforeUnload ) ;
139+ window . addEventListener ( 'pagehide' , UI . handlePageHide ) ;
140+
128141 // Bootstrap fallback input handler
129142 UI . keyboardinputReset ( ) ;
130143
@@ -1740,6 +1753,51 @@ const UI = {
17401753 UI . idleControlbar ( ) ;
17411754 } ,
17421755
1756+ _sendKeyUp ( keysym , code ) {
1757+ UI . rfb . sendKey ( keysym , code , false ) ;
1758+ } ,
1759+
1760+ // Release a single modifier key if it's pressed
1761+ _releaseModifierKey ( keyName ) {
1762+ const keyConfig = UI . _modifierKeys [ keyName ] ;
1763+ if ( ! keyConfig ) return false ;
1764+
1765+ const btn = document . getElementById ( keyConfig . buttonId ) ;
1766+ if ( ! btn || ! btn . classList . contains ( "noVNC_selected" ) ) {
1767+ return false ;
1768+ }
1769+
1770+ UI . _sendKeyUp ( keyConfig . keysym , keyConfig . code ) ;
1771+ btn . classList . remove ( "noVNC_selected" ) ;
1772+ return true ;
1773+ } ,
1774+
1775+ // Release all currently pressed modifier keys
1776+ _releaseAllModifierKeys ( ) {
1777+ if ( ! UI . rfb || UI . rfb . _rfbConnectionState !== 'connected' ) {
1778+ return false ;
1779+ }
1780+
1781+ let keysReleased = false ;
1782+
1783+ // Release all modifier keys
1784+ for ( const keyName in UI . _modifierKeys ) {
1785+ if ( UI . _releaseModifierKey ( keyName ) ) {
1786+ keysReleased = true ;
1787+ }
1788+ }
1789+
1790+ // Also check RFB's internal shift state (it tracks this separately)
1791+ if ( UI . rfb . _shiftPressed ) {
1792+ const shiftConfig = UI . _modifierKeys . shift ;
1793+ UI . _sendKeyUp ( shiftConfig . keysym , shiftConfig . code ) ;
1794+ keysReleased = true ;
1795+ }
1796+
1797+ return keysReleased ;
1798+ } ,
1799+
1800+
17431801 // Move focus to the screen in order to be able to use the
17441802 // keyboard right after these extra keys.
17451803 // The exception is when a virtual keyboard is used, because
@@ -1836,6 +1894,20 @@ const UI = {
18361894 selectbox . options . add ( optn ) ;
18371895 } ,
18381896
1897+ // Handle tab/window close events
1898+ // These fire when the user closes the tab, which doesn't call disconnect()
1899+ handleBeforeUnload ( event ) {
1900+ // Release modifier keys before tab closes
1901+ // This is critical for VMware VMs using websocket reverse proxy
1902+ UI . _releaseAllModifierKeys ( ) ;
1903+ } ,
1904+
1905+ handlePageHide ( event ) {
1906+ // Also handle pagehide as a fallback (fires in more browsers)
1907+ // Release modifier keys before page is hidden
1908+ UI . _releaseAllModifierKeys ( ) ;
1909+ } ,
1910+
18391911/* ------^-------
18401912 * /MISC
18411913 * ==============
0 commit comments