diff --git a/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx index 6d78143e..e0932926 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/date-cell.tsx @@ -17,6 +17,8 @@ type DateCellProps = { defaultMode?: "week" | "date"; /** When set, shows Clear in the popover so schedule can be removed (e.g. day-group → Unscheduled). */ onClear?: () => void; + /** Screen-reader-only status appended inside the button so the accessible name keeps the visible date and adds durable state. */ + srStatusSuffix?: string; }; export function DateCell({ @@ -29,6 +31,7 @@ export function DateCell({ onSaveWeek, defaultMode, onClear, + srStatusSuffix, }: DateCellProps) { const [anchorEl, setAnchorEl] = useState(null); const [cellMode, setCellMode] = useState<"week" | "date">( @@ -81,6 +84,7 @@ export function DateCell({ )} + {Boolean(srStatusSuffix) && {srStatusSuffix}} void; }; @@ -17,11 +23,26 @@ type DueDateCellProps = { * Board / slide-over Due Date editor. * Emergency → dedicated calendar Dialog; other types → standard DateCell popover. */ -export function DueDateCell({ type, value, q, highlight, onSave }: DueDateCellProps) { +export function DueDateCell({ + type, + value, + q, + highlight, + pastDue = false, + onSave, +}: DueDateCellProps) { const [open, setOpen] = useState(false); if (type !== "Emergency") { - return ; + return ( + + ); } return ( @@ -43,7 +64,7 @@ export function DueDateCell({ type, value, q, highlight, onSave }: DueDateCellPr } : undefined } - aria-label="Due Date" + aria-label={pastDue ? "Due Date, past due" : "Due Date"} aria-haspopup="dialog" > diff --git a/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx b/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx index 3dad9c2d..a98552ae 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills.tsx @@ -51,14 +51,18 @@ export function CarriedOverCounterPill({ count }: { count: number }) { return null; } + const label = `Carried over ${count} times`; + return ( - - {count} - + + + {count} + + ); } diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx index bb904aaa..aac347e1 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx @@ -141,6 +141,7 @@ export function WoTableRowCells({ value={row.dueDate} q={search} highlight={forceDueDateHighlight || row.isPastDue} + pastDue={row.isPastDue} onSave={(date) => onPatchRow({ dueDate: date })} /> diff --git a/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx b/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx new file mode 100644 index 00000000..277f4a73 --- /dev/null +++ b/src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx @@ -0,0 +1,34 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { + CarriedOverCounterPill, + RescheduleCounterPill, +} from "@/app/(protected)/workorders/_components/list/table/cells/wo-counter-pills"; +import { renderWithProviders } from "@/test/test-utils"; + +describe("CarriedOverCounterPill", () => { + it("hides the pill when the count is below 2", () => { + const { container } = renderWithProviders(); + expect(container).toBeEmptyDOMElement(); + }); + + it("names the count so it is not a bare number", () => { + renderWithProviders(); + const pill = screen.getByLabelText(/Carried over 3 times/i); + expect(pill).toHaveTextContent("3"); + }); + + it("is distinguishable from the reschedule pill beside it", () => { + renderWithProviders( + <> + + + , + ); + + // Both render the same visible digit, so the accessible names are the only + // thing telling a non-sighted user which counter is which. + expect(screen.getByLabelText(/Rescheduled 2 times/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Carried over 2 times/i)).toBeInTheDocument(); + }); +}); diff --git a/src/test/app/(protected)/workorders/due-date-past-due-announcement.test.tsx b/src/test/app/(protected)/workorders/due-date-past-due-announcement.test.tsx new file mode 100644 index 00000000..eddcf120 --- /dev/null +++ b/src/test/app/(protected)/workorders/due-date-past-due-announcement.test.tsx @@ -0,0 +1,42 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { DueDateCell } from "@/app/(protected)/workorders/_components/list/table/cells/due-date-cell"; +import { renderWithProviders } from "@/test/test-utils"; + +describe("Due Date past-due announcement", () => { + it("names the past-due state on the Emergency due date control", () => { + renderWithProviders( + , + ); + expect(screen.getByLabelText("Due Date, past due")).toBeInTheDocument(); + }); + + it("keeps the plain name when the work order is not past due", () => { + renderWithProviders(); + expect(screen.getByLabelText("Due Date")).toBeInTheDocument(); + expect(screen.queryByLabelText("Due Date, past due")).not.toBeInTheDocument(); + }); + + it("does not announce past due for a transient highlight alone", () => { + // `highlight` is also raised to draw attention to a row, which is not a + // durable state and must not be announced as one. + renderWithProviders( + , + ); + expect(screen.getByLabelText("Due Date")).toBeInTheDocument(); + }); + + it("announces past due on non-Emergency types too", () => { + renderWithProviders(); + // The visible date stays the leading part of the button's accessible name + // and the durable past-due state is appended to it. + expect(screen.getByRole("button", { name: /07-03\s*Past due/ })).toBeInTheDocument(); + }); + + it("does not announce past due for a non-Emergency transient highlight alone", () => { + // `highlight` is also raised to draw attention to a row, which is not a + // durable state and must not be announced as one. + renderWithProviders(); + expect(screen.getByRole("button", { name: "07-03" })).toBeInTheDocument(); + }); +});