defmodule Corex.Tooltip do @moduledoc ~S''' Phoenix implementation of [Zag.js Tooltip](https://zagjs.com/components/react/tooltip). ## Anatomy ### Minimal ```heex <.tooltip class="tooltip" show_arrow={false}> <:trigger>Hover me <:content>Tooltip content ``` ### With arrow ```heex <.tooltip class="tooltip"> <:trigger>Hover me <:content>Tooltip content ``` ### Placement ```heex <.tooltip class="tooltip" positioning={%Corex.Positioning{placement: "bottom"}}> <:trigger>Bottom <:content>Tooltip below ``` ## API Requires a stable `id` on `<.tooltip>`. | Function | Action | Returns | | -------- | ------ | ------- | | [`set_open/2`](#set_open/2) | Set open state (client) | `%Phoenix.LiveView.JS{}` | | [`set_open/3`](#set_open/3) | Set open state (server) | `socket` | ## Events Pick an event name and pass it to `on_*` on `<.tooltip>`. ### Server events | Event | When | Payload | | ----- | ---- | ------- | | `on_open_change="tooltip_open_changed"` | Open state changes | `%{"id" => id, "open" => boolean}` | | `on_trigger_value_change="tooltip_trigger_changed"` | Active trigger changes (multi-trigger) | `%{"id" => id, "value" => value}` | ### on_open_change ```heex <.tooltip class="tooltip" on_open_change="tooltip_open_changed"> <:trigger>Hover me <:content>Tooltip content ``` ```elixir def handle_event("tooltip_open_changed", %{"id" => _id, "open" => open}, socket) do {:noreply, assign(socket, :tooltip_open, open)} end ``` ### Client events | Event | When | `event.detail` | | ----- | ---- | -------------- | | `on_open_change_client="tooltip-open-changed"` | Open state changes | `id`, `open` | | `on_trigger_value_change_client="tooltip-trigger-changed"` | Active trigger changes | `id`, `value` | ## Patterns ### Multi-trigger One content panel, several triggers. Each trigger `value` must be unique. ```heex <.tooltip class="tooltip" on_trigger_value_change="tooltip_trigger_changed" > <:trigger value="a">First <:trigger value="b">Second <:content>Active: {@active_trigger} ``` ```elixir def handle_event("tooltip_trigger_changed", %{"value" => value}, socket) do {:noreply, assign(socket, :active_trigger, value)} end ``` ### Inside menu (or other roving-focus containers) When a tooltip trigger sits inside a menu item, set `focusable={false}` on `<:trigger>` so open focus does not land on the trigger and open the tooltip. Prefer `trigger_tag={:span}` to avoid nested ` <% end %> <% end %>
{render_slot(Enum.at(@content, 0), @slot_assigns)}
""" end api_doc(~S""" Set tooltip open state from a control (`phx-click`). ```heex <.action phx-click={Corex.Tooltip.set_open("my-tooltip", true)}>Show <.tooltip id="my-tooltip" class="tooltip"> <:trigger>Target <:content>Hint ``` ```javascript document.getElementById("my-tooltip")?.dispatchEvent( new CustomEvent("corex:tooltip:set-open", { bubbles: false, detail: { open: true }, }) ); ``` """) def set_open(tooltip_id, open) when is_binary(tooltip_id) and is_boolean(open) do JS.dispatch("corex:tooltip:set-open", to: "##{tooltip_id}", detail: %{open: open}, bubbles: false ) end api_doc(~S""" Set open state from `handle_event`. ```heex <.action phx-click="show_tip">Show <.tooltip id="my-tooltip" class="tooltip"> <:trigger>Target <:content>Hint ``` ```elixir def handle_event("show_tip", _, socket) do {:noreply, Corex.Tooltip.set_open(socket, "my-tooltip", true)} end ``` """) def set_open(socket, tooltip_id, open) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(tooltip_id) and is_boolean(open) do LiveView.push_event(socket, "tooltip_set_open", %{ tooltip_id: tooltip_id, open: open }) end defp trigger_connect_assigns(id, dir, orientation, disabled, trigger_tag, trigger_slot) do %Trigger{ id: id, dir: dir, open: false, disabled: disabled, orientation: orientation, tag: trigger_tag, value: Map.get(trigger_slot, :value), focusable: Map.get(trigger_slot, :focusable, true), tabindex: Map.get(trigger_slot, :tabindex) } end defp validate_triggers!(triggers) when is_list(triggers) do case triggers do [] -> :ok [_] -> :ok many -> values = Enum.map(many, &Map.get(&1, :value)) if Enum.any?(values, &(is_nil(&1) or &1 == "")) do raise ArgumentError, "Corex.Tooltip: each <:trigger> must include a non-empty value attribute when there are multiple triggers" end if length(Enum.uniq(values)) != length(values) do raise ArgumentError, "Corex.Tooltip: trigger value attributes must be unique" end end end end