defmodule LiveViewContinuity.Tabs do @moduledoc """ Horizontal, manual-activation tabs with patch-safe browser focus. See `TABS.md` for the observable interaction contract. """ use Phoenix.Component alias Phoenix.LiveView.JS attr(:id, :string, required: true) attr(:value, :string, required: true) attr(:on_select, :string, required: true) attr(:label, :string, required: true) attr(:class, :any, default: nil) attr(:rest, :global) slot :tab, required: true do attr(:id, :string, required: true) attr(:label, :string, required: true) attr(:disabled, :boolean) attr(:tab_class, :any) attr(:panel_class, :any) end def tabs(assigns) do validate!(assigns.id, assigns.label, assigns.tab, assigns.value) ~H"""
""" end attr(:id, :string, required: true) attr(:value, :string, required: true) attr(:on_select, :string, required: true) attr(:label, :string, required: true) attr(:class, :any, default: nil) attr(:rest, :global) slot :tab, required: true do attr(:id, :string, required: true) attr(:label, :string, required: true) attr(:disabled, :boolean) attr(:tab_class, :any) end def tab_list(assigns) do validate!(assigns.id, assigns.label, assigns.tab, assigns.value) ~H"""
""" end attr(:root_id, :string, required: true) attr(:id, :string, required: true) attr(:active, :boolean, required: true) attr(:class, :any, default: nil) attr(:rest, :global) slot(:inner_block, required: true) def tab_panel(assigns) do validate_dom_id!(assigns.root_id, "tabs id") validate_dom_id!(assigns.id, "logical tab id") ~H""" """ end defp validate!(root_id, label, tabs, value) do validate_dom_id!(root_id, "tabs id") if String.trim(label) == "", do: raise(ArgumentError, "tabs label cannot be blank") validate_tabs!(tabs, value) end defp validate_tabs!([], _value), do: raise(ArgumentError, "tabs requires at least one tab") defp validate_tabs!(tabs, value) do ids = Enum.map(tabs, & &1.id) Enum.each(ids, &validate_dom_id!(&1, "logical tab id")) if Enum.any?(tabs, &(String.trim(&1.label) == "")), do: raise(ArgumentError, "tabs requires non-blank tab labels") if length(ids) != length(Enum.uniq(ids)), do: raise(ArgumentError, "tabs requires unique logical tab IDs") if Enum.all?(tabs, &(&1[:disabled] || false)), do: raise(ArgumentError, "tabs requires at least one enabled tab") selected = Enum.find(tabs, &(&1.id == value)) if is_nil(selected), do: raise(ArgumentError, "tabs value must identify an existing tab, got: #{inspect(value)}") if selected[:disabled], do: raise(ArgumentError, "tabs value cannot identify a disabled tab, got: #{inspect(value)}") end defp validate_dom_id!(value, name) do if value == "" or String.contains?(value, <<0>>) or Regex.match?(~r/[\t\n\f\r ]/, value), do: raise(ArgumentError, "#{name} must be a non-empty string without ASCII whitespace or NUL") end defp tab_id(root, logical), do: root <> "-tab-" <> logical defp panel_id(root, logical), do: root <> "-panel-" <> logical end