LiveInteractionContracts.Components.Tooltip (live_interaction_contracts v1.0.0)

Copy Markdown View Source

Headless, unstyled, patch-safe tooltip — popover with a hover/focus trigger instead of a click trigger. Simpler than <.popover>: no cursor, no aria-activedescendant, no item navigation, and (unlike popover/menu) no client-managed ARIA reflection either, since tooltips have no expanded state.

HEEx-native anatomy (same idiom as <.popover> / <.menu>)

one function component + named slots + a single explicit id; the component derives all wiring — trigger id, aria-describedby — from that id.

<.tooltip id="save-tip">
  <:trigger>Save</:trigger>
  <:content>Save this document</:content>
</.tooltip>

Contract compliance (baked in — see CONTRACT_TOOLTIP.md)

  • open state is browser-owned: the top layer via the native popover attribute, exactly like <.popover>; the server never renders or owns it, and it is never mirrored into a server-reconciled attribute
  • the popover attribute is a server-rendered constant (a client-added one would be stripped by reconciliation, KERNEL rule 1.2)
  • role="tooltip" is a server-rendered constant on the content element
  • aria-describedby on the trigger is a server-rendered constant — it always points at @id, the server never mutates it, so unlike popover's aria-expanded or menu's aria-activedescendant it needs no phx-mounted={JS.ignore_attributes([...])} protection; there is no client-managed ARIA at all on this component
  • there is no native declarative hover trigger, so a minimal hook (LicTooltip) opens the tooltip on mouseenter/focus and closes it on mouseleave/blur by calling showPopover()/hidePopover(); the hook is only the trigger for the state change, the open state itself still lives in the top layer
  • content is server-rendered HEEx (the slot)
  • no aria-expanded (tooltips have no expanded state), no selection, no cursor

Usage

<.tooltip id="save-tip">
  <:trigger>Save</:trigger>
  <:content>Save this document</:content>
</.tooltip>

Unstyled by design: bring your own classes via the slots and :rest.

Summary

Functions

tooltip(assigns)

Attributes

  • id (:string) (required) - stable id; all wiring is derived from it.

  • mode (:string) - "manual" (default) = only the hook opens/closes it via showPopover/hidePopover; "auto" additionally participates in native light-dismiss/auto-close-siblings. Defaults to "manual". Must be one of "auto", or "manual".

  • placement (:string) - optional zero-JS anchored positioning (CSS Anchor Positioning; see reports/POSITIONING_SPIKE_REPORT.md). Server-owned static style attrs; flips at the viewport edge; follows the anchor through patches/scroll as plain layout. Older engines show it unanchored (documented version floor; no JS fallback). Do not combine with a custom style on the trigger or tooltip element.

    Defaults to nil. Must be one of nil, "bottom", or "top".

  • trigger_attrs (:map) - extra attrs spread onto the trigger <button> (class, phx-click, aria-label, …). First real-app adoption finding (TASKS.md M3): icon-only action buttons want to BE the trigger, so the trigger must accept the button's own attrs. The component still owns id / aria-describedby / data-lic-tooltip / phx-hook — keys it derives are not overridable.

    Defaults to %{}.

  • Global attributes are accepted. extra attrs on the tooltip content element.

Slots

  • trigger (required)
  • content (required)