@@ -255,20 +255,22 @@ export default class DataBrowser extends React.Component {
255255 this . stopAutoScroll = this . stopAutoScroll . bind ( this ) ;
256256 this . performAutoScrollStep = this . performAutoScrollStep . bind ( this ) ;
257257 this . pauseAutoScrollWithResume = this . pauseAutoScrollWithResume . bind ( this ) ;
258- this . handleNativeContextMenu = this . handleNativeContextMenu . bind ( this ) ;
259- this . handleNativeContextMenuClose = this . handleNativeContextMenuClose . bind ( this ) ;
260258 this . handlePanelMouseEnter = this . handlePanelMouseEnter . bind ( this ) ;
261259 this . handlePanelMouseLeave = this . handlePanelMouseLeave . bind ( this ) ;
262260 this . handlePanelHeaderMouseEnter = this . handlePanelHeaderMouseEnter . bind ( this ) ;
263261 this . handlePanelHeaderMouseLeave = this . handlePanelHeaderMouseLeave . bind ( this ) ;
264262 this . handleOptionKeyDown = this . handleOptionKeyDown . bind ( this ) ;
265263 this . handleOptionKeyUp = this . handleOptionKeyUp . bind ( this ) ;
264+ this . handleMouseButtonDown = this . handleMouseButtonDown . bind ( this ) ;
265+ this . handleMouseButtonUp = this . handleMouseButtonUp . bind ( this ) ;
266266 this . saveOrderTimeout = null ;
267267 this . aggregationPanelRef = React . createRef ( ) ;
268268 this . autoScrollIntervalId = null ;
269269 this . autoScrollTimeoutId = null ;
270270 this . autoScrollResumeTimeoutId = null ;
271271 this . autoScrollAnimationId = null ;
272+ this . mouseButtonPressed = false ;
273+ this . nativeContextMenuTracker = null ;
272274 this . panelHeaderLeaveTimeoutId = null ;
273275 this . panelColumnRefs = [ ] ;
274276 this . activePanelIndex = - 1 ;
@@ -355,18 +357,11 @@ export default class DataBrowser extends React.Component {
355357 // Option key listeners for pausing auto-scroll
356358 document . body . addEventListener ( 'keydown' , this . handleOptionKeyDown ) ;
357359 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 ) ;
358363 // Native context menu detection for auto-scroll pause
359- // Use capture phase to ensure we detect the event before the menu handles it
360- document . addEventListener ( 'contextmenu' , this . handleNativeContextMenu , true ) ;
361- // Listen for events that indicate the context menu was closed:
362- // - mousemove: user moved mouse after menu closed (most reliable)
363- // - keydown: Escape key closes the menu
364- // - blur: switching windows/tabs closes the menu
365- // NOTE: click/mousedown don't fire while native context menu is open
366- // NOTE: scroll is not used because auto-scroll itself triggers it
367- window . addEventListener ( 'mousemove' , this . handleNativeContextMenuClose ) ;
368- window . addEventListener ( 'keydown' , this . handleNativeContextMenuClose , true ) ;
369- window . addEventListener ( 'blur' , this . handleNativeContextMenuClose ) ;
364+ this . nativeContextMenuTracker = this . setupNativeContextMenuDetection ( ) ;
370365
371366 // Load keyboard shortcuts from server
372367 try {
@@ -408,10 +403,11 @@ export default class DataBrowser extends React.Component {
408403 // Option key listeners cleanup
409404 document . body . removeEventListener ( 'keydown' , this . handleOptionKeyDown ) ;
410405 document . body . removeEventListener ( 'keyup' , this . handleOptionKeyUp ) ;
411- document . removeEventListener ( 'contextmenu' , this . handleNativeContextMenu , true ) ;
412- window . removeEventListener ( 'mousemove' , this . handleNativeContextMenuClose ) ;
413- window . removeEventListener ( 'keydown' , this . handleNativeContextMenuClose , true ) ;
414- 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+ }
415411 if ( this . autoScrollTimeoutId ) {
416412 clearTimeout ( this . autoScrollTimeoutId ) ;
417413 }
@@ -1467,7 +1463,8 @@ export default class DataBrowser extends React.Component {
14671463 nativeContextMenuOpen ||
14681464 disableKeyControls ||
14691465 hoverBlocked ||
1470- optionKeyPressed
1466+ optionKeyPressed ||
1467+ this . mouseButtonPressed
14711468 ) ;
14721469 }
14731470
@@ -1582,31 +1579,67 @@ export default class DataBrowser extends React.Component {
15821579 // Schedule resume after 1000ms of inactivity
15831580 this . autoScrollResumeTimeoutId = setTimeout ( ( ) => {
15841581 if ( this . state . isAutoScrolling && this . state . autoScrollPaused ) {
1582+ // Clear so the 2-second post-block delay doesn't stack on top
1583+ this . autoScrollWasBlocked = false ;
15851584 this . setState ( { autoScrollPaused : false } ) ;
15861585 }
15871586 } , 1000 ) ;
15881587 }
15891588
1590- handleNativeContextMenu ( ) {
1591- // Pause auto-scroll when native browser context menu is opened
1592- if ( this . state . isAutoScrolling && ! this . state . nativeContextMenuOpen ) {
1589+ setupNativeContextMenuDetection ( ) {
1590+ let cleanup = ( ) => { } ;
1591+
1592+ const onContextMenu = ( ) => {
15931593 this . setState ( { nativeContextMenuOpen : true } ) ;
1594- }
1595- }
15961594
1597- handleNativeContextMenuClose ( e ) {
1598- // Only process if native context menu is open
1599- if ( ! this . state . nativeContextMenuOpen ) {
1600- return ;
1601- }
1595+ // Remove previous close listeners if any
1596+ cleanup ( ) ;
16021597
1603- // For keydown events, only handle Escape key
1604- if ( e && e . type === 'keydown' && e . key !== 'Escape' ) {
1605- return ;
1606- }
1598+ const close = ( ) => {
1599+ cleanup ( ) ;
1600+ this . setState ( { nativeContextMenuOpen : false } ) ;
1601+ } ;
1602+
1603+ const onPointerDown = ( ) => close ( ) ;
1604+ const onPointerMove = ( ) => close ( ) ;
1605+ const onKey = ( ) => close ( ) ;
1606+ const onVisibility = ( ) => {
1607+ if ( document . visibilityState === 'hidden' ) {
1608+ close ( ) ;
1609+ }
1610+ } ;
1611+ const onBlur = ( ) => close ( ) ;
1612+
1613+ window . addEventListener ( 'pointerdown' , onPointerDown , true ) ;
1614+ window . addEventListener ( 'keydown' , onKey , true ) ;
1615+ document . addEventListener ( 'visibilitychange' , onVisibility , true ) ;
1616+ window . addEventListener ( 'blur' , onBlur , true ) ;
1617+
1618+ // Delay pointermove registration to skip movement during the right-click gesture
1619+ const pointerMoveTimerId = setTimeout ( ( ) => {
1620+ window . addEventListener ( 'pointermove' , onPointerMove , true ) ;
1621+ } , 300 ) ;
1622+
1623+ cleanup = ( ) => {
1624+ clearTimeout ( pointerMoveTimerId ) ;
1625+ window . removeEventListener ( 'pointerdown' , onPointerDown , true ) ;
1626+ window . removeEventListener ( 'pointermove' , onPointerMove , true ) ;
1627+ window . removeEventListener ( 'keydown' , onKey , true ) ;
1628+ document . removeEventListener ( 'visibilitychange' , onVisibility , true ) ;
1629+ window . removeEventListener ( 'blur' , onBlur , true ) ;
1630+ cleanup = ( ) => { } ;
1631+ } ;
1632+ } ;
16071633
1608- // mousemove, Escape key, or blur all indicate the menu is closed
1609- this . setState ( { nativeContextMenuOpen : false } ) ;
1634+ window . addEventListener ( 'contextmenu' , onContextMenu , true ) ;
1635+
1636+ return {
1637+ isOpen : ( ) => this . state . nativeContextMenuOpen ,
1638+ dispose : ( ) => {
1639+ window . removeEventListener ( 'contextmenu' , onContextMenu , true ) ;
1640+ cleanup ( ) ;
1641+ } ,
1642+ } ;
16101643 }
16111644
16121645 handlePanelMouseEnter ( ) {
@@ -1661,11 +1694,24 @@ export default class DataBrowser extends React.Component {
16611694 }
16621695 }
16631696
1697+ handleMouseButtonDown ( e ) {
1698+ if ( e . button === 0 ) {
1699+ this . mouseButtonPressed = true ;
1700+ }
1701+ }
1702+
1703+ handleMouseButtonUp ( e ) {
1704+ if ( e . button === 0 ) {
1705+ this . mouseButtonPressed = false ;
1706+ }
1707+ }
1708+
16641709 startAutoScroll ( ) {
16651710 if ( this . state . isAutoScrolling ) {
16661711 return ;
16671712 }
16681713
1714+ this . autoScrollWasBlocked = false ;
16691715 this . setState ( { isAutoScrolling : true , autoScrollPaused : false } , ( ) => {
16701716 this . performAutoScrollStep ( ) ;
16711717 } ) ;
@@ -1684,6 +1730,7 @@ export default class DataBrowser extends React.Component {
16841730 cancelAnimationFrame ( this . autoScrollAnimationId ) ;
16851731 this . autoScrollAnimationId = null ;
16861732 }
1733+ this . autoScrollWasBlocked = false ;
16871734 this . setState ( {
16881735 isAutoScrolling : false ,
16891736 autoScrollPaused : false ,
@@ -1701,13 +1748,22 @@ export default class DataBrowser extends React.Component {
17011748 }
17021749
17031750 if ( this . isAutoScrollBlocked ( ) ) {
1704- // When blocked (modal, context menu, editing, or manual pause), keep checking but don't scroll
1751+ this . autoScrollWasBlocked = true ;
17051752 this . autoScrollTimeoutId = setTimeout ( ( ) => {
17061753 this . performAutoScrollStep ( ) ;
17071754 } , 100 ) ;
17081755 return ;
17091756 }
17101757
1758+ // After unblocking, wait 2 seconds before resuming
1759+ if ( this . autoScrollWasBlocked ) {
1760+ this . autoScrollWasBlocked = false ;
1761+ this . autoScrollTimeoutId = setTimeout ( ( ) => {
1762+ this . performAutoScrollStep ( ) ;
1763+ } , 2000 ) ;
1764+ return ;
1765+ }
1766+
17111767 // Get the scrollable container
17121768 const container = this . aggregationPanelRef ?. current ;
17131769 if ( ! container ) {
@@ -1738,6 +1794,7 @@ export default class DataBrowser extends React.Component {
17381794
17391795 const animateScroll = ( currentTime ) => {
17401796 if ( ! this . state . isAutoScrolling || this . isAutoScrollBlocked ( ) ) {
1797+ this . autoScrollWasBlocked = true ;
17411798 // If stopped or blocked during animation, schedule next check
17421799 this . autoScrollTimeoutId = setTimeout ( ( ) => {
17431800 this . performAutoScrollStep ( ) ;
0 commit comments