fix(flows): wrap add-node menu groups in DropdownMenuGroup (#336)

Clicking "Add node" in the flow builder crashed the whole page to a
"This page couldn't load" error boundary, in both the list and canvas
views.

The add-node menus render a DropdownMenuLabel per category. That is
base-ui's Menu.GroupLabel, which calls useMenuGroupRootContext() during
render and THROWS ("MenuGroupContext is missing") when it has no
Menu.Group ancestor. Both menus wrapped each category in a plain <div>
instead of a DropdownMenuGroup, so the label had no group context. The
menu content only mounts when the menu opens, which is why initial
render was fine and the crash fired exactly on click.

Wrap each category's label + items in DropdownMenuGroup (base-ui
Menu.Group), which provides the context Menu.GroupLabel needs. The
between-group separator moves into a keyed Fragment so it stays a
sibling of the groups.

Adds a regression test pinning the contract: a bare DropdownMenuLabel
throws, a group-wrapped one does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Arnas Donauskas 2026-07-07 11:45:01 +03:00
parent 5dbc786fdc
commit f90563b35c
3 changed files with 95 additions and 45 deletions

View File

@ -17,7 +17,7 @@
* are list-only and have no canvas analogue.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
CircleAlert,
Plus,
@ -39,6 +39,7 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
@ -589,8 +590,13 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-border bg-popover">
{groupNodeTypesByCategory(types).map((group, i) => (
<div key={group.id}>
// A DropdownMenuGroup (base-ui Menu.Group) is REQUIRED here:
// DropdownMenuLabel is base-ui's Menu.GroupLabel, which throws
// at render if it can't find a Menu.Group ancestor context. A
// plain <div> wrapper made opening this menu crash the page.
<Fragment key={group.id}>
{i > 0 && <DropdownMenuSeparator />}
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground text-[11px] font-semibold tracking-wider uppercase">
{group.label}
</DropdownMenuLabel>
@ -603,7 +609,8 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
</DropdownMenuItem>
);
})}
</div>
</DropdownMenuGroup>
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>

View File

@ -36,7 +36,7 @@
* list view reads.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
applyNodeChanges,
Background,
@ -87,6 +87,7 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
@ -737,8 +738,13 @@ function CanvasAddNodeButton() {
className="border-border bg-popover w-[268px] p-1.5"
>
{groupNodeTypesByCategory(ADD_NODE_TYPES).map((group, i) => (
<div key={group.id}>
// DropdownMenuGroup (base-ui Menu.Group) is REQUIRED: the
// DropdownMenuLabel below is base-ui's Menu.GroupLabel, which
// throws at render without a Menu.Group ancestor. A plain <div>
// here crashed the page when this menu opened (issue #336).
<Fragment key={group.id}>
{i > 0 && <DropdownMenuSeparator />}
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground px-2 py-1.5 text-[11px] font-semibold tracking-wider uppercase">
{group.label}
</DropdownMenuLabel>
@ -767,7 +773,8 @@ function CanvasAddNodeButton() {
</DropdownMenuItem>
);
})}
</div>
</DropdownMenuGroup>
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>

View File

@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest'
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { DropdownMenuGroup, DropdownMenuLabel } from './dropdown-menu'
/**
* Regression for issue #336 — "clicking Add node reloads the page".
*
* DropdownMenuLabel is base-ui's Menu.GroupLabel, which reads a required
* Menu.Group context and THROWS at render when it's missing. The flow
* builder's add-node menu wrapped its labels in a plain <div> instead of
* a DropdownMenuGroup, so opening the menu crashed the whole page. These
* tests pin the contract so the div-wrapper regression can't come back.
*/
describe('DropdownMenuLabel requires a DropdownMenuGroup ancestor', () => {
it('throws when rendered without a group (the #336 crash)', () => {
expect(() =>
renderToStaticMarkup(
React.createElement(DropdownMenuLabel, null, 'Messaging'),
),
).toThrow()
})
it('renders when wrapped in a DropdownMenuGroup (the fix)', () => {
expect(() =>
renderToStaticMarkup(
React.createElement(
DropdownMenuGroup,
null,
React.createElement(DropdownMenuLabel, null, 'Messaging'),
),
),
).not.toThrow()
})
})