mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
fix(vendors): add accessible trade overflow tooltip
This commit is contained in:
parent
99df031f68
commit
0b4dafc868
3 changed files with 132 additions and 15 deletions
11
e2e/vendors/vendors.spec.ts
vendored
11
e2e/vendors/vendors.spec.ts
vendored
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
|||
<Stack direction="row" spacing={0.5} sx={{ alignItems: "center" }}>
|
||||
<span>{primaryTrade}</span>
|
||||
{extraTrades > 0 && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
borderRadius: "9999px",
|
||||
backgroundColor: "var(--color-bg-muted)",
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
fontSize: 10.5,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
+{extraTrades}
|
||||
</Box>
|
||||
<Tooltip title={omittedTrades.join(", ")} placement="top" arrow>
|
||||
<Box
|
||||
component="span"
|
||||
tabIndex={0}
|
||||
aria-label={`+${extraTrades} more ${extraTrades === 1 ? "trade" : "trades"}: ${omittedTrades.join(", ")}`}
|
||||
data-testid="vendor-trade-badge"
|
||||
className="focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[var(--color-primary)]"
|
||||
onClick={stopPropagation}
|
||||
onKeyDown={stopTradeBadgeKeyDown}
|
||||
sx={{
|
||||
borderRadius: "9999px",
|
||||
backgroundColor: "var(--color-bg-muted)",
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
fontSize: 10.5,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
+{extraTrades}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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(<VendorsListPage />, { 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(<VendorsListPage />, { 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(<VendorsListPage />, { 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(<VendorsListPage />, { 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) => ({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue