defmodule LiveViewContinuity.Disclosure do @moduledoc """ An unstyled standalone disclosure with browser-owned, patch-safe expanded state. See `DISCLOSURE.md` for the observable interaction contract. """ use Phoenix.Component alias Phoenix.LiveView.JS attr(:id, :string, required: true) attr(:default_expanded, :boolean, default: false) attr(:class, :any, default: nil) attr(:panel_class, :any, default: nil) attr(:rest, :global) slot :trigger, required: true do attr(:class, :any) end slot(:inner_block, required: true) def disclosure(assigns) do validate_id!(assigns.id) trigger = one!(assigns.trigger, "trigger") one!(assigns.inner_block, "body") assigns = assign(assigns, :trigger_entry, trigger) ~H"""
"-panel"} class={@panel_class} aria-labelledby={@id <> "-trigger"} aria-hidden={to_string(!@default_expanded)} hidden={!@default_expanded} data-lvc-disclosure-panel phx-mounted={JS.ignore_attributes(["hidden", "aria-hidden"])} > {render_slot(@inner_block)}
""" end defp one!([entry], _name), do: entry defp one!(entries, name), do: raise( ArgumentError, "disclosure requires exactly one #{name} slot, got: #{length(entries)}" ) defp validate_id!(id) when is_binary(id) do if id == "" or String.contains?(id, <<0>>) or Regex.match?(~r/[\t\n\f\r ]/, id), do: raise( ArgumentError, "disclosure id must be a non-empty string without ASCII whitespace or NUL" ) end defp validate_id!(_id), do: raise(ArgumentError, "disclosure id must be a string") end