Current section
Files
Jump to
Current section
Files
lib/components/modal.ex
defmodule SigmaKit.Components.Modal do
use Phoenix.LiveComponent
alias Phoenix.LiveView.JS
import SigmaKit.Components.Js, only: [show: 2, hide: 2]
import SigmaKit.Components.Buttons, only: [button: 1]
@doc """
Renders a modal.
## Examples
<.modal id="confirm-modal">
This is a modal.
</.modal>
JS commands may be passed to the `:on_cancel` to configure
the closing/cancel event, for example:
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
This is another modal.
</.modal>
"""
attr :id, :string, required: true, doc: "the unique id of the modal"
attr :show, :boolean, default: false, doc: "whether the modal is visible"
attr :title, :string, default: nil, doc: "the title of the modal"
attr :on_cancel, JS, default: %JS{}, doc: "the JS command to run when the modal is closed"
attr :form, :any,
default: nil,
doc:
"If a form object is passed the modal will render form buttons and process submit/change events"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :noclose, :boolean, default: false, doc: "whether the modal can be closed"
attr :submit, :string, default: "submit", doc: "the phx-submit event for the form"
attr :change, :string, default: "validate", doc: "the phx-change event for the form"
attr :submit_text, :string, default: "Submit", doc: "the text for the submit button"
attr :target, :any, default: nil, doc: "the target for the form events"
attr :thin, :boolean, default: false, doc: "whether the modal is wide"
attr :wide, :boolean, default: false, doc: "whether the modal is wide"
attr :error, :string, default: nil, doc: "an error message to display"
slot :inner_block, required: true, doc: "the slot for the modal content"
slot :description, doc: "the slot for the modal description/subheading"
slot :actions, doc: "the slot for modal actions, such as buttons"
def modal(assigns) do
~H"""
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class={[
"relative z-50",
"hidden"
]}
>
<div
id={"#{@id}-bg"}
class="bg-black/10 backdrop-blur fixed inset-0 transition-opacity"
aria-hidden="true"
/>
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-descibedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center text-zinc-800">
<div class={[
"w-full p-4 sm:p-6 lg:py-8",
@wide && "max-w-4xl",
@thin && "max-w-sm",
!@wide && !@thin && "max-w-lg"
]}>
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={!@noclose && JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
phx-click-away={!@noclose && JS.exec("data-cancel", to: "##{@id}")}
class={[
"ring-zinc-700/10 relative rounded bg-zinc-50 shadow-lg ring-1 transition overflow-hidden"
]}
>
<div :if={@title} class="font-bold py-2 px-4 border-b bg-zinc-100">
{@title}
<p :if={assigns[:description]} class="text-sm mt-1 font-medium">
{render_slot(@description)}
</p>
</div>
<div :if={@error} class="text-center bg-red-200 text-red-600 font-bold p-2">
{@error}
</div>
<div :if={assigns[:form] == nil}>
<div id={"#{@id}-content"} class="p-8">
{render_slot(@inner_block)}
</div>
<div
:if={SigmaKit.Util.present?(assigns[:actions])}
class="mt-4 py-2 px-4 flex justify-end gap-2 bg-zinc-100 border-t"
>
{render_slot(@actions)}
</div>
</div>
<div :if={assigns[:form] != nil}>
<.form
:let={f}
id={"#{@id}-form"}
as={@as}
for={@form}
phx-submit={@submit}
phx-change={@change}
phx-target={@target}
>
<div>
<div id={"#{@id}-content"} class="py-4 px-8 space-y-6">
{render_slot(@inner_block, f)}
</div>
</div>
<div class="py-4 px-8 flex justify-end gap-4 bg-zinc-100 shadow-inset shadow-black mt-12 border-t">
<div :if={SigmaKit.Util.present?(assigns[:actions])} class="flex-grow">
{render_slot(@actions)}
</div>
<.button type="button" phx-click={JS.exec("data-cancel", to: "##{@id}")}>
Cancel
</.button>
<.button primary>
{@submit_text}
</.button>
</div>
</.form>
</div>
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
time: 300,
transition:
{"transition-all transform ease-out duration-300", "opacity-0 scale-75",
"opacity-100 scale-100"}
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
end
end