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
+ )}
);
}