Skip to content

Commit bfe4e8a

Browse files
authored
fix: Info panel auto-scrolling not pausing during UI interaction like text selection or context menu display (#3165)
1 parent 890494b commit bfe4e8a

1 file changed

Lines changed: 153 additions & 67 deletions

File tree

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

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

Comments
 (0)