import type { JSX, ValidComponent } from "@solidjs/web"; /** * `` — the popup container rendered inside ``. * Mirrors `packages/react/src/menu/popup/MenuPopup.tsx`. * * Wraps `FloatingFocusManager` to trap focus while open (modal) or keep * focus reachable while open (non-modal). Carries role="menu" from * `useRole`. * * ### Port deviations * * - `useHoverFloatingInteraction` keeps the popup open while the cursor * is over the menu surface during hover-open sessions. * - `finalFocus` accepts the same shape as React but the interaction-type * callback form is not ported. */ import { dynamic } from "@solidjs/web"; import { createMemo, omit } from "solid-js"; import { composeRefs, userRef } from "../../internals/compose-refs"; import { FloatingFocusManager } from "../../internals/floating-ui/components/floating-focus-manager"; import { useHoverFloatingInteraction } from "../../internals/floating-ui/hooks/use-hover-floating-interaction"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps } from "../../internals/types"; import type { TransitionStatus } from "../../internals/use-transition-status/use-transition-status"; import type { MenuAlign, MenuSide, } from "../positioner/menu-positioner-context"; import { useMenuPositionerContext } from "../positioner/menu-positioner-context"; import { useMenuRootContext } from "../root/menu-root-context"; export interface MenuPopupState { open: boolean; side: MenuSide; align: MenuAlign; transitionStatus: TransitionStatus; nested: boolean; instant: string | undefined; } export interface MenuPopupProps extends BaseUIComponentProps<"div", MenuPopupState> { /** * Tag/component to render. Defaults to `"div"`. Replaces the * pre-spike `render` prop (axis G). */ as?: ValidComponent; /** Override where focus goes when the popup closes. */ finalFocus?: | boolean | HTMLElement | { current: HTMLElement | null } | undefined; } export function MenuPopup(props: MenuPopupProps): JSX.Element { const root = useMenuRootContext(); const positioner = useMenuPositionerContext(); useHoverFloatingInteraction(root.floatingRootContext, { enabled: () => root.open(), closeDelay: () => 0, }); const setPopupRef = (el: HTMLElement | null): void => { root.popupRef.current = el; }; const state = createMemo(() => ({ open: root.open(), side: positioner.side(), align: positioner.align(), transitionStatus: root.transitionStatus(), nested: root.nested(), instant: root.instantType(), })); const internal = { get "aria-labelledby"() { return root.activeTriggerElement()?.id ?? ""; }, get "aria-orientation"() { return root.orientation(); }, get "data-rootownerid"() { return root.rootId(); }, get "data-open"() { return state().open ? "" : undefined; }, get "data-closed"() { return state().open ? undefined : ""; }, get "data-side"() { return state().side; }, get "data-align"() { return state().align; }, get "data-nested"() { return state().nested ? "" : undefined; }, get "data-instant"() { return state().instant ? "" : undefined; }, get "data-starting-style"() { return state().transitionStatus === "starting" ? "" : undefined; }, get "data-ending-style"() { return state().transitionStatus === "ending" ? "" : undefined; }, }; const userClassStyle = resolveClassStyle(props, () => state()); const userProps = omit(props, "as", "class", "style", "attrs", "finalFocus"); let merged: Record = mergeRootProps( root.interactions.getFloatingProps(), internal, ); merged = mergeRootProps(merged, userClassStyle); merged = mergeRootProps(merged, userProps); const Tag = dynamic(() => props.as ?? "div"); const popupElement = ( ); return ( {popupElement} ); }