Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1927,13 +1927,19 @@ summary::-webkit-details-marker {
}

.answer-footer-search-input {
appearance: none;
padding-inline: 0.35rem;
font-size: 16px;
font-weight: 560;
line-height: 1.2;
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. */
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2036,6 +2036,7 @@ export function MasterSearchHeader({
pr-* utility, which let text run under an overlaid button. */}
<div className="flex min-w-0 flex-1 items-center overflow-hidden">
<input
type="search"
Comment thread
BigSimmo marked this conversation as resolved.
ref={bindQueryInputRef}
data-testid="global-search-input"
autoFocus={queryInputAutoFocus}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
7 changes: 5 additions & 2 deletions src/components/document-viewer/non-pdf-source-preview.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,7 +156,10 @@ function InlineImagePreview({

return (
<div className="flex flex-col items-center gap-3 bg-[color:var(--surface-inset)] p-2 sm:p-3">
<div className="relative w-full min-h-64 overflow-hidden bg-[color:var(--surface-inset)] sm:min-h-72">
<div
data-testid="non-pdf-image-stage"
className="relative h-[clamp(16rem,60vh,36rem)] w-full overflow-hidden bg-[color:var(--surface-inset)] sm:h-[clamp(18rem,60vh,36rem)]"
>
{/* 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
Expand All@@ -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"
/>
<button
ref={triggerRef}
Expand Down
14 changes: 12 additions & 2 deletions src/components/factsheets/factsheet-detail-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -808,9 +808,19 @@ function FactsheetPrintSheet({ factsheet, blocks }: { factsheet: Factsheet; bloc
<tbody>
{block.items.map((fact) => (
<tr key={fact.k}>
<td style={{ padding: "5px 8px", border: "1px solid #ddd", color: "#555", width: "40%" }}>
<th
scope="row"
style={{
padding: "5px 8px",
border: "1px solid #ddd",
color: "#555",
width: "40%",
textAlign: "left",
fontWeight: 400,
}}
>
{fact.k}
</td>
</th>
<td style={{ padding: "5px 8px", border: "1px solid #ddd", fontWeight: 600, color: "#111" }}>
{fact.v}
</td>
Expand Down
24 changes: 12 additions & 12 deletions src/components/ward-management/ward-management-modes.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -439,13 +439,13 @@ function QueueView({ role }: { role: WardRole }) {
<table className={styles.dataTable}>
<thead>
<tr>
<th>Patient</th>
<th>Priority</th>
<th>Wait</th>
<th>Need</th>
<th>Health service</th>
<th>Blocker</th>
<th>Top candidate</th>
<th scope="col">Patient</th>
<th scope="col">Priority</th>
<th scope="col">Wait</th>
<th scope="col">Need</th>
<th scope="col">Health service</th>
<th scope="col">Blocker</th>
<th scope="col">Top candidate</th>
</tr>
</thead>
<tbody>
Expand DownExpand Up@@ -515,11 +515,11 @@ function CapacityView() {
<table className={styles.dataTable}>
<thead>
<tr>
<th>Unit</th>
<th>Health service</th>
<th>Capability cue</th>
<th>Five bed states</th>
<th>Freshness</th>
<th scope="col">Unit</th>
<th scope="col">Health service</th>
<th scope="col">Capability cue</th>
<th scope="col">Five bed states</th>
<th scope="col">Freshness</th>
</tr>
</thead>
<tbody>
Expand Down
5 changes: 5 additions & 0 deletions tests/document-viewer-non-pdf-preview.dom.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand Down
17 changes: 17 additions & 0 deletions tests/factsheet-detail-header.dom.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,23 @@ 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();

// 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");
}
});
Comment thread
BigSimmo marked this conversation as resolved.

it("names the way back without spending the row on its label", () => {
renderFactsheet("sertraline");
const back = screen.getByRole("link", { name: "Back to all factsheets" });
Expand Down
5 changes: 5 additions & 0 deletions tests/mode-menu-prefetch.dom.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,6 +90,11 @@ describe("mode menu destination prefetch", () => {
window.localStorage.clear();
});

it("exposes the shared composer as a semantic search input", () => {
render(<MasterSearchHeader {...headerProps()} />);
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();
Expand Down
16 changes: 16 additions & 0 deletions tests/ui-universal-search.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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, {
Expand Down
7 changes: 7 additions & 0 deletions tests/ward-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 `<Link href="...">` per view so
* `tests/route-reachability.test.ts` can find the hrefs by static AST scan. Reading the
Expand All@@ -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(/<th scope="col">/g)).toHaveLength(12);
expect(modesSource).not.toMatch(/<th>/);
});

it("keeps the production route reachable from the Tools catalogue", () => {
expect(toolCatalogRecordById("ward-management").href).toBe("/ward-management");
});
Expand Down
Loading