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
attr :show, :boolean, default: false
attr :title, :string, default: nil
attr :on_cancel, JS, default: %JS{}
attr :form, :any, default: nil
attr :as, :any, default: nil
attr :noclose, :boolean, default: false
attr :submit, :string, default: "submit"
attr :change, :string, default: "validate"
attr :submit_text, :string, default: "Submit"
attr :target, :any, default: nil
attr :wide, :boolean, default: true
attr :error, :string, default: nil
slot :inner_block, required: true
slot :description
slot :actions
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",
!@show && "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-white">
<div class={["w-full max-w-lg p-4 sm:p-6 lg:py-8", @wide && "max-w-3xl"]}>
<.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 hidden={!@show} rounded-2xl bg-black shadow-lg ring-1 transition overflow-hidden"
>
<div :if={@title} class="font-bold py-2 px-4">
{@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-800 text-red-100 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={assigns[:actions]} class="mt-4 py-2 px-4 flex justify-end gap-2">
{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-gray-800 shadow-inset shadow-black mt-12">
<div :if={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", "opacity-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