defmodule LiveViewContinuity.Menu do @moduledoc """ An unstyled action and navigation menu with patch-safe client interaction state. See `MENU.md` for the observable interaction contract. """ use Phoenix.Component alias Phoenix.LiveView.JS attr(:id, :string, required: true) attr(:on_action, :string, required: true) attr(:class, :any, default: nil) attr(:rest, :global) slot :trigger, required: true do attr(:class, :any) end slot :item, required: true do attr(:id, :string, required: true) attr(:disabled, :boolean) attr(:class, :any) attr(:close_on_action, :boolean) attr(:navigate, :string) attr(:typeahead_text, :string) end def menu(assigns) do validate_items!(assigns.item) assigns = case assigns.trigger do [trigger_entry] -> assign(assigns, :trigger_entry, trigger_entry) entries -> raise ArgumentError, "menu requires exactly one trigger slot, got: #{length(entries)}" end ~H"""
"-popup"} role="menu" popover="auto" aria-labelledby={@id <> "-trigger"} > <.menu_item :for={item <- @item} :key={item.id} menu_id={@id} item={item} />
""" end attr(:menu_id, :string, required: true) attr(:item, :map, required: true) defp menu_item(assigns) do ~H""" <.link :if={@item[:navigate] && !@item[:disabled]} id={@menu_id <> "-item-" <> @item.id} navigate={@item.navigate} role="menuitem" tabindex="-1" aria-disabled={to_string(@item[:disabled] || false)} class={@item[:class]} data-lvc-item data-lvc-logical-id={@item.id} data-lvc-typeahead={@item[:typeahead_text]} data-lvc-close-on-action={to_string(Map.get(@item, :close_on_action, true))} phx-mounted={JS.ignore_attributes(["tabindex", "data-lvc-active"])} > {render_slot(@item)} "-item-" <> @item.id} role="menuitem" tabindex="-1" aria-disabled="true" class={@item[:class]} data-lvc-item data-lvc-logical-id={@item.id} data-lvc-typeahead={@item[:typeahead_text]} data-lvc-close-on-action="true" phx-mounted={JS.ignore_attributes(["tabindex", "data-lvc-active"])} > {render_slot(@item)} """ end defp validate_items!(items) do Enum.each(items, fn item -> case item[:navigate] do nil -> :ok navigate when is_binary(navigate) and navigate != "" -> if Map.get(item, :close_on_action, true) == false do raise ArgumentError, "menu navigation item #{inspect(item.id)} cannot set close_on_action to false" end _ -> raise ArgumentError, "menu navigation item #{inspect(item.id)} requires a nonempty navigate path" end end) end end