From e73d0c5ee724434439b730be5a9c0b5de2c38d80 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 12:59:48 +0000 Subject: [PATCH 1/6] fix(forms): expand Form information rows with DisclosureGroup Replace the static tick-row cards (truncated text + decorative chevron) with the design-system DisclosureGroup so taps reveal full content. Co-authored-by: BigSimmo --- docs/design-system/COMPONENTS.md | 4 +- docs/design-system/adoption-manifest.json | 10 ++-- src/components/forms/form-detail-page.tsx | 60 +++++++++---------- .../forms-information-disclosure.dom.test.tsx | 44 ++++++++++++++ 4 files changed, 81 insertions(+), 37 deletions(-) create mode 100644 tests/forms-information-disclosure.dom.test.tsx diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 6890c0be1e..93ea6fcfe2 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -947,7 +947,7 @@ tests to run. Registered public components: 53 Components with a valid design-sync preview: 53 -Components with product imports: 30 +Components with product imports: 31 This generated snapshot is a local source-derived inventory. It does not assert remote design-project publication. @@ -966,7 +966,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | `ConfirmDialog` | layout | yes | yes | no | yes | no | 0 | | `DateDisplay` | source | yes | yes | inherited-global-root | yes | no | 3 | | `Disclosure` | layout | yes | yes | no | yes | no | 0 | -| `DisclosureGroup` | layout | yes | yes | no | yes | no | 0 | +| `DisclosureGroup` | layout | yes | yes | inherited-global-root | yes | no | 1 | | `DoseLine` | answer | yes | yes | no | yes | no | 0 | | `DownloadLink` | controls | yes | yes | no | yes | no | 0 | | `EmptyState` | feedback | yes | yes | inherited-global-root | yes | no | 12 | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 151c19f916..0148c38b7e 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -535,8 +535,8 @@ "source": "src/components/ui/disclosure.tsx", "sourceExported": true, "entryExported": true, - "directImportFiles": [], - "productImportFiles": [], + "directImportFiles": ["src/components/forms/form-detail-page.tsx"], + "productImportFiles": ["src/components/forms/form-detail-page.tsx"], "designSync": { "listedInSourceMap": true, "listedInDtsProps": true, @@ -552,8 +552,8 @@ }, "built": true, "locallyRegistered": true, - "v2ShellMounted": false, - "v2MountMode": "none", + "v2ShellMounted": true, + "v2MountMode": "inherited-global-root", "proofDeclared": true, "baselineCommitted": false }, @@ -3243,7 +3243,7 @@ "summary": { "registeredComponentCount": 53, "previewCount": 53, - "productImportedComponentCount": 30, + "productImportedComponentCount": 31, "rootCount": 55, "productionRouteCount": 47, "nextUiEntryCount": 99 diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index 444c14a072..096749fc2c 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -43,6 +43,7 @@ import { } from "@/components/ui-primitives"; import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; import { FormCodeBadge, splitFormCode } from "@/components/forms/form-code-badge"; +import { DisclosureGroup } from "@/components/ui/disclosure"; import { appModeHomeHref } from "@/lib/app-modes"; import { formCatalogDetails, formTitleForCode, type FormRecord } from "@/lib/form-catalog"; import type { ServiceChipTone, ServiceContact, ServiceCriterion, ServiceSummaryCard } from "@/lib/service-ranker"; @@ -602,21 +603,34 @@ function RailCard({ icon: Icon, title, children }: { icon: LucideIcon; title: st ); } -function InfoRow({ label, value, icon: Icon }: { label: string; value: string | null | undefined; icon: LucideIcon }) { - return ( -
- - - -
-

{label}

-

- {displayText(value)} -

-
- -
- ); +function formInformationIcon(label: string): LucideIcon { + const normalized = label.toLowerCase(); + if (normalized.includes("only")) return Route; + if (normalized.includes("sign")) return Clipboard; + if (normalized.includes("clinical")) return Info; + if (normalized.includes("source")) return FileText; + if (normalized.includes("pathway")) return Navigation; + return CircleCheck; +} + +function formInformationItems(rows: Array<{ label: string; value: string | null | undefined }>) { + return rows.map((row) => { + const Icon = formInformationIcon(row.label); + const value = displayText(row.value); + return { + id: `form-info-${row.label}`, + title: ( + + + + + {row.label} + + ), + description: value, + content:

{value}

, + }; + }); } export function FormDetailPage({ form }: { form: FormRecord }) { @@ -857,21 +871,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
- {detailRows.map((row) => { - const label = row.label.toLowerCase(); - const Icon = label.includes("only") - ? Route - : label.includes("sign") - ? Clipboard - : label.includes("clinical") - ? Info - : label.includes("source") - ? FileText - : label.includes("pathway") - ? Navigation - : CircleCheck; - return ; - })} +
{/* The `-mobile`/`-desktop` id pairs below are the section anchors diff --git a/tests/forms-information-disclosure.dom.test.tsx b/tests/forms-information-disclosure.dom.test.tsx new file mode 100644 index 0000000000..3d7b0c6bda --- /dev/null +++ b/tests/forms-information-disclosure.dom.test.tsx @@ -0,0 +1,44 @@ +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { FormDetailPage } from "@/components/forms/form-detail-page"; +import { getFormRecord } from "@/lib/forms"; + +vi.mock("@/components/account-data-provider", () => ({ + useAccountData: () => ({ + isSaved: () => false, + setFavourite: vi.fn(async () => true), + }), +})); + +describe("Form information disclosures", () => { + it("expands a tick row to reveal the full information content", async () => { + const user = userEvent.setup(); + const form = getFormRecord("form-1a"); + expect(form).toBeTruthy(); + if (!form) return; + + const doesNotAuthorise = form.referralInfo?.find((row) => row.label === "Does not authorise"); + expect(doesNotAuthorise?.value).toBeTruthy(); + const fullText = doesNotAuthorise!.value; + + render(); + + const section = screen.getByRole("region", { name: "Form information" }); + const trigger = within(section).getByRole("button", { name: /Does not authorise/i }); + expect(trigger).toHaveAttribute("aria-expanded", "false"); + + const panelId = trigger.getAttribute("aria-controls"); + expect(panelId).toBeTruthy(); + const panel = document.getElementById(panelId!); + expect(panel).toBeTruthy(); + expect(panel).toHaveAttribute("hidden"); + + await user.click(trigger); + + expect(trigger).toHaveAttribute("aria-expanded", "true"); + expect(panel).not.toHaveAttribute("hidden"); + expect(within(panel!).getByText(fullText)).toBeVisible(); + }); +}); From f5dd1dea495e8d6e9bd5106dcf0a4d062ee02292 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 13:01:55 +0000 Subject: [PATCH 2/6] fix(forms): satisfy Form information disclosure typecheck Widen optional ServiceInfoRow values and narrow the DOM test text before assertions. Co-authored-by: BigSimmo --- src/components/forms/form-detail-page.tsx | 2 +- tests/forms-information-disclosure.dom.test.tsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index 096749fc2c..566613aaa7 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -613,7 +613,7 @@ function formInformationIcon(label: string): LucideIcon { return CircleCheck; } -function formInformationItems(rows: Array<{ label: string; value: string | null | undefined }>) { +function formInformationItems(rows: Array<{ label: string; value?: string | null }>) { return rows.map((row) => { const Icon = formInformationIcon(row.label); const value = displayText(row.value); diff --git a/tests/forms-information-disclosure.dom.test.tsx b/tests/forms-information-disclosure.dom.test.tsx index 3d7b0c6bda..36f1281b77 100644 --- a/tests/forms-information-disclosure.dom.test.tsx +++ b/tests/forms-information-disclosure.dom.test.tsx @@ -20,8 +20,9 @@ describe("Form information disclosures", () => { if (!form) return; const doesNotAuthorise = form.referralInfo?.find((row) => row.label === "Does not authorise"); - expect(doesNotAuthorise?.value).toBeTruthy(); - const fullText = doesNotAuthorise!.value; + const fullText = doesNotAuthorise?.value; + expect(fullText).toEqual(expect.any(String)); + if (!fullText) return; render(); @@ -30,15 +31,17 @@ describe("Form information disclosures", () => { expect(trigger).toHaveAttribute("aria-expanded", "false"); const panelId = trigger.getAttribute("aria-controls"); - expect(panelId).toBeTruthy(); - const panel = document.getElementById(panelId!); + expect(panelId).toEqual(expect.any(String)); + if (!panelId) return; + const panel = document.getElementById(panelId); expect(panel).toBeTruthy(); + if (!panel) return; expect(panel).toHaveAttribute("hidden"); await user.click(trigger); expect(trigger).toHaveAttribute("aria-expanded", "true"); expect(panel).not.toHaveAttribute("hidden"); - expect(within(panel!).getByText(fullText)).toBeVisible(); + expect(within(panel).getByText(fullText)).toBeVisible(); }); }); From 061619d46ec5180a5f637b8c25130a66aaceb636 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 13:05:14 +0000 Subject: [PATCH 3/6] docs(ledger): record forms info disclosure fix review Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 114f425ae7..42db89a89a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -752,3 +752,5 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-08 | claude/ds-tap-and-linkaction | 6916c80526603514d91bd29d224959dd420af59c | M5 LinkAction tone refusal plus re-measured corrections to outstanding-issues #270, #118 and #269 — final reviewed head, adds the tone?: never fix, its type-contract test and both regenerated manifests | PR #1720, superseding the 824c1b74a record. Codex found the Omit form still accepted tone through a spread; verified with a focused tsc probe before changing anything (Omit accepted the spread with no diagnostic, tone?: never rejected it with TS2345), because excess-property checking only fires on object literals. Fixed with tone?: never plus a type-level contract test that stops compiling if the prop widens back. CodeRabbit's future-dated finding fixed in ff307cc5b. CodeRabbit's ledger-scope finding does not apply: that row records a different ref and head and was accurate as written, but a superseding row for the final #1719 head was appended anyway since its scope grew after the review pass | tsc -p tsconfig.typecheck.json --noEmit exit 0 zero diagnostics; lint exit 0; check:design-system-contract exit 0 (676 production files, legacy shadow aliases 228 confirming the #262 re-measure, adoption 53 components 55 roots, design-sync 53 components and 7 guidelines); check-icon-scale.mjs --strict exit 0; vitest threads pool 3 files 164 tests passed; check:outstanding-issues pass; check:branch-review-ledger pass; prettier --check . pass whole-tree; main merged in with merge-tree proven clean first and an id-set proof over both merge parents showing 274 ids each side, none lost, none invented | | 2026-08-08 | claude/ds-close-276 (PR #1724) | 75c89993f3ea23b70a250f605b21437b4ea9aac8 | PR #1724 review-and-fix | fixed Codex P2 wrong #118 Lighthouse cause (150 overwrite vs 151 pin); dispositioned CodeRabbit #276 archive claim as false (issues:done move); merge-tree clean; required CI was green on prior tip 8ae8c48f; no Bugbot findings | check:outstanding-issues pass; prettier --check docs/outstanding-issues.md pass; no provider-backed checks | | 2026-08-08 | claude/ds-close-276 (PR #1724) | 4baa9a1b42fa05731a6f983b3e0d0ebbd37f5271 | PR #1724 review-and-fix | synced origin/main (#1725 conflict on outstanding-issues resolved by preferring main queue then re-applying #276 done + corrected #118 diagnosis); Codex P2 fixed; CodeRabbit #276 archive claim dispositioned false; merge-tree clean after sync | check:outstanding-issues pass; prettier --check docs/outstanding-issues.md pass; merge-tree clean vs origin/main; no provider-backed checks | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 | f5dd1dea495e8d6e9bd5106dcf0a4d062ee02292 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | +| 2026-08-08 | cursor/forms-info-disclosure-68d6 | 8f25e6c482d8e4cd879098d7cfd73b7f8603e478 | forms-info-disclosure | fixed Form information tick rows to expand via DisclosureGroup | verify:pr-local; forms-information-disclosure.dom.test; check:design-system-adoption | From 80b6c8e758b108d528df5cb2ba69cc18eed4d62d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 14:51:02 +0000 Subject: [PATCH 4/6] fix(ui): keep Disclosure preview out of the accessible name Form information rows were announcing the full clinical value as the button name. Mark the preview aria-hidden, restore sm+ wrapping, and assert label-only names in Disclosure and Forms DOM tests. Co-authored-by: BigSimmo --- src/components/ui/disclosure.tsx | 13 ++++++++- .../forms-information-disclosure.dom.test.tsx | 3 +- tests/ui-v2-components.dom.test.tsx | 29 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/components/ui/disclosure.tsx b/src/components/ui/disclosure.tsx index a19da858fa..3d343d1087 100644 --- a/src/components/ui/disclosure.tsx +++ b/src/components/ui/disclosure.tsx @@ -97,7 +97,18 @@ export function Disclosure({ /> {title} - {description ? {description} : null} + {/* Visual preview only: keep it out of the accessible name so the + trigger stays label-sized. Full copy lives in the panel for SR + once expanded. Wrap from sm+ so desktop scanners are not forced + through a tap the way phone truncation requires. */} + {description ? ( + + ) : null} {meta ? {meta} : null} diff --git a/tests/forms-information-disclosure.dom.test.tsx b/tests/forms-information-disclosure.dom.test.tsx index 36f1281b77..fd478131ec 100644 --- a/tests/forms-information-disclosure.dom.test.tsx +++ b/tests/forms-information-disclosure.dom.test.tsx @@ -27,7 +27,8 @@ describe("Form information disclosures", () => { render(); const section = screen.getByRole("region", { name: "Form information" }); - const trigger = within(section).getByRole("button", { name: /Does not authorise/i }); + const trigger = within(section).getByRole("button", { name: "Does not authorise" }); + expect(trigger).toHaveAccessibleName("Does not authorise"); expect(trigger).toHaveAttribute("aria-expanded", "false"); const panelId = trigger.getAttribute("aria-controls"); diff --git a/tests/ui-v2-components.dom.test.tsx b/tests/ui-v2-components.dom.test.tsx index e4274488a1..7925ebb3e0 100644 --- a/tests/ui-v2-components.dom.test.tsx +++ b/tests/ui-v2-components.dom.test.tsx @@ -281,6 +281,35 @@ describe("Disclosure / Progress", () => { expect(screen.getByRole("heading", { level: 4, name: "Monitoring" })).toBeVisible(); }); + it("keeps description as a visual preview outside the accessible name", async () => { + const user = userEvent.setup(); + render( + + Treatment, detention, transport, restraint, seclusion or force by itself. + , + ); + + const trigger = screen.getByRole("button", { name: "Does not authorise" }); + expect(trigger).toHaveAccessibleName("Does not authorise"); + expect(trigger).toHaveAttribute("aria-expanded", "false"); + + await user.click(trigger); + expect(trigger).toHaveAttribute("aria-expanded", "true"); + const panelId = trigger.getAttribute("aria-controls"); + expect(panelId).toEqual(expect.any(String)); + if (!panelId) return; + const panel = document.getElementById(panelId); + expect(panel).toBeTruthy(); + if (!panel) return; + expect( + within(panel).getByText("Treatment, detention, transport, restraint, seclusion or force by itself."), + ).toBeVisible(); + }); + it("animates determinate progress with scaleX rather than width", () => { render(); const fill = screen.getByTestId("progress-fill"); From fa618caf72762ecd905d5f028619404a8666a19c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 16:53:02 +0000 Subject: [PATCH 5/6] fix(forms): use index-scoped disclosure ids for duplicate labels formInformationItems keyed only on row.label collided when referral info repeated the same label, breaking DisclosureGroup open state. Include the row index in each id and assert duplicate labels stay independent in forms-information-disclosure.dom.test. --- src/components/forms/form-detail-page.tsx | 4 +- .../forms-information-disclosure.dom.test.tsx | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index 566613aaa7..9c5ff6c50e 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -614,11 +614,11 @@ function formInformationIcon(label: string): LucideIcon { } function formInformationItems(rows: Array<{ label: string; value?: string | null }>) { - return rows.map((row) => { + return rows.map((row, index) => { const Icon = formInformationIcon(row.label); const value = displayText(row.value); return { - id: `form-info-${row.label}`, + id: `form-info-${index}-${row.label}`, title: ( diff --git a/tests/forms-information-disclosure.dom.test.tsx b/tests/forms-information-disclosure.dom.test.tsx index fd478131ec..a0cb9473ed 100644 --- a/tests/forms-information-disclosure.dom.test.tsx +++ b/tests/forms-information-disclosure.dom.test.tsx @@ -45,4 +45,47 @@ describe("Form information disclosures", () => { expect(panel).not.toHaveAttribute("hidden"); expect(within(panel).getByText(fullText)).toBeVisible(); }); + + it("keeps duplicate labels on independent disclosure triggers", async () => { + const user = userEvent.setup(); + const base = getFormRecord("form-1a"); + expect(base).toBeTruthy(); + if (!base) return; + + const form = { + ...base, + referralInfo: [ + { label: "Clinical note", value: "First clinical detail" }, + { label: "Clinical note", value: "Second clinical detail" }, + ], + }; + + render(); + + const section = screen.getByRole("region", { name: "Form information" }); + const triggers = within(section).getAllByRole("button", { name: "Clinical note" }); + expect(triggers).toHaveLength(2); + + const panelIds = triggers.map((trigger) => trigger.getAttribute("aria-controls")); + expect(panelIds.every((id) => Boolean(id))).toBe(true); + expect(new Set(panelIds).size).toBe(2); + + await user.click(triggers[0]); + expect(triggers[0]).toHaveAttribute("aria-expanded", "true"); + expect(triggers[1]).toHaveAttribute("aria-expanded", "false"); + + const firstPanel = document.getElementById(panelIds[0]!); + expect(firstPanel).toBeTruthy(); + if (!firstPanel) return; + expect(firstPanel).not.toHaveAttribute("hidden"); + expect(within(firstPanel).getByText("First clinical detail")).toBeVisible(); + + await user.click(triggers[1]); + expect(triggers[0]).toHaveAttribute("aria-expanded", "true"); + expect(triggers[1]).toHaveAttribute("aria-expanded", "true"); + const secondPanel = document.getElementById(panelIds[1]!); + expect(secondPanel).toBeTruthy(); + if (!secondPanel) return; + expect(within(secondPanel).getByText("Second clinical detail")).toBeVisible(); + }); }); From c8bbe0ee2dc7094bfea2a000320be4e8e7a00184 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:00:47 +0800 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/ui-v2-components.dom.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ui-v2-components.dom.test.tsx b/tests/ui-v2-components.dom.test.tsx index 7925ebb3e0..dd08ff2db0 100644 --- a/tests/ui-v2-components.dom.test.tsx +++ b/tests/ui-v2-components.dom.test.tsx @@ -293,10 +293,12 @@ describe("Disclosure / Progress", () => { , ); + const description = "Treatment, detention, transport, restraint, seclusion or force by itself."; + const trigger = screen.getByRole("button", { name: "Does not authorise" }); expect(trigger).toHaveAccessibleName("Does not authorise"); + expect(within(trigger).getByText(description)).toBeVisible(); expect(trigger).toHaveAttribute("aria-expanded", "false"); - await user.click(trigger); expect(trigger).toHaveAttribute("aria-expanded", "true"); const panelId = trigger.getAttribute("aria-controls");