From c71c7d6ed6d28517b90eeb9ef790c1a42ad050db Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 26 Feb 2026 15:24:29 +0100 Subject: [PATCH] fix: color contrast dark mode and keyboard moadals --- CODE_GUIDELINES.md | 5 +- assets/css/app.css | 7 +- assets/js/app.js | 26 + lib/mv_web/components/core_components.ex | 2 +- .../live/custom_field_live/form_component.ex | 1 + .../live/custom_field_live/index_component.ex | 30 +- lib/mv_web/live/datafields_live.ex | 48 +- lib/mv_web/live/group_live/show.ex | 37 +- lib/mv_web/live/member_live/form.ex | 559 +++++++++-------- lib/mv_web/live/member_live/show.ex | 63 +- .../show/membership_fees_component.ex | 39 +- lib/mv_web/live/role_live/show.ex | 196 +++--- lib/mv_web/live/user_live/form.ex | 590 +++++++++--------- lib/mv_web/live/user_live/show.ex | 204 +++--- 14 files changed, 1067 insertions(+), 740 deletions(-) diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index 48e2e8e..b3f1c3f 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -3016,6 +3016,8 @@ end - [ ] Tables have proper structure (th, scope, caption) - [ ] ARIA labels used for icon-only buttons - [ ] Modals/dialogs: focus moves into modal, aria-labelledby, keyboard dismiss (Escape) +- [ ] ARIA state attributes use string values `"true"` / `"false"` (not boolean), e.g. `aria-selected`, `aria-pressed`, `aria-expanded`. +- [ ] Tabs: when using `role="tablist"` / `role="tab"`, use roving tabindex (only active tab `tabindex="0"`) and ArrowLeft/ArrowRight to switch tabs. ### 8.11 Modals and Dialogs @@ -3043,7 +3045,8 @@ Use a consistent, keyboard-accessible pattern for all confirmation and form moda **Closing:** - Cancel button closes the modal (e.g. `phx-click="cancel_delete_modal"`). -- Optionally support Escape to close via `phx-window-keydown` on the LiveView/LiveComponent. +- **MUST** support Escape to close (WCAG / WAI-ARIA dialog pattern): add `phx-keydown="dialog_keydown"` on the `` and handle `dialog_keydown` with `key: "Escape"` to close (same effect as Cancel). +- **MUST** return focus to the trigger element when the modal closes (WCAG 2.4.3): give the trigger button a stable `id`, use the `FocusRestore` hook on a parent element, and on close (Cancel or Escape) call `push_event(socket, "focus_restore", %{id: "trigger-id"})` so keyboard users land where they started (e.g. "Delete member" button). **Reference implementation:** Delete data field modal in `CustomFieldLive.IndexComponent` (input + `phx-mounted={JS.focus()}` on input; `aria-labelledby` on dialog). Delete role modal in `RoleLive.Show` (no input; `phx-mounted={JS.focus()}` on Cancel button). diff --git a/assets/css/app.css b/assets/css/app.css index 094c030..6f00298 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -538,9 +538,14 @@ /* Dark theme: primary is purple; ensure content is light and meets 4.5:1. */ [data-theme="dark"] { - --color-primary-content: oklch(0.97 0.02 277); --color-error: oklch(55% 0.253 17.585); --color-error-content: oklch(98% 0 0); + + --color-primary: oklch(72% 0.17 45); + --color-primary-content: oklch(0.18 0.02 47); + + --color-secondary: oklch(48% 0.233 277.117); + --color-secondary-content: oklch(98% 0 0); } /* This file is for your main application CSS */ diff --git a/assets/js/app.js b/assets/js/app.js index c17e7b5..b7d1a45 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -94,6 +94,32 @@ Hooks.TableRowKeydown = { } } +// FocusRestore hook: WCAG 2.4.3 — when a modal closes, focus returns to the trigger element (e.g. "Delete member" button) +Hooks.FocusRestore = { + mounted() { + this.handleEvent("focus_restore", ({id}) => { + const el = document.getElementById(id) + if (el) el.focus() + }) + } +} + +// TabListKeydown hook: WCAG tab pattern — prevent default for ArrowLeft/ArrowRight so the server can handle tab switch (roving tabindex) +Hooks.TabListKeydown = { + mounted() { + this.handleKeydown = (e) => { + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { + e.preventDefault() + } + } + this.el.addEventListener('keydown', this.handleKeydown) + }, + + destroyed() { + this.el.removeEventListener('keydown', this.handleKeydown) + } +} + // SidebarState hook: Manages sidebar expanded/collapsed state Hooks.SidebarState = { mounted() { diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 5f12f0a..78b8bfb 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -477,7 +477,7 @@ defmodule MvWeb.CoreComponents do tabindex="0" role="button" aria-haspopup="menu" - aria-expanded={@open} + aria-expanded={if @open, do: "true", else: "false"} aria-controls={@id} aria-label={@button_label} class={["btn"] ++ @button_focus_classes ++ [@button_class]} diff --git a/lib/mv_web/live/custom_field_live/form_component.ex b/lib/mv_web/live/custom_field_live/form_component.ex index aac67dc..3872e56 100644 --- a/lib/mv_web/live/custom_field_live/form_component.ex +++ b/lib/mv_web/live/custom_field_live/form_component.ex @@ -111,6 +111,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do )}

<.button + id="delete-custom-field-trigger" type="button" variant="danger" phx-click="request_delete" diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index abb19df..f9dca11 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -106,6 +106,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do class="modal modal-open" role="dialog" aria-labelledby="delete-custom-field-modal-title" + phx-keydown="dialog_keydown" >