lib/phoenix/ui/components/accordion.ex

defmodule Phoenix.UI.Components.Accordion do
  @moduledoc """
  Provides accordion component.
  """
  import Phoenix.UI.{Components.Collapse, Components.Heroicon}

  use Phoenix.UI, :component

  @default_icon_color "slate"
  @default_icon_name_closed "chevron-up"
  @default_icon_name_opened "chevron-down"
  @default_open false
  @default_panel_variant "vertical"

  @doc """
  Renders accordion component.

  ## Examples

      ```
      <.accordion>
        <:header>
          Collapsible Item #1
        </:header>
        <:panel>
          Collapsible content
        </:panel>
      </.accordion>
      ```

  """
  @spec accordion(Socket.assigns()) :: Rendered.t()
  def accordion(raw_assigns) do
    assigns =
      raw_assigns
      |> assign_new(:open, fn -> @default_open end)
      |> assign(:icon_wrapper_class, "absolute right-0 top-1/2 -translate-y-1/2 cursor-pointer")
      |> build_container_attrs()
      |> build_header_closed_attrs()
      |> build_header_opened_attrs()
      |> build_icon_closed_attrs()
      |> build_icon_opened_attrs()
      |> build_panel_attrs()

    ~H"""
    <div {@container_attrs}>
      <div {@header_closed_attrs} phx-click={close_accordion(@id)}>
        <%= render_slot(@header) %>
        <div class={@icon_wrapper_class}>
          <.heroicon {@icon_closed_attrs} />
        </div>
      </div>
      <div {@header_opened_attrs} phx-click={open_accordion(@id)}>
        <%= render_slot(@header) %>
        <div class={@icon_wrapper_class}>
          <.heroicon {@icon_opened_attrs} />
        </div>
      </div>
      <.collapse {@panel_attrs}>
        <%= render_slot(@panel) %>
      </.collapse>
    </div>
    """
  end

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

  @doc """
  Closes accordion by selector.

  ## Examples

      iex> close_accordion(selector)
      %JS{}

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

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

  @spec close_accordion(struct(), String.t()) :: struct()
  def close_accordion(%JS{} = js, selector) do
    js
    |> JS.hide(to: "##{selector} .accordion-header.closed", time: 0)
    |> JS.show(to: "##{selector} .accordion-header.opened", time: 0)
    |> close_collapse("##{selector} .accordion-panel")
  end

  @doc """
  Opens accordion by selector.

  ## Examples

      iex> open_accordion(selector)
      %JS{}

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

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

  @spec open_accordion(struct(), String.t()) :: struct()
  def open_accordion(%JS{} = js, selector) do
    js
    |> JS.hide(to: "##{selector} .accordion-header.opened", time: 0)
    |> JS.show(to: "##{selector} .accordion-header.closed", time: 0)
    |> open_collapse("##{selector} .accordion-panel")
  end

  ### Container Attrs ##########################

  defp build_container_attrs(assigns) do
    class = build_class(~w(
      accordion
      #{Map.get(assigns, :extend_class)}
    ))

    attrs =
      assigns
      |> assigns_to_attributes([:close_icon, :extend_class, :header, :open_icon, :open, :panel])
      |> Keyword.put_new(:class, class)

    assign(assigns, :container_attrs, attrs)
  end

  ### Header Attrs (Closed) ##########################

  defp build_header_closed_attrs(%{header: [header]} = assigns) do
    class = build_class(~w(
      accordion-header closed relative cursor-pointer
      #{classes(:header_closed, :open, assigns)}
      #{Map.get(header, :extend_class)}
    ))

    attrs =
      header
      |> assigns_to_attributes([:extend_class])
      |> Keyword.put_new(:class, class)

    assign(assigns, :header_closed_attrs, attrs)
  end

  ### Header Attrs (Open) ##########################

  defp build_header_opened_attrs(%{header: [header]} = assigns) do
    class = build_class(~w(
      accordion-header opened relative cursor-pointer
      #{classes(:header_opened, :open, assigns)}
      #{Map.get(header, :extend_class)}
    ))

    attrs =
      header
      |> assigns_to_attributes([:extend_class])
      |> Keyword.put_new(:class, class)

    assign(assigns, :header_opened_attrs, attrs)
  end

  ### Icon Attrs (Closed) ##########################

  defp build_icon_closed_attrs(assigns) do
    icon = Map.get(assigns, :open_icon, [])

    extend_class = build_class(~w(
      accordion-header-icon
      #{Keyword.get(icon, :extend_class)}
    ))

    attrs =
      icon
      |> assigns_to_attributes([:extend_class])
      |> Keyword.put_new(:extend_class, extend_class)
      |> Keyword.put_new(:color, @default_icon_color)
      |> Keyword.put_new(:name, @default_icon_name_closed)

    assign(assigns, :icon_closed_attrs, attrs)
  end

  ### Icon Attrs (Opened) ##########################

  defp build_icon_opened_attrs(assigns) do
    icon = Map.get(assigns, :close_icon, [])

    extend_class = build_class(~w(
      accordion-header-icon
      #{Keyword.get(icon, :extend_class)}
    ))

    attrs =
      icon
      |> assigns_to_attributes([:extend_class])
      |> Keyword.put_new(:extend_class, extend_class)
      |> Keyword.put_new(:color, @default_icon_color)
      |> Keyword.put_new(:name, @default_icon_name_opened)

    assign(assigns, :icon_opened_attrs, attrs)
  end

  ### Panel Attrs ##########################

  defp build_panel_attrs(%{panel: [panel]} = assigns) do
    extend_class = build_class(~w(
      accordion-panel
      #{Map.get(panel, :extend_class)}
    ))

    attrs =
      panel
      |> assigns_to_attributes([:extend_class])
      |> Keyword.put_new(:variant, @default_panel_variant)
      |> Keyword.put(:extend_class, extend_class)
      |> Keyword.put(:open, Map.get(assigns, :open))

    assign(assigns, :panel_attrs, attrs)
  end

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

  # Header (Open) - Open
  defp classes(:header_closed, :open, %{open: false}), do: "hidden"

  # Header (Closed) - Open
  defp classes(:header_opened, :open, %{open: true}), do: "hidden"

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