fix(a11y): WCAG 2 AA contrast, labels and dropdown
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Moritz 2026-03-04 16:19:28 +01:00
parent 8025858060
commit 70c3ca82ea
Signed by: moritz
GPG key ID: 1020A035E5DD0824
8 changed files with 134 additions and 37 deletions

View file

@ -548,4 +548,96 @@
--color-secondary-content: oklch(98% 0 0); --color-secondary-content: oklch(98% 0 0);
} }
/* ============================================
WCAG 2.2 AA: Tab list inactive tab text contrast (4.5:1)
============================================ */
#member-tablist .tab:not(.tab-active) {
color: oklch(0.35 0.02 285);
}
[data-theme="dark"] #member-tablist .tab:not(.tab-active) {
color: oklch(0.72 0.02 257);
}
/* ============================================
WCAG 2.2 AA: Link contrast - primary and accent
============================================ */
[data-theme="light"] .link.link-primary {
color: oklch(0.45 0.15 35);
}
[data-theme="light"] .link.link-primary:hover {
color: oklch(0.38 0.14 35);
}
[data-theme="dark"] .link.link-primary {
color: oklch(0.82 0.14 45);
}
[data-theme="dark"] .link.link-primary:hover {
color: oklch(0.88 0.12 45);
}
[data-theme="dark"] .link.link-accent {
color: oklch(0.82 0.18 292);
}
[data-theme="dark"] .link.link-accent:hover {
color: oklch(0.88 0.16 292);
}
/* ============================================
WCAG 2.2 AA: Danger zone heading contrast (dark theme)
============================================ */
[data-theme="dark"] #danger-zone-heading.text-error {
color: oklch(0.78 0.18 25);
}
/* ============================================
WCAG 2.2 AA: Blue link contrast in dark theme
============================================ */
[data-theme="dark"] a.text-blue-700,
[data-theme="dark"] a.text-blue-600,
[data-theme="dark"] a.hover\:text-blue-800 {
color: oklch(0.72 0.16 255);
}
[data-theme="dark"] a.text-blue-700:hover,
[data-theme="dark"] a.text-blue-600:hover {
color: oklch(0.82 0.14 255);
}
/* ============================================
WCAG 2.2 AA: Password / form label on light box in dark theme
============================================ */
[data-theme="dark"] .bg-gray-50 {
background-color: var(--color-base-200);
color: var(--color-base-content);
}
[data-theme="dark"] .bg-gray-50 .label,
[data-theme="dark"] .bg-gray-50 .mb-1.label,
[data-theme="dark"] .bg-gray-50 .text-gray-600,
[data-theme="dark"] .bg-gray-50 .text-gray-700,
[data-theme="dark"] .bg-gray-50 strong,
[data-theme="dark"] .bg-gray-50 p,
[data-theme="dark"] .bg-gray-50 li {
color: var(--color-base-content);
}
/* Dark mode: orange/red info boxes (admin note, OIDC warning) dark bg, light text */
[data-theme="dark"] .bg-orange-50 {
background-color: oklch(0.32 0.06 55);
border-color: oklch(0.42 0.08 55);
color: var(--color-base-content);
}
[data-theme="dark"] .bg-orange-50 .text-orange-800,
[data-theme="dark"] .bg-orange-50 p,
[data-theme="dark"] .bg-orange-50 strong {
color: var(--color-base-content);
}
[data-theme="dark"] .bg-red-50 {
background-color: oklch(0.32 0.08 25);
border-color: oklch(0.42 0.12 25);
color: var(--color-base-content);
}
[data-theme="dark"] .bg-red-50 .text-red-800,
[data-theme="dark"] .bg-red-50 .text-red-700,
[data-theme="dark"] .bg-red-50 p,
[data-theme="dark"] .bg-red-50 strong {
color: var(--color-base-content);
}
/* This file is for your main application CSS */ /* This file is for your main application CSS */

View file

@ -562,13 +562,17 @@ defmodule MvWeb.CoreComponents do
phx-target={@phx_target} phx-target={@phx_target}
> >
<%= if @checkboxes do %> <%= if @checkboxes do %>
<input <%!-- Visual-only indicator: do not nest an interactive control (checkbox) inside the button for screen reader and focus correctness (WCAG 2.1.2). --%>
type="checkbox" <span
checked={Map.get(@selected, item.value, true)} class={
class="checkbox checkbox-sm checkbox-primary pointer-events-none" if Map.get(@selected, item.value, true),
tabindex="-1" do: "text-primary",
else: "text-base-300"
}
aria-hidden="true" aria-hidden="true"
/> >
<.icon name="hero-check" class="size-4 shrink-0" />
</span>
<% end %> <% end %>
<span>{item.label}</span> <span>{item.label}</span>
</button> </button>

View file

@ -32,9 +32,9 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
<.icon name="hero-arrow-left" class="size-4" /> <.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")} {gettext("Back")}
</.button> </.button>
<h3 class="card-title"> <h2 class="card-title text-xl">
{if @custom_field, do: gettext("Edit Data Field"), else: gettext("New Data Field")} {if @custom_field, do: gettext("Edit Data Field"), else: gettext("New Data Field")}
</h3> </h2>
</div> </div>
<.form <.form

View file

@ -304,22 +304,20 @@ defmodule MvWeb.GlobalSettingsLive do
} }
/> />
<div class="form-control"> <div class="form-control">
<label class="label cursor-pointer justify-start gap-2"> <.input
<.input field={@form[:oidc_only]}
field={@form[:oidc_only]} type="checkbox"
type="checkbox" class="checkbox checkbox-sm"
class="checkbox checkbox-sm" disabled={@oidc_only_env_set or not @oidc_configured}
disabled={@oidc_only_env_set or not @oidc_configured} label={
/> if @oidc_only_env_set do
<span class="label-text"> gettext("Only OIDC sign-in (hide password login)") <>
{gettext("Only OIDC sign-in (hide password login)")} " (" <> gettext("From OIDC_ONLY") <> ")"
<%= if @oidc_only_env_set do %> else
<span class="label-text-alt text-base-content/70 ml-1"> gettext("Only OIDC sign-in (hide password login)")
({gettext("From OIDC_ONLY")}) end
</span> }
<% end %> />
</span>
</label>
<p class="label-text-alt text-base-content/70 mt-1"> <p class="label-text-alt text-base-content/70 mt-1">
{gettext( {gettext(
"When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button." "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."

View file

@ -88,16 +88,17 @@ defmodule MvWeb.ImportLive.Components do
phx-submit="start_import" phx-submit="start_import"
data-testid="csv-upload-form" data-testid="csv-upload-form"
> >
<fieldset class="mb-2 fieldset w-md"> <fieldset class="mb-2 fieldset w-md" aria-labelledby="csv_file_label">
<label for="csv_file"> <label id="csv_file_label" class="label block">
<span class="mb-1 label">{gettext("CSV File")}</span> <span class="mb-1 label text-base-content">{gettext("CSV File")}</span>
<.live_file_input
upload={@uploads.csv_file}
id="csv_file"
class="file-input file-input-bordered block"
aria-describedby="csv_file_help"
aria-label={gettext("CSV File")}
/>
</label> </label>
<.live_file_input
upload={@uploads.csv_file}
id="csv_file"
class="file-input file-input-bordered"
aria-describedby="csv_file_help"
/>
<p class="text-sm text-base-content/60 mt-2" id="csv_file_help"> <p class="text-sm text-base-content/60 mt-2" id="csv_file_help">
{gettext("CSV files only, maximum %{size} MB", size: @csv_import_max_file_size_mb)} {gettext("CSV files only, maximum %{size} MB", size: @csv_import_max_file_size_mb)}
</p> </p>

View file

@ -50,9 +50,9 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
<.icon name="hero-arrow-left" class="size-4" /> <.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")} {gettext("Back")}
</.button> </.button>
<h3 class="card-title"> <h2 class="card-title text-xl">
{gettext("Edit Field: %{field}", field: @field_label)} {gettext("Edit Field: %{field}", field: @field_label)}
</h3> </h2>
</div> </div>
<.form <.form

View file

@ -215,14 +215,16 @@ defmodule MvWeb.MemberLive.Form do
<.form_section title={gettext("Membership Fee")}> <.form_section title={gettext("Membership Fee")}>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="label"> <label for={@form[:membership_fee_type_id].id} class="label">
<span class="label-text font-semibold">{gettext("Membership Fee Type")}</span> <span class="label-text font-semibold">{gettext("Membership Fee Type")}</span>
</label> </label>
<select <select
id={@form[:membership_fee_type_id].id}
class="select select-bordered w-full" class="select select-bordered w-full"
name={@form[:membership_fee_type_id].name} name={@form[:membership_fee_type_id].name}
phx-change="validate" phx-change="validate"
value={@form[:membership_fee_type_id].value || ""} value={@form[:membership_fee_type_id].value || ""}
aria-label={gettext("Membership Fee Type")}
> >
<%!-- No "None" option: a membership fee type is required (validated in Member resource). --%> <%!-- No "None" option: a membership fee type is required (validated in Member resource). --%>
<option value="">{gettext("Select a membership fee type")}</option> <option value="">{gettext("Select a membership fee type")}</option>

View file

@ -90,7 +90,7 @@ defmodule MvWeb.UserLive.Show do
{MvWeb.Helpers.MemberHelpers.display_name(@user.member)} {MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
</.link> </.link>
<% else %> <% else %>
<span class="italic text-gray-500">{gettext("No member linked")}</span> <span class="italic text-base-content/70">{gettext("No member linked")}</span>
<% end %> <% end %>
</:item> </:item>
</.list> </.list>