Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
32d6d87
fix(forms): expand details without repeated preview
BigSimmo Aug 12, 2026
6cdd58c
Merge branch 'main' into codex/fix-form-section-expansion-issues
BigSimmo Aug 12, 2026
4b795ee
Merge branch 'main' into codex/fix-form-section-expansion-issues
BigSimmo Aug 12, 2026
16b4eaf
fix: handle extended disclosures in print
BigSimmo Aug 12, 2026
8249fd1
Merge latest main into PR 1869
BigSimmo Aug 12, 2026
98c96fe
chore: prepare exact design-sync regeneration
BigSimmo Aug 12, 2026
227a6b6
chore: regenerate disclosure design-sync contract
github-actions[bot] Aug 12, 2026
b9476db
chore: remove temporary PR 1869 generator
BigSimmo Aug 12, 2026
d24e0cf
chore: add one-shot formatter for PR 1869
BigSimmo Aug 12, 2026
08d03cc
style: format print disclosure regression
github-actions[bot] Aug 12, 2026
b3448dd
chore: remove temporary PR 1869 formatter
BigSimmo Aug 12, 2026
38f93a5
Merge branch 'main' into codex/fix-form-section-expansion-issues
BigSimmo Aug 12, 2026
ff530e2
test: stabilise print disclosure regression
BigSimmo Aug 12, 2026
fd7ce66
Merge latest main into PR 1869
BigSimmo Aug 12, 2026
dbb9f1b
chore: add one-shot design metadata refresh
BigSimmo Aug 12, 2026
fb545d7
chore: refresh generated design metadata
github-actions[bot] Aug 12, 2026
f4f69db
chore: trigger final CI after metadata refresh
BigSimmo Aug 12, 2026
1880994
fix: force collapsed disclosure panels open in print
BigSimmo Aug 12, 2026
c126d2b
Merge branch 'main' into codex/fix-form-section-expansion-issues
BigSimmo Aug 12, 2026
18dc0e1
chore: add one-shot print assertion refresh
BigSimmo Aug 12, 2026
ee84305
test: expect important print display override
github-actions[bot] Aug 12, 2026
a1d7b3a
chore: trigger final CI after print assertion refresh
BigSimmo Aug 12, 2026
e9d05f2
Merge branch 'main' into codex/fix-form-section-expansion-issues
BigSimmo Aug 12, 2026
8ec056e
chore: stage focused PR 1869 print fix
BigSimmo Aug 12, 2026
a15e28d
fix: make collapsed disclosure panels printable
github-actions[bot] Aug 12, 2026
cb7bf05
chore: trigger exact-head CI after disclosure print fix
BigSimmo Aug 12, 2026
f863e31
docs(design-system): register DisclosureGroup form tests
BigSimmo Aug 12, 2026
05b0869
chore: stage PR 1869 manifest formatting fix
BigSimmo Aug 12, 2026
7e0cb86
fix: correct PR 1869 formatter workflow syntax
BigSimmo Aug 12, 2026
5cf3f98
chore: add one-shot specifier assertion fix
BigSimmo Aug 12, 2026
085894c
chore: harden one-shot specifier assertion fix
BigSimmo Aug 12, 2026
1172f5a
docs(design-system): format DisclosureGroup test registration
github-actions[bot] Aug 12, 2026
45affe3
chore: use shallow checkout for PR 1869 manifest fix
BigSimmo Aug 12, 2026
a55fd58
chore: remove completed PR 1869 formatter workflow
BigSimmo Aug 12, 2026
d75a7c3
chore: stage generated metadata with specifier fix
BigSimmo Aug 12, 2026
5224749
test: scope specifier wording to visible output
github-actions[bot] Aug 12, 2026
8d3b2cd
chore: trigger final CI after specifier test fix
BigSimmo Aug 12, 2026
ae416b0
chore: remove unrelated specifier test change
BigSimmo Aug 12, 2026
b3d7e9d
chore: stage DisclosureGroup coverage registration
BigSimmo Aug 12, 2026
72a6fcd
fix: install locked dependencies for coverage registration
BigSimmo Aug 12, 2026
1365cee
chore: remove one-shot PR 1869 helper workflow
BigSimmo Aug 12, 2026
e0f1e44
chore: stage fast DisclosureGroup coverage registration
BigSimmo Aug 12, 2026
c0b8fc0
test: register DisclosureGroup behavioural coverage
github-actions[bot] Aug 12, 2026
aba4c9b
chore: trigger final exact-head CI
BigSimmo Aug 12, 2026
6972368
Merge main into codex/fix-form-section-expansion-issues
BigSimmo Aug 12, 2026
42174cf
Merge branch 'main' into codex/fix-form-section-expansion-issues
BigSimmo Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .design-sync/config.json

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,11 @@
"preview": ".design-sync/previews/Disclosure.tsx",
"previewValid": true
},
"testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"],
"testFiles": [
"tests/design-sync-visual-exports.test.ts",
"tests/ui-forms-section-nav.spec.ts",
"tests/ui-v2-components.dom.test.tsx"
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"baseline": {
"targetLayer": "v2",
"liveLayer": "v2",
Expand Down Expand Up @@ -559,7 +563,11 @@
"preview": ".design-sync/previews/DisclosureGroup.tsx",
"previewValid": true
},
"testFiles": ["tests/design-sync-visual-exports.test.ts"],
"testFiles": [
"tests/design-sync-visual-exports.test.ts",
"tests/forms-information-disclosure.dom.test.tsx",
"tests/ui-forms-section-nav.spec.ts"
],
"baseline": {
"targetLayer": "v2",
"liveLayer": "v2",
Expand Down
4 changes: 4 additions & 0 deletions src/components/forms/form-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,10 @@ function formInformationItems(rows: Array<{ label: string; value?: string | null
</span>
),
description: value,
// The collapsed line is a preview of this same value. Opening the row
// replaces that preview with the fully wrapped answer instead of echoing
// it in a visually separate, bordered panel.
extendDescription: true,
content: <p className={cn("text-sm leading-6", textMuted)}>{value}</p>,
};
});
Expand Down
43 changes: 30 additions & 13 deletions src/components/ui/disclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type DisclosureProps = {
/** Right-aligned summary that stays visible while collapsed — a count, a status. */
meta?: ReactNode;
description?: ReactNode;
/** Replace the collapsed preview with the panel when open, so the copy reads as one continuous answer. */
extendDescription?: boolean;
defaultOpen?: boolean;
/** Controlled mode. Omit both to let the component own its state. */
open?: boolean;
Expand All @@ -25,19 +27,18 @@ export type DisclosureProps = {
* `aria-controls`. `aria-expanded` alone says something opened but never what —
* that exact gap was the `AccessibleTable` expander defect.
*
* The panel is kept in the DOM and hidden with `hidden` rather than unmounted, so
* the content is there to be revealed rather than re-fetched. On screen, `hidden`
* is `display:none`, so a collapsed panel is genuinely out of the accessibility
* tree and out of Ctrl-F — that is correct for a control the reader can open, and
* the docstring here used to claim otherwise.
* The panel stays mounted and uses the author-level `hidden` utility while
* collapsed rather than the HTML `hidden` attribute. On screen the utility is
* `display:none`, so a collapsed panel is genuinely out of the accessibility
* tree and out of Ctrl-F — that is correct for a control the reader can open.
*
* Print is the case where it is NOT correct. A printed page has no disclosure to
* open, so a collapsed section prints as if the guideline never mentioned it —
* exactly the failure this component is supposed to prevent, made permanent on
* paper and unnoticeable, because the reader holding the printout has no way to
* tell a section was omitted. `print:block` on the panel (author styles beat the
* UA `[hidden]` rule) expands every collapsed section for print; the chevron is
* dropped, since a rotated arrow means nothing on paper.
* tell a section was omitted. `print:block` overrides the author-level collapse
* utility and expands every collapsed section for print; the
* chevron is dropped, since a rotated arrow means nothing on paper.
*
* No height animation. Animating `height` or `grid-template-rows` forces layout
* every frame and is a measurable CLS contributor; the chevron rotates on
Expand All @@ -48,6 +49,7 @@ export function Disclosure({
children,
meta,
description,
extendDescription = false,
defaultOpen = false,
open: controlledOpen,
onOpenChange,
Expand Down Expand Up @@ -101,10 +103,14 @@ export function Disclosure({
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 ? (
{description && !(extendDescription && open) ? (
Comment thread
BigSimmo marked this conversation as resolved.
<span
aria-hidden="true"
className={cn("block truncate text-xs sm:whitespace-normal sm:leading-5", textMuted)}
className={cn(
"block truncate text-xs sm:whitespace-normal sm:leading-5",
extendDescription && "print:hidden",
textMuted,
)}
>
{description}
</span>
Expand All @@ -117,9 +123,12 @@ export function Disclosure({
id={panelId}
role="region"
aria-labelledby={`${id}-trigger`}
hidden={!open}
data-open={open ? "true" : "false"}
className="border-t border-[color:var(--border)] px-3 py-3 print:block"
className={cn(
"px-3 py-3 print:block",
!open && "hidden",
extendDescription ? "pt-0" : "border-t border-[color:var(--border)]",
)}
>
{children}
</div>
Expand All @@ -128,7 +137,14 @@ export function Disclosure({
}

export type DisclosureGroupProps = {
items: Array<{ id: string; title: ReactNode; description?: ReactNode; meta?: ReactNode; content: ReactNode }>;
items: Array<{
id: string;
title: ReactNode;
description?: ReactNode;
extendDescription?: boolean;
meta?: ReactNode;
content: ReactNode;
}>;
exclusive?: boolean;
className?: string;
headingLevel?: 2 | 3 | 4 | 5 | 6;
Expand All @@ -149,6 +165,7 @@ export function DisclosureGroup({ items, exclusive = false, className, headingLe
key={item.id}
title={item.title}
description={item.description}
extendDescription={item.extendDescription}
meta={item.meta}
headingLevel={headingLevel}
open={openIds.includes(item.id)}
Expand Down
10 changes: 7 additions & 3 deletions tests/forms-information-disclosure.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ vi.mock("@/components/account-data-provider", () => ({
}),
}));

// Adoption evidence: these form assertions exercise DisclosureGroup.items[].extendDescription.
describe("Form information disclosures", () => {
it("expands a tick row to reveal the full information content", async () => {
const user = userEvent.setup();
Expand All @@ -42,12 +43,15 @@ describe("Form information disclosures", () => {
const panel = document.getElementById(panelId);
expect(panel).toBeTruthy();
if (!panel) return;
expect(panel).toHaveAttribute("hidden");
expect(panel).not.toHaveAttribute("hidden");
expect(panel).toHaveClass("hidden", "print:block");

await user.click(trigger);

expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(panel).not.toHaveAttribute("hidden");
expect(within(trigger).queryByText(fullText)).not.toBeInTheDocument();
expect(panel).not.toHaveClass("hidden");
expect(panel).not.toHaveClass("border-t");
expect(within(panel).getByText(fullText)).toBeVisible();
});

Expand Down Expand Up @@ -82,7 +86,7 @@ describe("Form information disclosures", () => {
const firstPanel = document.getElementById(panelIds[0]!);
expect(firstPanel).toBeTruthy();
if (!firstPanel) return;
expect(firstPanel).not.toHaveAttribute("hidden");
expect(firstPanel).not.toHaveClass("hidden");
expect(within(firstPanel).getByText("First clinical detail")).toBeVisible();

await user.click(triggers[1]);
Expand Down
57 changes: 57 additions & 0 deletions tests/ui-forms-section-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,64 @@ import { visibleByTestId } from "./playwright-settlement";
*/
const FORM_ROUTE = "/forms/transport-crisis-form";

// Adoption evidence: these browser regressions exercise DisclosureGroup.items[].extendDescription.
test.describe("Forms section navigation", () => {
test("expands information previews into one continuous answer", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(FORM_ROUTE, { waitUntil: "domcontentloaded" });

const section = page.getByRole("region", { name: "Form information" });
const trigger = section.getByRole("button", { name: "Does not authorise" });
const preview = "Psychiatric treatment or detention beyond the linked authority.";

await expect(trigger.getByText(preview)).toBeVisible({ timeout: 20_000 });
await trigger.click();

const panel = page.locator(`#${await trigger.getAttribute("aria-controls")}`);
await expect(trigger.getByText(preview)).toHaveCount(0);
await expect(panel.getByText(preview)).toBeVisible();
await expect(panel).not.toHaveClass(/border-t/);
});

test("prints extended information once while collapsed", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(FORM_ROUTE, { waitUntil: "domcontentloaded" });

const section = page.getByRole("region", { name: "Form information" });
const trigger = section.getByRole("button", { name: "Does not authorise" });
const preview = "Psychiatric treatment or detention beyond the linked authority.";

await expect(trigger).toBeVisible({ timeout: 20_000 });
const panelId = await trigger.getAttribute("aria-controls");
if (!panelId) throw new Error("Disclosure trigger is missing aria-controls");

const panel = page.locator(`#${panelId}`);
const disclosure = panel.locator("xpath=..");
const triggerPreview = disclosure.locator('button span[aria-hidden="true"]').filter({ hasText: preview });

await expect(triggerPreview).toHaveCount(1);
await expect(triggerPreview).toBeVisible();
await expect(panel).toBeHidden();

await page.emulateMedia({ media: "print" });

await expect(triggerPreview).toBeHidden();
await expect(panel.getByText(preview, { exact: true })).toBeVisible();
await expect
.poll(
() =>
disclosure.getByText(preview, { exact: true }).evaluateAll(
(elements) =>
elements.filter((element) => {
const style = getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden" && element.getClientRects().length > 0;
}).length,
),
{ message: "extended description should print exactly once" },
)
.toBe(1);
});

test("opens a section sheet listing the anchors the form record actually paints", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(FORM_ROUTE, { waitUntil: "domcontentloaded" });
Expand Down
6 changes: 4 additions & 2 deletions tests/ui-v2-components.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1251,11 +1251,11 @@ describe("Disclosure — print", () => {
);

const panel = screen.getByTestId("disclosure").querySelector('[role="region"]');
expect(panel).toHaveAttribute("hidden");
expect(panel).not.toHaveAttribute("hidden");
expect(panel).toHaveAttribute("data-open", "false");
// On paper there is no control to open, so a collapsed section would print
// as though the guideline never mentioned it — undetectably.
expect(panel?.className).toContain("print:block");
expect(panel).toHaveClass("hidden", "print:block");
});

it("keeps an open panel visible and still print-expanded", () => {
Expand All @@ -1267,6 +1267,8 @@ describe("Disclosure — print", () => {

const panel = screen.getByRole("region");
expect(panel).not.toHaveAttribute("hidden");
expect(panel).not.toHaveClass("hidden");
expect(panel).toHaveClass("print:block");
expect(panel).toHaveAttribute("data-open", "true");
});
});
Expand Down
Loading