[CB] Render data viewer context menu on the root level - #4363
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 23 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull request overview
This PR moves the data viewer context menu rendering to the DataGrid root level so the menu can be positioned globally (rather than per-cell) and re-open reliably on a single right-click (fixing issue #9308).
Changes:
- Introduces a grid-level
TableMenuContext+DataGridMenuContextProviderto manage active cell + menu coordinates. - Replaces the per-cell embedded context menu with a lightweight trigger button and a single root-level
CellMenu. - Adds a new
useViewLeavehook to close the menu when the originating cell leaves the viewport, and adds a right-click mousedown propagation blocker to avoid premature dismiss.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Menu/DataGridMenuContextProvider.tsx | Adds grid-level provider to centralize context-menu state and rendering. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Menu/CellMenu.tsx | Implements a single root-level context menu bound to the active cell via context. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Menu/CellMenu.module.css | Hides the menu anchor element since menu is positioned via coordinates. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx | Replaces embedded menu with a trigger button that opens the root-level menu. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css | Styles the new menu trigger button. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx | Wraps the grid in the new menu provider and blocks right-click dismiss behavior. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/TableMenuContext.ts | Introduces shared menu state contract for cell/menu integration. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx | Routes menu open/close through the shared context and closes on viewport leave. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellContext.ts | Extends setMenuVisibility to accept an optional position. |
| webapp/packages/core-blocks/src/useViewLeave.ts | Adds a new hook to detect when an element leaves the viewport. |
| webapp/packages/core-blocks/src/index.ts | Re-exports useViewLeave. |
| webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx | Removes the old per-cell menu implementation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| menuPosition, | ||
| openMenu(cellKey: IGridDataKey, x: number, y: number) { | ||
| this.activeCellKey = cellKey; | ||
| this.menuPosition.position = { x, y }; |
There was a problem hiding this comment.
Please use menuPosition.open(event)
| editionState: DatabaseEditChangeType | null; | ||
| isMenuVisible: boolean; | ||
| setMenuVisibility(visible: boolean): void; | ||
| setMenuVisibility(visible: boolean, position?: { x: number; y: number }): void; |
There was a problem hiding this comment.
This api should be reworked. There should be openMenu handler that takes event and cell id. This handler should call useContextMenuPosition. Because position is only needed when you open menu and you dont need it when close it
| cellContext.setMenuVisibility(visible); | ||
| function handleMenuTriggerClick(event: React.MouseEvent<HTMLButtonElement>) { | ||
| const rect = event.currentTarget.getBoundingClientRect(); | ||
| cellContext.setMenuVisibility(true, { x: rect.right - 20, y: rect.bottom }); |
There was a problem hiding this comment.
Cell formatter should not make any calculactions with coordinates. openMenu(event) should
| focusable={!cellContext.isMenuVisible} | ||
| className={s(style, { formatter: true })} | ||
| value={cellHolder.value as boolean | null} | ||
| focusable |
There was a problem hiding this comment.
We still should have the way to check whether menu is opened, please keep old behaviour
| onStateSwitch={handleCellMenuStateSwitch} | ||
| /> | ||
| </div> | ||
| <button |
There was a problem hiding this comment.
Please never user native buttons, use our components from ariakit
|
|
||
| const { activeCellKey, menuPosition } = tableMenuContext; | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
this logic should not be inside cellMenu, it should be on MenuContextProvider level
|
|
||
| function syncFocus(data: DatabaseDataSelectActionsData<Partial<IGridDataKey>>) { | ||
| if (data.type === 'focus') { | ||
| const colIdx = data.key?.column ? tableData.getColumnIndexFromColumnKey(data.key.column) : -1; |
There was a problem hiding this comment.
Please do not modify this method
There was a problem hiding this comment.
you sure? we are going to loose this behavior that we can re-open on right click the new cell (it was before so this is gonna be a regression). the thing is this focus sync steals the focus which causes the bug
Screen.Recording.2026-05-27.at.14.59.13.mov
There was a problem hiding this comment.
this focus is weird. i dont like its defered behavior with setTimeout
something went wrong when we have added it
There was a problem hiding this comment.
wdym by something went wrong? Is something works incorrectly? As I remember we always had some async logic but in the different place
| import type { IGridDataKey } from '@cloudbeaver/plugin-data-viewer'; | ||
|
|
||
| export interface ITableMenuContext { | ||
| activeCellKey: IGridDataKey | null; |
There was a problem hiding this comment.
Why do we need activeCellKey? we already have cell and position inside cellContext, we should use them?
There was a problem hiding this comment.
we moved menu outside of cell context so we need to track active cell id to pass it here:
| import type { IContextMenuPosition } from '@cloudbeaver/core-blocks'; | ||
| import type { IGridDataKey } from '@cloudbeaver/plugin-data-viewer'; | ||
|
|
||
| export interface ITableMenuContext { |
There was a problem hiding this comment.
You probably dont need this. You have all 3 fields returned from useContextMenuPosition hook already that lets you open and close menu.
There was a problem hiding this comment.
we need to track active cell key and is opened status
There was a problem hiding this comment.
Maybe we can use Popover component from aria-kit and PopoverProvider?
Then we could use PopoverAnchor in CellFormatter that will position our menu positioned relative to the anchor element
|
|
||
| this.isMenuVisible = visibility; | ||
| openMenu(event: React.MouseEvent): void { | ||
| this.tableMenuContext.openMenu(this.cell, event); |
| context.set(DATA_CONTEXT_DV_SIMPLE, dataGridContext.simple, id); | ||
| context.set(DATA_CONTEXT_DV_ACTIONS, dataGridContext.actions, id); | ||
| context.set(DATA_CONTEXT_DV_PRESENTATION_ACTIONS, spreadsheetActions, id); | ||
| context.set(DATA_CONTEXT_DV_RESULT_KEY, tableMenuContext.activeCell, id); | ||
| }); |
| }; | ||
| this.openAt(x, y); | ||
| }, | ||
| openAt(x: number, y: number) { |
| ['getAnchorRect', 'handleVisibleSwitch'], | ||
| ); | ||
|
|
||
| const showAtPosition = !!contextMenuPosition?.position; |
There was a problem hiding this comment.
Could you please explain why do we need such changes?
There was a problem hiding this comment.
case:
- open menu via right click
- do not close it
- open another menu in another cell via rightclick
expected behavior: menu reopens using new position
current behavior without changes: menu stays on the same position, only items of the menu changes
There was a problem hiding this comment.
it changes position only if in the tree above react rerendered something. then its gonna rerender and change menu position
| const gridContext = useContext(DataGridContext); | ||
| const container = gridContext.getScrollContainer(); | ||
|
|
||
| function handleScroll() { |
There was a problem hiding this comment.
this function should be created inside effect to mantain the reference and avoid memory leaks
const handleScroll = () => menu.closeMenu();
There was a problem hiding this comment.
used useCallback to keep the link still
| return; | ||
| } | ||
|
|
||
| container.addEventListener('scroll', handleScroll, { capture: true }); |
There was a problem hiding this comment.
please add some delta value, so user can scroll a little before closing menu, also add throttle
| position: IContextMenuPosition; | ||
| id: string; | ||
| openMenu(activeCell: IGridDataKey, event: React.MouseEvent | React.KeyboardEvent): void; | ||
| closeMenu(): void; |
There was a problem hiding this comment.
we dont need this method, you can call position.close, also we delete context on menu opening
There was a problem hiding this comment.
this means the context the whole time is gonna be there even if menu is closed
this can cause some issues, i don't think it is expected
moreover it doesn't work this way. menu doesn't close at all
open/close menu on this level API is fine so I'll keep it, there is no biggie here
| simple: boolean; | ||
| isGridInFocus: () => boolean; | ||
| getDataGridApi: () => DataGridRef | null; | ||
| getScrollContainer: () => HTMLDivElement | null; |
There was a problem hiding this comment.
lets call it just getContainer, its only one for now
closes https://github.com/dbeaver/pro/issues/9308