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) => ({