diff --git a/src/__tests__/page-header-h1-guard.test.ts b/src/__tests__/page-header-h1-guard.test.ts new file mode 100644 index 000000000..7603d1ea7 --- /dev/null +++ b/src/__tests__/page-header-h1-guard.test.ts @@ -0,0 +1,85 @@ +/** + * A module page's title is a `PageHeader` (UI standards §1, §5): the H1 on + * one tier (`text-2xl font-bold tracking-tight`), its explainer muted under + * it, its actions beside it. + * + * Four in-app headings were hand-rolled beside it — the insights sub-page + * shell (`text-xl sm:text-2xl font-semibold`), the medication detail, the + * check-in and its result (`font-semibold`) — and each read a step lighter + * than every other page title. They go through `PageHeader` now; this guard + * keeps a new `
{dose}
+- {t(`mentalHealth.instrumentDescription.${key}`)} -
-
{t("mentalHealth.saved")}
diff --git a/src/components/mental-health/check-in-wizard.tsx b/src/components/mental-health/check-in-wizard.tsx
index fbabe3364..4613839d2 100644
--- a/src/components/mental-health/check-in-wizard.tsx
+++ b/src/components/mental-health/check-in-wizard.tsx
@@ -45,6 +45,7 @@ import {
UNANSWERED,
} from "./check-in-nav";
import type { InstrumentId } from "./types";
+import { PageHeader } from "@/components/ui/page-header";
const FUNCTIONAL_SCALE = [0, 1, 2, 3] as const;
@@ -142,13 +143,10 @@ export function CheckInWizard({
{/* In-test header — instrument title + ONE standardized explanation line.
No disclaimer here (§2); the landing carries it. */}
- {t(`mentalHealth.instrumentDescription.${key}`)}
-
- {t(`mentalHealth.instrument.${key}`)}
-
- ]*class="[^"]*text-2xl font-bold tracking-tight/,
+ );
+ expect(html).not.toContain("tabindex");
+ });
+
+ it("becomes a programmatic focus target with a visible ring", () => {
+ const ref = createRef
]*tabindex="-1"/);
+ expect(html).toContain("focus-visible:ring-2");
+ });
+
+ it("keeps the accessory beside the heading, outside its name", () => {
+ const html = renderToStaticMarkup(
+
]*>Pulse<\/h1>Beta<\/span>/);
+ });
+});
diff --git a/src/components/ui/page-header.tsx b/src/components/ui/page-header.tsx
index 7b52036e7..141cd8b2c 100644
--- a/src/components/ui/page-header.tsx
+++ b/src/components/ui/page-header.tsx
@@ -1,4 +1,4 @@
-import type { ReactNode } from "react";
+import type { ReactNode, Ref } from "react";
import { BackLink } from "@/components/ui/back-link";
import { cn } from "@/lib/utils";
@@ -25,6 +25,14 @@ import { cn } from "@/lib/utils";
* row — and only one of the two may be a real `
`. Without this the two
* shells hand-rolled the whole block to get the second copy, which is how
* they drifted apart from every module page in the first place.
+ *
+ * `focusable` makes the H1 a programmatic focus target (`tabIndex={-1}`
+ * with a visible ring) and `headingRef` hands the caller the element: a
+ * sub-page or a result screen moves focus to its title after a client
+ * navigation. `titleAccessory` sits beside the H1 and
+ * wraps with it (a nudge glyph, a badge) without becoming part of the
+ * heading's accessible name. Both exist so the insights sub-page shell and
+ * the check-in screens stop hand-rolling their own heading.
*/
/**
* A long one-word German title (the illness page was once
@@ -44,6 +52,9 @@ export function PageHeader({
topSlot,
actions,
stackActionsOnPhone = false,
+ headingRef,
+ focusable = false,
+ titleAccessory,
className,
}: {
title: ReactNode;
@@ -61,6 +72,12 @@ export function PageHeader({
* the title and leave this off.
*/
stackActionsOnPhone?: boolean;
+ /** The H1 element, for a caller that moves focus to it. */
+ headingRef?: Ref