Current section
Files
Jump to
Current section
Files
live_interaction_contracts
CONTRACT_DIALOG.md
CONTRACT_DIALOG.md
# CONTRACT_DIALOG — the dialog adapter (amber reference)
Native `<dialog>` is a **red** machine: LiveView reconciliation strips its `open`
attribute on every patch and closes it (see `RED_FIXTURES.md`). This contract defines
a **contract-aware adapter** that makes it usable — and classifies it **amber, never
green**.
## Why amber, not green
Popover is **green**: it is patch-safe *by nature* — its open state lives in the top
layer, not in a server-reconciled attribute, so nothing has to be done. The dialog
adapter is **amber**: it is patch-safe *by an explicit patch rule* —
`ignore open attribute during patch` — plus hook timing and lossy observation. It is a
contract-aware adapter, not pure browser delegation. The distinction is the point: it
makes the library's three capabilities legible —
- **green machine** — native delegation works directly (popover, scroll, input focus);
- **red machine** — raw use breaks (raw `<dialog>`, `<details>`);
- **amber adapter** — repaired to usable with a contract + conformance (this).
## The adapter
```elixir
alias Phoenix.LiveView.JS
```
```heex
<dialog id="dlg"
phx-hook="CloseRelay"
phx-mounted={JS.ignore_attributes(["open"])}>
...server-rendered content...
<button onclick="this.closest('dialog').close()">Cancel</button>
</dialog>
```
Responsibilities (none optional):
1. **Server controls existence.** The dialog is rendered only when authorized;
existence-in-DOM is the authorization boundary (server-owned).
2. **Browser controls the modal machine.** Open via `showModal()` (top layer,
backdrop, background inert), close via `close()` / `requestClose()`. Never
`<dialog open>` — that is a *non-modal* dialog and skips modal semantics.
3. **LiveView must not patch `open`.** `phx-mounted={JS.ignore_attributes(["open"])}`
makes reconciliation skip the `open` attribute so a patch cannot close the dialog —
while server-rendered content inside it still patches normally.
4. **JS owns showModal/close and the close relay.**
5. **Close is lossy observation only.** `close`/`cancel` may be relayed to the server
for cleanup/logging, but the server must never derive canonical state or
authorization from it (see `../archive/observation-spike/OBSERVATION_SPIKE_REPORT.md`).
## Conformance — 7/7 required to hold amber (fuse)
Verified across Chromium/Firefox/WebKit, two runs, deterministic
(`priv/dialog-adapter-spike/`, driver self-asserts; raw `<dialog>` control must still
close):
1. **content patch** — adapter stays open **and content updates** (the property
`phx-update="ignore"` cannot give — it freezes content).
2. **attribute patch** — stays open.
3. **sibling patch** — stays open.
4. **close relay** — Escape → `["cancel","close"]`, button → `["close"]`, observed.
5. **disconnect/reconnect** — stays open (see caveat).
6. **focus / inert** — `:modal` true; focus contained; a background element **cannot**
take focus; a background hit-test does **not** reach the background button; focus
**restores to the trigger** on close.
7. **remove/reinsert reset** — the reinserted dialog is a **new node**, **initially
closed**; reopening works and `ignore` re-registers (content patches keep it open),
the new hook relays, no stale state.
Raw `<dialog>` (control) must fail #1–#3 (stay red). The suite trips the fuse if the
adapter drops below 7/7 or the raw control stops failing.
## Caveats (why it stays amber even at 7/7)
- **`phx-mounted` timing.** `ignore_attributes` registers *after* mount, so it cannot
protect an attribute set during a disconnected render. It survives an ordinary
socket reconnect (which preserves nodes/hooks); a full page reload remounts, but by
then the modal is already gone with the page — so the window does not bite an
already-open modal, but it must be documented, not hidden.
- **Dependence on a patch rule.** Correctness rests on `ignore_attributes(["open"])`
behaving as documented; the conformance suite is what keeps that honest per version.
- **Lossy observation.** The close relay is best-effort (three known loss modes,
`../archive/observation-spike/OBSERVATION_SPIKE_REPORT.md`); never authoritative.
## Status & non-goals
- **Amber reference adapter.** A documented contract with a passing conformance suite —
the analogue of the green *popover* reference, one tier down.
- **No polished public component API is frozen** (same posture as popover). A
`<.dialog>` HEEx component would be a thin derivation of this contract.
- **`<details>` is out of scope here.** It has no `showModal`/`close`/modal semantics;
its adapter shape differs and needs its own spike. Do not extrapolate from dialog.
*Evidence: `priv/dialog-adapter-spike/dialog-adapter-results.json`; mechanism cross-ref
`reports/CONFORMANCE_REPORT.md` (raw dialog red + ignore probe), `../archive/observation-spike/OBSERVATION_SPIKE_REPORT.md`.*