From fab3f4a28063d949051b1a2065c06f731b25d403 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Sun, 30 Aug 2026 14:54:46 +0000 Subject: [PATCH 1/3] test(app): cover coarse-pointer tab close for BM-67 --- apps/app/src/components/ui/tab-pill.test.tsx | 96 ++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/apps/app/src/components/ui/tab-pill.test.tsx b/apps/app/src/components/ui/tab-pill.test.tsx index d0fd0c2277..43d7b226a6 100644 --- a/apps/app/src/components/ui/tab-pill.test.tsx +++ b/apps/app/src/components/ui/tab-pill.test.tsx @@ -6,6 +6,39 @@ import { TabPill } from "./tab-pill"; afterEach(cleanup); +const EXPECTED_CLOSE_BUTTON_CLASS = + "pointer-events-none absolute left-1.5 top-1/2 z-10 -translate-y-1/2 inline-flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted-foreground/15 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none max-md:pointer-coarse:size-5 opacity-0 hover:opacity-100 group-hover/tab-pill:pointer-events-auto group-hover/tab-pill:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100 disabled:opacity-30 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100"; +const EXPECTED_CLOSABLE_LEADING_VISUAL_CLASS = + "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 max-md:pointer-coarse:size-5 max-md:pointer-coarse:[&_svg]:size-5 mr-1.5 group-hover/tab-pill:opacity-0 tab-pill-close-focus-visible:opacity-0 pointer-coarse:opacity-0"; +const EXPECTED_LEADING_VISUAL_CLASS = + "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 max-md:pointer-coarse:size-5 max-md:pointer-coarse:[&_svg]:size-5 mr-1.5"; + +function getLeadingVisual() { + const leadingVisual = screen.getByTestId("leading-visual").parentElement; + if (!leadingVisual) { + throw new Error("Expected a leading visual wrapper"); + } + return leadingVisual; +} + +function renderClosableTab() { + render( + B} + title="Browser" + isActive={false} + onSelect={vi.fn()} + closeAction={{ onClose: vi.fn(), closeLabel: "Close Browser" }} + />, + ); + + return { + closeButton: screen.getByRole("button", { name: "Close Browser" }), + leadingVisual: getLeadingVisual(), + }; +} + describe("TabPill", () => { it("keeps an icon-only tab reachable by its accessible name", () => { render( @@ -43,4 +76,67 @@ describe("TabPill", () => { .getAttribute("aria-pressed"), ).toBe("false"); }); + + it("reveals a close action for any coarse pointer", () => { + const { closeButton } = renderClosableTab(); + + expect(closeButton.className).toBe(EXPECTED_CLOSE_BUTTON_CLASS); + expect(closeButton.classList).toContain("pointer-coarse:opacity-100"); + expect(closeButton.classList).toContain( + "pointer-coarse:pointer-events-auto", + ); + }); + + it("hides a closable leading visual for any coarse pointer", () => { + const { leadingVisual } = renderClosableTab(); + + expect(leadingVisual.className).toBe( + EXPECTED_CLOSABLE_LEADING_VISUAL_CLASS, + ); + expect(leadingVisual.classList).toContain("pointer-coarse:opacity-0"); + }); + + it("keeps the fine-pointer close reveal classes", () => { + const { closeButton } = renderClosableTab(); + + expect(closeButton.classList).toContain("hover:opacity-100"); + expect(closeButton.classList).toContain( + "group-hover/tab-pill:pointer-events-auto", + ); + expect(closeButton.classList).toContain( + "group-hover/tab-pill:opacity-100", + ); + }); + + it("does not width-gate close visibility or pointer interaction", () => { + const { closeButton, leadingVisual } = renderClosableTab(); + + expect(closeButton.classList).not.toContain( + "max-md:pointer-coarse:pointer-events-auto", + ); + expect(closeButton.classList).not.toContain( + "max-md:pointer-coarse:opacity-100", + ); + expect(leadingVisual.classList).not.toContain( + "max-md:pointer-coarse:opacity-0", + ); + }); + + it("keeps a leading visual visible without a close action", () => { + render( + B} + title="Browser" + isActive={false} + onSelect={vi.fn()} + closeAction={null} + />, + ); + + const leadingVisual = getLeadingVisual(); + + expect(leadingVisual.className).toBe(EXPECTED_LEADING_VISUAL_CLASS); + expect(leadingVisual.classList).not.toContain("pointer-coarse:opacity-0"); + }); }); From a17bfa92d3c27bc7a68ad10b0af8183a8fba9f4c Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Sun, 30 Aug 2026 14:57:50 +0000 Subject: [PATCH 2/3] fix(app): expose tab close on coarse pointers for BM-67 --- apps/app/src/components/ui/tab-pill.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/src/components/ui/tab-pill.tsx b/apps/app/src/components/ui/tab-pill.tsx index e268508d5f..bc7cc4ebc8 100644 --- a/apps/app/src/components/ui/tab-pill.tsx +++ b/apps/app/src/components/ui/tab-pill.tsx @@ -9,7 +9,7 @@ const TAB_PILL_DEFAULT_LABEL_MAX_WIDTH_CLASS = "max-w-[180px]"; const TAB_PILL_AFFORDANCE_BUTTON_BASE_CLASS = "inline-flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted-foreground/15 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none max-md:pointer-coarse:size-5"; const TAB_PILL_AFFORDANCE_ICON_CLASS = "size-3.5 max-md:pointer-coarse:size-5"; -const TAB_PILL_CLOSE_BUTTON_CLASS = `pointer-events-none absolute left-1.5 top-1/2 z-10 -translate-y-1/2 ${TAB_PILL_AFFORDANCE_BUTTON_BASE_CLASS} opacity-0 hover:opacity-100 group-hover/tab-pill:pointer-events-auto group-hover/tab-pill:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100 disabled:opacity-30 max-md:pointer-coarse:pointer-events-auto max-md:pointer-coarse:opacity-100`; +const TAB_PILL_CLOSE_BUTTON_CLASS = `pointer-events-none absolute left-1.5 top-1/2 z-10 -translate-y-1/2 ${TAB_PILL_AFFORDANCE_BUTTON_BASE_CLASS} opacity-0 hover:opacity-100 group-hover/tab-pill:pointer-events-auto group-hover/tab-pill:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100 disabled:opacity-30 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100`; const TAB_PILL_LEADING_VISUAL_CLASS = "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 max-md:pointer-coarse:size-5 max-md:pointer-coarse:[&_svg]:size-5"; @@ -75,7 +75,7 @@ export function TabPill({ TAB_PILL_LEADING_VISUAL_CLASS, !iconOnly && "mr-1.5", closeAction - ? "group-hover/tab-pill:opacity-0 tab-pill-close-focus-visible:opacity-0 max-md:pointer-coarse:opacity-0" + ? "group-hover/tab-pill:opacity-0 tab-pill-close-focus-visible:opacity-0 pointer-coarse:opacity-0" : null, )} > From 1a3e2b1068f1c7a814896530d62deb0c1c3474f1 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Sun, 30 Aug 2026 15:13:35 +0000 Subject: [PATCH 3/3] fix(app): ungate coarse-pointer tab sizing for BM-67 --- apps/app/src/components/ui/tab-pill.test.tsx | 29 ++++++++++++++------ apps/app/src/components/ui/tab-pill.tsx | 8 +++--- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/apps/app/src/components/ui/tab-pill.test.tsx b/apps/app/src/components/ui/tab-pill.test.tsx index 43d7b226a6..ab15313896 100644 --- a/apps/app/src/components/ui/tab-pill.test.tsx +++ b/apps/app/src/components/ui/tab-pill.test.tsx @@ -7,11 +7,11 @@ import { TabPill } from "./tab-pill"; afterEach(cleanup); const EXPECTED_CLOSE_BUTTON_CLASS = - "pointer-events-none absolute left-1.5 top-1/2 z-10 -translate-y-1/2 inline-flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted-foreground/15 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none max-md:pointer-coarse:size-5 opacity-0 hover:opacity-100 group-hover/tab-pill:pointer-events-auto group-hover/tab-pill:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100 disabled:opacity-30 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100"; + "pointer-events-none absolute left-1.5 top-1/2 z-10 -translate-y-1/2 inline-flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted-foreground/15 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none pointer-coarse:size-5 opacity-0 hover:opacity-100 group-hover/tab-pill:pointer-events-auto group-hover/tab-pill:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100 disabled:opacity-30 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100"; const EXPECTED_CLOSABLE_LEADING_VISUAL_CLASS = - "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 max-md:pointer-coarse:size-5 max-md:pointer-coarse:[&_svg]:size-5 mr-1.5 group-hover/tab-pill:opacity-0 tab-pill-close-focus-visible:opacity-0 pointer-coarse:opacity-0"; + "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 pointer-coarse:size-5 pointer-coarse:[&_svg]:size-5 mr-1.5 group-hover/tab-pill:opacity-0 tab-pill-close-focus-visible:opacity-0 pointer-coarse:opacity-0"; const EXPECTED_LEADING_VISUAL_CLASS = - "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 max-md:pointer-coarse:size-5 max-md:pointer-coarse:[&_svg]:size-5 mr-1.5"; + "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 pointer-coarse:size-5 pointer-coarse:[&_svg]:size-5 mr-1.5"; function getLeadingVisual() { const leadingVisual = screen.getByTestId("leading-visual").parentElement; @@ -33,10 +33,14 @@ function renderClosableTab() { />, ); - return { - closeButton: screen.getByRole("button", { name: "Close Browser" }), - leadingVisual: getLeadingVisual(), - }; + const closeButton = screen.getByRole("button", { name: "Close Browser" }); + const closeIcon = closeButton.querySelector("svg"); + const pill = closeButton.parentElement; + if (!closeIcon || !pill) { + throw new Error("Expected a close icon and pill container"); + } + + return { closeButton, closeIcon, leadingVisual: getLeadingVisual(), pill }; } describe("TabPill", () => { @@ -109,7 +113,7 @@ describe("TabPill", () => { }); it("does not width-gate close visibility or pointer interaction", () => { - const { closeButton, leadingVisual } = renderClosableTab(); + const { closeButton, closeIcon, leadingVisual, pill } = renderClosableTab(); expect(closeButton.classList).not.toContain( "max-md:pointer-coarse:pointer-events-auto", @@ -120,6 +124,15 @@ describe("TabPill", () => { expect(leadingVisual.classList).not.toContain( "max-md:pointer-coarse:opacity-0", ); + expect( + [closeButton, closeIcon, leadingVisual, pill].flatMap((element) => + [ + "max-md:pointer-coarse:size-5", + "max-md:pointer-coarse:[&_svg]:size-5", + "max-md:pointer-coarse:h-9", + ].filter((className) => element.classList.contains(className)), + ), + ).toEqual([]); }); it("keeps a leading visual visible without a close action", () => { diff --git a/apps/app/src/components/ui/tab-pill.tsx b/apps/app/src/components/ui/tab-pill.tsx index bc7cc4ebc8..4e63edb76a 100644 --- a/apps/app/src/components/ui/tab-pill.tsx +++ b/apps/app/src/components/ui/tab-pill.tsx @@ -7,11 +7,11 @@ import { CONTEXT_SELECTION_SURFACE_CLASS } from "./context-selection"; const TAB_PILL_DEFAULT_LABEL_MAX_WIDTH_CLASS = "max-w-[180px]"; const TAB_PILL_AFFORDANCE_BUTTON_BASE_CLASS = - "inline-flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted-foreground/15 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none max-md:pointer-coarse:size-5"; -const TAB_PILL_AFFORDANCE_ICON_CLASS = "size-3.5 max-md:pointer-coarse:size-5"; + "inline-flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted-foreground/15 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none pointer-coarse:size-5"; +const TAB_PILL_AFFORDANCE_ICON_CLASS = "size-3.5 pointer-coarse:size-5"; const TAB_PILL_CLOSE_BUTTON_CLASS = `pointer-events-none absolute left-1.5 top-1/2 z-10 -translate-y-1/2 ${TAB_PILL_AFFORDANCE_BUTTON_BASE_CLASS} opacity-0 hover:opacity-100 group-hover/tab-pill:pointer-events-auto group-hover/tab-pill:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100 disabled:opacity-30 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100`; const TAB_PILL_LEADING_VISUAL_CLASS = - "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 max-md:pointer-coarse:size-5 max-md:pointer-coarse:[&_svg]:size-5"; + "inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-3.5 pointer-coarse:size-5 pointer-coarse:[&_svg]:size-5"; interface TabPillCloseAction { onClose: () => void; @@ -51,7 +51,7 @@ export function TabPill({ return (