defmodule PhiaUi.Components.Sortable do @moduledoc """ Sortable drag-and-drop list components for PhiaUI. Provides `drag_handle/1`, `drop_indicator/1`, `sortable_list/1`, and `sortable_item/1`. Pair `sortable_list/1` with the `PhiaSortable` hook for full drag-and-drop reordering with keyboard navigation and ARIA announcements. ## Minimal example <.sortable_list id="tasks" on_reorder="reorder_tasks"> <.sortable_item :for={{item, idx} <- Enum.with_index(@items)} id={item.id} index={idx}> {item.title} ## With drag handles <.sortable_list id="tasks" handle={true} on_reorder="reorder_tasks"> <.sortable_item :for={{item, idx} <- Enum.with_index(@items)} id={item.id} index={idx}> <.drag_handle /> {item.title} """ use Phoenix.Component import PhiaUi.ClassMerger, only: [cn: 1] # --------------------------------------------------------------------------- # drag_handle/1 # --------------------------------------------------------------------------- attr :aria_label, :string, default: "Drag to reorder" attr :size, :string, values: ~w(sm md lg), default: "md" attr :class, :string, default: nil attr :rest, :global @doc """ Renders a drag handle button with a 6-dot grip icon. Place this inside a `sortable_item/1`. When `handle={true}` is set on the parent `sortable_list/1`, only dragging from the handle initiates a reorder. """ def drag_handle(assigns) do ~H""" """ end defp handle_size_class("sm"), do: "w-3 h-4" defp handle_size_class("md"), do: "w-4 h-5" defp handle_size_class("lg"), do: "w-5 h-6" # --------------------------------------------------------------------------- # drop_indicator/1 # --------------------------------------------------------------------------- attr :orientation, :string, values: ~w(horizontal vertical), default: "horizontal" attr :active, :boolean, default: false attr :class, :string, default: nil attr :rest, :global @doc """ Renders a thin line that marks the drop position between sortable items. The hook toggles the active state by setting `data-active` on the element. """ def drop_indicator(assigns) do ~H""" """ end defp drop_indicator_class("horizontal", false), do: "h-0.5 w-full bg-transparent transition-colors" defp drop_indicator_class("horizontal", true), do: "h-0.5 w-full bg-primary transition-colors" defp drop_indicator_class("vertical", false), do: "w-0.5 h-full bg-transparent transition-colors" defp drop_indicator_class("vertical", true), do: "w-0.5 h-full bg-primary transition-colors" # --------------------------------------------------------------------------- # sortable_list/1 # --------------------------------------------------------------------------- attr :id, :string, required: true attr :orientation, :string, values: ~w(vertical horizontal), default: "vertical" attr :handle, :boolean, default: false attr :on_reorder, :string, default: "reorder" attr :class, :string, default: nil attr :rest, :global slot :inner_block, required: true @doc """ Renders a sortable list container backed by the `PhiaSortable` hook. Set `handle={true}` when each item contains a `drag_handle/1` — the hook will then only initiate a drag when the user grabs the handle, not the whole item. The hook emits `on_reorder` (default `"reorder"`) with `%{old_index: N, new_index: M, id: "dom-id"}`. """ def sortable_list(assigns) do ~H""" """ end defp list_orientation_class("vertical"), do: "flex-col" defp list_orientation_class("horizontal"), do: "flex-row flex-wrap" # --------------------------------------------------------------------------- # sortable_item/1 # --------------------------------------------------------------------------- attr :id, :string, required: true attr :index, :integer, required: true attr :disabled, :boolean, default: false attr :class, :string, default: nil attr :rest, :global slot :inner_block, required: true slot :handle @doc """ Renders a single sortable list item. Pass a `drag_handle/1` in the `:handle` slot (or inline in `inner_block`) if using handle-based dragging. The `data-index` attribute is used by the hook to calculate reorder positions. """ def sortable_item(assigns) do ~H"""
  • {render_slot(@handle)} {render_slot(@inner_block)}
  • """ end end