@@ -197,6 +197,7 @@ export default class DataBrowser extends React.Component {
197197 nativeContextMenuOpen : false , // Whether the browser's native context menu is open
198198 mouseOutsidePanel : true , // Whether the mouse is outside the AggregationPanel
199199 mouseOverPanelHeader : false , // Whether the mouse is over the panel header row
200+ commandKeyPressed : false , // Whether the Command/Meta key is currently pressed
200201 optionKeyPressed : false , // Whether the Option/Alt key is currently pressed (pauses auto-scroll)
201202 } ;
202203
@@ -254,20 +255,22 @@ export default class DataBrowser extends React.Component {
254255 this . stopAutoScroll = this . stopAutoScroll . bind ( this ) ;
255256 this . performAutoScrollStep = this . performAutoScrollStep . bind ( this ) ;
256257 this . pauseAutoScrollWithResume = this . pauseAutoScrollWithResume . bind ( this ) ;
257- this . handleNativeContextMenu = this . handleNativeContextMenu . bind ( this ) ;
258- this . handleNativeContextMenuClose = this . handleNativeContextMenuClose . bind ( this ) ;
259258 this . handlePanelMouseEnter = this . handlePanelMouseEnter . bind ( this ) ;
260259 this . handlePanelMouseLeave = this . handlePanelMouseLeave . bind ( this ) ;
261260 this . handlePanelHeaderMouseEnter = this . handlePanelHeaderMouseEnter . bind ( this ) ;
262261 this . handlePanelHeaderMouseLeave = this . handlePanelHeaderMouseLeave . bind ( this ) ;
263262 this . handleOptionKeyDown = this . handleOptionKeyDown . bind ( this ) ;
264263 this . handleOptionKeyUp = this . handleOptionKeyUp . bind ( this ) ;
264+ this . handleMouseButtonDown = this . handleMouseButtonDown . bind ( this ) ;
265+ this . handleMouseButtonUp = this . handleMouseButtonUp . bind ( this ) ;
265266 this . saveOrderTimeout = null ;
266267 this . aggregationPanelRef = React . createRef ( ) ;
267268 this . autoScrollIntervalId = null ;
268269 this . autoScrollTimeoutId = null ;
269270 this . autoScrollResumeTimeoutId = null ;
270271 this . autoScrollAnimationId = null ;
272+ this . mouseButtonPressed = false ;
273+ this . nativeContextMenuTracker = null ;
271274 this . panelHeaderLeaveTimeoutId = null ;
272275 this . panelColumnRefs = [ ] ;
273276 this . activePanelIndex = - 1 ;
@@ -354,18 +357,11 @@ export default class DataBrowser extends React.Component {
354357 // Option key listeners for pausing auto-scroll
355358 document . body . addEventListener ( 'keydown' , this . handleOptionKeyDown ) ;
356359 document . body . addEventListener ( 'keyup' , this . handleOptionKeyUp ) ;
360+ // Left mouse button listener for pausing auto-scroll
361+ window . addEventListener ( 'mousedown' , this . handleMouseButtonDown ) ;
362+ window . addEventListener ( 'mouseup' , this . handleMouseButtonUp ) ;
357363 // Native context menu detection for auto-scroll pause
358- // Use capture phase to ensure we detect the event before the menu handles it
359- document . addEventListener ( 'contextmenu' , this . handleNativeContextMenu , true ) ;
360- // Listen for events that indicate the context menu was closed:
361- // - mousemove: user moved mouse after menu closed (most reliable)
362- // - keydown: Escape key closes the menu
363- // - blur: switching windows/tabs closes the menu
364- // NOTE: click/mousedown don't fire while native context menu is open
365- // NOTE: scroll is not used because auto-scroll itself triggers it
366- window . addEventListener ( 'mousemove' , this . handleNativeContextMenuClose ) ;
367- window . addEventListener ( 'keydown' , this . handleNativeContextMenuClose , true ) ;
368- window . addEventListener ( 'blur' , this . handleNativeContextMenuClose ) ;
364+ this . nativeContextMenuTracker = this . setupNativeContextMenuDetection ( ) ;
369365
370366 // Load keyboard shortcuts from server
371367 try {
@@ -407,10 +403,11 @@ export default class DataBrowser extends React.Component {
407403 // Option key listeners cleanup
408404 document . body . removeEventListener ( 'keydown' , this . handleOptionKeyDown ) ;
409405 document . body . removeEventListener ( 'keyup' , this . handleOptionKeyUp ) ;
410- document . removeEventListener ( 'contextmenu' , this . handleNativeContextMenu , true ) ;
411- window . removeEventListener ( 'mousemove' , this . handleNativeContextMenuClose ) ;
412- window . removeEventListener ( 'keydown' , this . handleNativeContextMenuClose , true ) ;
413- window . removeEventListener ( 'blur' , this . handleNativeContextMenuClose ) ;
406+ window . removeEventListener ( 'mousedown' , this . handleMouseButtonDown ) ;
407+ window . removeEventListener ( 'mouseup' , this . handleMouseButtonUp ) ;
408+ if ( this . nativeContextMenuTracker ) {
409+ this . nativeContextMenuTracker . dispose ( ) ;
410+ }
414411 if ( this . autoScrollTimeoutId ) {
415412 clearTimeout ( this . autoScrollTimeoutId ) ;
416413 }
@@ -1466,7 +1463,8 @@ export default class DataBrowser extends React.Component {
14661463 nativeContextMenuOpen ||
14671464 disableKeyControls ||
14681465 hoverBlocked ||
1469- optionKeyPressed
1466+ optionKeyPressed ||
1467+ this . mouseButtonPressed
14701468 ) ;
14711469 }
14721470
@@ -1492,47 +1490,47 @@ export default class DataBrowser extends React.Component {
14921490
14931491 handleAutoScrollKeyDown ( e ) {
14941492 // Command/Meta key = keyCode 91 (left) or 93 (right)
1495- // Only detect when panels are visible and auto-scroll is enabled
1493+ // Only track that Command key is held; don't stop auto-scroll or enter
1494+ // recording mode until the user actually scrolls (handled in handleAutoScrollWheel)
14961495 if ( ( e . keyCode === 91 || e . keyCode === 93 ) && this . state . autoScrollEnabled && this . state . isPanelVisible && ! this . state . isRecordingAutoScroll ) {
1497- // Stop any existing auto-scroll first
1498- if ( this . state . isAutoScrolling ) {
1499- this . stopAutoScroll ( ) ;
1500- }
1501- this . setState ( {
1502- isRecordingAutoScroll : true ,
1503- recordedScrollDelta : 0 ,
1504- recordingScrollStart : null ,
1505- recordingScrollEnd : null ,
1506- } ) ;
1496+ this . setState ( { commandKeyPressed : true } ) ;
15071497 }
15081498 }
15091499
15101500 handleAutoScrollKeyUp ( e ) {
15111501 // Command/Meta key = keyCode 91 (left) or 93 (right)
1512- if ( ( e . keyCode === 91 || e . keyCode === 93 ) && this . state . isRecordingAutoScroll ) {
1513- const { recordedScrollDelta, recordingScrollStart, recordingScrollEnd } = this . state ;
1502+ if ( e . keyCode === 91 || e . keyCode === 93 ) {
1503+ if ( this . state . isRecordingAutoScroll ) {
1504+ const { recordedScrollDelta, recordingScrollStart, recordingScrollEnd } = this . state ;
15141505
1515- // Only start auto-scroll if we actually recorded some scrolling
1516- if ( recordedScrollDelta !== 0 && recordingScrollStart !== null ) {
1517- // Calculate delay: time between scroll end and key release
1518- const scrollEndTime = recordingScrollEnd || Date . now ( ) ;
1519- const delay = Math . max ( 200 , Date . now ( ) - scrollEndTime ) ; // Minimum 200ms delay
1506+ // Only start auto-scroll if we actually recorded some scrolling
1507+ if ( recordedScrollDelta !== 0 && recordingScrollStart !== null ) {
1508+ // Calculate delay: time between scroll end and key release
1509+ const scrollEndTime = recordingScrollEnd || Date . now ( ) ;
1510+ const delay = Math . max ( 200 , Date . now ( ) - scrollEndTime ) ; // Minimum 200ms delay
15201511
1521- this . setState ( {
1522- isRecordingAutoScroll : false ,
1523- autoScrollAmount : recordedScrollDelta ,
1524- autoScrollDelay : delay ,
1525- } , ( ) => {
1526- this . startAutoScroll ( ) ;
1527- } ) ;
1512+ this . setState ( {
1513+ commandKeyPressed : false ,
1514+ isRecordingAutoScroll : false ,
1515+ autoScrollAmount : recordedScrollDelta ,
1516+ autoScrollDelay : delay ,
1517+ } , ( ) => {
1518+ this . startAutoScroll ( ) ;
1519+ } ) ;
1520+ } else {
1521+ // No scroll was recorded, just reset
1522+ this . setState ( {
1523+ commandKeyPressed : false ,
1524+ isRecordingAutoScroll : false ,
1525+ recordedScrollDelta : 0 ,
1526+ recordingScrollStart : null ,
1527+ recordingScrollEnd : null ,
1528+ } ) ;
1529+ }
15281530 } else {
1529- // No scroll was recorded, just reset
1530- this . setState ( {
1531- isRecordingAutoScroll : false ,
1532- recordedScrollDelta : 0 ,
1533- recordingScrollStart : null ,
1534- recordingScrollEnd : null ,
1535- } ) ;
1531+ // Command key released without entering recording mode (no scroll occurred);
1532+ // just clear the key state, auto-scroll continues undisturbed
1533+ this . setState ( { commandKeyPressed : false } ) ;
15361534 }
15371535 }
15381536 }
@@ -1547,6 +1545,20 @@ export default class DataBrowser extends React.Component {
15471545 recordingScrollStart : prevState . recordingScrollStart || now ,
15481546 recordingScrollEnd : now ,
15491547 } ) ) ;
1548+ } else if ( this . state . commandKeyPressed ) {
1549+ // First scroll while Command key is held: stop any existing auto-scroll
1550+ // and enter recording mode
1551+ if ( this . state . isAutoScrolling ) {
1552+ this . stopAutoScroll ( ) ;
1553+ }
1554+ const deltaY = e . deltaY ;
1555+ const now = Date . now ( ) ;
1556+ this . setState ( {
1557+ isRecordingAutoScroll : true ,
1558+ recordedScrollDelta : deltaY ,
1559+ recordingScrollStart : now ,
1560+ recordingScrollEnd : now ,
1561+ } ) ;
15501562 } else if ( this . state . isAutoScrolling ) {
15511563 // User manually scrolled during auto-scroll, pause it and schedule resume
15521564 this . pauseAutoScrollWithResume ( ) ;
@@ -1567,31 +1579,68 @@ export default class DataBrowser extends React.Component {
15671579 // Schedule resume after 1000ms of inactivity
15681580 this . autoScrollResumeTimeoutId = setTimeout ( ( ) => {
15691581 if ( this . state . isAutoScrolling && this . state . autoScrollPaused ) {
1582+ // Clear so the 2-second post-block delay doesn't stack on top
1583+ this . autoScrollLastUnblockedAt = 0 ;
1584+ this . autoScrollIsBlocked = false ;
15701585 this . setState ( { autoScrollPaused : false } ) ;
15711586 }
15721587 } , 1000 ) ;
15731588 }
15741589
1575- handleNativeContextMenu ( ) {
1576- // Pause auto-scroll when native browser context menu is opened
1577- if ( this . state . isAutoScrolling && ! this . state . nativeContextMenuOpen ) {
1590+ setupNativeContextMenuDetection ( ) {
1591+ let cleanup = ( ) => { } ;
1592+
1593+ const onContextMenu = ( ) => {
15781594 this . setState ( { nativeContextMenuOpen : true } ) ;
1579- }
1580- }
15811595
1582- handleNativeContextMenuClose ( e ) {
1583- // Only process if native context menu is open
1584- if ( ! this . state . nativeContextMenuOpen ) {
1585- return ;
1586- }
1596+ // Remove previous close listeners if any
1597+ cleanup ( ) ;
15871598
1588- // For keydown events, only handle Escape key
1589- if ( e && e . type === 'keydown' && e . key !== 'Escape' ) {
1590- return ;
1591- }
1599+ const close = ( ) => {
1600+ cleanup ( ) ;
1601+ this . setState ( { nativeContextMenuOpen : false } ) ;
1602+ } ;
15921603
1593- // mousemove, Escape key, or blur all indicate the menu is closed
1594- this . setState ( { nativeContextMenuOpen : false } ) ;
1604+ const onPointerDown = ( ) => close ( ) ;
1605+ const onPointerMove = ( ) => close ( ) ;
1606+ const onKey = ( ) => close ( ) ;
1607+ const onVisibility = ( ) => {
1608+ if ( document . visibilityState === 'hidden' ) {
1609+ close ( ) ;
1610+ }
1611+ } ;
1612+ const onBlur = ( ) => close ( ) ;
1613+
1614+ window . addEventListener ( 'pointerdown' , onPointerDown , true ) ;
1615+ window . addEventListener ( 'keydown' , onKey , true ) ;
1616+ document . addEventListener ( 'visibilitychange' , onVisibility , true ) ;
1617+ window . addEventListener ( 'blur' , onBlur , true ) ;
1618+
1619+ // Delay pointermove registration to skip movement during the right-click gesture
1620+ const pointerMoveTimerId = setTimeout ( ( ) => {
1621+ window . addEventListener ( 'pointermove' , onPointerMove , true ) ;
1622+ } , 300 ) ;
1623+
1624+ cleanup = ( ) => {
1625+ clearTimeout ( pointerMoveTimerId ) ;
1626+ window . removeEventListener ( 'pointerdown' , onPointerDown , true ) ;
1627+ window . removeEventListener ( 'pointermove' , onPointerMove , true ) ;
1628+ window . removeEventListener ( 'keydown' , onKey , true ) ;
1629+ document . removeEventListener ( 'visibilitychange' , onVisibility , true ) ;
1630+ window . removeEventListener ( 'blur' , onBlur , true ) ;
1631+ cleanup = ( ) => { } ;
1632+ } ;
1633+ } ;
1634+
1635+ window . addEventListener ( 'contextmenu' , onContextMenu , true ) ;
1636+
1637+ return {
1638+ isOpen : ( ) => this . state . nativeContextMenuOpen ,
1639+ dispose : ( ) => {
1640+ window . removeEventListener ( 'contextmenu' , onContextMenu , true ) ;
1641+ cleanup ( ) ;
1642+ } ,
1643+ } ;
15951644 }
15961645
15971646 handlePanelMouseEnter ( ) {
@@ -1646,11 +1695,28 @@ export default class DataBrowser extends React.Component {
16461695 }
16471696 }
16481697
1698+ handleMouseButtonDown ( e ) {
1699+ if ( e . button === 0 ) {
1700+ this . mouseButtonPressed = true ;
1701+ }
1702+ }
1703+
1704+ handleMouseButtonUp ( e ) {
1705+ if ( e . button === 0 ) {
1706+ this . mouseButtonPressed = false ;
1707+ if ( this . state . isAutoScrolling ) {
1708+ this . autoScrollLastUnblockedAt = Date . now ( ) ;
1709+ }
1710+ }
1711+ }
1712+
16491713 startAutoScroll ( ) {
16501714 if ( this . state . isAutoScrolling ) {
16511715 return ;
16521716 }
16531717
1718+ this . autoScrollLastUnblockedAt = 0 ;
1719+ this . autoScrollIsBlocked = false ;
16541720 this . setState ( { isAutoScrolling : true , autoScrollPaused : false } , ( ) => {
16551721 this . performAutoScrollStep ( ) ;
16561722 } ) ;
@@ -1669,9 +1735,12 @@ export default class DataBrowser extends React.Component {
16691735 cancelAnimationFrame ( this . autoScrollAnimationId ) ;
16701736 this . autoScrollAnimationId = null ;
16711737 }
1738+ this . autoScrollLastUnblockedAt = 0 ;
1739+ this . autoScrollIsBlocked = false ;
16721740 this . setState ( {
16731741 isAutoScrolling : false ,
16741742 autoScrollPaused : false ,
1743+ commandKeyPressed : false ,
16751744 isRecordingAutoScroll : false ,
16761745 recordedScrollDelta : 0 ,
16771746 recordingScrollStart : null ,
@@ -1685,13 +1754,29 @@ export default class DataBrowser extends React.Component {
16851754 }
16861755
16871756 if ( this . isAutoScrollBlocked ( ) ) {
1688- // When blocked (modal, context menu, editing, or manual pause), keep checking but don't scroll
1757+ this . autoScrollIsBlocked = true ;
16891758 this . autoScrollTimeoutId = setTimeout ( ( ) => {
16901759 this . performAutoScrollStep ( ) ;
16911760 } , 100 ) ;
16921761 return ;
16931762 }
16941763
1764+ // Wait 1 second from the most recent unblock before scrolling
1765+ if ( this . autoScrollIsBlocked ) {
1766+ this . autoScrollIsBlocked = false ;
1767+ this . autoScrollLastUnblockedAt = Date . now ( ) ;
1768+ }
1769+ if ( this . autoScrollLastUnblockedAt ) {
1770+ const elapsed = Date . now ( ) - this . autoScrollLastUnblockedAt ;
1771+ if ( elapsed < 1000 ) {
1772+ this . autoScrollTimeoutId = setTimeout ( ( ) => {
1773+ this . performAutoScrollStep ( ) ;
1774+ } , 1000 - elapsed ) ;
1775+ return ;
1776+ }
1777+ this . autoScrollLastUnblockedAt = 0 ;
1778+ }
1779+
16951780 // Get the scrollable container
16961781 const container = this . aggregationPanelRef ?. current ;
16971782 if ( ! container ) {
@@ -1722,6 +1807,7 @@ export default class DataBrowser extends React.Component {
17221807
17231808 const animateScroll = ( currentTime ) => {
17241809 if ( ! this . state . isAutoScrolling || this . isAutoScrollBlocked ( ) ) {
1810+ this . autoScrollIsBlocked = true ;
17251811 // If stopped or blocked during animation, schedule next check
17261812 this . autoScrollTimeoutId = setTimeout ( ( ) => {
17271813 this . performAutoScrollStep ( ) ;
0 commit comments