# CONTRACT_TOOLTIP (v1, frozen)

Tooltip is `popover` (open/close, browser-owned) with a **hover/focus trigger**
instead of a click trigger. It is simpler than menu: no cursor, no
`aria-activedescendant`, no item navigation — and, unlike popover and menu, no
client-managed ARIA reflection at all.

## Classification: green

Tooltip inherits popover's green classification for open/close (see
REFERENCE_POPOVER_CONTRACT.md): open state lives in the top layer via the
native `popover` attribute, never in a server-reconciled attribute or server
assign. All eight conformance points below passed on every engine, across
both runs.

**Why it is a cleaner green than popover/menu, not just an equally clean
one:** popover's own contract document notes that even a green machine's
component still touches the amber `ignore_attributes` technique, because
`aria-expanded` is a client-managed attribute reflecting browser state onto
the trigger. Menu carries three such reflected attributes
(`aria-expanded`, `aria-activedescendant`, `data-highlighted`). Tooltip has
**none** — there is no expanded state to reflect (tooltips are not
disclosure widgets) and no cursor. `aria-describedby` is a plain
server-rendered constant that never changes, so the component needs zero
`phx-mounted={JS.ignore_attributes([...])}` calls. It is the first
component in this library where the whole contract is discharged by
Delegation alone, with no ARIA-reflection escape hatch at all.

## Responsibilities

* **Open/closed state** — browser-owned, identical mechanism to popover:
  the top layer via the native `popover` attribute. The server never
  renders or drives it.
* **Structural attributes** — `popover` and `role="tooltip"` are
  server-rendered constants in the component template, per KERNEL rule 1.2.
* **The trigger reference** — `aria-describedby={tooltip_id}` on the
  trigger is a server-rendered constant, always pointing at the caller's
  single explicit `id`. It is not client-managed and needs no
  `ignore_attributes` protection, unlike popover's `aria-expanded` or
  menu's `aria-activedescendant`/`data-highlighted`.
* **The hover/focus trigger** — there is no native declarative hover
  trigger (unlike `popovertarget`, which is click-only), so a minimal hook
  (`LicTooltip`) opens the tooltip on `mouseenter`/`focus` and closes it on
  `mouseleave`/`blur`, by calling `showPopover()`/`hidePopover()`. The hook
  is the user's proxy for a gesture the platform has no declarative trigger
  for — it only *calls* the state transition; the open **state** itself
  still lives in the top layer, never in a client attribute or server
  assign.
* **Content** — server-rendered HEEx (the `:content` slot); the tooltip is
  a shell, not an island.
* **No cursor, no selection, no `aria-expanded`.** Tooltips have no
  expanded state and no navigable children — the anatomy is deliberately
  minimal.

## Conformance points (8), verified on chromium / firefox / webkit, 2 runs each

(Point 8, added with the `placement` attr: zero-JS anchored positioning — glued on
open, geometry byte-stable across a patch, follows a patch-moved anchor, flips at
the viewport edge, tracks scroll.)

1. **Hover opens** — `mouseenter` on the trigger calls `showPopover()`;
   `:popover-open` becomes true and `aria-describedby` points at the
   tooltip id. Passed, all engines.
2. **Mouseleave closes** — moving the pointer off the trigger calls
   `hidePopover()`; `:popover-open` becomes false. Passed, all engines.
3. **Focus opens** — focusing the trigger opens the tooltip, verified
   against `document.activeElement` (not a client flag). Passed, all
   engines.
4. **Blur closes** — blurring the trigger closes the tooltip. Passed, all
   engines.
5. **Stays open across a content patch, and the content actually
   updates** — a patch that changes the `:content` slot's rendered text
   leaves `:popover-open` true, the new text is observed in the DOM, and
   the tooltip node's identity marker survives. Passed, all engines.
6. **Stays open across an attribute patch and a sibling patch** — a patch
   that changes an attribute on the tooltip element itself, and a
   separate patch to a fully unrelated sibling, both leave `:popover-open`
   true. Passed, all engines.
7. **`aria-describedby` survives a trigger re-render, and node identity is
   preserved throughout** — a patch that changes the trigger's own
   displayed label leaves `aria-describedby` unchanged (it was never
   client-managed, so there is nothing to strip), the tooltip stays open,
   and the tooltip element's JS expando marker (`__mark`, set before any
   patch in the run) is still present, proving the same DOM node
   persisted. Passed, all engines.

## Honest caveats

* **A patch that moves a hovered trigger closes the tooltip on Chromium only.**
  When a server patch shifts the trigger out from under a stationary pointer,
  Chromium fires `mouseleave` (Firefox/WebKit do not fire boundary events on
  layout-only movement), so the hook closes the tooltip there. For a tooltip this
  is arguably the *correct* UX (the user is no longer hovering the thing being
  explained), but it is an engine difference consumers should know: hover-opened
  tooltips do not survive anchor-moving patches on Chromium; focus-opened ones do
  on all engines. Found by the positioning conformance test.

* **Playwright hover requires an actual pointer move.** After a full page
  navigation, the OS-level simulated cursor can already be sitting at the
  same viewport coordinates as the freshly re-rendered trigger (identical
  layout across reloads), and at least one engine then does not fire a new
  `mouseenter` because the pointer "didn't move" relative to its last
  recorded position. This is not a defect in the component or hook — it is
  a test-harness gotcha specific to driving *hover* (none of the other
  reference components in this library are hover-triggered, so it had not
  come up before). The driver works around it by moving the mouse to a
  neutral point before every `hover()` call. Anyone re-testing a
  hover-triggered machine with Playwright needs this same workaround; it
  is worth adding to BUILDING_COMPONENTS.md's gotcha catalogue for the
  next hover-driven component.
* **No hover-intent delay / no touch fallback.** The reference hook opens
  and closes immediately on `mouseenter`/`mouseleave` with no debounce,
  and has no tap-to-show behavior for touch input (tooltips are
  conventionally hover/focus-only; touch accessibility for tooltips is a
  known, separately-scoped UX problem, not attempted here).
* **`showPopover()`/`hidePopover()` are guarded against
  `InvalidStateError`.** Calling `showPopover()` on an already-open
  popover (e.g. `mouseenter` firing while focus already opened it) throws
  natively; the hook checks `:popover-open` before calling either method.
  This is defensive plumbing, not a conformance point in its own right.
* **Single trigger/content pair only.** No nested tooltips, no delayed
  "tooltip of a tooltip". Anchored `placement` inherits the frozen constraints in
  REFERENCE_POPOVER_CONTRACT.md §Positioning; additional placement modes need their
  own conformance rows.
* **`mode="manual"` by default.** The component defaults to `popover="manual"`
  rather than `"auto"` so the hook is the sole owner of open/close;
  `"auto"` remains available for callers who want native light-dismiss/
  auto-close-siblings behavior layered on top, but that combination is not
  separately conformance-tested here.

## Relationship to the kernel

Tooltip is the second reuse of the popover anatomy (`one function
component + named slots + a single explicit id, derive all wiring`) and
the first case where "derive all wiring" produces **zero** client-managed
attributes — a useful data point that the anatomy's amber
`ignore_attributes` touch (noted in both REFERENCE_POPOVER_CONTRACT.md and
CONTRACT_MENU.md) is a property of *what a given machine reflects*, not an
unavoidable tax on every green machine built on this anatomy.

*Evidence: `priv/tooltip-reference/tooltip-reference-results.json`
(version-specific engine numbers live there, not in this file, per
BUILDING_COMPONENTS.md §5).*
