mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(documents): make filter dropdowns responsive and update the e2e flows
Give each facet trigger a leading icon and show the text label from sm up (and on phones only while the facet is active, truncated), so three facets plus the search never overflow a 360-430px row — the bar stays one line without a horizontal scroller. Drop the now-dead filter.groupLabel key and point the vault e2e flows at the dropdown menu items instead of the removed inline chips.
This commit is contained in:
1 parent
71698f3248
commit
4b50ac8b6a
8 files changed
+66
-23
No files matched your search
+17
-10
@@ -108,7 +108,7 @@ test.describe("document vault", () => {
|
||||
|
||||
// ── §1A — the 30-second doctor flow, three independent routes ─────────
|
||||
|
||||
test("doctor flow A: the type chip reaches the MRT report", async ({
|
||||
test("doctor flow A: the type filter reaches the MRT report", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/documents");
|
||||
@@ -116,7 +116,11 @@ test.describe("document vault", () => {
|
||||
page.getByRole("heading", { name: "Documents" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Imaging", exact: true }).click();
|
||||
// Open the type dropdown, tick Imaging (multi-select stays open), then
|
||||
// dismiss the menu.
|
||||
await page.getByRole("button", { name: "All types" }).click();
|
||||
await page.getByRole("menuitemcheckbox", { name: "Imaging" }).click();
|
||||
await page.keyboard.press("Escape");
|
||||
// Autumn 2025 sits below the newest sections — scroll the window down.
|
||||
await scrollUntilVisible(page, openButton(page, "MRT Knie"));
|
||||
await openButton(page, "MRT Knie").click();
|
||||
@@ -128,13 +132,15 @@ test.describe("document vault", () => {
|
||||
await expect(sheet.locator("iframe")).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
|
||||
test("doctor flow B: the condition chip reaches the MRT report", async ({
|
||||
test("doctor flow B: the condition filter reaches the MRT report", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/documents");
|
||||
// Open the condition dropdown and pick the linked episode (single-select
|
||||
// → the menu closes on choice).
|
||||
await page.locator('[data-slot="document-condition-filter"]').click();
|
||||
await page
|
||||
.locator('[data-slot="document-filter-bar"]')
|
||||
.getByRole("button", { name: "Knie", exact: true })
|
||||
.getByRole("menuitemcheckbox", { name: "Knie", exact: true })
|
||||
.click();
|
||||
|
||||
// The Knie filter narrows the corpus to the linked MRT trio.
|
||||
@@ -162,12 +168,13 @@ test.describe("document vault", () => {
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/documents?kind=IMAGING&year=2025");
|
||||
// A deep-linked facet surfaces on its dropdown trigger label.
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Imaging", exact: true }),
|
||||
).toHaveAttribute("aria-pressed", "true");
|
||||
page.locator('[data-slot="document-type-filter"]'),
|
||||
).toContainText("Imaging");
|
||||
await expect(
|
||||
page.getByRole("button", { name: "2025", exact: true }),
|
||||
).toHaveAttribute("aria-pressed", "true");
|
||||
page.locator('[data-slot="document-year-filter"]'),
|
||||
).toContainText("2025");
|
||||
await scrollUntilVisible(page, openButton(page, "MRT Knie"));
|
||||
|
||||
await page.goto(`/documents?episode=${KNIE_EPISODE_ID}`);
|
||||
@@ -345,7 +352,7 @@ test.describe("document vault", () => {
|
||||
await expect(openButton(page, "MRT Knie")).toBeVisible();
|
||||
|
||||
// Tab from the search into the grid's single roving slot (bounded walk
|
||||
// across the chip rail), then arrows move the active card.
|
||||
// across the filter dropdowns), then arrows move the active card.
|
||||
let reachedGrid = false;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await page.keyboard.press("Tab");
|
||||
|
||||
@@ -8044,7 +8044,6 @@
|
||||
"filter": {
|
||||
"searchLabel": "Dokumente durchsuchen",
|
||||
"searchPlaceholder": "Titel oder Dateiname suchen…",
|
||||
"groupLabel": "Dokumente filtern",
|
||||
"typeGroup": "Dokumenttyp",
|
||||
"conditionGroup": "Erkrankung",
|
||||
"yearGroup": "Jahr",
|
||||
|
||||
@@ -8044,7 +8044,6 @@
|
||||
"filter": {
|
||||
"searchLabel": "Search documents",
|
||||
"searchPlaceholder": "Search title or filename…",
|
||||
"groupLabel": "Filter documents",
|
||||
"typeGroup": "Document type",
|
||||
"conditionGroup": "Condition",
|
||||
"yearGroup": "Year",
|
||||
|
||||
@@ -8044,7 +8044,6 @@
|
||||
"filter": {
|
||||
"searchLabel": "Buscar documentos",
|
||||
"searchPlaceholder": "Buscar por título o nombre de archivo…",
|
||||
"groupLabel": "Filtrar documentos",
|
||||
"typeGroup": "Tipo de documento",
|
||||
"conditionGroup": "Afección",
|
||||
"yearGroup": "Año",
|
||||
|
||||
@@ -8044,7 +8044,6 @@
|
||||
"filter": {
|
||||
"searchLabel": "Rechercher des documents",
|
||||
"searchPlaceholder": "Rechercher un titre ou un nom de fichier…",
|
||||
"groupLabel": "Filtrer les documents",
|
||||
"typeGroup": "Type de document",
|
||||
"conditionGroup": "Affection",
|
||||
"yearGroup": "Année",
|
||||
|
||||
@@ -8044,7 +8044,6 @@
|
||||
"filter": {
|
||||
"searchLabel": "Cerca documenti",
|
||||
"searchPlaceholder": "Cerca titolo o nome file…",
|
||||
"groupLabel": "Filtra documenti",
|
||||
"typeGroup": "Tipo di documento",
|
||||
"conditionGroup": "Patologia",
|
||||
"yearGroup": "Anno",
|
||||
|
||||
@@ -8044,7 +8044,6 @@
|
||||
"filter": {
|
||||
"searchLabel": "Szukaj dokumentów",
|
||||
"searchPlaceholder": "Szukaj tytułu lub nazwy pliku…",
|
||||
"groupLabel": "Filtruj dokumenty",
|
||||
"typeGroup": "Typ dokumentu",
|
||||
"conditionGroup": "Schorzenie",
|
||||
"yearGroup": "Rok",
|
||||
|
||||
@@ -19,7 +19,15 @@
|
||||
* sticks below the top edge inside the shell's scroll container (sticky,
|
||||
* translucent background, border-b — no viewport-height tricks).
|
||||
*/
|
||||
import { ChevronDown, ScanSearch, Search, X } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
Calendar,
|
||||
ChevronDown,
|
||||
ListFilter,
|
||||
ScanSearch,
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { RefObject } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -42,6 +50,19 @@ const TRIGGER_ACTIVE = "border-primary/40 bg-primary/10 text-foreground";
|
||||
/** Shared trigger chrome: compact, fixed, never shrinks below its label. */
|
||||
const TRIGGER_CLASSES = "shrink-0 gap-1.5 font-normal";
|
||||
|
||||
/**
|
||||
* The facet label is the whole point of the trigger — but three text labels
|
||||
* plus the search will not fit a 360px phone. So the label shows from `sm`
|
||||
* up, and on a phone only when the facet is ACTIVE (truncated), while the
|
||||
* leading icon always carries the facet identity. The trailing chevron is
|
||||
* desktop-only chrome. Net: inactive phone triggers collapse to a single
|
||||
* icon, the row stays one line at every width, and the accessible name is
|
||||
* pinned via `aria-label` so an icon-only trigger still announces its state.
|
||||
*/
|
||||
function facetLabelClass(active: boolean): string {
|
||||
return cn("max-w-28 truncate", active ? "inline" : "hidden sm:inline");
|
||||
}
|
||||
|
||||
export interface ConditionChip {
|
||||
episodeId: string;
|
||||
name: string;
|
||||
@@ -150,10 +171,17 @@ export function DocumentFilterBar({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="document-type-filter"
|
||||
aria-label={typeLabel}
|
||||
className={cn(TRIGGER_CLASSES, kindCount > 0 && TRIGGER_ACTIVE)}
|
||||
>
|
||||
<span className="max-w-32 truncate">{typeLabel}</span>
|
||||
<ChevronDown className="size-3.5 opacity-60" aria-hidden />
|
||||
<ListFilter className="size-4 shrink-0" aria-hidden />
|
||||
<span className={facetLabelClass(kindCount > 0)}>
|
||||
{typeLabel}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className="hidden size-3.5 opacity-60 sm:inline"
|
||||
aria-hidden
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -191,13 +219,20 @@ export function DocumentFilterBar({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="document-condition-filter"
|
||||
aria-label={conditionLabel}
|
||||
className={cn(
|
||||
TRIGGER_CLASSES,
|
||||
activeCondition && TRIGGER_ACTIVE,
|
||||
)}
|
||||
>
|
||||
<span className="max-w-32 truncate">{conditionLabel}</span>
|
||||
<ChevronDown className="size-3.5 opacity-60" aria-hidden />
|
||||
<Activity className="size-4 shrink-0" aria-hidden />
|
||||
<span className={facetLabelClass(Boolean(activeCondition))}>
|
||||
{conditionLabel}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className="hidden size-3.5 opacity-60 sm:inline"
|
||||
aria-hidden
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -230,14 +265,21 @@ export function DocumentFilterBar({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="document-year-filter"
|
||||
aria-label={yearLabel}
|
||||
className={cn(
|
||||
TRIGGER_CLASSES,
|
||||
"tabular-nums",
|
||||
activeYear !== undefined && TRIGGER_ACTIVE,
|
||||
)}
|
||||
>
|
||||
<span>{yearLabel}</span>
|
||||
<ChevronDown className="size-3.5 opacity-60" aria-hidden />
|
||||
<Calendar className="size-4 shrink-0" aria-hidden />
|
||||
<span className={facetLabelClass(activeYear !== undefined)}>
|
||||
{yearLabel}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className="hidden size-3.5 opacity-60 sm:inline"
|
||||
aria-hidden
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
||||
Reference in new issue
Block a user