# usePopover

## Usage

Headless hook for click-triggered popovers with focus trapping. Combines `useLayer` with `useFocusTrap`, auto-focus, light dismiss, and Escape handling for accessible dialog-like popover behavior.

```ts
import { usePopover } from '@astryx-vue/core/Popover'
```

Bind `triggerRef` to the element that should open the popover. The hook attaches the click and keyboard handlers and writes `aria-haspopup`, `aria-expanded`, and `aria-controls` on that element. Render the surface yourself and spread `surfaceProps` and `contentStyle` on it:

```vue
<script setup lang="ts">
import { usePopover } from '@astryx-vue/core/Popover'

const popover = usePopover({ dialogLabel: 'Quick actions' })
</script>

<template>
  <button :ref="popover.triggerRef" type="button" @click="popover.toggle()">
    Open actions
  </button>
  <Teleport to="body">
    <div
      v-show="popover.isOpen"
      :ref="popover.contentRef"
      v-bind="popover.surfaceProps"
      :style="popover.contentStyle"
    >
      Actions
    </div>
  </Teleport>
</template>
```

`contentStyle` carries the measured coordinates. Give the surface `position: fixed` in your own CSS. Every painted surface is the canonical `popover` target.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use for interactive content such as menus, pickers, forms, and command panels that need focus management. |
| Do | Prefer the `Popover` component for standard trigger-content pairs; use the hook for custom trigger patterns. |
| Do | Use popover as the broad canonical surface target. |
| Do | When a custom composition needs its own theme refinement, pass and document an owned class such as `selector-popup`. It refines the popover surface rather than creating another anatomy part. |
| Don't | Use for non-interactive hover previews: use `useHoverCard` or `useTooltip` instead. |

## Examples

Custom quick-actions popover built with `usePopover` for trigger refs, ARIA attributes, and focus trapping.

### usePopover — Quick Actions

Custom quick-actions popover using usePopover for trigger refs, ARIA attributes, and focus trapping.

```vue
<script setup lang="ts">
import { usePopover } from '@astryx-vue/core/Popover'

const popover = usePopover({
  dialogLabel: 'Quick actions',
  closeButtonSelector: '.hook-popover__close',
})
const { isOpen, surfaceProps, contentStyle, toggle, hide } = popover
</script>

<template>
  <div class="hook-popover">
    <button
      :ref="popover.triggerRef"
      type="button"
      class="hook-popover__trigger"
      @click="toggle"
    >
      Open actions
    </button>

    <Teleport to="body">
      <div
        v-show="isOpen"
        :ref="popover.contentRef"
        v-bind="surfaceProps"
        class="hook-popover__surface"
        :style="contentStyle"
      >
        <p class="hook-popover__title">
          Quick actions
        </p>
        <div class="hook-popover__actions">
          <button type="button" class="hook-popover__action">
            Duplicate
          </button>
          <button type="button" class="hook-popover__action">
            Archive
          </button>
        </div>
        <button type="button" class="hook-popover__close" @click="hide">
          Close
        </button>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.hook-popover {
  display: flex;
  justify-content: center;
}

.hook-popover__trigger {
  padding: 8px 16px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-control, 8px);
  background: var(--astryx-color-background-surface, #fff);
  color: var(--astryx-color-text-primary, #171717);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

.hook-popover__surface {
  position: fixed;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 12px;
  box-sizing: border-box;
  width: 15rem;
  padding: 12px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-container, 12px);
  background: var(--astryx-color-background-surface, #fff);
  box-shadow: 0 4px 8px oklch(0 0 0 / 10%);
  color: var(--astryx-color-text-primary, #171717);
  font-size: 0.875rem;
}

.hook-popover__title {
  margin: 0;
  font-weight: 600;
}

.hook-popover__actions {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.hook-popover__action {
  padding: 6px 8px;
  border: 0;
  border-radius: var(--astryx-radius-control, 8px);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.hook-popover__action:hover {
  background: var(--astryx-color-background-muted, #f5f5f5);
}

.hook-popover__close {
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: none;
  color: var(--astryx-color-text-secondary, #525252);
  font: inherit;
  font-size: 0.75rem;
  text-decoration: underline;
  cursor: pointer;
}
</style>
```

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `placement` | `'above' \| 'below' \| 'start' \| 'end' \| MaybeRefOrGetter` | `'below'` | Side of the trigger the popover is placed on. Logical: `start` / `end` mirror under RTL. Flips when there is no room. |
| `alignment` | `'start' \| 'center' \| 'end' \| MaybeRefOrGetter` | `'start'` | Alignment along the placement axis. |
| `isOpen` | `boolean \| undefined \| MaybeRefOrGetter` | — | Controlled open state. Leave it out to let the hook own the state. |
| `isEnabled` | `boolean \| MaybeRefOrGetter` | `true` | Whether the trigger opens the popover. |
| `hasLightDismiss` | `boolean \| MaybeRefOrGetter` | `true` | Whether clicking outside dismisses the popover. |
| `hasEscapeDismiss` | `boolean \| MaybeRefOrGetter` | `true` | Whether pressing Escape dismisses the popover. |
| `hasAutoFocus` | `boolean \| MaybeRefOrGetter` | `true` | Whether to focus the first genuine content control when opened. A dialog with none falls back to the content container. |
| `closeButtonSelector` | `string` | — | Selector of the control that initial focus must skip, such as the component's own close button. |
| `dialogLabel` | `string \| MaybeRefOrGetter` | — | Accessible label for the popover dialog. Provide one when there is no visible title. |
| `role` | `'dialog' \| 'none' \| MaybeRefOrGetter` | `'dialog'` | ARIA role on the content container. Use `none` for listbox/menu popups whose own content role should be exposed and whose trigger keeps DOM focus. |
| `isModal` | `boolean \| MaybeRefOrGetter` | `true` | Whether a dialog-role popover is modal (`aria-modal`). |
| `onShow` | `() => void` | — | Called when the popover becomes visible. |
| `onHide` | `() => void` | — | Called when the popover is hidden. Use this to return focus to the trigger when needed. |

## Returns

| Field | Type | Description |
| --- | --- | --- |
| `isOpen` | `Ref<boolean>` | Whether the popover is currently open. |
| `triggerRef` | `Ref` | Element that opens the popover. Bind it with `:ref`; the hook attaches the handlers and ARIA attributes. |
| `anchorRef` | `Ref` | Positioning anchor. Falls back to `triggerRef` when unset. |
| `contentRef` | `Ref` | Content container used by focus trapping and dismissal. |
| `id` | `string` | Unique ID for `aria-controls`. |
| `anchorId` | `string` | CSS identifier for the anchor element, for advanced positioning cases. |
| `contentStyle` | `ComputedRef` | Inline coordinates for the surface. Spread it on the content element's `style`. |
| `resolvedPlacement` | `ComputedRef<'above' \| 'below' \| 'start' \| 'end'>` | Placement actually in effect, after flipping for available space. |
| `surfaceProps` | `ComputedRef<Record<string, unknown>>` | Attributes for the content element: `id`, `role`, `aria-modal`, `aria-label`, `tabindex`. |
| `triggerProps` | `ComputedRef<Record<string, unknown>>` | The ARIA attributes the hook writes onto `triggerRef`: `aria-haspopup`, `aria-expanded`, `aria-controls`. |
| `show` | `() => void` | Imperatively show the popover. |
| `hide` | `() => void` | Imperatively hide the popover. |
| `toggle` | `() => void` | Toggle the popover open or closed. |
