From f8ce39e32b2402290572577e19a76919363d2132 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 13:26:32 +0000 Subject: [PATCH 1/4] fix(search-band): drop the sort segments from the phone results bar The shared results band puts count, query, sort and Filter on one line. On a phone the two sort segments ("Relevance" / "A-Z") take roughly half of that line, so the query truncates to pay for a control that is set about once a session and already defaults to the order a phone reader wants. ResultSortControl is now sm-and-up. Only the affordance moves: `?sort=` still carries an alpha order onto a phone from a link or a wider session, and the results honour it. The display class lives in the component's own base string because `cn` here is a plain join with no Tailwind conflict resolution, so a caller's `hidden` would resolve by stylesheet order rather than by intent. Pages whose only utility is sort (forms) would have kept an empty utilities group mounted on a phone -- and in `inline` placement below 414px that child is `w-full basis-full`, i.e. a blank second line. Guard it with hasPhoneUtilities. Tests move rather than disappear: ui-smoke asserts the group is hidden at 390px and exercises the A-Z/Relevance interaction at the 1440px viewport the same test already resizes to, and the two ui-tools differentials phone tests assert hidden while keeping the 44px tap floor on the page filter. The 320-540px clip sweep measured the sort node by name, which would have measured a display:none element and passed blindly at every width; it now measures the last rendered control. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013mcqh46SDdiR5KxScihkFQ --- docs/search-chrome-behaviour.md | 14 ++++-- .../search-results-header-band.tsx | 33 ++++++++++++-- tests/ui-accessibility.spec.ts | 7 +-- tests/ui-smoke.spec.ts | 43 ++++++++++++------- tests/ui-tools.spec.ts | 30 ++++++------- 5 files changed, 85 insertions(+), 42 deletions(-) diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md index 8765adbc31..2c3170abc1 100644 --- a/docs/search-chrome-behaviour.md +++ b/docs/search-chrome-behaviour.md @@ -79,9 +79,17 @@ chrome and changes to it land on every mode at once. Keep these rules: it appears. The `role="status"` / `aria-live="polite"` announcement stays either way — except while faulted, when the spine goes `aria-live="off"` and the fault panel's `role="alert"` makes the single announcement instead of both speaking. -3. **Sort is a segmented control, not a select.** Two values do not justify a menu you must open - to read. `ResultSortControl` renders `sortOptions` as `aria-pressed` buttons inside a - `role="group"` named "Sort results"; add a third order only if it still fits the rail. +3. **Sort is a segmented control, not a select — and it is `sm`-and-up.** Two values do not + justify a menu you must open to read. `ResultSortControl` renders `sortOptions` as + `aria-pressed` buttons inside a `role="group"` named "Sort results"; add a third order only if + it still fits the rail. Below 640px it is `hidden`: the two segments cost roughly half the + band's one line, and the query truncated to pay for a control set about once a session. Only + the affordance is `sm`-and-up — `?sort=` still carries an alpha order onto a phone and the + results honour it. The display class belongs in the component's own base string, because `cn` + is a plain join with no Tailwind conflict resolution. A page whose only utility is sort hides + the whole utilities group below `sm` (`hasPhoneUtilities`) rather than leaving an empty flex + child — in `inline` placement under 414px that child is `w-full basis-full`, i.e. a blank + second line. 4. **Native selects are pinned to 16px below `sm`.** The unlayered iOS anti-zoom rule in `globals.css` ("Interactive element defaults") deliberately beats Tailwind's `text-*` utilities on `input`/`select`/`textarea`. Do not fight it with `!important` or a per-call-site diff --git a/src/components/clinical-dashboard/search-results-header-band.tsx b/src/components/clinical-dashboard/search-results-header-band.tsx index 94e168b75f..b86520a55d 100644 --- a/src/components/clinical-dashboard/search-results-header-band.tsx +++ b/src/components/clinical-dashboard/search-results-header-band.tsx @@ -65,7 +65,9 @@ function singularNoun(plural: string) { } /** Sort is a two-state choice, so it reads as a segmented control rather than a - select: a dropdown over two values makes you open a menu to learn nothing. */ + select: a dropdown over two values makes you open a menu to learn nothing. + It is a `sm`-and-up control — see `ResultSortControl` for why the phone line + drops it. */ const sortOptions: ReadonlyArray<{ value: ResultSortValue; label: string }> = [ { value: "relevance", label: "Relevance" }, { value: "alpha", label: "A–Z" }, @@ -259,6 +261,14 @@ export function SearchResultsHeaderBand({ const hasUtilities = Boolean( onSortChange || onViewChange || onSaveSearch || utilityControls || pageMobileControls || (partial && onRetry), ); + // Sort is the one utility that does not render below `sm`. A page whose only + // utility is sort would otherwise keep this group mounted on a phone as an + // empty flex child — and in `inline` placement below 414px that child is + // `w-full basis-full`, so it would take a whole empty second line under the + // count. Hide the group itself there instead of leaving a ghost row. + const hasPhoneUtilities = Boolean( + onViewChange || onSaveSearch || utilityControls || pageMobileControls || (partial && onRetry), + ); // See `mobileControlsPlacement`: absent a declaration, a page control is // assumed to be a full-width select and keeps its own row, and a band with no // page control collapses to one line. @@ -428,7 +438,8 @@ export function SearchResultsHeaderBand({