diff --git a/README.md b/README.md index 3a5599cc33..59a858c3e0 100644 --- a/README.md +++ b/README.md @@ -1453,14 +1453,14 @@ The info panel supports automatic scrolling, which is useful for hands-free brow **How to use:** 1. Enable auto-scroll via *Settings > Info Panel > Auto-scroll*. -2. Hold the **Option** (Alt) key while scrolling in the panel to record the scroll amount. -3. Release the Option key after pausing for the desired interval between scrolls. +2. Hold the **Command** (⌘) key while scrolling in the panel to record the scroll amount. +3. Release the Command key after pausing for the desired interval between scrolls. 4. Auto-scrolling begins automatically, repeating the recorded scroll amount and pause interval. **Controls:** - **Escape key**: Stop auto-scrolling. -- **Option key**: Stop current auto-scroll and start recording a new scroll pattern. +- **Command key**: Stop current auto-scroll and start recording a new scroll pattern. - **Manual scroll**: Temporarily pauses auto-scrolling, which resumes after inactivity. - **Auto-scroll button**: Click the highlighted "Auto-scroll" button in the toolbar to stop. diff --git a/src/components/BrowserMenu/BrowserMenu.react.js b/src/components/BrowserMenu/BrowserMenu.react.js index 9f4dcb0919..5d97a9dd26 100644 --- a/src/components/BrowserMenu/BrowserMenu.react.js +++ b/src/components/BrowserMenu/BrowserMenu.react.js @@ -16,10 +16,22 @@ export default class BrowserMenu extends React.Component { constructor() { super(); - this.state = { open: false, openToLeft: false }; + this.state = { open: false, openToLeft: false, openChildKey: null, closeAllTrigger: 0 }; this.wrapRef = React.createRef(); } + componentDidUpdate(prevProps) { + // Close if shouldClose changed to true (sibling submenu opened), + // OR if closeAllTrigger incremented (MenuItem was hovered) + const shouldCloseChanged = this.props.shouldClose && !prevProps.shouldClose; + const closeAllTriggered = this.props.closeAllTrigger !== undefined && + prevProps.closeAllTrigger !== undefined && + this.props.closeAllTrigger !== prevProps.closeAllTrigger; + if ((shouldCloseChanged || closeAllTriggered) && this.state.open) { + this.setState({ open: false }); + } + } + render() { let menu = null; const isSubmenu = !!this.props.parentClose; @@ -54,38 +66,57 @@ export default class BrowserMenu extends React.Component { : styles.body } style={{ - minWidth: this.wrapRef.current.clientWidth, + // Only apply minWidth for top-level menus, not submenus ...(isSubmenu - ? { - top: 0, - left: this.state.openToLeft - ? 0 - : `${this.wrapRef.current.clientWidth - 3}px`, - transform: this.state.openToLeft - ? 'translateX(calc(-100% + 3px))' - : undefined, - } - : {}), + ? (() => { + // Find the parent menu container to get its width for proper positioning + const parentMenuBody = this.wrapRef.current.closest(`.${styles.subMenuBody}`) || + this.wrapRef.current.closest(`.${styles.subMenuBodyLeft}`) || + this.wrapRef.current.closest(`.${styles.body}`); + const parentWidth = parentMenuBody ? parentMenuBody.clientWidth : this.wrapRef.current.clientWidth; + return { + top: 0, + left: this.state.openToLeft + ? 0 + : `${parentWidth - 3}px`, + transform: this.state.openToLeft + ? 'translateX(calc(-100% + 3px))' + : undefined, + }; + })() + : { minWidth: this.wrapRef.current.clientWidth }), }} > - {React.Children.map(this.props.children, (child) => { + {React.Children.map(this.props.children, (child, index) => { if (React.isValidElement(child)) { if (child.type === BrowserMenu) { + const childKey = `submenu-${index}`; + const shouldClose = this.state.openChildKey !== null && this.state.openChildKey !== childKey; return React.cloneElement(child, { ...child.props, parentClose: () => { this.setState({ open: false }); this.props.parentClose?.(); }, + childKey, + shouldClose, + closeAllTrigger: this.state.closeAllTrigger, + onSubmenuOpen: (key) => this.setState({ openChildKey: key }), }); } - // Pass closeMenu prop to all other children (like MenuItem) + // Pass closeMenu and onItemHover props to all other children (like MenuItem) return React.cloneElement(child, { ...child.props, closeMenu: () => { this.setState({ open: false }); this.props.parentClose?.(); }, + onItemHover: () => { + this.setState(prev => ({ + openChildKey: null, + closeAllTrigger: prev.closeAllTrigger + 1, + })); + }, }); } return child; @@ -106,11 +137,17 @@ export default class BrowserMenu extends React.Component { if (!this.props.disabled) { if (isSubmenu) { entryEvents.onMouseEnter = () => { - const rect = this.wrapRef.current.getBoundingClientRect(); - const width = this.wrapRef.current.clientWidth; - const openToLeft = rect.right + width > window.innerWidth; + // Find the parent menu container to get its right edge for proper positioning + const parentMenuBody = this.wrapRef.current.closest(`.${styles.subMenuBody}`) || + this.wrapRef.current.closest(`.${styles.subMenuBodyLeft}`) || + this.wrapRef.current.closest(`.${styles.body}`); + const parentRect = parentMenuBody ? parentMenuBody.getBoundingClientRect() : this.wrapRef.current.getBoundingClientRect(); + const estimatedSubmenuWidth = 150; // Estimate for edge detection + const openToLeft = parentRect.right + estimatedSubmenuWidth > window.innerWidth; this.setState({ open: true, openToLeft }); this.props.setCurrent?.(null); + // Notify parent that this submenu is now open (to close sibling submenus) + this.props.onSubmenuOpen?.(this.props.childKey); }; } else { entryEvents.onClick = () => { @@ -119,19 +156,35 @@ export default class BrowserMenu extends React.Component { }; } } + const wrapEvents = {}; + if (isSubmenu && !this.props.disabled) { + wrapEvents.onMouseLeave = (event) => { + // Only close submenu if mouse is moving to a sibling item in the parent menu + // Don't close if moving outside the menu entirely + const relatedTarget = event.relatedTarget; + if (!relatedTarget) { + return; + } + // Find the parent menu body that contains this submenu + const parentMenuBody = this.wrapRef.current.closest(`.${styles.subMenuBody}`) || + this.wrapRef.current.closest(`.${styles.subMenuBodyLeft}`) || + this.wrapRef.current.closest(`.${styles.body}`); + // Check if mouse is moving to another item in the same parent menu (sibling) + const isMovingToSibling = parentMenuBody && + parentMenuBody.contains(relatedTarget) && + !this.wrapRef.current.contains(relatedTarget); + if (isMovingToSibling) { + this.setState({ open: false }); + } + }; + } return ( -
+
{this.props.icon && } {this.props.title} - {isSubmenu && - React.Children.toArray(this.props.children).some(c => React.isValidElement(c) && c.type === BrowserMenu) && ( - + {isSubmenu && this.props.children && ( + )}
{menu} diff --git a/src/components/BrowserMenu/BrowserMenu.scss b/src/components/BrowserMenu/BrowserMenu.scss index 1cbf889244..299d82165a 100644 --- a/src/components/BrowserMenu/BrowserMenu.scss +++ b/src/components/BrowserMenu/BrowserMenu.scss @@ -11,6 +11,25 @@ display: inline-block; } +.menu { + position: relative; +} + +// Make submenu entries stretch to full width within menu bodies +.body, .subMenuBody, .subMenuBodyLeft { + > .wrap { + display: block; + + > .entry { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 14px; + white-space: nowrap; + } + } +} + .entry { height: 30px; padding: 8px; @@ -113,15 +132,23 @@ } .submenuArrow { - margin-left: 4px; - fill: #66637A; + margin-left: 12px; + flex-shrink: 0; + font-size: 18px; + line-height: 1; + color: rgba(255, 255, 255, 0.5); + + &::after { + content: '›'; + } } .entry:hover .submenuArrow { - fill: white; + color: white; } .item { + position: relative; padding: 4px 14px; white-space: nowrap; cursor: pointer; @@ -129,6 +156,10 @@ &:hover { background: $blue; + + .submenuArrow { + border-left-color: white; + } } &.disabled { diff --git a/src/components/BrowserMenu/MenuItem.react.js b/src/components/BrowserMenu/MenuItem.react.js index 8460c9282e..5b0a8267cc 100644 --- a/src/components/BrowserMenu/MenuItem.react.js +++ b/src/components/BrowserMenu/MenuItem.react.js @@ -8,7 +8,7 @@ import React from 'react'; import styles from 'components/BrowserMenu/BrowserMenu.scss'; -const MenuItem = ({ text, shortcut, disabled, active, greenActive, onClick, disableMouseDown = false, closeMenu }) => { +const MenuItem = ({ text, shortcut, disabled, active, greenActive, onClick, disableMouseDown = false, closeMenu, onItemHover }) => { const classes = [styles.item]; if (disabled) { classes.push(styles.disabled); @@ -35,6 +35,7 @@ const MenuItem = ({ text, shortcut, disabled, active, greenActive, onClick, disa className={classes.join(' ')} onClick={handleClick} onMouseDown={disableMouseDown ? undefined : handleClick} // This is needed - onClick alone doesn't work in this context + onMouseEnter={onItemHover} style={{ position: 'relative', zIndex: 9999, diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index 23e2e10045..385bbdff24 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -156,7 +156,7 @@ const MenuSection = ({ level, items, path, setPath, hide, hoveredItemOffset }) = ); }; -const ContextMenu = ({ x, y, items }) => { +const ContextMenu = ({ x, y, items, onHide }) => { const [path, setPath] = useState([0]); // Track the pixel offset of the hovered item for each level const [hoveredOffsets, setHoveredOffsets] = useState([0]); @@ -171,6 +171,7 @@ const ContextMenu = ({ x, y, items }) => { setVisible(false); setPath([0]); setHoveredOffsets([0]); + onHide?.(); }; // Combined setter that updates both path and offsets @@ -235,6 +236,7 @@ ContextMenu.propTypes = { x: PropTypes.number.isRequired.describe('X context menu position.'), y: PropTypes.number.isRequired.describe('Y context menu position.'), items: PropTypes.array.isRequired.describe('Array with tree representation of context menu items.'), + onHide: PropTypes.func.describe('Callback when context menu is hidden.'), }; export default ContextMenu; diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 93b6732434..80463c074c 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -2032,7 +2032,8 @@ class Browser extends DashboardView { this.state.showCloneSelectedRowsDialog || this.state.showEditRowDialog || this.state.showPermissionsDialog || - this.state.showExportSelectedRowsDialog + this.state.showExportSelectedRowsDialog || + this.state.showAddToConfigDialog ); } diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index 323de52e55..d2887aeff3 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -98,6 +98,8 @@ const BrowserToolbar = ({ toggleShowPanelCheckbox, autoScrollEnabled, toggleAutoScroll, + autoScrollRequireHover, + toggleAutoScrollRequireHover, isAutoScrolling, stopAutoScroll, toggleGraphPanel, @@ -390,25 +392,6 @@ const BrowserToolbar = ({ {onAddRow &&
} - - {scrollToTop && ( - - )} - Scroll to top - - } - onClick={() => { - toggleScrollToTop(); - }} - /> @@ -431,7 +414,7 @@ const BrowserToolbar = ({ - {syncPanelScroll && ( + {batchNavigate && ( )} - Sync panel scrolling + Batch-navigate panels } onClick={() => { - toggleSyncPanelScroll(); + toggleBatchNavigate(); }} /> - {batchNavigate && ( + {showPanelCheckbox && ( )} - Batch-navigate panels + Show panel selection } onClick={() => { - toggleBatchNavigate(); + toggleShowPanelCheckbox(); }} /> + - {showPanelCheckbox && ( + {scrollToTop && ( )} - Show panel selection + Scroll to top } onClick={() => { - toggleShowPanelCheckbox(); + toggleScrollToTop(); }} /> - {autoScrollEnabled && ( + {syncPanelScroll && ( )} - Auto-scroll + Sync panel scrolling } onClick={() => { - toggleAutoScroll(); + toggleSyncPanelScroll(); }} /> + + + {autoScrollEnabled && ( + + )} + Enabled + + } + onClick={() => { + toggleAutoScroll(); + }} + /> + + {autoScrollRequireHover && ( + + )} + Require hover + + } + onClick={() => { + toggleAutoScrollRequireHover(); + }} + /> +
diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index 3168e28c75..41cbc9d4ef 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -39,6 +39,7 @@ const AGGREGATION_PANEL_COUNT = 'aggregationPanelCount'; const GRAPH_PANEL_VISIBLE = 'graphPanelVisible'; const GRAPH_PANEL_WIDTH = 'graphPanelWidth'; const AGGREGATION_PANEL_AUTO_SCROLL = 'aggregationPanelAutoScroll'; +const AGGREGATION_PANEL_AUTO_SCROLL_REQUIRE_HOVER = 'aggregationPanelAutoScrollRequireHover'; function formatValueForCopy(value, type) { if (value === undefined) { @@ -123,6 +124,8 @@ export default class DataBrowser extends React.Component { ]?.[props.className]; const storedAutoScroll = window.localStorage?.getItem(AGGREGATION_PANEL_AUTO_SCROLL) === 'true'; + const storedAutoScrollRequireHover = + window.localStorage?.getItem(AGGREGATION_PANEL_AUTO_SCROLL_REQUIRE_HOVER) !== 'false'; const storedGraphPanelVisible = window.localStorage?.getItem(GRAPH_PANEL_VISIBLE) === 'true'; const storedGraphPanelWidth = window.localStorage?.getItem(GRAPH_PANEL_WIDTH); @@ -182,14 +185,18 @@ export default class DataBrowser extends React.Component { isCreatingNewGraph: false, // Auto-scroll feature state autoScrollEnabled: storedAutoScroll, // Whether auto-scroll feature is enabled (menu setting) + autoScrollRequireHover: storedAutoScrollRequireHover, // Whether auto-scroll requires mouse hover over panel isAutoScrolling: false, // Whether auto-scroll is currently active - isRecordingAutoScroll: false, // Whether we're recording (Option key held during scroll) + isRecordingAutoScroll: false, // Whether we're recording (Command key held during scroll) autoScrollAmount: 0, // The registered scroll amount (pixels) autoScrollDelay: 1000, // The registered wait time (ms) autoScrollPaused: false, // Whether auto-scroll is currently paused recordingScrollStart: null, // Timestamp when scroll recording started - recordingScrollEnd: null, // Timestamp when scrolling ended (before Option key release) + recordingScrollEnd: null, // Timestamp when scrolling ended (before Command key release) recordedScrollDelta: 0, // Accumulated scroll delta during recording + 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 }; this.handleResizeDiv = this.handleResizeDiv.bind(this); @@ -238,6 +245,7 @@ export default class DataBrowser extends React.Component { this.deleteGraphConfig = this.deleteGraphConfig.bind(this); this.selectGraph = this.selectGraph.bind(this); this.toggleAutoScroll = this.toggleAutoScroll.bind(this); + this.toggleAutoScrollRequireHover = this.toggleAutoScrollRequireHover.bind(this); this.handleAutoScrollKeyDown = this.handleAutoScrollKeyDown.bind(this); this.handleAutoScrollKeyUp = this.handleAutoScrollKeyUp.bind(this); this.handleAutoScrollWheel = this.handleAutoScrollWheel.bind(this); @@ -245,12 +253,19 @@ 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.saveOrderTimeout = null; this.aggregationPanelRef = React.createRef(); this.autoScrollIntervalId = null; this.autoScrollTimeoutId = null; this.autoScrollResumeTimeoutId = null; this.autoScrollAnimationId = null; + this.panelHeaderLeaveTimeoutId = null; this.panelColumnRefs = []; this.activePanelIndex = -1; this.isWheelScrolling = false; @@ -333,6 +348,18 @@ export default class DataBrowser extends React.Component { // Auto-scroll event listeners document.body.addEventListener('keydown', this.handleAutoScrollKeyDown); document.body.addEventListener('keyup', this.handleAutoScrollKeyUp); + // 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); // Load keyboard shortcuts from server try { @@ -371,6 +398,10 @@ export default class DataBrowser extends React.Component { // Auto-scroll cleanup document.body.removeEventListener('keydown', this.handleAutoScrollKeyDown); document.body.removeEventListener('keyup', this.handleAutoScrollKeyUp); + document.removeEventListener('contextmenu', this.handleNativeContextMenu, true); + window.removeEventListener('mousemove', this.handleNativeContextMenuClose); + window.removeEventListener('keydown', this.handleNativeContextMenuClose, true); + window.removeEventListener('blur', this.handleNativeContextMenuClose); if (this.autoScrollTimeoutId) { clearTimeout(this.autoScrollTimeoutId); } @@ -380,6 +411,9 @@ export default class DataBrowser extends React.Component { if (this.autoScrollAnimationId) { cancelAnimationFrame(this.autoScrollAnimationId); } + if (this.panelHeaderLeaveTimeoutId) { + clearTimeout(this.panelHeaderLeaveTimeoutId); + } } async componentDidUpdate(prevProps, prevState) { @@ -1385,6 +1419,45 @@ export default class DataBrowser extends React.Component { }); } + /** + * Checks if auto-scroll should be blocked due to user interactions. + * Auto-scroll pauses when: + * - A modal is displayed (script confirmation, graph dialog) + * - A context menu is displayed (custom or native browser menu) + * - The user is editing a cell in the databrowser table + * - Manual scroll pause is active + */ + isAutoScrollBlocked() { + const { + autoScrollPaused, + editing, + contextMenuItems, + showScriptConfirmationDialog, + showGraphDialog, + nativeContextMenuOpen, + mouseOutsidePanel, + mouseOverPanelHeader, + autoScrollRequireHover, + } = this.state; + + // disableKeyControls is true when parent Browser has a modal open + const { disableKeyControls } = this.props; + + // Check hover-related blocking only if autoScrollRequireHover is enabled + const hoverBlocked = autoScrollRequireHover && (mouseOutsidePanel || mouseOverPanelHeader); + + return ( + autoScrollPaused || + editing || + (contextMenuItems && contextMenuItems.length > 0) || + showScriptConfirmationDialog || + showGraphDialog || + nativeContextMenuOpen || + disableKeyControls || + hoverBlocked + ); + } + toggleAutoScroll() { this.setState(prevState => { const newAutoScroll = !prevState.autoScrollEnabled; @@ -1397,10 +1470,18 @@ export default class DataBrowser extends React.Component { }); } + toggleAutoScrollRequireHover() { + this.setState(prevState => { + const newRequireHover = !prevState.autoScrollRequireHover; + window.localStorage?.setItem(AGGREGATION_PANEL_AUTO_SCROLL_REQUIRE_HOVER, String(newRequireHover)); + return { autoScrollRequireHover: newRequireHover }; + }); + } + handleAutoScrollKeyDown(e) { - // Option/Alt key = keyCode 18 + // Command/Meta key = keyCode 91 (left) or 93 (right) // Only detect when panels are visible and auto-scroll is enabled - if (e.keyCode === 18 && this.state.autoScrollEnabled && this.state.isPanelVisible && !this.state.isRecordingAutoScroll) { + 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(); @@ -1415,8 +1496,8 @@ export default class DataBrowser extends React.Component { } handleAutoScrollKeyUp(e) { - // Option/Alt key = keyCode 18 - if (e.keyCode === 18 && this.state.isRecordingAutoScroll) { + // 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; // Only start auto-scroll if we actually recorded some scrolling @@ -1471,12 +1552,70 @@ export default class DataBrowser extends React.Component { this.setState({ autoScrollPaused: true }); } - // Schedule resume after 500ms of inactivity + // Schedule resume after 1000ms of inactivity this.autoScrollResumeTimeoutId = setTimeout(() => { if (this.state.isAutoScrolling && this.state.autoScrollPaused) { this.setState({ autoScrollPaused: false }); } - }, 500); + }, 1000); + } + + handleNativeContextMenu() { + // Pause auto-scroll when native browser context menu is opened + if (this.state.isAutoScrolling && !this.state.nativeContextMenuOpen) { + this.setState({ nativeContextMenuOpen: true }); + } + } + + handleNativeContextMenuClose(e) { + // Only process if native context menu is open + if (!this.state.nativeContextMenuOpen) { + return; + } + + // For keydown events, only handle Escape key + if (e && e.type === 'keydown' && e.key !== 'Escape') { + return; + } + + // mousemove, Escape key, or blur all indicate the menu is closed + this.setState({ nativeContextMenuOpen: false }); + } + + handlePanelMouseEnter() { + if (this.state.mouseOutsidePanel) { + this.setState({ mouseOutsidePanel: false }); + } + } + + handlePanelMouseLeave() { + if (!this.state.mouseOutsidePanel) { + this.setState({ mouseOutsidePanel: true }); + } + } + + handlePanelHeaderMouseEnter() { + // Cancel any pending leave timeout + if (this.panelHeaderLeaveTimeoutId) { + clearTimeout(this.panelHeaderLeaveTimeoutId); + this.panelHeaderLeaveTimeoutId = null; + } + if (!this.state.mouseOverPanelHeader) { + this.setState({ mouseOverPanelHeader: true }); + } + } + + handlePanelHeaderMouseLeave() { + // Use a small delay to allow moving between adjacent headers without resuming scroll + if (this.panelHeaderLeaveTimeoutId) { + clearTimeout(this.panelHeaderLeaveTimeoutId); + } + this.panelHeaderLeaveTimeoutId = setTimeout(() => { + this.panelHeaderLeaveTimeoutId = null; + if (this.state.mouseOverPanelHeader) { + this.setState({ mouseOverPanelHeader: false }); + } + }, 50); } startAutoScroll() { @@ -1517,8 +1656,8 @@ export default class DataBrowser extends React.Component { return; } - if (this.state.autoScrollPaused) { - // When paused, keep checking but don't scroll + if (this.isAutoScrollBlocked()) { + // When blocked (modal, context menu, editing, or manual pause), keep checking but don't scroll this.autoScrollTimeoutId = setTimeout(() => { this.performAutoScrollStep(); }, 100); @@ -1554,8 +1693,8 @@ export default class DataBrowser extends React.Component { } const animateScroll = (currentTime) => { - if (!this.state.isAutoScrolling || this.state.autoScrollPaused) { - // If stopped or paused during animation, schedule next check + if (!this.state.isAutoScrolling || this.isAutoScrollBlocked()) { + // If stopped or blocked during animation, schedule next check this.autoScrollTimeoutId = setTimeout(() => { this.performAutoScrollStep(); }, 100); @@ -2373,6 +2512,8 @@ export default class DataBrowser extends React.Component { className={styles.aggregationPanelContainer} ref={this.aggregationPanelRef} onWheel={this.handleAutoScrollWheel} + onMouseEnter={this.handlePanelMouseEnter} + onMouseLeave={this.handlePanelMouseLeave} > {this.state.panelCount > 1 ? (
this.onMouseEnterPanelCheckBox(objectId)} + onMouseEnter={() => { + this.onMouseEnterPanelCheckBox(objectId); + this.handlePanelHeaderMouseEnter(); + }} + onMouseLeave={this.handlePanelHeaderMouseLeave} onContextMenu={(e) => { e.preventDefault(); this.handlePanelHeaderContextMenu(e, objectId); @@ -2570,6 +2715,8 @@ export default class DataBrowser extends React.Component { toggleShowPanelCheckbox={this.toggleShowPanelCheckbox} autoScrollEnabled={this.state.autoScrollEnabled} toggleAutoScroll={this.toggleAutoScroll} + autoScrollRequireHover={this.state.autoScrollRequireHover} + toggleAutoScrollRequireHover={this.toggleAutoScrollRequireHover} isAutoScrolling={this.state.isAutoScrolling} stopAutoScroll={this.stopAutoScroll} toggleGraphPanel={this.toggleGraphPanelVisibility} @@ -2583,6 +2730,7 @@ export default class DataBrowser extends React.Component { x={this.state.contextMenuX} y={this.state.contextMenuY} items={this.state.contextMenuItems} + onHide={() => this.setState({ contextMenuX: null, contextMenuY: null, contextMenuItems: null })} /> )} {this.state.showScriptConfirmationDialog && (