Dialog and confirmation components: one dialog pattern, one confirmation rule #615

Closed
opened 2026-10-20 14:47:16 +02:00 by simon · 0 comments
Owner

Motivation

There is no dialog component: 13 dialogs are hand-copied <dialog class="modal modal-open"> blocks with Escape wired in three different ways and focus return missing in five. Missing phx-targets caused a crash and Escape closing the wrong dialog (fixed minimally in U1). Four actions still use the browser's data-confirm. Five confirmation strengths exist without a rule, and there are three different ways to delete from a form. DESIGN_GUIDELINES §10.2–§10.4 define one dialog pattern and one confirmation rule.

Visible improvement

Every dialog behaves the same: it opens above everything, Escape and the backdrop close it, focus goes in and comes back; confirmations ask exactly as much as the action warrants.

Scope (ordered steps, just ci-dev green after each)

  1. Build <.modal>: server-rendered <dialog>, opened with showModal() via a colocated hook, JS.ignore_attributes("open"), backdrop <form method="dialog" class="modal-backdrop">, Escape/backdrop/Cancel → on_cancel, focus in on open, JS.push_focus/pop_focus for focus return, aria-labelledby/aria-describedby, one title level and style. Component tests for open, Escape, focus return.
  2. Build <.confirm_dialog> on top: simple and typed confirmation (type_to_confirm, affected count shown as a warning callout, confirm disabled until the string matches), danger confirm for destructive actions.
  3. Migrate the simple confirmations (member, user, role, cycle delete; deactivate with danger).
  4. Migrate the typed confirmations (data field: slug; group: name) and decide the confirmation string for “delete all cycles” (today “Yes”; DESIGN_GUIDELINES §10.2 forbids generic words).
  5. Migrate the remaining dialogs (four fee dialogs, fee type “Change amount?” which today lacks role, label and focus), including any legacy-pattern dialog added to the guard's exception list since U2 (DESIGN_GUIDELINES §10.3, interim rule).
  6. Replace every data-confirm: fee type delete (moves to the Danger zone of the fee type edit page, per §14) and join request approve/reject (decide whether they need a confirmation; if yes, the simple dialog).
  7. One way to delete from a form (§10.4): Danger zone → dialog in the same LiveView; remove the ?confirm_delete=1 navigation (group form) and the parent-dialog message (data fields). Every delete handler re-checks :destroy on the server with the actor and deletes the record held in assigns, not one named by an id from the event (DESIGN_GUIDELINES §14, Authorization visibility).
  8. Shrink the CI guard's exception list for data-confirm and raw dialog markup to zero.
  9. Showcase section: modal, simple and typed confirmation, both themes.

Out of scope

  • Small-entity edit routes (U9); relation removal with Undo (U9).
  • Redesign of the fees tab beyond its dialogs.

Acceptance criteria

  • DESIGN_GUIDELINES §10.2, §10.3, §10.4 and §14 (confirmation) are met; no data-confirm and no raw modal markup remain in views.
  • Tests: for each migrated dialog, open via its server event, confirm path and cancel path (Phoenix.LiveViewTest); for each delete flow, a forged delete event from an actor without destroy permission deletes nothing; component tests cover Escape and focus return.
  • Before/after screenshots of a simple and a typed confirmation in light and dark theme.
  • Keyboard/screen reader check: focus moves in (Cancel or the input), Tab stays inside, Escape closes, focus returns to the trigger; the title is announced.
  • Showcase section added.
  • just ci-dev green after every step.

Dependencies

  • U3 (button API, tokens); #539 (component modules).
  • U1's minimal dialog fixes are superseded here.
  • Should land before the fee-management expansion (payment status).

Anchors (verified against main @ d9093a1f, re-check before filing)

  • Fee dialogs: lib/mv_web/live/member_live/show/membership_fees_component.ex:306,353,395,458
  • Deactivate: lib/mv_web/live/member_live/show/deactivate_component.ex:77
  • Fee type “Change amount?”: lib/mv_web/live/membership_fee_type_live/form.ex:154
  • data-confirm: lib/mv_web/live/membership_fee_settings_live.ex:248 (and the disabled variant nearby), lib/mv_web/live/join_request_live/show.ex:203,211
  • Delete via navigation: lib/mv_web/live/group_live/form.ex:121
  • Data field delete dialog: lib/mv_web/live/custom_field_live/index_component.ex:112

Caution

  • Tests open dialogs via server events today (e.g. custom_field_live/deletion_test.exs, member_live/deactivate_test.exs, show_membership_fees_test.exs); keep dialog content server-rendered so they keep working.
  • JS.ignore_attributes("open") must be set, or a LiveView patch closes an open dialog.
## Motivation There is no dialog component: 13 dialogs are hand-copied `<dialog class="modal modal-open">` blocks with Escape wired in three different ways and focus return missing in five. Missing `phx-target`s caused a crash and Escape closing the wrong dialog (fixed minimally in U1). Four actions still use the browser's `data-confirm`. Five confirmation strengths exist without a rule, and there are three different ways to delete from a form. DESIGN_GUIDELINES §10.2–§10.4 define one dialog pattern and one confirmation rule. ## Visible improvement Every dialog behaves the same: it opens above everything, Escape and the backdrop close it, focus goes in and comes back; confirmations ask exactly as much as the action warrants. ## Scope (ordered steps, `just ci-dev` green after each) 1. Build `<.modal>`: server-rendered `<dialog>`, opened with `showModal()` via a colocated hook, `JS.ignore_attributes("open")`, backdrop `<form method="dialog" class="modal-backdrop">`, Escape/backdrop/Cancel → `on_cancel`, focus in on open, `JS.push_focus`/`pop_focus` for focus return, `aria-labelledby`/`aria-describedby`, one title level and style. Component tests for open, Escape, focus return. 2. Build `<.confirm_dialog>` on top: simple and typed confirmation (`type_to_confirm`, affected count shown as a warning callout, confirm disabled until the string matches), `danger` confirm for destructive actions. 3. Migrate the simple confirmations (member, user, role, cycle delete; deactivate with `danger`). 4. Migrate the typed confirmations (data field: slug; group: name) and decide the confirmation string for “delete all cycles” (today “Yes”; DESIGN_GUIDELINES §10.2 forbids generic words). 5. Migrate the remaining dialogs (four fee dialogs, fee type “Change amount?” which today lacks role, label and focus), including any legacy-pattern dialog added to the guard's exception list since U2 (DESIGN_GUIDELINES §10.3, interim rule). 6. Replace every `data-confirm`: fee type delete (moves to the Danger zone of the fee type edit page, per §14) and join request approve/reject (decide whether they need a confirmation; if yes, the simple dialog). 7. One way to delete from a form (§10.4): Danger zone → dialog in the same LiveView; remove the `?confirm_delete=1` navigation (group form) and the parent-dialog message (data fields). Every delete handler re-checks `:destroy` on the server with the actor and deletes the record held in assigns, not one named by an id from the event (DESIGN_GUIDELINES §14, Authorization visibility). 8. Shrink the CI guard's exception list for `data-confirm` and raw dialog markup to zero. 9. Showcase section: modal, simple and typed confirmation, both themes. ## Out of scope - Small-entity edit routes (U9); relation removal with Undo (U9). - Redesign of the fees tab beyond its dialogs. ## Acceptance criteria - DESIGN_GUIDELINES §10.2, §10.3, §10.4 and §14 (confirmation) are met; no `data-confirm` and no raw `modal` markup remain in views. - Tests: for each migrated dialog, open via its server event, confirm path and cancel path (`Phoenix.LiveViewTest`); for each delete flow, a forged delete event from an actor without destroy permission deletes nothing; component tests cover Escape and focus return. - Before/after screenshots of a simple and a typed confirmation in light and dark theme. - Keyboard/screen reader check: focus moves in (Cancel or the input), Tab stays inside, Escape closes, focus returns to the trigger; the title is announced. - Showcase section added. - `just ci-dev` green after every step. ## Dependencies - U3 (button API, tokens); #539 (component modules). - U1's minimal dialog fixes are superseded here. - Should land before the fee-management expansion (payment status). ## Anchors (verified against `main` @ `d9093a1f`, re-check before filing) - Fee dialogs: `lib/mv_web/live/member_live/show/membership_fees_component.ex:306,353,395,458` - Deactivate: `lib/mv_web/live/member_live/show/deactivate_component.ex:77` - Fee type “Change amount?”: `lib/mv_web/live/membership_fee_type_live/form.ex:154` - `data-confirm`: `lib/mv_web/live/membership_fee_settings_live.ex:248` (and the disabled variant nearby), `lib/mv_web/live/join_request_live/show.ex:203,211` - Delete via navigation: `lib/mv_web/live/group_live/form.ex:121` - Data field delete dialog: `lib/mv_web/live/custom_field_live/index_component.ex:112` ## Caution - Tests open dialogs via server events today (e.g. `custom_field_live/deletion_test.exs`, `member_live/deactivate_test.exs`, `show_membership_fees_test.exs`); keep dialog content server-rendered so they keep working. - `JS.ignore_attributes("open")` must be set, or a LiveView patch closes an open dialog.
simon closed this issue 2026-10-23 22:33:44 +02:00
Sign in to join this conversation.
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
local-it/mitgliederverwaltung#615
No description provided.