mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(meds): notification section title id no longer duplicates the section heading
Split the two title ids so the Switch's aria-labelledby resolves to the row label rather than the section heading. axe duplicate-id gone; the announced name now matches the user's mental model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
359b103e90
commit
9532f14f3b
2 files changed
+83
-4
No files matched your search
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* v1.5.5 F-1 H-1 — pin the split title ids on the notifications row.
|
||||
*
|
||||
* The earlier single `TITLE_ID` constant landed on both the section
|
||||
* heading (`<MedicationDetailSection titleId={…}>`) and the inner
|
||||
* `<span>` carrying the switch's `aria-labelledby`, producing a
|
||||
* duplicate-id axe failure. The fix splits the two ids: the section
|
||||
* heading carries `*-heading`, the inner row label carries
|
||||
* `*-row-label`, and the switch's `aria-labelledby` points at the
|
||||
* row label.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { NotificationsSection } from "@/components/medications/sections/notifications-section";
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
user: { id: "test-user", username: "tester", role: "USER" },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
function makeClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function render(node: React.ReactNode, client: QueryClient) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<QueryClientProvider client={client}>{node}</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<NotificationsSection> — split title ids (F-1 H-1)", () => {
|
||||
it("renders the section heading and the row label with distinct ids", () => {
|
||||
const client = makeClient();
|
||||
const html = render(
|
||||
<NotificationsSection
|
||||
medicationId="med-1"
|
||||
notificationsEnabled={true}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
// Section heading + row label sit on different DOM nodes so axe
|
||||
// duplicate-id passes. The §10 invariant 4 row-as-hit-target also
|
||||
// needs the row label to be the aria-labelledby target.
|
||||
expect(html).toContain('id="medication-detail-notifications-heading"');
|
||||
expect(html).toContain('id="medication-detail-notifications-row-label"');
|
||||
expect(html).not.toContain('id="medication-detail-notifications-title"');
|
||||
|
||||
// The switch reaches the row label, not the section heading.
|
||||
expect(html).toContain(
|
||||
'aria-labelledby="medication-detail-notifications-row-label"',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -57,7 +57,15 @@ interface UserPrefsResponse {
|
||||
}
|
||||
|
||||
const SWITCH_ID = "medication-detail-notifications-switch";
|
||||
const TITLE_ID = "medication-detail-notifications-title";
|
||||
// v1.5.5 F-1 H-1 — split the title id so the section heading and
|
||||
// the inner switch label resolve to distinct DOM nodes. The earlier
|
||||
// single `TITLE_ID` sat on both `<MedicationDetailSection>`'s heading
|
||||
// (`titleId` prop) and the inner `<span>` carrying the switch's
|
||||
// `aria-labelledby`, surfacing as an axe duplicate-id failure on
|
||||
// every detail-page render and pointing the announced name at the
|
||||
// section heading rather than the user's "Send a reminder" target.
|
||||
const SECTION_TITLE_ID = "medication-detail-notifications-heading";
|
||||
const ROW_TITLE_ID = "medication-detail-notifications-row-label";
|
||||
const HELPER_ID = "medication-detail-notifications-helper";
|
||||
|
||||
export function NotificationsSection({
|
||||
@@ -131,7 +139,7 @@ export function NotificationsSection({
|
||||
|
||||
return (
|
||||
<MedicationDetailSection
|
||||
titleId={TITLE_ID}
|
||||
titleId={SECTION_TITLE_ID}
|
||||
title={t("medications.detail.notifications.title")}
|
||||
dataSlot="medication-detail-notifications-section"
|
||||
>
|
||||
@@ -155,7 +163,7 @@ export function NotificationsSection({
|
||||
>
|
||||
<span className="space-y-1">
|
||||
<span
|
||||
id={TITLE_ID}
|
||||
id={ROW_TITLE_ID}
|
||||
className="text-foreground block text-sm font-medium"
|
||||
>
|
||||
{t("medications.detail.notifications.switchLabel")}
|
||||
@@ -174,7 +182,7 @@ export function NotificationsSection({
|
||||
checked={localEnabled}
|
||||
disabled={submitting}
|
||||
onCheckedChange={(checked) => void flip(checked)}
|
||||
aria-labelledby={TITLE_ID}
|
||||
aria-labelledby={ROW_TITLE_ID}
|
||||
aria-describedby={HELPER_ID}
|
||||
/>
|
||||
</label>
|
||||
|
||||
Reference in new issue
Block a user