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}
);
}