From 0b4dafc868edee216360b2ecd767a7d721ffc7ad Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Mon, 14 Sep 2026 12:26:05 -0300 Subject: [PATCH] fix(vendors): add accessible trade overflow tooltip --- e2e/vendors/vendors.spec.ts | 11 ++- .../vendors/_components/vendors-table.tsx | 43 ++++++--- .../(protected)/vendors/vendors-list.test.tsx | 93 +++++++++++++++++++ 3 files changed, 132 insertions(+), 15 deletions(-) diff --git a/e2e/vendors/vendors.spec.ts b/e2e/vendors/vendors.spec.ts index e8f54bef..bf98e8c5 100644 --- a/e2e/vendors/vendors.spec.ts +++ b/e2e/vendors/vendors.spec.ts @@ -409,7 +409,16 @@ test.describe("Vendor directory prototype parity", () => { ); expect((await page.locator("tbody tr").first().boundingBox())?.height).toBe(52); await expect(page.getByText("Gateway Plumbing")).toBeVisible(); - await expect(page.getByText("+1")).toBeVisible(); + const tradeBadge = page.getByTestId("vendor-trade-badge").first(); + await expect(tradeBadge).toHaveText("+1"); + await tradeBadge.hover(); + await expect(page.getByRole("tooltip")).toHaveText("Backflow Preventers"); + await tradeBadge.focus(); + await expect(tradeBadge).toBeFocused(); + await tradeBadge.click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await tradeBadge.press("Enter"); + await expect(page.getByRole("dialog")).toHaveCount(0); await expect(page.getByText("2 active technicians")).toBeVisible(); await expect(page.getByText("2 active · 1 inactive")).toBeVisible(); await captureVisual(page, "/tmp/seahaven-vendor-implemented-list.png"); diff --git a/src/app/(protected)/vendors/_components/vendors-table.tsx b/src/app/(protected)/vendors/_components/vendors-table.tsx index e2651e39..de239409 100644 --- a/src/app/(protected)/vendors/_components/vendors-table.tsx +++ b/src/app/(protected)/vendors/_components/vendors-table.tsx @@ -51,6 +51,12 @@ function stopPropagation(event: MouseEvent): void { event.stopPropagation(); } +function stopTradeBadgeKeyDown(event: KeyboardEvent): void { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + event.stopPropagation(); +} + interface TruncatedTextProps { value: string; testId: string; @@ -132,7 +138,8 @@ interface VendorTableRowProps { function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps) { const trades = splitTrades(row.tradeSpecialties); const primaryTrade = trades[0] ?? "—"; - const extraTrades = trades.length > 1 ? trades.length - 1 : 0; + const omittedTrades = trades.slice(1); + const extraTrades = omittedTrades.length; const { locationText, phoneText } = getVendorDisplayValues(row); const rowKey = String(row.id ?? `company-${row.companyId ?? ""}`); @@ -204,19 +211,27 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps) {primaryTrade} {extraTrades > 0 && ( - - +{extraTrades} - + + + +{extraTrades} + + )} )} diff --git a/src/test/app/(protected)/vendors/vendors-list.test.tsx b/src/test/app/(protected)/vendors/vendors-list.test.tsx index b3572218..dd0070b8 100644 --- a/src/test/app/(protected)/vendors/vendors-list.test.tsx +++ b/src/test/app/(protected)/vendors/vendors-list.test.tsx @@ -244,6 +244,99 @@ describe("VendorsListPage", () => { ); }); + it("shows omitted trade names on the +N badge tooltip on hover and keyboard focus", async () => { + setupDefaults(); + const multiTradeVendor: VendorListItem = { + ...activeVendor, + tradeSpecialties: "Backflow Preventers, Water Heaters, Drain Cleaning", + }; + useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => { + if (params.pageSize === 1) return result([], 1); + return params.isActive ? result([multiTradeVendor], 1) : result([], 0); + }); + + renderWithProviders(, { route: "/vendors", withAuth: false }); + + const badge = screen.getByTestId("vendor-trade-badge"); + expect(badge).toHaveTextContent("+2"); + expect(screen.getByText("Backflow Preventers")).toBeInTheDocument(); + expect(badge).toHaveAttribute("aria-label", "+2 more trades: Water Heaters, Drain Cleaning"); + + await userEvent.hover(badge); + const tooltip = await screen.findByRole("tooltip"); + expect(tooltip).toHaveTextContent("Water Heaters, Drain Cleaning"); + expect(tooltip).not.toHaveTextContent(/Backflow/); + await userEvent.unhover(badge); + await waitFor(() => expect(screen.queryByRole("tooltip")).not.toBeInTheDocument()); + + fireEvent.keyDown(document, { key: "Tab" }); + badge.focus(); + expect(badge).toHaveFocus(); + expect(await screen.findByRole("tooltip")).toHaveTextContent("Water Heaters, Drain Cleaning"); + }); + + it("does not show the +N badge when the vendor has a single trade", () => { + setupDefaults(); + useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => { + if (params.pageSize === 1) return result([], 1); + return params.isActive ? result([activeVendor], 1) : result([], 0); + }); + + renderWithProviders(, { route: "/vendors", withAuth: false }); + + expect(screen.queryByTestId("vendor-trade-badge")).not.toBeInTheDocument(); + expect(screen.getByText("Backflow Preventers")).toBeInTheDocument(); + }); + + it("does not open the vendor detail when the +N badge is clicked or activated by keyboard", async () => { + setupDefaults(); + useVendorCompanyRoster.mockReturnValue({ + data: activeRoster, + isLoading: false, + isError: false, + error: null, + refetch: vi.fn(), + }); + const multiTradeVendor: VendorListItem = { + ...activeVendor, + tradeSpecialties: "Backflow Preventers, Water Heaters, Drain Cleaning", + }; + useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => { + if (params.pageSize === 1) return result([], 1); + return params.isActive ? result([multiTradeVendor], 1) : result([], 0); + }); + + renderWithProviders(, { route: "/vendors", withAuth: false }); + + const badge = screen.getByTestId("vendor-trade-badge"); + + await userEvent.click(badge); + expect(screen.queryByRole("heading", { level: 2, name: "Adam Whyte" })).not.toBeInTheDocument(); + + badge.focus(); + await userEvent.keyboard("{Enter}"); + await userEvent.keyboard("{Space}"); + expect(screen.queryByRole("heading", { level: 2, name: "Adam Whyte" })).not.toBeInTheDocument(); + }); + + it("relies on the MUI tooltip instead of a native title attribute on the +N badge", () => { + setupDefaults(); + const multiTradeVendor: VendorListItem = { + ...activeVendor, + tradeSpecialties: "Backflow Preventers, Water Heaters", + }; + useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => { + if (params.pageSize === 1) return result([], 1); + return params.isActive ? result([multiTradeVendor], 1) : result([], 0); + }); + + renderWithProviders(, { route: "/vendors", withAuth: false }); + + const badge = screen.getByTestId("vendor-trade-badge"); + expect(badge).not.toHaveAttribute("title"); + expect(badge).toHaveAttribute("tabIndex", "0"); + }); + it("reports the filtered totalCount in the footer when page rows are fewer than total", () => { setupDefaults(); const pageVendors = Array.from({ length: 3 }, (_, index) => ({