defmodule LiveViewContinuity.Tooltip do @moduledoc """ An unstyled, descriptive tooltip with patch-safe browser interaction state. See `TOOLTIP.md` for the observable interaction contract. """ use Phoenix.Component alias Phoenix.LiveView.JS attr(:id, :string, required: true) attr(:delay, :integer, default: 600) attr(:disabled, :boolean, default: false) attr(:describedby, :string, default: nil) attr(:class, :any, default: nil) attr(:trigger_class, :any, default: nil) attr(:tooltip_class, :any, default: nil) attr(:rest, :global) slot(:trigger, required: true) slot(:inner_block, required: true) def tooltip(assigns) do validate_id!(assigns.id) validate_delay!(assigns.delay) validate_idrefs!(assigns.describedby) trigger = one!(assigns.trigger, "trigger") one!(assigns.inner_block, "body") assigns = assign(assigns, :trigger_entry, trigger) ~H"""
"-popup"} role="tooltip" popover="manual" class={@tooltip_class} data-lvc-tooltip-popup > {render_slot(@inner_block)}
""" end defp one!([entry], _name), do: entry defp one!(entries, name), do: raise(ArgumentError, "tooltip requires exactly one #{name} slot, got: #{length(entries)}") defp validate_id!(id) do if id == "" or String.contains?(id, <<0>>) or Regex.match?(~r/[\t\n\f\r ]/, id), do: raise( ArgumentError, "tooltip id must be a non-empty string without ASCII whitespace or NUL" ) end defp validate_delay!(delay) when is_integer(delay) and delay >= 0, do: :ok defp validate_delay!(_), do: raise(ArgumentError, "tooltip delay must be a nonnegative integer") defp validate_idrefs!(nil), do: :ok defp validate_idrefs!(value) when is_binary(value) do tokens = String.split(value, ~r/[\t\n\f\r ]+/, trim: true) if tokens == [] or Enum.any?(tokens, &String.contains?(&1, <<0>>)), do: raise(ArgumentError, "tooltip describedby must contain valid IDREF tokens") end defp validate_idrefs!(_), do: raise(ArgumentError, "tooltip describedby must be an IDREF string") end