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:
Marc-André Bombeck committed 2026-07-08 00:03:04 +02:00
1 parent 71698f3248
commit 4b50ac8b6a
8 files changed
+66 -23

No files matched your search

+17 -10
View File
@@ -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");
-1
View File
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Dokumente durchsuchen",
"searchPlaceholder": "Titel oder Dateiname suchen…",
"groupLabel": "Dokumente filtern",
"typeGroup": "Dokumenttyp",
"conditionGroup": "Erkrankung",
"yearGroup": "Jahr",
-1
View File
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Search documents",
"searchPlaceholder": "Search title or filename…",
"groupLabel": "Filter documents",
"typeGroup": "Document type",
"conditionGroup": "Condition",
"yearGroup": "Year",
-1
View File
@@ -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",
-1
View File
@@ -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",
-1
View File
@@ -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",
-1
View File
@@ -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