import type { Placement } from "@floating-ui/react"
import {
FloatingFocusManager,
FloatingPortal,
} from "@floating-ui/react"
import type { ReactElement, ReactNode } from "react"
import { PANEL_SURFACE_CLASS } from "../Overlay/overlayPanelClass.ts"
import { useAnchoredOverlay } from "../Overlay/useAnchoredOverlay.ts"
import { toClassName } from "../toClassName.ts"
export type PopoverProps = {
children: ReactNode
className?: string
/**
* Required, and it becomes the panel's `aria-label`. A
* `role="dialog"` takes **no accessible name from its content**,
* so an unnamed one is announced as "dialog" and is unfindable
* by `getByRole("dialog", { name })` — the same trap `Spinner`
* walked into in M3 with `role="status"`.
*/
heading: string
isVisible: boolean
/**
* Outside press and Escape both land here, through floating-ui's
* `useDismiss`. Wire it to a `useVisibility()`'s `hide`.
*/
onDismiss: () => void
placement?: Placement
/**
* The control the panel hangs off. **Cloned, not wrapped** — an
* extra `
` around a button is how a toolbar's layout
* quietly changes.
*/
trigger: ReactElement
}
/**
* Replaces mux-magic's `@radix-ui/react-popover` re-export, and it
* is a rewrite rather than a swap for one reason: Radix owns
* `open`, so wrapping it leaves `useVisibility` and Radix both
* believing they hold it. `@floating-ui/react` is the layer Radix,
* Base UI, and Ark UI are all *built on*, and it is controlled by
* construction — you pass state in and it never stores any.
*
* What floating-ui is used for is what only it can do: collision
* handling (`flip`, `shift`), a real dismiss layer, and focus
* management — all of it consolidated in `useAnchoredOverlay`.
* Positioning strategy and the ARIA wiring come from the platform and
* from `useRole`.
*
* ### Portalled, not the top layer
*
* The panel goes in a `FloatingPortal` to `document.body`. The top
* layer (`popover="manual"`) was reached for first — it needs no
* portal and no `z-index` — but it does not escape an
* `overflow: hidden` ancestor's *clipping* in every case the fleet
* hit, and a portal does. The objection a portal used to raise, that
* it moves the panel out of a scoped `canvas` query, is answered two
* ways: `useRole` writes `aria-controls` on the trigger pointing at
* the panel `id`, so trigger→panel stays followable across the
* boundary; and the story scopes panel queries to
* `within(document.body)` while the structural `expectAgentDrivable`
* needs no change. `strategy: "fixed"` keeps the coordinates
* viewport-relative.
*/
export const Popover = ({
children,
className,
heading,
isVisible,
onDismiss,
placement = "bottom-start",
trigger,
}: PopoverProps): ReactNode => {
const {
clonedTrigger,
context,
floatingStyles,
getFloatingProps,
setFloating,
} = useAnchoredOverlay({
isVisible,
offsetValue: 8,
onDismiss,
placement,
role: "dialog",
trigger,
})
return (
<>
{clonedTrigger}
{isVisible ? (
{children}
) : null}
>
)
}