diff --git a/e2e/vendors/vendors.spec.ts b/e2e/vendors/vendors.spec.ts
index bd55400c..7c838f06 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..3e851435 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;
@@ -129,10 +135,54 @@ interface VendorTableRowProps {
onOpenEdit: (row: VendorListItem) => void;
}
+function VendorTradeCell({
+ primaryTrade,
+ omittedTrades,
+}: {
+ primaryTrade: string;
+ omittedTrades: string[];
+}) {
+ const extraTrades = omittedTrades.length;
+ return (
+
+ {primaryTrade === "—" ? (
+ "—"
+ ) : (
+
+ {primaryTrade}
+ {extraTrades > 0 && (
+
+
+ +{extraTrades}
+
+
+ )}
+
+ )}
+
+ );
+}
+
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 { locationText, phoneText } = getVendorDisplayValues(row);
const rowKey = String(row.id ?? `company-${row.companyId ?? ""}`);
@@ -197,30 +247,7 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps)
color="var(--color-muted-foreground)"
/>
-
- {primaryTrade === "—" ? (
- "—"
- ) : (
-
- {primaryTrade}
- {extraTrades > 0 && (
-
- +{extraTrades}
-
- )}
-
- )}
-
+
{
);
});
+ 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) => ({