diff --git a/src/components/flows/flow-builder.tsx b/src/components/flows/flow-builder.tsx index d644d76..ae459da 100644 --- a/src/components/flows/flow-builder.tsx +++ b/src/components/flows/flow-builder.tsx @@ -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,21 +590,27 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) { {groupNodeTypesByCategory(types).map((group, i) => ( -
+ // 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
wrapper made opening this menu crash the page. + {i > 0 && } - - {group.label} - - {group.types.map((t) => { - const meta = NODE_META[t]; - return ( - onAdd(t)}> - - {meta.label} - - ); - })} -
+ + + {group.label} + + {group.types.map((t) => { + const meta = NODE_META[t]; + return ( + onAdd(t)}> + + {meta.label} + + ); + })} + + ))} diff --git a/src/components/flows/flow-canvas.tsx b/src/components/flows/flow-canvas.tsx index 29bea17..10e668b 100644 --- a/src/components/flows/flow-canvas.tsx +++ b/src/components/flows/flow-canvas.tsx @@ -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,37 +738,43 @@ function CanvasAddNodeButton() { className="border-border bg-popover w-[268px] p-1.5" > {groupNodeTypesByCategory(ADD_NODE_TYPES).map((group, i) => ( -
+ // 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
+ // here crashed the page when this menu opened (issue #336). + {i > 0 && } - - {group.label} - - {group.types.map((t) => { - const meta = NODE_META[t]; - return ( - handleAdd(t)} - className="gap-3 py-2" - > - - - - {meta.label} + + + {group.label} + + {group.types.map((t) => { + const meta = NODE_META[t]; + return ( + handleAdd(t)} + className="gap-3 py-2" + > + + + + {meta.label} + + + {meta.blurb} + - - {meta.blurb} - - - - ); - })} -
+ + ); + })} + + ))} diff --git a/src/components/ui/dropdown-menu-group-label.test.tsx b/src/components/ui/dropdown-menu-group-label.test.tsx new file mode 100644 index 0000000..998c366 --- /dev/null +++ b/src/components/ui/dropdown-menu-group-label.test.tsx @@ -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
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() + }) +})