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 f0d8ba2ed..d23dcab54 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 @@ -244,21 +244,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 eaa175b0b..49b8bfe60 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 @@ -79,6 +79,21 @@ 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."); + }); + it("does not duplicate visible descriptions in title attributes", () => { const assignments = makeAssignments(1); 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 3492a0cf0..f40247692 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 87bf73ab1..bfb763fe8 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 + )}
); }