Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
105 changes: 79 additions & 26 deletions src/components/BrowserMenu/BrowserMenu.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 }),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}}
>
{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;
Expand All @@ -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 = () => {
Expand All @@ -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 (
<div className={styles.wrap} ref={this.wrapRef}>
<div className={styles.wrap} ref={this.wrapRef} {...wrapEvents}>
<div className={classes.join(' ')} {...entryEvents}>
{this.props.icon && <Icon name={this.props.icon} width={14} height={14} />}
<span>{this.props.title}</span>
{isSubmenu &&
React.Children.toArray(this.props.children).some(c => React.isValidElement(c) && c.type === BrowserMenu) && (
<Icon
name="right-outline"
width={12}
height={12}
className={styles.submenuArrow}
/>
{isSubmenu && this.props.children && (
<span className={styles.submenuArrow} />
)}
</div>
{menu}
Expand Down
37 changes: 34 additions & 3 deletions src/components/BrowserMenu/BrowserMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -113,22 +132,34 @@
}

.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;
color: white;

&:hover {
background: $blue;

.submenuArrow {
border-left-color: white;
}
}

&.disabled {
Expand Down
3 changes: 2 additions & 1 deletion src/components/BrowserMenu/MenuItem.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/components/ContextMenu/ContextMenu.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -171,6 +171,7 @@ const ContextMenu = ({ x, y, items }) => {
setVisible(false);
setPath([0]);
setHoveredOffsets([0]);
onHide?.();
};

// Combined setter that updates both path and offsets
Expand Down Expand Up @@ -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;
3 changes: 2 additions & 1 deletion src/dashboard/Data/Browser/Browser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
Loading
Loading