// packages/core/src/components/popover/popover-content.tsx import type { JSX } from "solid-js"; import { Show, createEffect, onCleanup, splitProps } from "solid-js"; import { createDismiss } from "../../utilities/dismiss/create-dismiss"; import { createFocusTrap } from "../../utilities/focus-trap/create-focus-trap"; import { createScrollLock } from "../../utilities/scroll-lock/create-scroll-lock"; import { useInternalPopoverContext } from "./popover-context"; export interface PopoverContentProps extends JSX.HTMLAttributes { /** Keep mounted even when closed (for animation control). */ forceMount?: boolean | undefined; children?: JSX.Element; } /** * Popover content panel with `role="dialog"`. Uses floating positioning. * When modal: focus trap + scroll lock. When non-modal: Tab closes popover. * Wrap with Popover.Portal to render outside the DOM tree. */ export function PopoverContent(props: PopoverContentProps): JSX.Element { const [local, rest] = splitProps(props, ["children", "forceMount", "style"]); const ctx = useInternalPopoverContext(); const focusTrap = createFocusTrap(() => ctx.contentRef()); const scrollLock = createScrollLock(); const dismiss = createDismiss({ getContainer: () => ctx.contentRef(), onDismiss: () => ctx.setOpen(false), }); /** Non-modal Tab handler: Tab closes popover instead of trapping focus. */ const handleKeyDown: JSX.EventHandler = (e) => { if (!ctx.modal() && e.key === "Tab") { ctx.setOpen(false); } }; createEffect(() => { if (ctx.isOpen()) { dismiss.attach(); if (ctx.modal()) { focusTrap.activate(); scrollLock.lock(); } } else { focusTrap.deactivate(); scrollLock.unlock(); dismiss.detach(); } onCleanup(() => { focusTrap.deactivate(); scrollLock.unlock(); dismiss.detach(); }); }); return (
ctx.setContentRef(el)} id={ctx.contentId()} role="dialog" aria-modal={ctx.modal() || undefined} data-state={ctx.isOpen() ? "open" : "closed"} style={ typeof local.style === "string" ? `${styleToString(ctx.floatingStyle())};${local.style}` : { ...ctx.floatingStyle(), ...(local.style as JSX.CSSProperties) } } onKeyDown={handleKeyDown} {...rest} > {local.children}
); } /** Convert a JSX.CSSProperties object to an inline CSS string. */ function styleToString(style: JSX.CSSProperties): string { return Object.entries(style) .map(([k, v]) => `${k}:${v}`) .join(";"); }