From d908b7e1da6d5cd415000ddf968036d5d7c3baea Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Wed, 26 Aug 2026 18:10:29 -0300 Subject: [PATCH] [SH-117] Give work-order board indicators accessible names (#137) * fix(work-orders): give board indicators accessible names Two work-order indicators conveyed their state visually only. The carried-over pill rendered a bare number whose sole description was a title attribute. It sits beside the reschedule pill, which also renders as a small number, so the two counters were indistinguishable without a mouse. Past Due drove a box shadow, a tint and an icon colour, while the control kept the fixed name "Due Date". The state read by colour alone and never reached assistive technology. - Carried-over pill takes the reschedule pill's shape: an accessible name plus a tooltip, so both counters are identifiable. - Due Date announces the past-due state, on the Emergency dialog control and on the standard date popover. - Past-due is passed separately from `highlight`, which is also raised transiently to draw attention to a row and is not durable state to announce. * fix: associate past-due status with date control --------- Co-authored-by: Alexandre Brandizzi Co-authored-by: Codex Review Integration --- .../list/table/cells/date-cell.tsx | 4 ++ .../list/table/cells/due-date-cell.tsx | 27 ++++++++++-- .../list/table/cells/wo-counter-pills.tsx | 18 ++++---- .../list/table/wo-table-row-cells.tsx | 1 + .../carried-over-counter-pill.test.tsx | 34 +++++++++++++++ .../due-date-past-due-announcement.test.tsx | 42 +++++++++++++++++++ 6 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 src/test/app/(protected)/workorders/carried-over-counter-pill.test.tsx create mode 100644 src/test/app/(protected)/workorders/due-date-past-due-announcement.test.tsx 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(); + }); +});