From e42673bbd1892fa32c1af8f9950a2a67f874c282 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Fri, 21 Aug 2026 02:24:09 +0800
Subject: [PATCH 1/2] fix(ui): improve search, image, and table semantics
---
src/app/globals.css | 6 +++++
.../master-search-header.tsx | 1 +
.../non-pdf-source-preview.tsx | 7 ++++--
.../factsheets/factsheet-detail-page.tsx | 14 +++++++++--
.../ward-management/ward-management-modes.tsx | 24 +++++++++----------
...cument-viewer-non-pdf-preview.dom.test.tsx | 5 ++++
tests/factsheet-detail-header.dom.test.tsx | 10 ++++++++
tests/mode-menu-prefetch.dom.test.tsx | 5 ++++
tests/ward-management.test.ts | 7 ++++++
9 files changed, 63 insertions(+), 16 deletions(-)
diff --git a/src/app/globals.css b/src/app/globals.css
index 0dd4d243f4..12f7efaff9 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1927,6 +1927,7 @@ summary::-webkit-details-marker {
}
.answer-footer-search-input {
+ appearance: none;
padding-inline: 0.35rem;
font-size: 16px;
font-weight: 560;
@@ -1934,6 +1935,11 @@ summary::-webkit-details-marker {
letter-spacing: 0;
}
+.answer-footer-search-input::-webkit-search-cancel-button,
+.answer-footer-search-input::-webkit-search-decoration {
+ appearance: none;
+}
+
/* The universal composer form is the visible control and already owns its
focus-within border + halo. Suppress the nested text field's global focus
paint so the pill never renders competing or clipped concentric rings. */
diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx
index 32cac92f3f..85e7811c75 100644
--- a/src/components/clinical-dashboard/master-search-header.tsx
+++ b/src/components/clinical-dashboard/master-search-header.tsx
@@ -2036,6 +2036,7 @@ export function MasterSearchHeader({
pr-* utility, which let text run under an overlaid button. */}
-
+
{/* Eager, not lazy: for an image-source document this *is* the document,
and it sits above the fold. Lazy-loading it puts the largest element
on the page behind the browser's own lazy threshold and delays LCP
@@ -168,7 +171,7 @@ function InlineImagePreview({
decoding="async"
fetchPriority="high"
onError={() => setFailed(true)}
- className="mx-auto max-h-[min(70vh,36rem)] w-full object-contain"
+ className="h-full w-full object-contain"
/>
{block.items.map((fact) => (
-
+
{fact.k}
-
+
{fact.v}
diff --git a/src/components/ward-management/ward-management-modes.tsx b/src/components/ward-management/ward-management-modes.tsx
index 9b0d1e6ef5..67b3b636af 100644
--- a/src/components/ward-management/ward-management-modes.tsx
+++ b/src/components/ward-management/ward-management-modes.tsx
@@ -439,13 +439,13 @@ function QueueView({ role }: { role: WardRole }) {
- Patient
- Priority
- Wait
- Need
- Health service
- Blocker
- Top candidate
+ Patient
+ Priority
+ Wait
+ Need
+ Health service
+ Blocker
+ Top candidate
@@ -515,11 +515,11 @@ function CapacityView() {
- Unit
- Health service
- Capability cue
- Five bed states
- Freshness
+ Unit
+ Health service
+ Capability cue
+ Five bed states
+ Freshness
diff --git a/tests/document-viewer-non-pdf-preview.dom.test.tsx b/tests/document-viewer-non-pdf-preview.dom.test.tsx
index 768dbd49f0..147cf82637 100644
--- a/tests/document-viewer-non-pdf-preview.dom.test.tsx
+++ b/tests/document-viewer-non-pdf-preview.dom.test.tsx
@@ -34,6 +34,11 @@ describe("DocumentViewer non-PDF image preview", () => {
);
expect(screen.queryByTestId("image-lightbox")).not.toBeInTheDocument();
+ expect(screen.getByTestId("non-pdf-image-stage")).toHaveClass(
+ "h-[clamp(16rem,60vh,36rem)]",
+ "sm:h-[clamp(18rem,60vh,36rem)]",
+ );
+ expect(screen.getByRole("img", { name: "Clinical chart" })).toHaveClass("h-full", "w-full", "object-contain");
fireEvent.click(screen.getByRole("button", { name: "Expand image: Clinical chart" }));
expect(await screen.findByTestId("image-lightbox")).toBeInTheDocument();
expect(screen.getByTestId("image-lightbox-stage")).toHaveAttribute("data-source-mode", "url");
diff --git a/tests/factsheet-detail-header.dom.test.tsx b/tests/factsheet-detail-header.dom.test.tsx
index 43fa06631e..b48865ad3b 100644
--- a/tests/factsheet-detail-header.dom.test.tsx
+++ b/tests/factsheet-detail-header.dom.test.tsx
@@ -59,6 +59,16 @@ describe("factsheet detail header", () => {
expect(within(printPortal as HTMLElement).getAllByRole("heading", { level: 2 }).length).toBeGreaterThan(0);
});
+ it("associates printable fact values with row headers", () => {
+ renderFactsheet("sertraline");
+ const printPortal = document.querySelector(".factsheet-print-portal");
+ expect(printPortal).not.toBeNull();
+
+ const rowHeaders = within(printPortal as HTMLElement).getAllByRole("rowheader");
+ expect(rowHeaders.length).toBeGreaterThan(0);
+ for (const rowHeader of rowHeaders) expect(rowHeader).toHaveAttribute("scope", "row");
+ });
+
it("names the way back without spending the row on its label", () => {
renderFactsheet("sertraline");
const back = screen.getByRole("link", { name: "Back to all factsheets" });
diff --git a/tests/mode-menu-prefetch.dom.test.tsx b/tests/mode-menu-prefetch.dom.test.tsx
index 1a12cf09d1..229dfc3890 100644
--- a/tests/mode-menu-prefetch.dom.test.tsx
+++ b/tests/mode-menu-prefetch.dom.test.tsx
@@ -90,6 +90,11 @@ describe("mode menu destination prefetch", () => {
window.localStorage.clear();
});
+ it("exposes the shared composer as a semantic search input", () => {
+ render( );
+ expect(screen.getByTestId("global-search-input")).toHaveAttribute("type", "search");
+ });
+
it("keeps calculator submission enabled when document data is unavailable", async () => {
const user = userEvent.setup();
const onAsk = vi.fn();
diff --git a/tests/ward-management.test.ts b/tests/ward-management.test.ts
index b81f490e8f..14f8e91805 100644
--- a/tests/ward-management.test.ts
+++ b/tests/ward-management.test.ts
@@ -7,6 +7,8 @@ import { movementById, wardMovements } from "../src/components/ward-management/w
import { NOW_ANCHOR, allUnits } from "../src/components/ward-management/ward-sites";
import { toolCatalogRecordById } from "../src/lib/tools-catalog";
+const modesSource = readFileSync("src/components/ward-management/ward-management-modes.tsx", "utf8");
+
/**
* The mode strip renders one literal ` ` per view so
* `tests/route-reachability.test.ts` can find the hrefs by static AST scan. Reading the
@@ -23,6 +25,11 @@ function routeFileFor(href: string) {
}
describe("Ward Flow synthetic prototype", () => {
+ it("declares every queue and capacity header as a column header", () => {
+ expect(modesSource.match(//g)).toHaveLength(12);
+ expect(modesSource).not.toMatch(/ /);
+ });
+
it("keeps the production route reachable from the Tools catalogue", () => {
expect(toolCatalogRecordById("ward-management").href).toBe("/ward-management");
});
From f01f7e5f27d425da132dc62df564a4a085e27635 Mon Sep 17 00:00:00 2001
From: BigSimmo
Date: Fri, 21 Aug 2026 07:34:22 +0000
Subject: [PATCH 2/2] fix(search): keep the query when Escape dismisses the
command dropdown
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Switching the composer input to `type="search"` handed Escape to Chromium's
native "clear the search field" gesture. The command-surface handler closed the
dropdown and returned without preventing the default, so a reader dismissing the
suggestions also lost everything they had typed.
Escape on this input means "dismiss the dropdown" and nothing else, so the
handler now suppresses the native behaviour. The event still bubbles, so every
other Escape owner is unaffected.
Also assert the printable factsheet row headers per row rather than over the set
of headers that happen to exist — a fact row rendered with no row header would
have passed the old assertion while leaving its value unassociated.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01WeLU8jf7tffA2es9Pk6uSn
---
.../universal-search-command-surface.tsx | 5 +++++
tests/factsheet-detail-header.dom.test.tsx | 13 ++++++++++---
tests/ui-universal-search.spec.ts | 16 ++++++++++++++++
3 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx
index 7bb6253a74..12f269ce38 100644
--- a/src/components/clinical-dashboard/universal-search-command-surface.tsx
+++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx
@@ -996,6 +996,11 @@ export function UniversalSearchCommandSurface({
return;
}
if (event.key === "Escape") {
+ // The composer input is `type="search"`, so Chromium treats Escape as the
+ // native "clear the search field" gesture and wipes the query (dispatching
+ // an input event) on the way out. Escape here means "dismiss the dropdown"
+ // and nothing else, so suppress the native behaviour and keep the query.
+ event.preventDefault();
onDropdownOpenChange(false);
setActiveIndex(-1);
return;
diff --git a/tests/factsheet-detail-header.dom.test.tsx b/tests/factsheet-detail-header.dom.test.tsx
index b48865ad3b..d0e72818e3 100644
--- a/tests/factsheet-detail-header.dom.test.tsx
+++ b/tests/factsheet-detail-header.dom.test.tsx
@@ -64,9 +64,16 @@ describe("factsheet detail header", () => {
const printPortal = document.querySelector(".factsheet-print-portal");
expect(printPortal).not.toBeNull();
- const rowHeaders = within(printPortal as HTMLElement).getAllByRole("rowheader");
- expect(rowHeaders.length).toBeGreaterThan(0);
- for (const rowHeader of rowHeaders) expect(rowHeader).toHaveAttribute("scope", "row");
+ // Assert per row, not over the set of headers that happen to exist: a fact row
+ // rendered with no row header at all would leave its value unassociated in a
+ // screen reader, and a headers-only assertion passes straight over it.
+ const rows = within(printPortal as HTMLElement).getAllByRole("row");
+ expect(rows.length).toBeGreaterThan(0);
+ for (const row of rows) {
+ const rowHeaders = within(row).getAllByRole("rowheader");
+ expect(rowHeaders).toHaveLength(1);
+ expect(rowHeaders[0]).toHaveAttribute("scope", "row");
+ }
});
it("names the way back without spending the row on its label", () => {
diff --git a/tests/ui-universal-search.spec.ts b/tests/ui-universal-search.spec.ts
index 05da62d9ae..debf6acab5 100644
--- a/tests/ui-universal-search.spec.ts
+++ b/tests/ui-universal-search.spec.ts
@@ -208,6 +208,22 @@ test.describe("universal search typeahead", () => {
expect(await input.getAttribute("aria-activedescendant")).toBeNull();
});
+ test("Escape dismisses the dropdown without erasing the typed query", async ({ page }) => {
+ // The composer input is `type="search"`, whose native Chromium Escape gesture
+ // clears the field. Escape must only dismiss the dropdown; the query stays put
+ // so a reader can reopen or edit it instead of retyping from scratch.
+ await mockUniversalSearch(page);
+ const input = await openComposer(page);
+ await input.fill("acamprosate");
+
+ const listbox = page.getByRole("listbox", { name: "Documents search suggestions" });
+ await expect(listbox).toBeVisible();
+
+ await input.press("Escape");
+ await expect(listbox).toBeHidden();
+ await expect(input).toHaveValue("acamprosate");
+ });
+
test("does not count document-only hits as visible Medication rows", async ({ page }) => {
await page.route(/\/api\/search\/universal(?:\?.*)?$/, async (route) => {
await fulfillUniversalSearch(route, {