fix(ui): keep public forms accessible

This commit is contained in:
SimpleTest 2026-07-24 21:33:49 +03:00
parent ba872dda63
commit f69bb9c342
6 changed files with 56 additions and 24 deletions

View File

@ -2,6 +2,17 @@ import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { newIsolatedContext } from "./helpers"; import { newIsolatedContext } from "./helpers";
const publicAccessibilityPaths = [
"/",
"/safety",
"/users/log-in",
"/users/register",
"/support",
"/account/delete",
"/legal/content-removal",
"/legal/take-it-down",
];
async function expectAccessible(page: import("@playwright/test").Page): Promise<void> { async function expectAccessible(page: import("@playwright/test").Page): Promise<void> {
const results = await new AxeBuilder({ page }).analyze(); const results = await new AxeBuilder({ page }).analyze();
@ -32,7 +43,7 @@ test("public pages are accessible by keyboard in light and dark themes", async (
await page.getByRole("button", { name: `Use ${theme} theme` }).click(); await page.getByRole("button", { name: `Use ${theme} theme` }).click();
await expect(page.locator("html")).toHaveAttribute("data-theme", theme); await expect(page.locator("html")).toHaveAttribute("data-theme", theme);
for (const path of ["/", "/safety", "/users/log-in", "/users/register"]) { for (const path of publicAccessibilityPaths) {
await page.goto(path); await page.goto(path);
await expectAccessible(page); await expectAccessible(page);
} }
@ -51,14 +62,21 @@ test("public navigation does not overflow narrow or wide viewports", async ({ br
const context = await newIsolatedContext(browser); const context = await newIsolatedContext(browser);
const page = await context.newPage(); const page = await context.newPage();
await page.setViewportSize(viewport); await page.setViewportSize(viewport);
await page.goto("/");
for (const path of ["/", "/support", "/legal/content-removal", "/legal/take-it-down"]) {
await page.goto(path);
const dimensions = await page.evaluate(() => ({ const dimensions = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth, clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth, scrollWidth: document.documentElement.scrollWidth,
})); }));
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); expect(dimensions.scrollWidth, `${path} overflowed at ${viewport.width}px`).toBeLessThanOrEqual(
dimensions.clientWidth,
);
}
await page.goto("/");
await expect(page.getByRole("navigation", { name: "Primary navigation" })).toBeVisible(); await expect(page.getByRole("navigation", { name: "Primary navigation" })).toBeVisible();
await expect(page.locator("body > header")).toHaveCount(1); await expect(page.locator("body > header")).toHaveCount(1);

View File

@ -294,9 +294,11 @@ defmodule WhoNeedHelpWeb.CoreComponents do
def input(%{type: "select"} = assigns) do def input(%{type: "select"} = assigns) do
~H""" ~H"""
<div class="fieldset mb-2"> <div class="fieldset mb-2 w-full min-w-0">
<label for={@id}> <label for={@id} class="block w-full min-w-0 max-w-full">
<span :if={@label} class="label mb-1">{@label}</span> <span :if={@label} class="label mb-1 max-w-full whitespace-normal break-words">
{@label}
</span>
<select <select
id={@id} id={@id}
name={@name} name={@name}
@ -315,9 +317,11 @@ defmodule WhoNeedHelpWeb.CoreComponents do
def input(%{type: "textarea"} = assigns) do def input(%{type: "textarea"} = assigns) do
~H""" ~H"""
<div class="fieldset mb-2"> <div class="fieldset mb-2 w-full min-w-0">
<label for={@id}> <label for={@id} class="block w-full min-w-0 max-w-full">
<span :if={@label} class="label mb-1">{@label}</span> <span :if={@label} class="label mb-1 max-w-full whitespace-normal break-words">
{@label}
</span>
<textarea <textarea
id={@id} id={@id}
name={@name} name={@name}
@ -336,9 +340,11 @@ defmodule WhoNeedHelpWeb.CoreComponents do
# All other inputs text, datetime-local, url, password, etc. are handled here... # All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do def input(assigns) do
~H""" ~H"""
<div class="fieldset mb-2"> <div class="fieldset mb-2 w-full min-w-0">
<label for={@id}> <label for={@id} class="block w-full min-w-0 max-w-full">
<span :if={@label} class="label mb-1">{@label}</span> <span :if={@label} class="label mb-1 max-w-full whitespace-normal break-words">
{@label}
</span>
<input <input
type={@type} type={@type}
name={@name} name={@name}
@ -399,8 +405,8 @@ defmodule WhoNeedHelpWeb.CoreComponents do
|> assign(:errors, Enum.map(errors, &translate_error(&1))) |> assign(:errors, Enum.map(errors, &translate_error(&1)))
~H""" ~H"""
<fieldset class="fieldset mb-2"> <fieldset class="fieldset mb-2 w-full min-w-0">
<legend class="label mb-1">{@label}</legend> <legend class="label mb-1 max-w-full whitespace-normal break-words">{@label}</legend>
<div <div
id={"#{@id}-picker"} id={"#{@id}-picker"}
phx-hook="DateTimePicker" phx-hook="DateTimePicker"

View File

@ -2,7 +2,9 @@
<div> <div>
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<div class="text-sm font-semibold text-error">{gettext("CONTENT REMOVAL")}</div> <div class="border-l-4 border-error pl-2 text-sm font-semibold text-base-content">
{gettext("CONTENT REMOVAL")}
</div>
<h1 class="mt-1 text-4xl font-black">{gettext("Your notices")}</h1> <h1 class="mt-1 text-4xl font-black">{gettext("Your notices")}</h1>
</div> </div>
<.link navigate={~p"/legal/content-removal"} class="btn btn-error"> <.link navigate={~p"/legal/content-removal"} class="btn btn-error">

View File

@ -1,6 +1,8 @@
<Layouts.app flash={@flash} current_scope={@current_scope} page_width={:compact}> <Layouts.app flash={@flash} current_scope={@current_scope} page_width={:compact}>
<div> <div>
<div class="text-sm font-semibold text-error">{gettext("CONTENT REMOVAL")}</div> <div class="border-l-4 border-error pl-2 text-sm font-semibold text-base-content">
{gettext("CONTENT REMOVAL")}
</div>
<h1 class="mt-2 text-4xl font-black">{gettext("Report illegal or harmful content")}</h1> <h1 class="mt-2 text-4xl font-black">{gettext("Report illegal or harmful content")}</h1>
<p class="mt-3 text-base-content/65"> <p class="mt-3 text-base-content/65">
{gettext( {gettext(
@ -21,7 +23,7 @@
"Use the dedicated urgent process. Never upload or reproduce the image or video." "Use the dedicated urgent process. Never upload or reproduce the image or video."
)} )}
</p> </p>
<.link navigate={~p"/legal/take-it-down"} class="btn btn-error btn-outline btn-sm mt-3"> <.link navigate={~p"/legal/take-it-down"} class="btn btn-error btn-sm mt-3">
{gettext("Open TAKE IT DOWN request")} {gettext("Open TAKE IT DOWN request")}
</.link> </.link>
</div> </div>

View File

@ -1,6 +1,8 @@
<Layouts.app flash={@flash} current_scope={@current_scope} page_width={:compact}> <Layouts.app flash={@flash} current_scope={@current_scope} page_width={:compact}>
<div> <div>
<div class="text-sm font-semibold text-error">{gettext("TAKE IT DOWN")}</div> <div class="border-l-4 border-error pl-2 text-sm font-semibold text-base-content">
{gettext("TAKE IT DOWN")}
</div>
<h1 class="mt-2 text-4xl font-black"> <h1 class="mt-2 text-4xl font-black">
{gettext("Request removal of intimate visual material")} {gettext("Request removal of intimate visual material")}
</h1> </h1>

View File

@ -1,6 +1,8 @@
<Layouts.app flash={@flash} current_scope={@current_scope} page_width={:compact}> <Layouts.app flash={@flash} current_scope={@current_scope} page_width={:compact}>
<div> <div>
<div class="text-sm font-semibold text-error">{gettext("ACCOUNT AND DATA")}</div> <div class="border-l-4 border-error pl-2 text-sm font-semibold text-base-content">
{gettext("ACCOUNT AND DATA")}
</div>
<h1 class="mt-2 text-4xl font-black">{gettext("Request account deletion")}</h1> <h1 class="mt-2 text-4xl font-black">{gettext("Request account deletion")}</h1>
<p class="mt-3 text-base-content/65"> <p class="mt-3 text-base-content/65">
{gettext( {gettext(