Corex. Dialog
(Corex v0.1.0)
View Source
Phoenix implementation of Zag.js Dialog.
Anatomy
Minimal
<.dialog class="dialog">
<:trigger>Open</:trigger>
<:content>
<p>Minimal content.</p>
</:content>
<:close_trigger>
<.heroicon name="hero-x-mark" class="icon" />
</:close_trigger>
</.dialog>Title and description
<.dialog class="dialog">
<:trigger>Open Dialog</:trigger>
<:title>Dialog Title</:title>
<:description>
Short description of what this dialog is for.
</:description>
<:content>
<p>Body content.</p>
</:content>
<:close_trigger>
<.heroicon name="hero-x-mark" class="icon" />
</:close_trigger>
</.dialog>Actions in content
<.dialog id="dialog-anatomy-actions" class="dialog">
<:trigger>Open Dialog</:trigger>
<:title>Confirm</:title>
<:description>Choose an action to continue.</:description>
<:content>
<p>Are you sure you want to continue?</p>
<div class="flex flex-wrap justify-end gap-2 mt-4">
<.action phx-click={Corex.Dialog.set_open("dialog-anatomy-actions", false)} class="button button--sm button--ghost">
Cancel
</.action>
<.action phx-click={Corex.Dialog.set_open("dialog-anatomy-actions", false)} class="button button--sm">
Continue
</.action>
</div>
</:content>
<:close_trigger>
<.heroicon name="hero-x-mark" class="icon" />
</:close_trigger>
</.dialog>API
Requires a stable id on <.dialog>.
| Function | Action | Returns |
|---|---|---|
set_open/2 | Set open state (client) | %Phoenix.LiveView.JS{} |
set_open/3 | Set open state (server) | socket |
set_open
<.action phx-click={Corex.Dialog.set_open("dialog-api", true)} class="button button--sm">
Open Dialog
</.action>
<.dialog id="dialog-api" class="dialog">
<:trigger>Open Dialog</:trigger>
<:title>Dialog Title</:title>
<:description>Dialog description.</:description>
<:content>
<p>Dialog content</p>
<.action phx-click={Corex.Dialog.set_open("dialog-api", false)} class="button button--sm">
Close
</.action>
</:content>
<:close_trigger>
<.heroicon name="hero-x-mark" class="icon" />
</:close_trigger>
</.dialog>def handle_event("open_dialog", _, socket) do
{:noreply, Corex.Dialog.set_open(socket, "dialog-api", true)}
endEvents
Server events
| Event | When | Payload |
|---|---|---|
on_open_change="dialog_open_changed" | Open state changes | %{"id" => id, "open" => open, "previousOpen" => previous} |
on_open_change
<.dialog class="dialog" on_open_change="dialog_open_changed">
<:trigger>Open Dialog</:trigger>
<:title>Dialog Title</:title>
<:content>
<p>Dialog content</p>
</:content>
<:close_trigger>
<.heroicon name="hero-x-mark" class="icon" />
</:close_trigger>
</.dialog>def handle_event("dialog_open_changed", %{"id" => id, "open" => open}, socket) do
{:noreply, assign(socket, :dialog_open, open)}
endClient events
| Event | When | event.detail |
|---|---|---|
on_open_change_client="dialog-open-changed" | Open state changes | id, open, previousOpen |
on_open_change_client
<.dialog id="dialog-events-client" class="dialog" on_open_change_client="dialog-open-changed">
<:trigger>Open</:trigger>
<:content><p>Content</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>document.getElementById("dialog-events-client")?.addEventListener("dialog-open-changed", (e) => {
console.log(e.detail);
});Animation
Set animation on <.dialog> (instant, js, or custom).
Instant
<.dialog class="dialog" modal animation="instant">
<:trigger>Open</:trigger>
<:title>Instant</:title>
<:content>
<p>Native show and hide without JS transitions.</p>
</:content>
<:close_trigger>
<.heroicon name="hero-x-mark" class="icon" />
</:close_trigger>
</.dialog>JS
Web Animations API via animation_options (Corex.Animation.Scale).
<.dialog
class="dialog"
modal
animation="js"
animation_options={%Corex.Animation.Scale{duration: 0.3, easing: "ease-out"}}
>
<:trigger>Open</:trigger>
<:title>JS</:title>
<:content><p>Scaled open and close.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>Custom
Set animation="custom" and on_open_change_client. The hook does not toggle hidden; listen for:
// event.detail — DialogOpenChangedDetail
{ id, open, previousOpen }<.dialog
class="dialog"
animation="custom"
on_open_change_client="my-dialog-open-changed"
>
<:trigger>Open</:trigger>
<:title>Custom</:title>
<:content>
<p>Motion animates open and close.</p>
</:content>
<:close_trigger>
<.heroicon name="hero-x-mark" class="icon" />
</:close_trigger>
</.dialog>import { animate } from "motion"
import {
findDialogBackdrop,
findDialogContent,
animateScaleOpen,
animateScaleClose,
} from "corex"
document.addEventListener("my-dialog-open-changed", (e) => {
const { id, open } = e.detail
const root = document.getElementById(id)
if (!root) return
const backdrop = findDialogBackdrop(root)
const content = findDialogContent(root)
if (open) {
if (backdrop) animateScaleOpen(backdrop, { animator: animate, duration: 0.5, easing: "ease-out" })
if (content) animateScaleOpen(content, { animator: animate, duration: 0.7, easing: [0.16, 1, 0.3, 1] })
} else {
if (backdrop) animateScaleClose(backdrop, { animator: animate, duration: 0.4, easing: "ease-in" })
if (content) animateScaleClose(content, { animator: animate, duration: 0.35, easing: "ease-in" })
}
})Alert dialog
Use role="alertdialog" with explicit modal and dismiss behavior. Set initial_focus to the id of the least destructive action.
<.dialog
id="delete-item-alert"
class="dialog"
role="alertdialog"
modal
close_on_interact_outside={false}
initial_focus="delete-item-alert-cancel"
final_focus="dialog:delete-item-alert:trigger"
>
<:trigger>Delete item</:trigger>
<:title>Delete this item?</:title>
<:description>This action cannot be undone.</:description>
<:content>
<div class="flex flex-wrap justify-end gap-2 mt-4">
<.action id="delete-item-alert-cancel" phx-click={Corex.Dialog.set_open("delete-item-alert", false)} class="button button--sm button--ghost">
Cancel
</.action>
<.action phx-click={Corex.Dialog.set_open("delete-item-alert", false)} class="button button--sm button--alert">
Delete
</.action>
</div>
</:content>
</.dialog>Focus
initial_focus is the id of a focusable element inside the dialog (for example a cancel .action). When omitted, Zag focuses the first focusable element in the content.
final_focus is the id of the element to receive focus when the dialog closes (for example the trigger: dialog:my-dialog:trigger). When omitted, Zag restores focus to the element that had focus before the dialog opened.
Style
Stack modifiers on the host (class on <.dialog>).
When prevent_scroll is enabled, Zag sets --scrollbar-width on the document root. Fixed or sticky app chrome can compensate with calc(... + var(--scrollbar-width, 0px)) at the app level; Corex does not apply this globally.
Default
<.dialog class="dialog">
<:trigger>Open</:trigger>
<:title>Default</:title>
<:content><p>Default size.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>Small
<.dialog class="dialog dialog--sm">
<:trigger>Open</:trigger>
<:title>Small</:title>
<:content><p>Compact dialog.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>Large
<.dialog class="dialog dialog--lg">
<:trigger>Open</:trigger>
<:title>Large</:title>
<:content><p>Spacious dialog.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>Text
<.dialog class="dialog dialog--text-xl">
<:trigger>Open</:trigger>
<:title>Larger type</:title>
<:content><p>Title, description, and body scale with the modifier.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>Radius
<.dialog class="dialog dialog--rounded-xl">
<:trigger>Open</:trigger>
<:title>Rounded panel</:title>
<:content><p>Corner radius on the content panel and close trigger.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>Side
<.dialog class="dialog dialog--side" modal>
<:trigger>Open</:trigger>
<:title>Side panel</:title>
<:content><p>Slides in from the edge.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>
Summary
Components
Renders a dialog component.
Renders the dialog close button. Use inside <:content> when not using the top-level <:close_trigger> slot. Pass the same id as the parent dialog.
Renders the dialog description. Use inside <:content> when not using the top-level <:description> slot. Pass the same id as the parent dialog.
Renders the dialog title. Use inside <:content> when not using the top-level <:title> slot. Pass the same id as the parent dialog.
Components
Renders a dialog component.
Attributes
id(:string) - The id of the dialog, useful for API to identify the dialog.open(:boolean) - The initial open state or the controlled open state. Defaults tofalse.controlled(:boolean) - Whether the dialog is controlled. In LiveView, pair with on_open_change when true. Defaults tofalse.modal(:boolean) - Whether the dialog is modal. Defaults tofalse.close_on_interact_outside(:boolean) - Whether to close the dialog when clicking outside. Defaults totrue.close_on_escape(:boolean) - Whether to close the dialog when pressing Escape. Defaults totrue.prevent_scroll(:boolean) - Whether to prevent body scroll when dialog is open. Defaults tofalse.restore_focus(:boolean) - Whether to restore focus when dialog closes. Defaults totrue.role(:string) - ARIA role for the dialog content (dialogoralertdialog). Defaults to"dialog". Must be one ofnil,"dialog", or"alertdialog".initial_focus(:string) - Id of a focusable element inside the dialog to receive focus when opened (for alerts, use the least destructive action). Defaults tonil.final_focus(:string) - Id of the element to receive focus when the dialog closes (for example the trigger iddialog:my-dialog:trigger). Defaults tonil.dir(:string) - The direction of the dialog. When nil, derived from document (html lang + config :rtl_locales). Defaults tonil. Must be one ofnil,"ltr", or"rtl".on_open_change(:string) - Server event name when the open state changes. Payload:%{id, open, previousOpen}(TS:DialogOpenChangedDetail). Defaults tonil.on_open_change_client(:string) - DOM event name dispatched when the open state changes.event.detailmatchesDialogOpenChangedDetail. Required foranimation="custom". Defaults tonil.animation(:string) - Open and close: native hidden (instant), Web Animations viaCorex.Animation.Scale(js), or events only (custom). Defaults to"js". Must be one of"instant","js", or"custom".animation_options(Corex.Animation.Scale) - Wired to the host whenanimationisjsonly. Custom transitions ignore this assign. SeeCorex.Animation.Scale(opacity, scale, timing,block_interaction). Defaults to%Corex.Animation.Scale{duration: 0.3, easing: "ease", opacity_start: 0.0, opacity_end: 1.0, scale_start: 0.96, scale_end: 1.0, block_interaction: false}.translation(Corex.Dialog.Translation) - Override translatable strings. Defaults tonil.- Global attributes are accepted.
Slots
trigger(required) - Accepts attributes:class(:string)aria_label(:string)title(:string)
content(required) - Accepts attributes:class(:string)
title- Accepts attributes:class(:string)
description- Accepts attributes:class(:string)
close_trigger- Accepts attributes:class(:string)
Renders the dialog close button. Use inside <:content> when not using the top-level <:close_trigger> slot. Pass the same id as the parent dialog.
Attributes
id(:string) (required)dir(:string) - Defaults tonil.Must be one ofnil,"ltr", or"rtl".translation(Corex.Dialog.Translation) - Override translatable strings. Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Renders the dialog description. Use inside <:content> when not using the top-level <:description> slot. Pass the same id as the parent dialog.
Attributes
id(:string) (required)dir(:string) - Defaults tonil.Must be one ofnil,"ltr", or"rtl".- Global attributes are accepted.
Slots
inner_block(required)
Renders the dialog title. Use inside <:content> when not using the top-level <:title> slot. Pass the same id as the parent dialog.
Attributes
id(:string) (required)dir(:string) - Defaults tonil.Must be one ofnil,"ltr", or"rtl".- Global attributes are accepted.
Slots
inner_block(required)
API
Set open state from a control (phx-click).
<.action phx-click={Corex.Dialog.set_open("my-dialog", true)}>Open</.action>
<.dialog id="my-dialog" class="dialog">
<:trigger>Open</:trigger>
<:content><p>Content.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>document.getElementById("my-dialog")?.dispatchEvent(
new CustomEvent("corex:dialog:set-open", {
bubbles: false,
detail: { open: true },
})
);
Set open state from handle_event.
<.action phx-click="open_dialog">Open</.action>
<.dialog id="my-dialog" class="dialog">
<:trigger>Open</:trigger>
<:content><p>Content.</p></:content>
<:close_trigger><.heroicon name="hero-x-mark" class="icon" /></:close_trigger>
</.dialog>def handle_event("open_dialog", _, socket) do
{:noreply, Corex.Dialog.set_open(socket, "my-dialog", true)}
end