# Dialog interaction contract

`LiveViewContinuity.Dialog.dialog/1` is an unstyled native modal dialog. `id`, boolean `open`, `on_open`, `on_close`, one trigger, one visible title, one body, and one explicit Close slot are required. One simple description and a dialog-scoped `initial_focus` CSS selector are optional. Root, trigger, native dialog, title, description, and Close classes can be supplied through their corresponding attributes; global attributes apply only to the root so consumers cannot override browser-owned native state.

The server owns desired intent and content. The hook renders that intent as `data-lvc-desired-open`; it never server-renders `open`. The browser owns the effective native top-layer state, actual focus, trigger `aria-expanded`, root `data-lvc-open`, and `data-lvc-close-reason`. The native `open` attribute and those narrow reflections are protected from patches.

The trigger calls `showModal()` immediately and sends exactly one empty `on_open` event. The handler **must acknowledge by assigning `open: true`**. A server `false` to `true` change also calls `showModal()`, without sending `on_open`. Escape uses native cancel/close and sends exactly one `on_close` event with `%{"reason" => "escape"}`. The explicit Close button calls `dialog.close()` immediately and sends one event with reason `close`; the handler **must acknowledge by assigning `open: false`**. Pending user intent protects against stale patches until that acknowledgement arrives. Server-driven close sends no user event.

On open, a unique, rendered, programmatically focusable `initial_focus` match receives focus; this may be a static heading with `tabindex="-1"`. Invalid, missing, ambiguous, disabled, inert, hidden, or unfocusable selectors safely fall back to the explicit Close button. Acknowledged patches preserve the native dialog node and focused control identity. If that control disappears, focus falls back. Closing relies on native focus restoration first and only repairs focus to the current connected trigger when focus dropped to the body/dialog.

While any Dialog instance is modal-open, a Dialog-local reference count locks document scrolling and preserves/restores the prior `documentElement.style.overflow` and `scrollbarGutter` values. Native modal semantics provide top-layer rendering and background inertness. Because native dialogs can still move sequential focus to `body` at the tab-sequence boundaries, the Dialog hook only wraps Tab from the last sequentially tabbable control to the first and Shift+Tab from the first to the last. It follows non-negative `tabIndex` order, excluding disabled, inert, unrendered, and hidden candidates. Shadow DOM, radio-group collapsing, and user-agent internal controls remain outside this first slice. It adds no portal, general FocusScope, `aria-hidden`, or backdrop-coordinate behavior.

Deferred: backdrop/light dismiss, nested dialogs, rejected or confirmed close requests, animations, non-modal mode, `alertdialog`, detached/multiple triggers, portals, arbitrary initial/final-focus callbacks, forms using `method="dialog"`, and a generic overlay/focus manager.
