# Popover

> A small panel that floats next to the thing that opened it, e.g. a menu, a filter or a profile card.

- Import: `import { Popover } from '@thekestongroup/ui'`
- Group: Overlay
- Live example: https://ui.keston.co/storybook/?path=/story/components-overlay-popover--default
- Source: https://github.com/thekestongroup/ui/blob/main/src/components/Popover/Popover.vue

## When to use

- Extra content or controls tied to one element: a filter form, a short menu, a profile card on hover.

## When not to use

- A word or two of help: use Tooltip, or InfoTip for a `?` beside a label.
- A larger task, or a form with several fields: use Slideover.
- Something people must respond to before continuing: use a dialog.

## Anatomy

| Part | Required | Notes |
|---|---|---|
| trigger | Yes | The element it opens from, usually a Button (default slot). |
| panel | Yes | White, black 15% border, 4px corners, shadow. Fades in and out. Content goes in the content slot. |
| arrow | No | Points at the trigger. |

## Properties

| Property | Type | Default | Description |
|---|---|---|---|
| mode | click \| hover | click | How it opens. See Behaviour. |
| open | boolean | — | Use `v-model:open` to control it. |
| defaultOpen | boolean | false | Open when first shown. |
| content | object | bottom, 8px away | Placement passed to Reka, e.g. `{ side: 'top', align: 'start' }`. |
| arrow | boolean | false | Show the arrow. |
| portal | boolean \| string \| element | true | Render at the end of the page (true), in a given element, or in place (false). |
| dismissible | boolean | true | Close on Escape and clicking outside. If false, emits `close:prevent`. |
| modal | boolean | false | Block the rest of the page while open. |
| openDelay / closeDelay | number (ms) | 0 / 0 | Hover mode only. |

## Behaviour

- **click:** opens and closes on click, tap or Enter. Focus moves into the panel, so it suits interactive content (menus, forms).
- **hover:** opens on mouse hover, and on click or tap, which keeps it open, so it also works on touch screens. Focus stays where it is, so it suits information.
- Escape and clicking outside close it; focus returns to the trigger.
- It flips and shifts to stay on screen.

## Accessibility

- The trigger gets `aria-expanded` and `aria-controls`; the panel is a dialog.
- In hover mode, put links or buttons in the panel only with `portal: false`, so Tab reaches them straight after the trigger.

## Platform adaptation

| Platform | Implementation | Differences |
|---|---|---|
| Web / desktop (Vue) | `Popover.vue` | Reka UI Popover. |
| iOS (SwiftUI) | — | `.popover` (a sheet on iPhone). |
| Android (Compose) | — | `Popup` or `DropdownMenu`. |

## Tokens used

| Token | Used for |
|---|---|
| `color-white` / `color-black` | Panel and border |

## API (from the source)

The component's exact props, model, events, slots and types, with their documentation comments.

```ts
// Popover.vue
/**
 * A floating panel attached to a trigger. Built on Reka UI's Popover, which
 * handles positioning, focus, Escape and clicking outside.
 *
 * - mode "click" (default): opens and closes on click / tap / Enter. Focus
 *   moves into the panel, so it suits interactive content (menus, forms).
 * - mode "hover": opens on mouse hover, and also on click / tap, which keeps
 *   it open (so it works on touch screens). Focus stays where it is, so it
 *   suits information, e.g. a field's help text.
 *
 * Slots: default (the trigger, e.g. a Button), content ({ close }), anchor
 * (position against something other than the trigger).
 */
export interface PopoverProps {
  /** How it opens. */
  mode?: 'click' | 'hover'
  /**
   * Where the panel goes and how it behaves, passed to Reka's PopoverContent,
   * e.g. `{ side: 'top', align: 'start' }`. Default
   * `{ side: 'bottom', sideOffset: 8, collisionPadding: 8 }`.
   */
  content?: Omit<PopoverContentProps, 'as' | 'asChild' | 'forceMount'>
  /** Show an arrow pointing at the trigger (or Reka arrow options). */
  arrow?: boolean | Omit<PopoverArrowProps, 'as' | 'asChild'>
  /**
   * Render the panel at the end of the page (true), inside a given element
   * (a selector or element), or in place (false).
   */
  portal?: boolean | string | HTMLElement
  /** Close on Escape and on clicking outside. If false, emits close:prevent instead. */
  dismissible?: boolean
  /** Block interaction with the rest of the page while open. */
  modal?: boolean
  /** Open when first shown. */
  defaultOpen?: boolean
  /** Hover mode: wait this long before opening, in ms. */
  openDelay?: number
  /** Hover mode: wait this long before closing, in ms (at least 100, so the pointer can reach the panel). */
  closeDelay?: number
  /** Extra classes for the panel and the arrow. */
  ui?: { content?: HTMLAttributes['class']; arrow?: HTMLAttributes['class'] }
}

const emit = defineEmits<{ 'close:prevent': [] }>()

defineSlots<{
  default?(): unknown
  content?(props: { close: () => void }): unknown
  anchor?(): unknown
}>()

/** Whether the panel is open. Optional: use v-model:open to control it. */
const openModel = defineModel<boolean | undefined>('open', { default: undefined })
```
