mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 04:52:07 +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);
|
expect((await page.locator("tbody tr").first().boundingBox())?.height).toBe(52);
|
||||||
await expect(page.getByText("Gateway Plumbing")).toBeVisible();
|
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 technicians")).toBeVisible();
|
||||||
await expect(page.getByText("2 active · 1 inactive")).toBeVisible();
|
await expect(page.getByText("2 active · 1 inactive")).toBeVisible();
|
||||||
await captureVisual(page, "/tmp/seahaven-vendor-implemented-list.png");
|
await captureVisual(page, "/tmp/seahaven-vendor-implemented-list.png");
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ function stopPropagation(event: MouseEvent): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopTradeBadgeKeyDown(event: KeyboardEvent): void {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
interface TruncatedTextProps {
|
interface TruncatedTextProps {
|
||||||
value: string;
|
value: string;
|
||||||
testId: string;
|
testId: string;
|
||||||
|
|
@ -132,7 +138,8 @@ interface VendorTableRowProps {
|
||||||
function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps) {
|
function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps) {
|
||||||
const trades = splitTrades(row.tradeSpecialties);
|
const trades = splitTrades(row.tradeSpecialties);
|
||||||
const primaryTrade = trades[0] ?? "—";
|
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 { locationText, phoneText } = getVendorDisplayValues(row);
|
||||||
const rowKey = String(row.id ?? `company-${row.companyId ?? ""}`);
|
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" }}>
|
<Stack direction="row" spacing={0.5} sx={{ alignItems: "center" }}>
|
||||||
<span>{primaryTrade}</span>
|
<span>{primaryTrade}</span>
|
||||||
{extraTrades > 0 && (
|
{extraTrades > 0 && (
|
||||||
<Box
|
<Tooltip title={omittedTrades.join(", ")} placement="top" arrow>
|
||||||
component="span"
|
<Box
|
||||||
sx={{
|
component="span"
|
||||||
borderRadius: "9999px",
|
tabIndex={0}
|
||||||
backgroundColor: "var(--color-bg-muted)",
|
aria-label={`+${extraTrades} more ${extraTrades === 1 ? "trade" : "trades"}: ${omittedTrades.join(", ")}`}
|
||||||
px: 0.75,
|
data-testid="vendor-trade-badge"
|
||||||
py: 0.25,
|
className="focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[var(--color-primary)]"
|
||||||
fontSize: 10.5,
|
onClick={stopPropagation}
|
||||||
lineHeight: 1.2,
|
onKeyDown={stopTradeBadgeKeyDown}
|
||||||
}}
|
sx={{
|
||||||
>
|
borderRadius: "9999px",
|
||||||
+{extraTrades}
|
backgroundColor: "var(--color-bg-muted)",
|
||||||
</Box>
|
px: 0.75,
|
||||||
|
py: 0.25,
|
||||||
|
fontSize: 10.5,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+{extraTrades}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</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", () => {
|
it("reports the filtered totalCount in the footer when page rows are fewer than total", () => {
|
||||||
setupDefaults();
|
setupDefaults();
|
||||||
const pageVendors = Array.from({ length: 3 }, (_, index) => ({
|
const pageVendors = Array.from({ length: 3 }, (_, index) => ({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue