fix(admin): delete tables from block actions menu - #2272
Conversation
Scope checkThis PR changes 614 lines across 8 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
🦋 Changeset detectedLatest commit: 7844a81 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 7844a81 | Jul 29 2026, 01:50 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 7844a81 | Jul 29 2026, 01:49 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 7844a81 | Jul 29 2026, 01:49 PM |
There was a problem hiding this comment.
The approach is sound: the actual table-deletion fix is the focused allowTableNodeSelection: true option on the TipTap Table extension, and the rest of the PR replaces the hand-rolled Floating UI overlay with Kumo's DropdownMenu primitive plus lockDragHandle pinning so the menu stays anchored during exit animations. That fits EmDash's conventions (Kumo components, Lingui strings, logical Tailwind, RTL-aware side selection).
I read the diff, the full changed files, the sibling HeadingDropdownMenu pattern, and checked against AGENTS.md (localization, RTL, comment discipline, changesets). The implementation is largely solid and well-tested — the new tests cover whole-table deletion, menu pinning while the drag handle is locked, exit-transition completion, Escape-to-submenu, and click-outside dismissal.
The remaining issues are small: two stale/misleading comments in block-menu.test.tsx and a couple of React-idiom suggestions in BlockMenu.tsx. Nothing here blocks the stated bug fix, but the stale comments should be fixed before merge.
Findings
-
[needs fixing]
packages/admin/tests/editor/block-menu.test.tsx:11This docstring is now misleading.
BlockMenuwas rewritten to use Kumo'sDropdownMenuprimitive and no longer callscreatePortalitself. Stale comments violate the comment discipline inAGENTS.md.* BlockMenu is a standalone component that takes an editor instance, * an anchor element, and open/close callbacks. It renders via Kumo's * DropdownMenu primitive. -
[needs fixing]
packages/admin/tests/editor/block-menu.test.tsx:143The wrapper comment still references the old Floating UI implementation. BlockMenu no longer uses
useFloating, so this will confuse the next person reading the test.* Wrapper component that renders BlockMenu with an anchor element. * This is needed because Kumo's DropdownMenu.Content anchors to a real DOM element. -
[suggestion]
packages/admin/src/components/editor/BlockMenu.tsx:175Assigning to a ref during render is a React anti-pattern; in concurrent/StrictMode renders the DOM node passed to
DropdownMenu.Contentcan be written speculatively before commit. Prefer syncing the anchor in a layout effect (or passing an element directly if Kumo supports it).const anchorRef = React.useRef<HTMLElement | null>(anchorElement); const menuActionsRef = React.useRef<{ unmount: () => void; close: () => void } | null>(null); const direction = getLocaleDir(i18n.locale); React.useLayoutEffect(() => { anchorRef.current = anchorElement; }, [anchorElement]); -
[suggestion]
packages/admin/src/components/editor/BlockMenu.tsx:189If the editor selection is no longer a
NodeSelectionwhen the user clicks Duplicate/Delete (e.g. focus moved), the action silently does nothing and the menu stays open. The old implementation closed the menu defensively in this branch. Consider keeping that fallback:const handleDuplicate = () => { if (!(editor.state.selection instanceof NodeSelection)) { onClose(); return; } -
[suggestion]
packages/admin/src/components/editor/BlockMenu.tsx:206Same defensive-close regression as
handleDuplicate— if the selection isn't aNodeSelection, Delete leaves the menu open.const handleDelete = () => { if (!(editor.state.selection instanceof NodeSelection)) { onClose(); return; }
There was a problem hiding this comment.
This PR is the right fix for #2266 and follows EmDash conventions well. Enabling allowTableNodeSelection resolves the table-deletion regression, and switching the block menu to Kumo's DropdownMenu aligns with the Kumo-only component policy. The exit-animation and pin-to-opening-block behavior are localized in BlockMenu and DragHandleWrapper, the new strings are wrapped for Lingui, RTL is handled via getLocaleDir, and both user-facing effects have changesets. I also like that the diff adds a regression test specifically for deleting a selected table rather than only asserting implementation details.
I checked the diff, full changed files, import usage, and the locales config. The implementation is mostly clean, but I found one real correctness issue and two quality nits:
BlockMenu.tsxwritesanchorRef.currentduring render. That is a React anti-pattern and can leave the menu anchored to a stale or detached element under concurrent rendering, which is exactly the positioning stability this PR is trying to guarantee.block-menu.test.tsxstill says the menu is rendered viacreatePortal, which is no longer true.- The test named for hover-between-blocks behavior doesn't actually exercise the
trigger-hoverpath it's protecting.
None of these are merge blockers, but the render-phase ref write should be fixed before merge.
Findings
-
[needs fixing]
packages/admin/src/components/editor/BlockMenu.tsx:173Writing
anchorRef.currentduring render is a React anti-pattern: refs are not render-state, and a discarded or concurrent render can leaveanchorRef.currentpointing to a stale or detached anchor node. That undermines the pinned-positioning goal of this PR.Move the sync into a layout effect so the ref is updated outside of render:
React.useLayoutEffect(() => { if (anchorElement) anchorRef.current = anchorElement; }, [anchorElement]); -
[suggestion]
packages/admin/tests/editor/block-menu.test.tsx:11The file JSDoc still says the menu renders "via createPortal to document.body", but the implementation now uses Kumo's
DropdownMenuand no longer callscreatePortal. Keep evergreen comments in sync with the code:* Renders through Kumo's dropdown primitive, anchored to the selected block. -
[suggestion]
packages/admin/tests/editor/block-menu.test.tsx:531This test hovers a highlighted menu item and then hovers a paragraph. Hovering a paragraph cannot fire Base UI's
trigger-hoverclose reason, so the test would pass even if theeventDetails.cancel()branch for"trigger-hover"were removed. It therefore won't catch the regression the PR title describes—pointer movement between blocks relocating/dismissing the menu.Rewrite it against the trigger (simulate the drag handle moving to a different block while the menu is open) and assert the menu anchor stays pinned to the original block. The sibling test
"keeps the controls and menu pinned to the block that opened the menu"is a good model.
Accepted 1–2: moved the ref update to Rejected 3: removing |
There was a problem hiding this comment.
This PR remains the right fix for #2266. It enables whole-table deletion via allowTableNodeSelection, switches the block menu to Kumo's DropdownMenu (consistent with the Kumo-only UI policy), and localizes the exit-animation / pin-to-opening-block behavior in BlockMenu and DragHandleWrapper. Strings are Lingui-wrapped, RTL direction is handled via getLocaleDir, and the regression tests now cover table deletion and drag-handle pinning.
Two of the three prior findings are addressed in this round:
- ✅ The render-phase
anchorRef.currentwrite is now insideReact.useLayoutEffect, so the anchor can't stale-point to a detached node under concurrent rendering. - ✅ The stale
block-menu.test.tsxJSDoc that still referencedcreatePortalhas been updated to describe the Kumo dropdown primitive.
One quality issue from the prior review is still present: the test that is meant to protect against the menu dismissing/relocating when the pointer moves between blocks doesn't actually exercise Base UI's trigger-hover close path. The remaining finding has details and a suggested fix.
I re-read the changed components, the interaction tests, the changesets, and the locale/config helper. No new logic bugs or AGENTS.md violations were introduced; the only open item is the test-coverage gap below.
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
93ae019 to
7844a81
Compare
What does this PR do?
Fixes the portable text editor's block actions menu so whole tables can be deleted. It also moves the menu to Kumo's dropdown primitive, keeps it pinned to the block that opened it, preserves its exit animation, and prevents pointer movement between blocks from dismissing or relocating it.
Closes #2266
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Try this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
fix/2266-table-block-delete. Updated automatically when the playground redeploys.