Skip to main content

lib/skua/components/menu.ex

defmodule Skua.Components.Menu do
  @moduledoc """
  A dropdown menu — a trigger button that opens a top-layer menu of actions.

      <.menu id="actions">
        <:trigger>Actions</:trigger>
        <.menu_label>Project</.menu_label>
        <.menu_item icon="hero-pencil" shortcut="⌘R" phx-click="rename">Rename</.menu_item>
        <.menu_item shortcut="⌘D" phx-click="duplicate">Duplicate</.menu_item>
        <.menu_separator />
        <.menu_item danger phx-click="delete">Delete project</.menu_item>
      </.menu>

  Built on the same top-layer `PanelStack` as the popover, with the W3C APG menu
  keyboard model (Arrow up/down move between items, Enter/Space activate, Escape
  closes, Home/End jump). Activating an item closes the menu.
  """
  use Phoenix.Component

  @doc "The menu: a trigger button + a top-layer `role=menu` panel of items."
  attr :id, :string, required: true
  attr :trigger_variant, :string, default: "secondary", values: ~w(primary secondary ghost danger)
  attr :width, :string, default: nil
  attr :placement, :string, default: "bottom", values: ~w(bottom right)
  attr :class, :any, default: nil
  attr :rest, :global
  slot :trigger, required: true
  slot :inner_block, required: true

  def menu(assigns) do
    ~H"""
    <span class="sk-anchor" style="position:relative;display:inline-flex">
      <button
        type="button"
        id={"#{@id}-trigger"}
        class={"sk-btn sk-btn--#{@trigger_variant} sk-focusable"}
        phx-hook="SkuaMenu"
        data-sk-panel={@id}
        aria-haspopup="menu"
        aria-expanded="false"
        aria-controls={@id}
        {@rest}
      >
        {render_slot(@trigger)}
        <svg class="sk-glyph sk-chev" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
          <path d="m6 9 6 6 6-6" />
        </svg>
      </button>
      <div
        id={@id}
        role="menu"
        class={["sk-panel sk-anim", @class]}
        popover="manual"
        data-state="closed"
        data-placement={@placement}
        style={@width && "min-width:#{@width}"}
      >
        <div class="sk-menu">{render_slot(@inner_block)}</div>
      </div>
    </span>
    """
  end

  @doc "A menu action. `danger` styles it destructive; `shortcut`/`icon` are optional."
  attr :danger, :boolean, default: false
  attr :icon, :string, default: nil, doc: "a heroicon name, e.g. \"hero-pencil\""
  attr :shortcut, :string, default: nil
  attr :class, :any, default: nil
  attr :rest, :global, include: ~w(disabled)
  slot :inner_block, required: true

  def menu_item(assigns) do
    ~H"""
    <button
      type="button"
      role="menuitem"
      tabindex="-1"
      class={["sk-item sk-menu-action", @danger && "sk-item--danger", @class]}
      {@rest}
    >
      <span :if={@icon} class="sk-lead"><span class={[@icon, "sk-glyph"]} /></span>
      {render_slot(@inner_block)}
      <span :if={@shortcut} class="sk-kbd">{@shortcut}</span>
    </button>
    """
  end

  @doc "A small uppercase section label inside a menu."
  slot :inner_block, required: true

  def menu_label(assigns) do
    ~H"""
    <div class="sk-menu-label" role="presentation">{render_slot(@inner_block)}</div>
    """
  end

  @doc "A divider between menu sections."
  def menu_separator(assigns) do
    ~H"""
    <div class="sk-sep" role="separator"></div>
    """
  end
end