defmodule LiveViewContinuity.Dialog do
@moduledoc """
A native modal dialog with server-owned intent and patch-safe browser state.
See `DIALOG.md` for the observable interaction contract.
"""
use Phoenix.Component
alias Phoenix.LiveView.JS
attr(:id, :string, required: true)
attr(:open, :boolean, required: true)
attr(:on_open, :string, required: true)
attr(:on_close, :string, required: true)
attr(:initial_focus, :string, default: nil)
attr(:class, :any, default: nil)
attr(:dialog_class, :any, default: nil)
attr(:rest, :global)
slot :trigger, required: true do
attr(:class, :any)
end
slot :title, required: true do
attr(:class, :any)
end
slot :description do
attr(:class, :any)
end
slot(:inner_block, required: true)
slot :close, required: true do
attr(:class, :any)
end
def dialog(assigns) do
validate_id!(assigns.id)
assigns = assign_entries!(assigns)
~H"""
"""
end
defp assign_entries!(assigns) do
trigger = one!(assigns.trigger, "trigger", true)
title = one!(assigns.title, "title", true)
description = one!(assigns.description, "description", false)
close = one!(assigns.close, "close", true)
one!(assigns.inner_block, "body", true)
assigns
|> assign(:trigger_entry, trigger)
|> assign(:title_entry, title)
|> assign(:description_entry, description)
|> assign(:close_entry, close)
end
defp one!([entry], _name, _required), do: entry
defp one!([], _name, false), do: nil
defp one!(entries, name, _),
do: raise(ArgumentError, "dialog requires exactly one #{name} slot, got: #{length(entries)}")
defp validate_id!(id) do
if id == "" or String.contains?(id, <<0>>) or Regex.match?(~r/[\t\n\f\r ]/, id),
do:
raise(
ArgumentError,
"dialog id must be a non-empty string without ASCII whitespace or NUL"
)
end
end