From f43af4a43a26fdd62ef4493269307315a1983427 Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Sun, 25 Oct 2026 12:59:44 +0100 Subject: [PATCH] f-1571 improve accessibility feedback for grader assignment filters --- .../GraderAssignmentsPage.tsx | 32 +++++----- .../spec/GraderAssignmentsPage.test.tsx | 15 +++++ .../src/components/ui/DataGrid.spec.tsx | 53 ++++++++++++++++ .../src/components/ui/DataGrid.tsx | 62 +++++++++++++------ 4 files changed, 128 insertions(+), 34 deletions(-) diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/GraderAssignmentsPage.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/GraderAssignmentsPage.tsx index 937e1cdd0..ddcf7d3cb 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/GraderAssignmentsPage.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/GraderAssignmentsPage.tsx @@ -245,21 +245,23 @@ export const GraderAssignmentsPage: React.FC = () => { return ( <> {viewToggle} -
- - data={rows} - columns={columns} - getRowId={(r) => String(r.id)} - onRowClick={(row) => navigate(`/grader/${row.id}`)} - enableColumnFilters - enableSortingRemoval={false} - initialSorting={[{ id: "duedate", desc: false }]} - pagination={pagination} - onPaginationChange={setPagination} - ariaLabel="Assignments" - emptyMessage="No assignments match the current filters." - /> -
+ + data={rows} + columns={columns} + getRowId={(r) => String(r.id)} + onRowClick={(row) => navigate(`/grader/${row.id}`)} + enableColumnFilters + filterResultsLabel="assignments" + filterResultsTotal={allRows.length} + tableFrameClassName={styles.tableWrap} + enableSortingRemoval={false} + initialSorting={[{ id: "duedate", desc: false }]} + pagination={pagination} + onPaginationChange={setPagination} + ariaLabel="Assignments" + emptyMessage="No assignments match the current filters." + data-tour="grader-assignment-picker" + /> ); } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/spec/GraderAssignmentsPage.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/spec/GraderAssignmentsPage.test.tsx index 30de98b7e..57dacaa28 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/spec/GraderAssignmentsPage.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderAssignmentsPage/spec/GraderAssignmentsPage.test.tsx @@ -78,4 +78,19 @@ describe("GraderAssignmentsPage table pagination", () => { expect(screen.getByText("Assignment 01")).toBeInTheDocument(); }); + + it("announces the number of assignments shown after filtering", () => { + renderWithMantine(); + + const status = screen.getByRole("status"); + + expect(status).toBeVisible(); + expect(status).toHaveTextContent("30 of 30 assignments shown."); + + fireEvent.change(screen.getByRole("textbox", { name: "Filter by Name" }), { + target: { value: "Assignment 01" } + }); + + expect(status).toHaveTextContent("1 of 30 assignments shown."); + }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.spec.tsx index 9bf0ef13e..a6f80033f 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.spec.tsx @@ -276,6 +276,42 @@ describe("DataGrid", () => { expect(screen.getByRole("textbox", { name: "Search assignments" })).toBeInTheDocument(); }); + it("announces the number of results after global filtering", async () => { + renderGrid({ + enableGlobalFilter: true, + globalFilterPlaceholder: "Search assignments", + filterResultsLabel: "assignments", + filterResultsTotal: 5 + }); + + const status = screen.getByRole("status"); + + expect(status).toHaveAttribute("aria-live", "polite"); + expect(status).toHaveAttribute("aria-atomic", "true"); + expect(status).toBeVisible(); + expect(status).toHaveTextContent("3 of 5 assignments shown."); + + await userEvent.type(screen.getByRole("textbox", { name: "Search assignments" }), "Alp"); + + expect(status).toHaveTextContent("1 of 5 assignments shown."); + }); + + it("can render the filter status outside the framed table content", () => { + renderGrid({ + enableGlobalFilter: true, + filterResultsLabel: "assignments", + tableFrameClassName: "table-frame" + }); + + const status = screen.getByRole("status"); + const frame = document.querySelector(".table-frame"); + + expect(frame).toBeInTheDocument(); + expect(frame).toContainElement(screen.getByRole("table")); + expect(frame).toContainElement(screen.getByText("Rows per page")); + expect(frame).not.toContainElement(status); + }); + it("renders its own scroll area with a sentinel in fillHeight mode", () => { const sentinelRef = vi.fn(); const { container } = renderGrid({ fillHeight: true, scrollSentinelRef: sentinelRef }); @@ -341,6 +377,23 @@ describe("DataGrid column filters", () => { expect(screen.queryByText("Charlie")).not.toBeInTheDocument(); }); + it("announces the number of results after column filtering", () => { + renderWithMantine( + + data={ROWS} + columns={FILTER_COLUMNS} + getRowId={(r) => r.id} + enableColumnFilters + /> + ); + + const status = screen.getByRole("status"); + + fireEvent.change(screen.getByPlaceholderText("filter-name"), { target: { value: "Alp" } }); + + expect(status).toHaveTextContent("1 of 3 results shown."); + }); + it("labels filter controls after their column header", () => { renderWithMantine( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx index e7bb23103..4e6b30f79 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx @@ -84,6 +84,9 @@ export interface DataGridProps { initialSorting?: SortingState; loading?: boolean; emptyMessage?: React.ReactNode; + filterResultsLabel?: string; + filterResultsTotal?: number; + tableFrameClassName?: string; ariaLabel?: string; minWidth?: number; fillHeight?: boolean; @@ -187,6 +190,9 @@ export function DataGrid({ initialSorting = [], loading = false, emptyMessage = "Nothing here yet", + filterResultsLabel = "results", + filterResultsTotal = data.length, + tableFrameClassName, ariaLabel, minWidth = 0, fillHeight = false, @@ -244,6 +250,8 @@ export function DataGrid({ const resolvedPageCount = table.getPageCount(); const rows = table.getRowModel().rows; + const filteredRowCount = table.getFilteredRowModel().rows.length; + const announcesFilterResults = enableGlobalFilter || enableColumnFilters; const tabbableRowId = rows.some((row) => row.id === keyboardRowId) ? keyboardRowId : rows[0]?.id; // Column filters shrink the table from the inside, so a page index that came @@ -422,25 +430,8 @@ export function DataGrid({ ); - return ( -
- {enableGlobalFilter && ( - - setGlobalFilter(e.currentTarget.value)} - placeholder={globalFilterPlaceholder} - aria-label={globalFilterPlaceholder} - leftSection={} - size="sm" - /> - - )} - + const tableContent = ( + <> {fillHeight ? (
{scrollSentinelRef && ( @@ -489,6 +480,39 @@ export function DataGrid({ /> )} + + ); + + return ( +
+ {enableGlobalFilter && ( + + setGlobalFilter(e.currentTarget.value)} + placeholder={globalFilterPlaceholder} + aria-label={globalFilterPlaceholder} + leftSection={} + size="sm" + /> + + )} + + {announcesFilterResults && !loading && ( + + {filteredRowCount} of {filterResultsTotal} {filterResultsLabel} shown. + + )} + + {tableFrameClassName ? ( +
{tableContent}
+ ) : ( + tableContent + )}
); }