Skip to content

Commit d7bb7cf

Browse files
committed
context scroll fix
1 parent 47931fc commit d7bb7cf

File tree

1 file changed

+92
-35
lines changed

1 file changed

+92
-35
lines changed

src/dashboard/Data/Browser/DataBrowser.react.js

Lines changed: 92 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)