[CB] Render data viewer context menu on the root level#4363
[CB] Render data viewer context menu on the root level#4363sergeyteleshev wants to merge 21 commits into
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | -2 |
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
|
|
||
| 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); | ||
| }); |
| clientY: event.currentTarget.getBoundingClientRect().top + event.currentTarget.getBoundingClientRect().height, | ||
| }); | ||
|
|
||
| this.cellContext.openMenu(event as unknown as React.MouseEvent); |
There was a problem hiding this comment.
it's a bit strange that we have event: React.KeyboardEvent as parameter of keyDown, but then we cast it to React.MouseEvent
|
|
||
| function handleCellMenuStateSwitch(visible: boolean): void { | ||
| cellContext.setMenuVisibility(visible); | ||
| function handleMenuTriggerClick(event: React.MouseEvent<HTMLButtonElement>) { |
There was a problem hiding this comment.
Idk, I expect button is opening when I focus it and press Enter. It's a web standard, I don't think we need to change it.
|
|
||
| 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.
wdym by something went wrong? Is something works incorrectly? As I remember we always had some async logic but in the different place
| useEffect(() => { | ||
| const container = dataGridContext.getContainer(); | ||
|
|
||
| if (!container) { | ||
| return; | ||
| } | ||
|
|
||
| function handleScroll() { | ||
| tableMenuState.closeMenu(); | ||
| } | ||
|
|
||
| container.addEventListener('scroll', handleScroll, { capture: true }); | ||
| return () => container.removeEventListener('scroll', handleScroll, { capture: true }); | ||
| }, [dataGridContext, tableMenuState]); |
There was a problem hiding this comment.
do we really need this? Usually I see that a many stays on it's place no matter if it's on the screen or not, or we could do smth like GitHub does:
closes https://github.com/dbeaver/pro/issues/9308