lib/phoenix/ui/components/tooltip.ex

defmodule Phoenix.UI.Components.Tooltip do
  @moduledoc """
  Provides tooltip component.
  """
  use Phoenix.UI, :component

  @default_color "slate"
  @default_position "top"
  @default_variant "simple"

  @doc """
  Renders tooltip component.

  ## Examples

      ```
      <.tooltip>
        content
      </.tooltip>
      ```

  """
  @spec tooltip(Socket.assigns()) :: Rendered.t()
  def tooltip(raw) do
    assigns =
      raw
      |> assign_new(:color, fn -> @default_color end)
      |> assign_new(:position, fn -> @default_position end)
      |> assign_new(:variant, fn -> @default_variant end)
      |> build_tooltip_attrs()

    ~H"""
    <div id={assigns[:id]} class="group relative inline-block">
      <%= render_slot(@inner_block) %>
      <div {@tooltip_attrs}>
        <%= if is_bitstring(@content) do %>
          <%= @content %>
        <% else %>
          <%= render_slot(@content) %>
        <% end %>
      </div>
    </div>
    """
  end

  ### Tooltip Attrs ##########################

  defp build_tooltip_attrs(assigns) do
    class = build_class(~w(
      z-50 invisible opacity-0 group-hover:visible group-hover:opacity-100 absolute text-xs rounded
      text-center whitespace-nowrap py-1 px-2 transition-all ease-in-out delay-150 duration-300
      #{classes(:color, assigns)}
      #{classes(:margin, assigns)}
      #{classes(:position, assigns)}
      #{classes(:variant, assigns)}
      #{Map.get(assigns, :extend_class)}
    ))

    attrs =
      assigns
      |> assigns_to_attributes([:color, :content, :id, :position, :variant])
      |> Keyword.put_new(:class, class)

    assign(assigns, :tooltip_attrs, attrs)
  end

  ### CSS Classes ##########################

  # Color
  defp classes(:color, %{color: color}), do: "bg-#{color}-800 text-#{color}-200"

  # Margin
  defp classes(:margin, %{position: "bottom_end"}), do: "mt-3"
  defp classes(:margin, %{position: "bottom_start"}), do: "mt-3"
  defp classes(:margin, %{position: "bottom"}), do: "mt-3"
  defp classes(:margin, %{position: "left_end"}), do: "mr-3"
  defp classes(:margin, %{position: "left_start"}), do: "mr-3"
  defp classes(:margin, %{position: "left"}), do: "mr-3"
  defp classes(:margin, %{position: "right_end"}), do: "ml-3"
  defp classes(:margin, %{position: "right_start"}), do: "ml-3"
  defp classes(:margin, %{position: "right"}), do: "ml-3"
  defp classes(:margin, %{position: "top_end"}), do: "mb-3"
  defp classes(:margin, %{position: "top_start"}), do: "mb-3"
  defp classes(:margin, %{position: "top"}), do: "mb-3"

  # Position
  defp classes(:position, %{position: "bottom_end"}), do: "top-full right-0"
  defp classes(:position, %{position: "bottom_start"}), do: "top-full left-0"
  defp classes(:position, %{position: "bottom"}), do: "top-full left-1/2 -translate-x-1/2"
  defp classes(:position, %{position: "left_end"}), do: "right-full bottom-0"
  defp classes(:position, %{position: "left_start"}), do: "right-full top-0"
  defp classes(:position, %{position: "left"}), do: "right-full top-1/2 -translate-y-1/2"
  defp classes(:position, %{position: "right_end"}), do: "left-full bottom-0"
  defp classes(:position, %{position: "right_start"}), do: "left-full top-0"
  defp classes(:position, %{position: "right"}), do: "left-full top-1/2 -translate-y-1/2"
  defp classes(:position, %{position: "top_end"}), do: "bottom-full right-0"
  defp classes(:position, %{position: "top_start"}), do: "bottom-full left-0"
  defp classes(:position, %{position: "top"}), do: "bottom-full left-1/2 -translate-x-1/2"

  # Variant
  defp classes(:variant, %{color: color, position: position, variant: "arrow"}) do
    case position do
      pos when pos in ["bottom_end", "bottom_start", "bottom"] ->
        "after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:border-solid after:border-b-8 after:border-x-transparent after:border-x-8 after:border-t-0 after:border-b-#{color}-800"

      pos when pos in ["left_end", "left_start", "left"] ->
        "after:absolute after:-right-1.5 after:top-1/2 after:-translate-y-1/2 after:border-solid after:border-l-8 after:border-y-transparent after:border-y-8 after:border-r-0 after:border-l-#{color}-800"

      pos when pos in ["right_end", "right_start", "right"] ->
        "after:absolute after:-left-1.5 after:top-1/2 after:-translate-y-1/2 after:border-solid after:border-r-8 after:border-y-transparent after:border-y-8 after:border-l-0 after:border-r-#{color}-800"

      pos when pos in ["top_end", "top_start", "top"] ->
        "after:absolute after:-bottom-1.5 after:left-1/2 after:-translate-x-1/2 after:border-solid after:border-t-8 after:border-x-transparent after:border-x-8 after:border-b-0 after:border-t-#{color}-800"
    end
  end

  defp classes(_rule_group, _assigns), do: nil
end