import * as React from "react" import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" import { cn } from "../../lib/utils" import { getAkashicPortalContainer } from "../../lib/getAkashicPortalContainer" // ABOVE BOTH A MODAL AND A FLOATING WINDOW. A menu is opened FROM a trigger, and that trigger can sit // inside a dialog OR inside a floating window (the meeting window's note panel has both) - so it has to // clear both or it renders behind the very surface that owns it. It was already effectively above modals // (equal `z-50`, portal appended later DOM order won); this makes it explicit rather than incidental. // See `lib/stackingLayers.ts`. import { Z_POPOVER_CLASS } from "../../lib/stackingLayers" /** * - the ui/* replacement for `@wordpress/components`' `` + ``. * * Why this exists * Five files use WP's `DropdownMenu` and `ui/*` had no equivalent, which is what blocked * `FhirCondition`'s conversion. It is a real menu widget - roving focus, escape-to-close, * outside-click dismissal, ARIA menu semantics - so hand-rolling it would have quietly dropped * accessibility that nothing in our test suite or type-check would flag. * * NO NEW DEPENDENCY. Built on the **`radix-ui` umbrella already in package.json**, which * re-exports `DropdownMenu`. (Adding `@radix-ui/react-dropdown-menu` separately was proposed and * then found unnecessary - the umbrella is where every other `ui/*` primitive here imports from.) * * API shape is taken from the real call sites * WP's is `{({ onClose }) => }`. * `icon` / `text` / `label` are kept so the five adopters convert mechanically rather than each * inventing a structure. * * `onClose` IS DELIBERATELY NOT REPRODUCED. Every WP call site currently writes * `onClick={() => { doThing(); onClose(); }}` - so "forgot to close the menu" is a bug each site can * make on its own. Radix closes on select, which makes the affordance structural instead of * remembered. A test pins it, because all five adopters will depend on it once they drop their * manual close. * * PORTALS TO `#akashic-overlay-root`, NEVER `document.body`. Radix defaults to the body, which * puts the menu outside our CSS containment: our scoped rules and root-declared design tokens do not * reach it, and - the direction that matters - the host theme's and other plugins' CSS do. * Same rule and same reason as `ui/dialog` and `ui/alert-dialog`. * * ROW SAFETY IS STRUCTURAL - it does not leak clicks to a clickable ancestor. * Both QuestionnairePage menus sit inside a clickable row and passed WP `toggleProps` whose onClick * called `preventDefault()` + `stopPropagation()` so opening the menu did not also fire the row. * * That could NOT be ported literally. Radix wires its trigger as * `onPointerDown: composeEventHandlers(props.onPointerDown,...open...)`, and `composeEventHandlers` * runs OURS first then SKIPS its own when `event.defaultPrevented` (read from * `@radix-ui/primitive`). **a literal port calls preventDefault and the menu NEVER OPENS.** * So: `stopPropagation` ONLY, never `preventDefault`, applied here once instead of remembered at * every call site - the same reasoning that removed `onClose`. Pinned by the row-safety tests, * which fail loudly against the un-guarded version (the row fired twice). */ /** stopPropagation, never preventDefault - see the row-safety note above. */ const containEvent = (e: React.SyntheticEvent) => e.stopPropagation() export interface DropdownMenuProps { /** Accessible name for the trigger. Maps to WP's `label`. */ label: string /** Visible trigger text. Maps to WP's `text`. */ text?: React.ReactNode /** Optional leading icon, e.g. a lucide element. Maps to WP's `icon`. */ icon?: React.ReactNode /** `DropdownMenuItem` children. NOT a render-prop - see the `onClose` note above. */ children: React.ReactNode /** Extra classes for the trigger. */ className?: string disabled?: boolean align?: "start" | "center" | "end" /** * Native tooltip on the trigger. QuestionnairesPage previously got this by wrapping the whole * menu in a WP ` {children} ) } export interface DropdownMenuGroupProps { /** * Optional heading. WP's `MenuGroup` takes one, and two adopters use it * ("Vital signs", "Create from template"); QuestionnairePage groups WITHOUT a label, purely to * get the dividing rule - so this must stay optional. */ label?: string children: React.ReactNode className?: string } export function DropdownMenuGroup({ label, children, className }: DropdownMenuGroupProps) { return ( {label ? ( {label} ) : null} {children} ) } export interface DropdownMenuItemProps { /** * Fired when the item is chosen. The menu closes on its own afterwards - do NOT add a manual * close call, that is precisely what this primitive removes. */ onSelect?: () => void children: React.ReactNode className?: string disabled?: boolean /** Leading icon, e.g. a lucide element. Maps to WP's `icon` + `iconPosition="left"`. */ icon?: React.ReactNode /** Maps to WP's `isDestructive` - the questionnaire builder's Delete depends on it reading red. */ destructive?: boolean } export function DropdownMenuItem({ onSelect, children, className, disabled = false, icon, destructive = false, }: DropdownMenuItemProps) { return ( onSelect?.()} className={cn( "relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5", "text-sm outline-none focus:bg-accent focus:text-accent-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", destructive && "text-destructive focus:bg-destructive/10 focus:text-destructive", className, )} > {icon} {children} ) } export default DropdownMenu