Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 153 additions & 67 deletions src/dashboard/Data/Browser/DataBrowser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export default class DataBrowser extends React.Component {
nativeContextMenuOpen: false, // Whether the browser's native context menu is open
mouseOutsidePanel: true, // Whether the mouse is outside the AggregationPanel
mouseOverPanelHeader: false, // Whether the mouse is over the panel header row
commandKeyPressed: false, // Whether the Command/Meta key is currently pressed
optionKeyPressed: false, // Whether the Option/Alt key is currently pressed (pauses auto-scroll)
};

Expand Down Expand Up @@ -254,20 +255,22 @@ export default class DataBrowser extends React.Component {
this.stopAutoScroll = this.stopAutoScroll.bind(this);
this.performAutoScrollStep = this.performAutoScrollStep.bind(this);
this.pauseAutoScrollWithResume = this.pauseAutoScrollWithResume.bind(this);
this.handleNativeContextMenu = this.handleNativeContextMenu.bind(this);
this.handleNativeContextMenuClose = this.handleNativeContextMenuClose.bind(this);
this.handlePanelMouseEnter = this.handlePanelMouseEnter.bind(this);
this.handlePanelMouseLeave = this.handlePanelMouseLeave.bind(this);
this.handlePanelHeaderMouseEnter = this.handlePanelHeaderMouseEnter.bind(this);
this.handlePanelHeaderMouseLeave = this.handlePanelHeaderMouseLeave.bind(this);
this.handleOptionKeyDown = this.handleOptionKeyDown.bind(this);
this.handleOptionKeyUp = this.handleOptionKeyUp.bind(this);
this.handleMouseButtonDown = this.handleMouseButtonDown.bind(this);
this.handleMouseButtonUp = this.handleMouseButtonUp.bind(this);
this.saveOrderTimeout = null;
this.aggregationPanelRef = React.createRef();
this.autoScrollIntervalId = null;
this.autoScrollTimeoutId = null;
this.autoScrollResumeTimeoutId = null;
this.autoScrollAnimationId = null;
this.mouseButtonPressed = false;
this.nativeContextMenuTracker = null;
this.panelHeaderLeaveTimeoutId = null;
this.panelColumnRefs = [];
this.activePanelIndex = -1;
Expand Down Expand Up @@ -354,18 +357,11 @@ export default class DataBrowser extends React.Component {
// Option key listeners for pausing auto-scroll
document.body.addEventListener('keydown', this.handleOptionKeyDown);
document.body.addEventListener('keyup', this.handleOptionKeyUp);
// Left mouse button listener for pausing auto-scroll
window.addEventListener('mousedown', this.handleMouseButtonDown);
window.addEventListener('mouseup', this.handleMouseButtonUp);
// Native context menu detection for auto-scroll pause
// Use capture phase to ensure we detect the event before the menu handles it
document.addEventListener('contextmenu', this.handleNativeContextMenu, true);
// Listen for events that indicate the context menu was closed:
// - mousemove: user moved mouse after menu closed (most reliable)
// - keydown: Escape key closes the menu
// - blur: switching windows/tabs closes the menu
// NOTE: click/mousedown don't fire while native context menu is open
// NOTE: scroll is not used because auto-scroll itself triggers it
window.addEventListener('mousemove', this.handleNativeContextMenuClose);
window.addEventListener('keydown', this.handleNativeContextMenuClose, true);
window.addEventListener('blur', this.handleNativeContextMenuClose);
this.nativeContextMenuTracker = this.setupNativeContextMenuDetection();

// Load keyboard shortcuts from server
try {
Expand Down Expand Up @@ -407,10 +403,11 @@ export default class DataBrowser extends React.Component {
// Option key listeners cleanup
document.body.removeEventListener('keydown', this.handleOptionKeyDown);
document.body.removeEventListener('keyup', this.handleOptionKeyUp);
document.removeEventListener('contextmenu', this.handleNativeContextMenu, true);
window.removeEventListener('mousemove', this.handleNativeContextMenuClose);
window.removeEventListener('keydown', this.handleNativeContextMenuClose, true);
window.removeEventListener('blur', this.handleNativeContextMenuClose);
window.removeEventListener('mousedown', this.handleMouseButtonDown);
window.removeEventListener('mouseup', this.handleMouseButtonUp);
if (this.nativeContextMenuTracker) {
this.nativeContextMenuTracker.dispose();
}
if (this.autoScrollTimeoutId) {
clearTimeout(this.autoScrollTimeoutId);
}
Expand Down Expand Up @@ -1466,7 +1463,8 @@ export default class DataBrowser extends React.Component {
nativeContextMenuOpen ||
disableKeyControls ||
hoverBlocked ||
optionKeyPressed
optionKeyPressed ||
this.mouseButtonPressed
);
}

Expand All @@ -1492,47 +1490,47 @@ export default class DataBrowser extends React.Component {

handleAutoScrollKeyDown(e) {
// Command/Meta key = keyCode 91 (left) or 93 (right)
// Only detect when panels are visible and auto-scroll is enabled
// Only track that Command key is held; don't stop auto-scroll or enter
// recording mode until the user actually scrolls (handled in handleAutoScrollWheel)
if ((e.keyCode === 91 || e.keyCode === 93) && this.state.autoScrollEnabled && this.state.isPanelVisible && !this.state.isRecordingAutoScroll) {
// Stop any existing auto-scroll first
if (this.state.isAutoScrolling) {
this.stopAutoScroll();
}
this.setState({
isRecordingAutoScroll: true,
recordedScrollDelta: 0,
recordingScrollStart: null,
recordingScrollEnd: null,
});
this.setState({ commandKeyPressed: true });
}
}

handleAutoScrollKeyUp(e) {
// Command/Meta key = keyCode 91 (left) or 93 (right)
if ((e.keyCode === 91 || e.keyCode === 93) && this.state.isRecordingAutoScroll) {
const { recordedScrollDelta, recordingScrollStart, recordingScrollEnd } = this.state;
if (e.keyCode === 91 || e.keyCode === 93) {
if (this.state.isRecordingAutoScroll) {
const { recordedScrollDelta, recordingScrollStart, recordingScrollEnd } = this.state;

// Only start auto-scroll if we actually recorded some scrolling
if (recordedScrollDelta !== 0 && recordingScrollStart !== null) {
// Calculate delay: time between scroll end and key release
const scrollEndTime = recordingScrollEnd || Date.now();
const delay = Math.max(200, Date.now() - scrollEndTime); // Minimum 200ms delay
// Only start auto-scroll if we actually recorded some scrolling
if (recordedScrollDelta !== 0 && recordingScrollStart !== null) {
// Calculate delay: time between scroll end and key release
const scrollEndTime = recordingScrollEnd || Date.now();
const delay = Math.max(200, Date.now() - scrollEndTime); // Minimum 200ms delay

this.setState({
isRecordingAutoScroll: false,
autoScrollAmount: recordedScrollDelta,
autoScrollDelay: delay,
}, () => {
this.startAutoScroll();
});
this.setState({
commandKeyPressed: false,
isRecordingAutoScroll: false,
autoScrollAmount: recordedScrollDelta,
autoScrollDelay: delay,
}, () => {
this.startAutoScroll();
});
} else {
// No scroll was recorded, just reset
this.setState({
commandKeyPressed: false,
isRecordingAutoScroll: false,
recordedScrollDelta: 0,
recordingScrollStart: null,
recordingScrollEnd: null,
});
}
} else {
// No scroll was recorded, just reset
this.setState({
isRecordingAutoScroll: false,
recordedScrollDelta: 0,
recordingScrollStart: null,
recordingScrollEnd: null,
});
// Command key released without entering recording mode (no scroll occurred);
// just clear the key state, auto-scroll continues undisturbed
this.setState({ commandKeyPressed: false });
}
}
}
Expand All @@ -1547,6 +1545,20 @@ export default class DataBrowser extends React.Component {
recordingScrollStart: prevState.recordingScrollStart || now,
recordingScrollEnd: now,
}));
} else if (this.state.commandKeyPressed) {
// First scroll while Command key is held: stop any existing auto-scroll
// and enter recording mode
if (this.state.isAutoScrolling) {
this.stopAutoScroll();
}
const deltaY = e.deltaY;
const now = Date.now();
this.setState({
isRecordingAutoScroll: true,
recordedScrollDelta: deltaY,
recordingScrollStart: now,
recordingScrollEnd: now,
});
} else if (this.state.isAutoScrolling) {
// User manually scrolled during auto-scroll, pause it and schedule resume
this.pauseAutoScrollWithResume();
Expand All @@ -1567,31 +1579,68 @@ export default class DataBrowser extends React.Component {
// Schedule resume after 1000ms of inactivity
this.autoScrollResumeTimeoutId = setTimeout(() => {
if (this.state.isAutoScrolling && this.state.autoScrollPaused) {
// Clear so the 2-second post-block delay doesn't stack on top
this.autoScrollLastUnblockedAt = 0;
this.autoScrollIsBlocked = false;
this.setState({ autoScrollPaused: false });
}
}, 1000);
}

handleNativeContextMenu() {
// Pause auto-scroll when native browser context menu is opened
if (this.state.isAutoScrolling && !this.state.nativeContextMenuOpen) {
setupNativeContextMenuDetection() {
let cleanup = () => {};

const onContextMenu = () => {
this.setState({ nativeContextMenuOpen: true });
}
}

handleNativeContextMenuClose(e) {
// Only process if native context menu is open
if (!this.state.nativeContextMenuOpen) {
return;
}
// Remove previous close listeners if any
cleanup();

// For keydown events, only handle Escape key
if (e && e.type === 'keydown' && e.key !== 'Escape') {
return;
}
const close = () => {
cleanup();
this.setState({ nativeContextMenuOpen: false });
};

// mousemove, Escape key, or blur all indicate the menu is closed
this.setState({ nativeContextMenuOpen: false });
const onPointerDown = () => close();
const onPointerMove = () => close();
const onKey = () => close();
const onVisibility = () => {
if (document.visibilityState === 'hidden') {
close();
}
};
const onBlur = () => close();

window.addEventListener('pointerdown', onPointerDown, true);
window.addEventListener('keydown', onKey, true);
document.addEventListener('visibilitychange', onVisibility, true);
window.addEventListener('blur', onBlur, true);

// Delay pointermove registration to skip movement during the right-click gesture
const pointerMoveTimerId = setTimeout(() => {
window.addEventListener('pointermove', onPointerMove, true);
}, 300);

cleanup = () => {
clearTimeout(pointerMoveTimerId);
window.removeEventListener('pointerdown', onPointerDown, true);
window.removeEventListener('pointermove', onPointerMove, true);
window.removeEventListener('keydown', onKey, true);
document.removeEventListener('visibilitychange', onVisibility, true);
window.removeEventListener('blur', onBlur, true);
cleanup = () => {};
};
};

window.addEventListener('contextmenu', onContextMenu, true);

return {
isOpen: () => this.state.nativeContextMenuOpen,
dispose: () => {
window.removeEventListener('contextmenu', onContextMenu, true);
cleanup();
},
};
}

handlePanelMouseEnter() {
Expand Down Expand Up @@ -1646,11 +1695,28 @@ export default class DataBrowser extends React.Component {
}
}

handleMouseButtonDown(e) {
if (e.button === 0) {
this.mouseButtonPressed = true;
}
}

handleMouseButtonUp(e) {
if (e.button === 0) {
this.mouseButtonPressed = false;
if (this.state.isAutoScrolling) {
this.autoScrollLastUnblockedAt = Date.now();
}
}
}

startAutoScroll() {
if (this.state.isAutoScrolling) {
return;
}

this.autoScrollLastUnblockedAt = 0;
this.autoScrollIsBlocked = false;
this.setState({ isAutoScrolling: true, autoScrollPaused: false }, () => {
this.performAutoScrollStep();
});
Expand All @@ -1669,9 +1735,12 @@ export default class DataBrowser extends React.Component {
cancelAnimationFrame(this.autoScrollAnimationId);
this.autoScrollAnimationId = null;
}
this.autoScrollLastUnblockedAt = 0;
this.autoScrollIsBlocked = false;
this.setState({
isAutoScrolling: false,
autoScrollPaused: false,
commandKeyPressed: false,
isRecordingAutoScroll: false,
recordedScrollDelta: 0,
recordingScrollStart: null,
Expand All @@ -1685,13 +1754,29 @@ export default class DataBrowser extends React.Component {
}

if (this.isAutoScrollBlocked()) {
// When blocked (modal, context menu, editing, or manual pause), keep checking but don't scroll
this.autoScrollIsBlocked = true;
this.autoScrollTimeoutId = setTimeout(() => {
this.performAutoScrollStep();
}, 100);
return;
}

// Wait 1 second from the most recent unblock before scrolling
if (this.autoScrollIsBlocked) {
this.autoScrollIsBlocked = false;
this.autoScrollLastUnblockedAt = Date.now();
}
if (this.autoScrollLastUnblockedAt) {
const elapsed = Date.now() - this.autoScrollLastUnblockedAt;
if (elapsed < 1000) {
this.autoScrollTimeoutId = setTimeout(() => {
this.performAutoScrollStep();
}, 1000 - elapsed);
return;
}
this.autoScrollLastUnblockedAt = 0;
}

// Get the scrollable container
const container = this.aggregationPanelRef?.current;
if (!container) {
Expand Down Expand Up @@ -1722,6 +1807,7 @@ export default class DataBrowser extends React.Component {

const animateScroll = (currentTime) => {
if (!this.state.isAutoScrolling || this.isAutoScrollBlocked()) {
this.autoScrollIsBlocked = true;
// If stopped or blocked during animation, schedule next check
this.autoScrollTimeoutId = setTimeout(() => {
this.performAutoScrollStep();
Expand Down
Loading