lib/petal_components/dropdown.ex

defmodule PetalComponents.Dropdown do
  use Phoenix.Component
  alias Phoenix.LiveView.JS
  alias PetalComponents.Heroicons
  import PetalComponents.Link

  @transition_in_base "transition transform ease-out duration-100"
  @transition_in_start "transform opacity-0 scale-95"
  @transition_in_end "transform opacity-100 scale-100"

  @transition_out_base "transition ease-in duration-75"
  @transition_out_start "transform opacity-100 scale-100"
  @transition_out_end "transform opacity-0 scale-95"

  # prop js_lib, :string, default: "alpine_js", options: ["alpine_js", "live_view_js"]
  # prop label, :string
  # prop placement, :string, default: options: ["left", "right"]
  # slot default
  @doc """
    <.dropdown label="Dropdown" js_lib="alpine_js|live_view_js">
      <.dropdown_menu_item link_type="button">
        <Heroicons.Outline.home class="w-5 h-5 text-gray-500" />
        Button item with icon
      </.dropdown_menu_item>
      <.dropdown_menu_item link_type="a" to="/" label="a item" />
      <.dropdown_menu_item link_type="live_patch" to="/" label="Live Patch item" />
      <.dropdown_menu_item link_type="live_redirect" to="/" label="Live Redirect item" />
    </.dropdown>
  """
  def dropdown(assigns) do
    assigns = assigns
      |> assign_new(:options_container_id, fn -> "dropdown_#{Enum.random(1..100000000)}" end)
      |> assign_new(:js_lib, fn -> "alpine_js" end)
      |> assign_new(:placement, fn -> "left" end)

    ~H"""
    <div {js_attributes("container", @js_lib, @options_container_id)} class="relative z-10 inline-block text-left">
      <div>
        <%= if @label do %>
          <button
            link_type="button"
            class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none"
            {js_attributes("button", @js_lib, @options_container_id)}
            aria-haspopup="true"
          >
            <%= @label %>
            <Heroicons.Solid.chevron_down class="w-5 h-5 ml-2 -mr-1" />
          </button>
        <% else %>
          <button
            link_type="button"
            class="flex items-center text-gray-400 rounded-full hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500"
            {js_attributes("button", @js_lib, @options_container_id)}
            aria-haspopup="true"
          >
            <span class="sr-only">Open options</span>
            <Heroicons.Solid.dots_vertical class="w-5 h-5" />
          </button>
        <% end %>
      </div>
      <div
        {js_attributes("options_container", @js_lib, @options_container_id)}
        class={placement_class(@placement) <> " absolute w-56 mt-2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"}
        role="menu"
        id={@options_container_id}
        aria-orientation="vertical"
        aria-labelledby="options-menu"
      >
        <div class="py-1" role="none">
          <%= render_slot(@inner_block) %>
        </div>
      </div>
    </div>
    """
  end

  def dropdown_menu_item(assigns) do
    assigns = assigns
      |> assign_new(:link_type, fn -> "button" end)
      |> assign_new(:inner_block, fn -> nil end)
      |> assign_new(:classes, fn -> dropdown_menu_item_classes() end)
      |> assign_new(:extra_attributes, fn ->
        Map.drop(assigns, [
          :inner_block,
          :link_type,
          :classes,
          :__changed__
        ])
      end)

    ~H"""
    <%= if @link_type == "button" do %>
      <button class={@classes}>
        <%= if @inner_block do %>
          <%= render_slot(@inner_block) %>
        <% else %>
          <%= @label %>
        <% end %>
      </button>
    <% else %>
      <.link link_type={@link_type} to={@to} class={@classes} {@extra_attributes}>
        <%= if @inner_block do %>
          <%= render_slot(@inner_block) %>
        <% else %>
          <%= @label %>
        <% end %>
      </.link>
    <% end %>
    """
  end

  def dropdown_menu_item_classes(),
    do:
      "block flex gap-2 items-center self-start justify-start px-4 py-2 text-sm text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 w-full text-left"

  def js_attributes("container", "alpine_js", _options_container_id) do
    %{
      "x-data": "{open: false}",
      "@keydown.escape.stop": "open = false",
      "@click.outside": "open = false"
    }
  end

  def js_attributes("button", "alpine_js", _options_container_id) do
    %{
      "@click": "open = !open",
      "x-bind:aria-expanded": "open.toString()"
    }
  end

  def js_attributes("options_container", "alpine_js", _options_container_id) do
    %{
      "x-cloak": true,
      "x-show": "open",
      "x-transition:enter": @transition_in_base,
      "x-transition:enter-start": @transition_in_start,
      "x-transition:enter-end": @transition_in_end,
      "x-transition:leave": @transition_out_base,
      "x-transition:leave-start": @transition_out_start,
      "x-transition:leave-end": @transition_out_end,
    }
  end

  def js_attributes("container", "live_view_js", options_container_id) do
    %{
      "phx-click-away": JS.hide(
        to: "##{options_container_id}",
        transition: {@transition_out_base, @transition_out_start, @transition_out_end}
      )
    }
  end

  def js_attributes("button", "live_view_js", options_container_id) do
    %{
      "phx-click": JS.toggle(
        to: "##{options_container_id}",
        display: "block",
        in: {@transition_in_base, @transition_in_start, @transition_in_end},
        out: {@transition_out_base, @transition_out_start, @transition_out_end}
      )
    }
  end

  def js_attributes("options_container", "live_view_js", _options_container_id) do
    %{
      style: "display: none;",
    }
  end

  def placement_class("left"), do: "right-0 origin-top-right"
  def placement_class("right"), do: "left-0 origin-top-left"
end