defmodule Corex.Tooltip do @moduledoc ~S''' Phoenix implementation of [Zag.js Tooltip](https://zagjs.com/components/react/tooltip). ## Examples ### Basic ```heex <.tooltip id="my-tooltip"> <:trigger>Hover me <:content>Tooltip content ``` ### Multiple triggers (Zag) One tooltip content, several triggers. Each `value` must be a stable non-empty string and unique within the component. ```heex <.tooltip id="shared-tip" show_arrow={false}> <:trigger value="a">First anchor <:trigger value="b">Second anchor <:content>Same panel for both triggers ``` For **multi-trigger** tooltips whose `<:content>` should follow the active trigger in **LiveView**, set **`on_trigger_value_change`** to a `handle_event/3` name and update assigns used inside `<:content>` (see the e2e **Tooltip · Pattern** page). ### Without arrow ```heex <.tooltip id="my-tooltip" show_arrow={false}> <:trigger>Hover me <:content>No arrow ``` ## API Control Use an id on the component for API control. **Client-side** ```heex ``` **Server-side** ```elixir def handle_event("open_tooltip", _, socket) do {:noreply, Corex.Tooltip.set_open(socket, "my-tooltip", true)} end ``` ## Styling Target parts with `data-scope="tooltip"` and `data-part`: ```css [data-scope="tooltip"][data-part="trigger"] {} [data-scope="tooltip"][data-part="positioner"] {} [data-scope="tooltip"][data-part="content"] {} [data-scope="tooltip"][data-part="arrow"] {} ``` Trigger and content have `data-state="open"` or `data-state="closed"`. On the root element, optional modifier classes: - `tooltip--{semantic}` - surface and ink (for example `tooltip--accent`, `tooltip--brand`) - `tooltip--size-{sm|md|lg|xl}` - max width, padding, and arrow scale - `tooltip--text-{scale}` - content font size only (for example `tooltip--text-sm`, `tooltip--text-xl`) ''' @doc type: :component use Phoenix.Component alias Corex.Positioning alias Corex.Tooltip.Anatomy.{Arrow, ArrowTip, Content, Positioner, Props, Trigger} alias Corex.Tooltip.Connect alias Phoenix.LiveView alias Phoenix.LiveView.JS attr(:id, :string, required: false, doc: "The id of the tooltip, useful for API to identify the tooltip" ) attr(:disabled, :boolean, default: false, doc: "Whether the tooltip is disabled") attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "The direction of the tooltip. When nil, derived from document" ) attr(:orientation, :string, default: "horizontal", values: ["horizontal", "vertical"], doc: "Layout orientation for CSS." ) attr(:open_delay, :integer, default: 0, doc: "Delay in ms before opening. Default from Zag is 400" ) attr(:close_delay, :integer, default: 0, doc: "Delay in ms before closing. Default from Zag is 150" ) attr(:positioning, Positioning, default: %Positioning{}, doc: "Positioning options for the floating content" ) attr(:close_on_escape, :boolean, default: true, doc: "Whether to close on Escape. Default true" ) attr(:close_on_click, :boolean, default: true, doc: "Whether to close on click. Default true" ) attr(:close_on_pointer_down, :boolean, default: false, doc: "Whether to close on pointer down on the trigger. Default false" ) attr(:close_on_scroll, :boolean, default: false, doc: "Whether to close on scroll. Default false" ) attr(:interactive, :boolean, default: true, doc: "Whether the tooltip content is interactive (stays open when hovering content)" ) attr(:on_open_change, :string, default: nil, doc: "The server event name when the open state changes" ) attr(:on_open_change_client, :string, default: nil, doc: "The client event name when the open state changes" ) attr(:on_trigger_value_change, :string, default: nil, doc: "LiveView event when the active trigger value changes (multi-trigger). Params include id (tooltip root) and value (trigger value string)." ) attr(:show_arrow, :boolean, default: true, doc: "Whether to show an arrow pointing to the trigger" ) attr(:trigger_tag, :atom, default: :button, values: [:button, :span], doc: "Use :span when the tooltip sits inside another button-like control (e.g. tree view row) to avoid nested <% end %> <% end %>
{render_slot(Enum.at(@content, 0), @slot_assigns)}
""" end @doc type: :api 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 @doc type: :api 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 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