lib/phoenix/ui/components/modal.ex

defmodule Phoenix.UI.Components.Modal do
  @moduledoc """
  Provides modal component.
  """
  import Phoenix.UI.Components.{Backdrop, Paper}

  use Phoenix.UI, :component

  @default_max_width :sm
  @default_open false

  @doc """
  Renders modal component.

  ## Examples

      ```
      <.modal id="basic_modal">
        <:header>
          <.typography variant="h2" margin={false}>
            Use Google's location service?
          </.typography>
        </:header>
        <:content>
          <.typography margin={false}>
            Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.
          </.typography>
        </:content>
        <:actions>
          <.button variant="text" color="slate" extend_class="mr-4">Disagree</.button>
          <.button variant="text">Agree</.button>
        </:actions>
      </.modal>
      ```

  """
  @spec modal(Socket.assigns()) :: Rendered.t()
  def modal(raw_assigns) do
    assigns =
      raw_assigns
      |> assign_new(:max_width, fn -> @default_max_width end)
      |> assign_new(:open, fn -> @default_open end)
      |> assign_new("phx-click-away", fn -> hide_modal("##{raw_assigns[:id]}") end)
      |> assign_new("phx-key", fn -> "escape" end)
      |> assign_new("phx-window-keydown", fn -> hide_modal("##{raw_assigns[:id]}") end)
      |> build_modal_attrs()

    ~H"""
    <.backdrop id={"#{@id}_modal_backdrop"} open={@open} phx-click={hide_modal("##{@id}")} />
    <.paper variant="elevated" {@modal_attrs}>
      <%= render_slot(@inner_block) %>
    </.paper>
    """
  end

  ### JS Interactions ##########################

  @doc """
  Hide modal matching selector.

  ## Examples

      iex> hide_modal(selector)
      %JS{}

      iex> hide_modal(js, selector)
      %JS{}

  """
  @spec hide_modal(String.t()) :: struct()
  def hide_modal(selector), do: hide_modal(%JS{}, selector)

  @spec hide_modal(struct(), String.t()) :: struct()
  def hide_modal(%JS{} = js, selector) do
    js
    |> JS.remove_attribute("open", to: selector)
    |> hide_backdrop("#{selector}_modal_backdrop")
  end

  @doc """
  Show modal matching selector.

  ## Examples

      iex> show_modal(selector)
      %JS{}

      iex> show_modal(js, selector)
      %JS{}

  """
  @spec show_modal(String.t()) :: struct()
  def show_modal(selector), do: show_modal(%JS{}, selector)

  @spec show_modal(struct(), String.t()) :: struct()
  def show_modal(%JS{} = js, selector) do
    js
    |> JS.set_attribute({"open", "true"}, to: selector)
    |> show_backdrop("#{selector}_modal_backdrop")
  end

  ### Modal Attrs ##########################

  defp build_modal_attrs(assigns) do
    extend_class = build_class(~w(
      overflow-auto z-50 w-full max-h-[75%]
      invisible opacity-0 open:visible open:opacity-100
      fixed top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2
      transition-all ease-in-out duration-300
      #{classes(:max_width, assigns)}
      #{Map.get(assigns, :extend_class)}
    ))

    attrs =
      assigns
      |> assigns_to_attributes([:extend_class, :max_width])
      |> Keyword.put(:extend_class, extend_class)

    assign(assigns, :modal_attrs, attrs)
  end

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

  # Max Width
  defp classes(:max_width, %{max_width: size}), do: "max-w-#{size}"

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