# useMenuPress

## Usage

The press model of a menu surface, for any pointer. A press that starts on a row is claimed: the highlight follows a held finger as it moves, and the row under the RELEASE is the row that acts — its click is dispatched for it, carrying the release's modifier keys. The click the browser reports at the end of a touch, aimed at the row where the touch began, is swallowed so nothing acts twice. The pointer is tracked at document level by `pointerId`, so a finger that slid off the row it landed on is still followed. A mouse press keeps the highlight on the row it landed on and lets that row's own click act; a mouse released outside the menu calls `onDismiss`; a finger released outside leaves the menu open; `pointercancel` or a second pointer ends the gesture with nothing acting.

`ContextMenu` already mounts it. Reach for it directly only when building a menu-like surface of your own: give it the menu root's ref, enable it while the menu is open, and spread `menuProps` onto the root. `onPointerDown` is the same handler on its own, for markup that cannot take an object spread.

Rows are the enabled `[role="menuitem"]`, `[role="menuitemradio"]`, and `[role="menuitemcheckbox"]` elements inside the root, and the menu boundary is `[role="menu"]`. Pass `itemSelector` to pick rows with a selector of your own, `onHighlight` to highlight some way other than DOM focus, and `onActivate` to act on the row yourself. `resetMenuPress()` forgets an armed click swallower; it is for unmount and tests.

The trigger side is optional. Give it `triggerRef` and `onTriggerPress`, spread `triggerProps` onto the trigger, and a primary-button press there opens the menu at once while a finger has to rest for `longPressDelayMs`. A right-click is left alone, so its own `contextmenu` route keeps working. Either way the press can slide straight into the menu: rows highlight under the held pointer and the row under the release acts. The click that ends that gesture is reported by `isTriggerClickFromPress()`, so the trigger's own click neither toggles nor reopens. `triggerProps.onContextMenu` suppresses the browser's own long-press menu for a finger; a mouse's right-click still reaches the application.

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

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Pass a selector for ENABLED rows only, so a disabled row or a divider under the pointer picks nothing instead of lighting up. |
| Do | Let the default highlight be DOM focus; the hook focuses the row under the pointer with `preventScroll`. Style the active row with `:focus`. Supply `onHighlight` only for a listbox that must keep focus on its combobox. |
| Do | Declare `touch-action` on the menu root: `pinch-zoom` when its rows fit, `pan-y pinch-zoom` when it scrolls, so the browser — not the hook — decides when a finger is scrolling, and a two-finger pinch still zooms the page. |
| Don't | Act on a row from its own `pointerdown` or `pointerup` handler as well; the hook already activates the row under the release, and a second path acts twice. |
| Don't | Read a row click with `detail` 0 as a keyboard activation; the hook dispatches its pointer activation with `detail` 0 too. The dispatched click is untrusted, so check `event.isTrusted` to tell them apart. |

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `MenuPressOptions` | — | Configuration object. Required. |
| `options.menuRef` | `Ref` | — | The menu root carrying `role="menu"`: the surface whose rows a press picks from. Required. |
| `options.isEnabled` | `MaybeRefOrGetter<boolean>` | `true` | Whether the model is live, read on each press. Pass the menu's open state. |
| `options.onDismiss` | `() => void` | — | A mouse was released outside the menu with nothing acting: close it. A finger released outside leaves the menu open, so this is never called then. |
| `options.itemSelector` | `string` | enabled `[role="menuitem"]`, `menuitemradio`, `menuitemcheckbox` | Selector matching the ENABLED rows. A pointer over anything else inside the menu (a divider, a heading, a disabled row) picks nothing. Rows must still sit inside a `[role="menu"]` boundary. |
| `options.triggerRef` | `Ref` | — | The control that opens the menu, when a press may start there. |
| `options.onTriggerPress` | `(pointerType: MenuPressPointerType) => boolean` | — | A mouse pressed the trigger, or a finger rested on it for `longPressDelayMs`: open the menu under the held pointer and return whether it opened. Return false when the press closed an open menu instead. |
| `options.onHighlight` | `(row: HTMLElement \| null) => void` | — | Move the highlight. Defaults to moving DOM focus with `preventScroll` onto the row. A picker that highlights through `aria-activedescendant` supplies its own. |
| `options.onActivate` | `(row: HTMLElement, release: PointerEvent) => void` | — | Act on the row under the release. Defaults to dispatching a click on the row that carries the release button and modifier keys. |
| `options.getScroller` | `() => HTMLElement \| null` | the overflowing menu root | The element to scroll while a held pointer rests within 24 px of its top or bottom edge. Return null for none. |
| `options.hitTest` | `(x: number, y: number) => Element \| null` | `document.elementFromPoint` | Resolves the element under a viewport point. A finger's events stay with the element it landed on, so the row under it is found by position. A test seam. |
| `options.longPressDelayMs` | `number` | `500` | How long a finger must rest on the trigger before the menu opens under it. |

## Returns

| Name | Type | Description |
| --- | --- | --- |
| `menuProps` | `{ onPointerdown, 'data-astryx-menu-press': '' }` | Spread onto the menu root (`v-bind="press.menuProps"`). Claims presses that begin inside it and marks the root as carrying the press model. |
| `onPointerDown` | `(event: PointerEvent) => void` | The same handler as `menuProps.onPointerdown`, on its own. Bind it to the menu root's `pointerdown` when an object spread is not an option. |
| `triggerProps` | `{ onPointerdown, onContextmenu }` | Spread onto the trigger: a primary-button press opens the menu at once; a finger held for the delay opens it with the finger still down. A right-click is left alone, so its `contextmenu` route keeps working. The `contextmenu` handler suppresses the browser's own long-press menu for a finger, and leaves a mouse's right-click to the application. |
| `isTriggerClickFromPress` | `() => boolean` | Whether the click reaching the trigger belongs to the gesture that just pressed it. That press already opened or closed the menu, so the click must neither toggle nor reopen. |
| `cancel` | `() => void` | End the gesture in flight with nothing acting, for when the menu closes under it. |

The module also exports:

- `resetMenuPress()`: forgets an armed stray-click swallower. Call it when a surface unmounts outside the hook's own teardown, and in tests.
- `isMenuPressActivation()`: true for the synchronous extent of a row activation the model dispatched, so a row that forwards activation to a link inside it can tell the model's click from the browser's stray one.
- `MENU_PRESS_MARKER`: the attribute name that marks a menu root.
- `menuPressStep`, `MENU_PRESS_SETTLE_MS` and their types: the pure state machine. The release of the press that opened the menu acts only once the pointer has entered the menu or the press has lasted 300 ms; otherwise the menu stays open and nothing acts.
