UNPKG

@loke/ui

Version:
241 lines (196 loc) 7.18 kB
--- name: popover type: core domain: overlays requires: [loke-ui] description: > Floating panels anchored to a trigger or custom element. Popover, PopoverTrigger, PopoverAnchor, PopoverPortal, PopoverContent, PopoverClose, PopoverArrow. Non-modal by default (modal=false). Popper positioning via Floating UI. CSS custom properties: --loke-popover-content-available-height, -width, -transform-origin, --loke-popover-trigger-height, -width. --- # Popover ## Setup Basic popover with portal, arrow, and close button. ```tsx import { Popover, PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverTrigger, } from "@loke/ui/popover"; function Example() { return ( <Popover> <PopoverTrigger>Open popover</PopoverTrigger> <PopoverPortal> <PopoverContent> <p>Popover content goes here.</p> <PopoverClose>Close</PopoverClose> <PopoverArrow /> </PopoverContent> </PopoverPortal> </Popover> ); } ``` `PopoverContent` renders with `role="dialog"` and positions via Floating UI (Popper). `PopoverArrow` points toward the trigger. `PopoverPortal` renders into `document.body`. `Popover` defaults to `modal={false}` — no focus trapping and no pointer event blocking on the background. ## Core Patterns ### Controlled open state ```tsx import { useState } from "react"; import { Popover, PopoverContent, PopoverPortal, PopoverTrigger, } from "@loke/ui/popover"; function ControlledPopover() { const [open, setOpen] = useState(false); return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger>Edit</PopoverTrigger> <PopoverPortal> <PopoverContent> <input placeholder="Name" /> <button type="button" onClick={() => setOpen(false)}> Save </button> </PopoverContent> </PopoverPortal> </Popover> ); } ``` ### Custom anchor element Use `PopoverAnchor` to position the popover against an element other than the trigger. Once `PopoverAnchor` is rendered, `PopoverTrigger` no longer acts as the positioning anchor. ```tsx import { Popover, PopoverAnchor, PopoverContent, PopoverPortal, PopoverTrigger, } from "@loke/ui/popover"; function AnchoredPopover() { return ( <Popover> <div className="input-row"> <PopoverAnchor asChild> <input placeholder="Search..." /> </PopoverAnchor> <PopoverTrigger>Filter</PopoverTrigger> </div> <PopoverPortal> {/* Content aligns to the input, not the button */} <PopoverContent align="start"> <p>Filter options</p> </PopoverContent> </PopoverPortal> </Popover> ); } ``` ### Modal popover with focus trapping ```tsx import { Popover, PopoverClose, PopoverContent, PopoverPortal, PopoverTrigger, } from "@loke/ui/popover"; function ModalPopover() { return ( <Popover modal> <PopoverTrigger>Edit profile</PopoverTrigger> <PopoverPortal> <PopoverContent> <input placeholder="Display name" /> <input placeholder="Email" /> <PopoverClose>Save</PopoverClose> </PopoverContent> </PopoverPortal> </Popover> ); } ``` `modal={true}` enables focus trapping (FocusScope), disables pointer events on the background (DismissableLayer), and hides background from screen readers (`aria-hidden`). Use for forms where accidental background interaction would be disruptive. ### CSS custom properties `PopoverContent` re-maps Popper internal vars to `--loke-popover-*` names. Use these to size content relative to available space or match trigger dimensions. ```css .popover-content { /* Constrain to available height in viewport */ max-height: var(--loke-popover-content-available-height); /* Match trigger width */ min-width: var(--loke-popover-trigger-width); /* Correct transform-origin for scale animations */ transform-origin: var(--loke-popover-content-transform-origin); } ``` Available properties set on `PopoverContent`: | Property | Value source | |---|---| | `--loke-popover-content-available-height` | `--loke-popper-available-height` | | `--loke-popover-content-available-width` | `--loke-popper-available-width` | | `--loke-popover-content-transform-origin` | `--loke-popper-transform-origin` | | `--loke-popover-trigger-height` | `--loke-popper-anchor-height` | | `--loke-popover-trigger-width` | `--loke-popper-anchor-width` | ## Common Mistakes ### PopoverAnchor and PopoverTrigger anchor confusion When `PopoverAnchor` is mounted, `hasCustomAnchor` becomes true and `PopoverTrigger` stops wrapping itself in a `PopperPrimitive.Anchor`. The popover positions against `PopoverAnchor`, not the trigger. Mixing both without understanding this causes the popover to appear in the wrong location. ```tsx // Wrong — expects popover to anchor to the trigger button <Popover> <PopoverAnchor asChild><div className="target" /></PopoverAnchor> <PopoverTrigger>Open</PopoverTrigger> {/* Popover positions against .target, not "Open" */} <PopoverPortal> <PopoverContent>...</PopoverContent> </PopoverPortal> </Popover> ``` Source: `src/components/popover/popover.tsx` — `hasCustomAnchor` logic in `PopoverTrigger`. ### Expecting non-modal popover to trap focus `Popover` defaults to `modal={false}`. In this mode there is no `FocusScope` trapping and no `disableOutsidePointerEvents`. Clicks and keyboard focus can leave the popover freely. Set `modal={true}` if you need a contained form experience. ```tsx // Wrong — user expects tab to stay inside <Popover> <PopoverTrigger>Edit</PopoverTrigger> <PopoverPortal> <PopoverContent> <input /> </PopoverContent> </PopoverPortal> </Popover> // Correct for focus-trapped forms <Popover modal> <PopoverTrigger>Edit</PopoverTrigger> <PopoverPortal> <PopoverContent> <input /> </PopoverContent> </PopoverPortal> </Popover> ``` Source: `src/components/popover/popover.tsx` — `modal` defaults to `false`. ### Using wrong CSS custom property names The internal Popper vars (`--loke-popper-*`) are not exposed on `PopoverContent`. They are re-mapped to `--loke-popover-*`. Referencing the raw popper names in CSS will silently resolve to nothing. ```css /* Wrong */ max-height: var(--loke-popper-available-height); /* Correct */ max-height: var(--loke-popover-content-available-height); ``` Source: `src/components/popover/popover.tsx` — CSS var re-mapping in `PopoverContentImpl`. ### Skipping PopoverPortal Without `PopoverPortal`, `PopoverContent` renders inline in the DOM tree. This causes z-index stacking failures and `overflow: hidden` clipping when the trigger is inside a scrollable container. ## Cross-references - See also: [tooltip](../tooltip/SKILL.md) — for non-interactive hover labels - See also: [dialog](../dialog/SKILL.md) — for blocking modal overlays - See also: [dropdown-menu](../dropdown-menu/SKILL.md) — for selection menus triggered by a button - See also: [overlay-infrastructure](../overlay-infrastructure/SKILL.md) — Popper, DismissableLayer, FocusScope internals