Corex.Dialog (Corex v0.1.0-beta.5)

View Source

Phoenix implementation of Zag.js Dialog.

Examples

Basic Usage

With top-level slots:

<.dialog id="my-dialog" class="dialog">
  <:trigger>Open Dialog</:trigger>
  <:title>Dialog Title</:title>
  <:description>
    This is a dialog description that explains what the dialog is about.
  </:description>
  <:content>
    <p>Dialog content goes here. You can add any content you want inside the dialog.</p>
  </:content>
  <:close_trigger>
    <.heroicon name="hero-x-mark" class="icon" />
  </:close_trigger>
</.dialog>

With title, description, and close trigger inside content (use the same id as the dialog):

<.dialog id="my-dialog" class="dialog">
  <:trigger>Open Dialog</:trigger>
  <:content>
    <.dialog_title id="my-dialog">Dialog Title</.dialog_title>
    <.dialog_description id="my-dialog">
      This is a dialog description that explains what the dialog is about.
    </.dialog_description>
    <p>Dialog content goes here. You can add any content you want inside the dialog.</p>
    <.dialog_close_trigger id="my-dialog">
      <.heroicon name="hero-x-mark" class="icon" />
    </.dialog_close_trigger>
  </:content>
</.dialog>

Controlled Mode

<.dialog
  id="my-dialog"
  controlled
  open={@dialog_open}
  on_open_change="dialog_changed">
  <:trigger>Open Dialog</:trigger>
  <:content>
    <:title>Dialog Title</:title>
    <:description>Dialog description goes here.</:description>
    <p>Dialog content</p>
    <:close_trigger>Close</:close_trigger>
  </:content>
</.dialog>
def handle_event("dialog_changed", %{"open" => open}, socket) do
  {:noreply, assign(socket, :dialog_open, open)}
end

Animation

Set animation on dialog (instant, js, or custom).

  • instant - Zag toggles the native hidden attribute, no animation.

  • js - Web Animations API drives opacity/scale via animation_options (Corex.Animation.Scale).

  • custom - the hook never re-applies hidden; the consumer drives the animation by listening to the CustomEvent whose type is on_open_change_client. The detail shape is:

    // event.detail (DialogOpenChangedDetail)
    { id, open, previousOpen }

    Closed visibility is provided by CSS baselines on [data-state="closed"] (see e2e/assets/corex/components/dialog.css), so the consumer only needs to drive the transition itself. Use the Scale helpers exported from corex (mirroring how Accordion / Tree view use the Height helpers).

import { animate } from "motion"
import {
  findDialogBackdrop,
  findDialogContent,
  animateScaleOpen,
  animateScaleClose,
} from "corex"

const reducedMotion = () =>
  window.matchMedia("(prefers-reduced-motion: reduce)").matches

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],
        scaleStart: 0.7,
        scaleEnd: 1,
      })
      if (!reducedMotion())
        animate(
          content,
          { y: [60, 0], filter: ["blur(12px)", "blur(0px)"] },
          { 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",
        scaleStart: 0.8,
        scaleEnd: 1,
      })
      if (!reducedMotion())
        animate(
          content,
          { y: [0, 40], filter: ["blur(0px)", "blur(12px)"] },
          { duration: 0.35, easing: "ease-in" },
        )
    }
  }
})

API Control

In order to use the API, you must use an id on the component

Client-side

<button phx-click={Corex.Dialog.set_open("my-dialog", true)}>
  Open Dialog
</button>

Server-side

def handle_event("open_dialog", _, socket) do
  {:noreply, Corex.Dialog.set_open(socket, "my-dialog", true)}
end

Styling

Use data attributes to target elements:

[data-scope="dialog"][data-part="root"] {}
[data-scope="dialog"][data-part="trigger"] {}
[data-scope="dialog"][data-part="backdrop"] {}
[data-scope="dialog"][data-part="positioner"] {}
[data-scope="dialog"][data-part="content"] {}
[data-scope="dialog"][data-part="title"] {}
[data-scope="dialog"][data-part="description"] {}
[data-scope="dialog"][data-part="close-trigger"] {}

If you wish to use the default Corex styling, you can use the class dialog on the component. This requires to install Mix.Tasks.Corex.Design first and import the component css file.

@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/dialog.css";

You can then use modifiers

<.dialog class="dialog dialog--accent dialog--lg">

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.

API

Sets the dialog open state from client-side. Returns a Phoenix.LiveView.JS command.

Sets the dialog open state from server-side. Pushes a LiveView event.

Components

dialog(assigns)

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 to false.
  • controlled (:boolean) - Whether the dialog is controlled. Only in LiveView, the on_open_change event is required. Defaults to false.
  • modal (:boolean) - Whether the dialog is modal. Defaults to false.
  • close_on_interact_outside (:boolean) - Whether to close the dialog when clicking outside. Defaults to true.
  • close_on_escape (:boolean) - Whether to close the dialog when pressing Escape. Defaults to true.
  • prevent_scroll (:boolean) - Whether to prevent body scroll when dialog is open. Defaults to false.
  • restore_focus (:boolean) - Whether to restore focus when dialog closes. Defaults to true.
  • dir (:string) - The direction of the dialog. When nil, derived from document (html lang + config :rtl_locales). Defaults to nil. Must be one of nil, "ltr", or "rtl".
  • on_open_change (:string) - Server event name when the open state changes. Payload: %{id, open, previousOpen} (TS: DialogOpenChangedDetail). Defaults to nil.
  • on_open_change_client (:string) - DOM event name dispatched when the open state changes. event.detail matches DialogOpenChangedDetail. Required for animation="custom". Defaults to nil.
  • animation (:string) - Open and close: native hidden (instant), Web Animations via Corex.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 when animation is js only. Custom transitions ignore this assign. See Corex.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 to nil.
  • aria_label (:string) - Accessible name when no visible dialog title is rendered; defaults to a translated Dialog label. Defaults to nil.
  • Global attributes are accepted.

Slots

  • trigger (required) - Accepts attributes:
    • class (:string)
    • aria_label (:string)
  • content (required) - Accepts attributes:
    • class (:string)
  • title - Accepts attributes:
    • class (:string)
  • description - Accepts attributes:
    • class (:string)
  • close_trigger - Accepts attributes:
    • class (:string)

dialog_close_trigger(assigns)

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 to nil.Must be one of nil, "ltr", or "rtl".
  • aria_label (:string) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

dialog_description(assigns)

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 to nil.Must be one of nil, "ltr", or "rtl".
  • Global attributes are accepted.

Slots

  • inner_block (required)

dialog_title(assigns)

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 to nil.Must be one of nil, "ltr", or "rtl".
  • Global attributes are accepted.

Slots

  • inner_block (required)

API

set_open(dialog_id, open)

Sets the dialog open state from client-side. Returns a Phoenix.LiveView.JS command.

Examples

<button phx-click={Corex.Dialog.set_open("my-dialog", true)}>
  Open Dialog
</button>

set_open(socket, dialog_id, open)

Sets the dialog open state from server-side. Pushes a LiveView event.

Examples

def handle_event("open_dialog", _params, socket) do
  socket = Corex.Dialog.set_open(socket, "my-dialog", true)
  {:noreply, socket}
end