@redsift/ds-mcp-server 12.5.1-muiv6-alpha.6 → 12.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/demos/patterns/_shared/StateDebugPanel.tsx +2 -2
- package/data/demos/patterns/_shared/columns.tsx +12 -3
- package/data/demos/patterns/_shared/defaults.ts +1 -1
- package/data/demos/patterns/_shared/filter-helpers.ts +1 -1
- package/data/demos/patterns/_shared/helpers.tsx +4 -2
- package/data/demos/patterns/_shared/server-logic.ts +1 -1
- package/data/demos/patterns/_shared/story-helpers.ts +154 -31
- package/data/demos/patterns/crossfiltered-datagrid-client-side/CrossfilteredDatagridClientSide.interaction.stories.tsx +149 -0
- package/data/demos/patterns/crossfiltered-datagrid-client-side/example.tsx +8 -0
- package/data/demos/patterns/crossfiltered-datagrid-client-side/with-loading.tsx +1 -1
- package/data/demos/patterns/crossfiltered-datagrid-server-side/CrossfilteredDatagridServerSide.interaction.stories.tsx +140 -0
- package/data/demos/patterns/crossfiltered-datagrid-server-side/example.tsx +18 -10
- package/data/demos/patterns/crossfiltered-datagrid-server-side/with-loading.tsx +1 -1
- package/data/demos/patterns/drilldowned-datagrid-client-side/DrilldownedDatagridClientSide.interaction.stories.tsx +89 -0
- package/data/demos/patterns/drilldowned-datagrid-client-side/example.tsx +1 -1
- package/data/demos/patterns/drilldowned-datagrid-client-side/with-loading.tsx +1 -1
- package/data/demos/patterns/drilldowned-datagrid-server-side/DrilldownedDatagridServerSide.interaction.stories.tsx +83 -0
- package/data/demos/patterns/drilldowned-datagrid-server-side/example.tsx +1 -1
- package/data/demos/patterns/drilldowned-datagrid-server-side/with-loading.tsx +1 -1
- package/data/demos/patterns/single-datagrid-client-side/SingleDatagridClientSide.interaction.stories.tsx +105 -1
- package/data/demos/patterns/single-datagrid-client-side/example.tsx +4 -4
- package/data/demos/patterns/single-datagrid-client-side/with-loading.tsx +1 -1
- package/data/demos/patterns/single-datagrid-server-side/SingleDatagridServerSide.interaction.stories.tsx +99 -2
- package/data/demos/patterns/single-datagrid-server-side/example.tsx +4 -4
- package/data/demos/patterns/single-datagrid-server-side/with-loading.tsx +1 -1
- package/data/demos/patterns/stateful-single-datagrid-client-side/StatefulSingleDatagridClientSide.interaction.stories.tsx +227 -1
- package/data/demos/patterns/stateful-single-datagrid-client-side/example.tsx +6 -5
- package/data/demos/patterns/stateful-single-datagrid-client-side/with-loading.tsx +1 -1
- package/data/demos/patterns/stateful-single-datagrid-server-side/StatefulSingleDatagridServerSide.interaction.stories.tsx +237 -1
- package/data/demos/patterns/stateful-single-datagrid-server-side/example.tsx +6 -5
- package/data/demos/patterns/stateful-single-datagrid-server-side/with-loading.tsx +1 -1
- package/data/demos/patterns/tabbed-datagrid-client-side/TabbedDatagridClientSide.interaction.stories.tsx +133 -0
- package/data/demos/patterns/tabbed-datagrid-client-side/with-loading.tsx +1 -1
- package/data/demos/patterns/tabbed-datagrid-server-side/TabbedDatagridServerSide.interaction.stories.tsx +131 -0
- package/data/demos/patterns/tabbed-datagrid-server-side/example.tsx +1 -1
- package/data/demos/patterns/tabbed-datagrid-server-side/with-loading.tsx +1 -1
- package/data/docs/components/dashboard/Dashboard.json +2 -2
- package/data/docs/components/products/DmarcSummaryBoxes.json +1 -1
- package/data/docs/components/table/DataGrid.json +7 -7
- package/data/docs/components/table/GridToolbarFilterSemanticField.json +1 -1
- package/data/docs/components/table/StatefulDataGrid.json +7 -7
- package/data/docs/components/table/Toolbar.json +8 -2
- package/data/docs/components-index.json +1 -1
- package/data/docs/components.json +28 -22
- package/data/docs/llms-full.txt +56 -46
- package/data/docs/llms.txt +5 -5
- package/data/docs/patterns-catalog.md +24 -25
- package/data/docs/patterns.json +4 -4
- package/data/metadata.json +2 -2
- package/data/patterns/crossfiltered-datagrid-server-side.mdx +1 -1
- package/data/patterns/drilldowned-datagrid-client-side.mdx +1 -1
- package/data/patterns/drilldowned-datagrid-server-side.mdx +1 -1
- package/data/patterns/single-datagrid-client-side.mdx +7 -7
- package/data/patterns/single-datagrid-server-side.mdx +4 -4
- package/data/patterns/stateful-single-datagrid-client-side.mdx +36 -21
- package/data/patterns/stateful-single-datagrid-server-side.mdx +46 -18
- package/data/patterns/tabbed-datagrid-server-side.mdx +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { GridApiPremium } from '@mui/x-data-grid-premium';
|
|
3
3
|
|
|
4
4
|
// localStorage key categories — must match @redsift/table internals
|
|
5
5
|
const LS_CATEGORIES = [
|
|
@@ -13,7 +13,7 @@ const LS_CATEGORIES = [
|
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
interface StateDebugPanelProps {
|
|
16
|
-
apiRef: React.MutableRefObject<
|
|
16
|
+
apiRef: React.MutableRefObject<GridApiPremium | null>;
|
|
17
17
|
useRouter: () => { pathname: string; search: string; historyReplace: (newSearch: string) => void };
|
|
18
18
|
localStorageVersion?: number;
|
|
19
19
|
}
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { createColumn, TextCell } from '@redsift/table';
|
|
3
3
|
import { Flexbox, Icon, IconButtonLink, Pill } from '@redsift/design-system';
|
|
4
4
|
import { mdiArrowRight, mdiCheck, mdiClose } from '@redsift/icons';
|
|
5
|
-
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-
|
|
5
|
+
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
|
|
6
6
|
import { Row } from './data';
|
|
7
7
|
|
|
8
8
|
// -- Option constants -------------------------------------------------------
|
|
@@ -80,6 +80,7 @@ export const columns: GridColDef<Row>[] = [
|
|
|
80
80
|
field: 'Items',
|
|
81
81
|
headerName: 'Item',
|
|
82
82
|
flex: 1,
|
|
83
|
+
display: 'flex',
|
|
83
84
|
...createColumn('string'),
|
|
84
85
|
renderCell: ({ value }: GridRenderCellParams) => <TextCell>{value}</TextCell>,
|
|
85
86
|
},
|
|
@@ -88,6 +89,7 @@ export const columns: GridColDef<Row>[] = [
|
|
|
88
89
|
field: 'Category',
|
|
89
90
|
headerName: 'Category',
|
|
90
91
|
width: 120,
|
|
92
|
+
display: 'flex',
|
|
91
93
|
...createColumn('singleSelect'),
|
|
92
94
|
valueOptions: CATEGORY_OPTIONS,
|
|
93
95
|
renderCell: ({ value }: GridRenderCellParams) => <Pill color={categoryColor(value)}>{value}</Pill>,
|
|
@@ -97,6 +99,7 @@ export const columns: GridColDef<Row>[] = [
|
|
|
97
99
|
field: 'Paid',
|
|
98
100
|
headerName: 'Price',
|
|
99
101
|
width: 100,
|
|
102
|
+
display: 'flex',
|
|
100
103
|
...createColumn('number'),
|
|
101
104
|
renderCell: ({ value }: GridRenderCellParams) => <TextCell>{formatCurrency(value as number)}</TextCell>,
|
|
102
105
|
},
|
|
@@ -105,8 +108,9 @@ export const columns: GridColDef<Row>[] = [
|
|
|
105
108
|
field: 'Date',
|
|
106
109
|
headerName: 'Date',
|
|
107
110
|
width: 140,
|
|
111
|
+
display: 'flex',
|
|
108
112
|
...createColumn('date'),
|
|
109
|
-
valueGetter: (
|
|
113
|
+
valueGetter: (value: string) => parseDate(value),
|
|
110
114
|
renderCell: ({ value }: GridRenderCellParams) => <TextCell>{value ? formatDate(value as Date) : '—'}</TextCell>,
|
|
111
115
|
},
|
|
112
116
|
// DateTime
|
|
@@ -114,8 +118,9 @@ export const columns: GridColDef<Row>[] = [
|
|
|
114
118
|
field: 'DateTime',
|
|
115
119
|
headerName: 'Date & Time',
|
|
116
120
|
width: 180,
|
|
121
|
+
display: 'flex',
|
|
117
122
|
...createColumn('dateTime'),
|
|
118
|
-
valueGetter: (
|
|
123
|
+
valueGetter: (value: string) => parseDate(value),
|
|
119
124
|
renderCell: ({ value }: GridRenderCellParams) => <TextCell>{value ? formatDateTime(value as Date) : '—'}</TextCell>,
|
|
120
125
|
},
|
|
121
126
|
// Boolean — in stock
|
|
@@ -123,6 +128,7 @@ export const columns: GridColDef<Row>[] = [
|
|
|
123
128
|
field: 'InStock',
|
|
124
129
|
headerName: 'In Stock',
|
|
125
130
|
width: 90,
|
|
131
|
+
display: 'flex',
|
|
126
132
|
type: 'boolean',
|
|
127
133
|
renderCell: ({ value }: GridRenderCellParams) =>
|
|
128
134
|
value ? (
|
|
@@ -136,6 +142,7 @@ export const columns: GridColDef<Row>[] = [
|
|
|
136
142
|
field: 'Allergens',
|
|
137
143
|
headerName: 'Allergens',
|
|
138
144
|
flex: 1,
|
|
145
|
+
display: 'flex',
|
|
139
146
|
...createColumn('multiSelect'),
|
|
140
147
|
valueOptions: ALLERGEN_OPTIONS,
|
|
141
148
|
sortable: false,
|
|
@@ -158,6 +165,7 @@ export const columns: GridColDef<Row>[] = [
|
|
|
158
165
|
field: 'Tags',
|
|
159
166
|
headerName: 'Tags',
|
|
160
167
|
flex: 1,
|
|
168
|
+
display: 'flex',
|
|
161
169
|
...createColumn('tags'),
|
|
162
170
|
valueOptions: TAG_OPTIONS,
|
|
163
171
|
sortable: false,
|
|
@@ -180,6 +188,7 @@ export const columns: GridColDef<Row>[] = [
|
|
|
180
188
|
field: 'actions',
|
|
181
189
|
headerName: '',
|
|
182
190
|
width: 56,
|
|
191
|
+
display: 'flex',
|
|
183
192
|
hideable: false,
|
|
184
193
|
sortable: false,
|
|
185
194
|
filterable: false,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
GridToolbarExport,
|
|
9
9
|
GridToolbarFilterButton,
|
|
10
10
|
GridToolbarQuickFilter,
|
|
11
|
-
} from '@mui/x-data-grid-
|
|
11
|
+
} from '@mui/x-data-grid-premium';
|
|
12
12
|
|
|
13
13
|
// -- Toolbar ----------------------------------------------------------------
|
|
14
14
|
|
|
@@ -35,7 +35,7 @@ export const BulkActionBar: React.FC<{ count: number; onLog?: () => void; onDele
|
|
|
35
35
|
}) => {
|
|
36
36
|
if (count === 0) return null;
|
|
37
37
|
return (
|
|
38
|
-
<Flexbox gap="8px" alignItems="center" style={{ padding: '8px 0' }}>
|
|
38
|
+
<Flexbox gap="8px" alignItems="center" style={{ padding: '8px 0' }} role="status">
|
|
39
39
|
<Pill color="info">{count} selected</Pill>
|
|
40
40
|
{onLog && (
|
|
41
41
|
<Button variant="secondary" onClick={onLog}>
|
|
@@ -60,6 +60,8 @@ export const NoRowsOverlay = () => (
|
|
|
60
60
|
justifyContent="center"
|
|
61
61
|
gap="12px"
|
|
62
62
|
style={{ height: '100%', padding: '48px 0' }}
|
|
63
|
+
role="status"
|
|
64
|
+
aria-label="No records found"
|
|
63
65
|
>
|
|
64
66
|
<Icon icon={mdiMagnify} size="large" color="grey" />
|
|
65
67
|
<Text variant="body" color="grey">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-
|
|
1
|
+
import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-premium';
|
|
2
2
|
import { Row, allRows } from './data';
|
|
3
3
|
import { Aggregates, computeAggregates, applyFilters } from './filter-helpers';
|
|
4
4
|
|
|
@@ -179,6 +179,27 @@ export const assertChartsRendered = async (canvasElement: HTMLElement, minCount
|
|
|
179
179
|
});
|
|
180
180
|
};
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Wait for chart container elements to be fully present in the DOM.
|
|
184
|
+
* Unlike `assertChartsRendered` (which checks for any SVG with data, including icons),
|
|
185
|
+
* this verifies the actual PieChart and BarChart containers have mounted.
|
|
186
|
+
*
|
|
187
|
+
* Note: we check for `.redsift-piechart` / `.redsift-barchart` only. These classes
|
|
188
|
+
* are only present when the chart has data (RenderedPieChart / RenderedBarChart),
|
|
189
|
+
* not during the loading state (LoadingPieChart). We intentionally do NOT check
|
|
190
|
+
* for `role="region"` because client-side crossfiltered charts wrapped in
|
|
191
|
+
* <WithFilters> receive `role="listbox"` (via FilterablePieChart / FilterableBarChart).
|
|
192
|
+
*/
|
|
193
|
+
export const waitForChartContainers = async (canvasElement: HTMLElement) => {
|
|
194
|
+
await waitFor(
|
|
195
|
+
() => {
|
|
196
|
+
expect(canvasElement.querySelector('.redsift-piechart')).toBeTruthy();
|
|
197
|
+
expect(canvasElement.querySelector('.redsift-barchart')).toBeTruthy();
|
|
198
|
+
},
|
|
199
|
+
{ timeout: 25000 }
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
182
203
|
// ---------------------------------------------------------------------------
|
|
183
204
|
// PieChart — slice count and legend labels
|
|
184
205
|
// ---------------------------------------------------------------------------
|
|
@@ -292,7 +313,7 @@ export const assertBulkActionBarVisible = async (canvasElement: HTMLElement, exp
|
|
|
292
313
|
expect(selectionPill).toBeTruthy();
|
|
293
314
|
expect(selectionPill!.textContent).toContain(`${expectedCount} selected`);
|
|
294
315
|
},
|
|
295
|
-
{ timeout:
|
|
316
|
+
{ timeout: 10000 }
|
|
296
317
|
);
|
|
297
318
|
};
|
|
298
319
|
|
|
@@ -313,6 +334,8 @@ export const assertBulkActionBarHidden = async (canvasElement: HTMLElement) => {
|
|
|
313
334
|
export const clickHeaderCheckbox = async (canvasElement: HTMLElement) => {
|
|
314
335
|
// Wait for the grid to be stable: rows present, no loading, checkbox exists.
|
|
315
336
|
// This prevents clicking during a mid-render where the grid ignores the click.
|
|
337
|
+
// Server-side grids may briefly re-render after MSW/router state changes, so
|
|
338
|
+
// allow 10 s for the checkbox to appear.
|
|
316
339
|
await waitFor(
|
|
317
340
|
() => {
|
|
318
341
|
const rows = canvasElement.querySelectorAll('.MuiDataGrid-row');
|
|
@@ -320,25 +343,42 @@ export const clickHeaderCheckbox = async (canvasElement: HTMLElement) => {
|
|
|
320
343
|
const headerCheckbox = canvasElement.querySelector('.MuiDataGrid-columnHeaderCheckbox input[type="checkbox"]');
|
|
321
344
|
expect(headerCheckbox).toBeTruthy();
|
|
322
345
|
},
|
|
323
|
-
{ timeout:
|
|
346
|
+
{ timeout: 10000 }
|
|
324
347
|
);
|
|
325
|
-
//
|
|
326
|
-
await
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
348
|
+
// Ensure no loading overlay is active (server-side grids replace content during fetch)
|
|
349
|
+
await waitFor(() => {
|
|
350
|
+
const loading = canvasElement.querySelector('.MuiDataGrid-overlayWrapper [role="progressbar"]');
|
|
351
|
+
expect(loading).toBeFalsy();
|
|
352
|
+
});
|
|
353
|
+
// Yield to let React finish any pending microtask re-renders
|
|
354
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
355
|
+
const headerCheckbox = canvasElement.querySelector(
|
|
356
|
+
'.MuiDataGrid-columnHeaderCheckbox input[type="checkbox"]'
|
|
357
|
+
) as HTMLInputElement;
|
|
358
|
+
// Use userEvent.click for reliable event dispatching — fireEvent.click on a hidden
|
|
359
|
+
// checkbox input doesn't trigger the native change event in Playwright/Chromium,
|
|
360
|
+
// causing server-side pagination grids to miss the selection update.
|
|
361
|
+
await userEvent.click(headerCheckbox);
|
|
362
|
+
// Wait for React and MUI DataGrid to process the selection change.
|
|
363
|
+
// Server-side grids with router state sync need extra time.
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
331
365
|
};
|
|
332
366
|
|
|
333
367
|
/** Click a row checkbox by row index (0-based, within the visible page). */
|
|
334
368
|
export const clickRowCheckbox = async (canvasElement: HTMLElement, rowIndex: number) => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
369
|
+
// Wait for BOTH the row AND its checkbox to be present.
|
|
370
|
+
// Server-side grids may render rows before checkboxes are mounted.
|
|
371
|
+
await waitFor(
|
|
372
|
+
() => {
|
|
373
|
+
const rows = canvasElement.querySelectorAll('.MuiDataGrid-row');
|
|
374
|
+
expect(rows.length).toBeGreaterThan(rowIndex);
|
|
375
|
+
const cb = rows[rowIndex].querySelector('input[type="checkbox"]');
|
|
376
|
+
expect(cb).toBeTruthy();
|
|
377
|
+
},
|
|
378
|
+
{ timeout: 5000 }
|
|
379
|
+
);
|
|
339
380
|
const rows = canvasElement.querySelectorAll('.MuiDataGrid-row');
|
|
340
|
-
const checkbox = rows[rowIndex].querySelector('input[type="checkbox"]')
|
|
341
|
-
if (!checkbox) throw new Error(`Could not find checkbox in row ${rowIndex}`);
|
|
381
|
+
const checkbox = rows[rowIndex].querySelector('input[type="checkbox"]')!;
|
|
342
382
|
await userEvent.click(checkbox);
|
|
343
383
|
// Wait for React and MUI DataGrid to process the selection change
|
|
344
384
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
@@ -641,9 +681,14 @@ export const clickBarChartBar = async (canvasElement: HTMLElement, key: string)
|
|
|
641
681
|
// Chart accessibility — region, screen reader content, keyboard nav
|
|
642
682
|
// ---------------------------------------------------------------------------
|
|
643
683
|
|
|
644
|
-
/**
|
|
684
|
+
/**
|
|
685
|
+
* Get all chart container elements. Uses `.redsift-chart-container` without a
|
|
686
|
+
* role filter because the role varies: standalone charts have `role="region"`,
|
|
687
|
+
* while client-side crossfiltered charts wrapped in <WithFilters> receive
|
|
688
|
+
* `role="listbox"` from FilterablePieChart / FilterableBarChart.
|
|
689
|
+
*/
|
|
645
690
|
export const getChartRegions = (canvasElement: HTMLElement): HTMLElement[] =>
|
|
646
|
-
Array.from(canvasElement.querySelectorAll('.redsift-chart-container
|
|
691
|
+
Array.from(canvasElement.querySelectorAll('.redsift-chart-container'));
|
|
647
692
|
|
|
648
693
|
/** Assert a chart container region exists with the given aria-label substring. */
|
|
649
694
|
export const assertChartRegion = async (canvasElement: HTMLElement, labelSubstring: string) => {
|
|
@@ -907,18 +952,39 @@ export const waitForPaginationEnabled = async (canvasElement: HTMLElement, direc
|
|
|
907
952
|
|
|
908
953
|
/**
|
|
909
954
|
* Change the page size via the MUI pagination "Rows per page" select.
|
|
910
|
-
*
|
|
955
|
+
* MUI Material v7's `useSlot()` applies `.MuiTablePagination-select` to the
|
|
956
|
+
* Select wrapper, NOT the inner display div that has the `onMouseDown` handler.
|
|
957
|
+
* We target `[role="combobox"]` inside the pagination toolbar instead, which is
|
|
958
|
+
* the actual interactive element rendered by `SelectInput`.
|
|
959
|
+
* `userEvent.click` in the Storybook Playwright runner doesn't reliably
|
|
960
|
+
* trigger mouseDown on MUI Select, so we use `fireEvent.mouseDown` directly,
|
|
961
|
+
* then `userEvent.click` to select the option in the dropdown.
|
|
911
962
|
*/
|
|
912
963
|
export const changePageSize = async (canvasElement: HTMLElement, newSize: number) => {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
964
|
+
// Wait for the pagination combobox to be present
|
|
965
|
+
await waitFor(
|
|
966
|
+
() => {
|
|
967
|
+
const el = canvasElement.querySelector('.MuiTablePagination-toolbar [role="combobox"]');
|
|
968
|
+
expect(el).toBeTruthy();
|
|
969
|
+
},
|
|
970
|
+
{ timeout: 5000 }
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
const select = canvasElement.querySelector('.MuiTablePagination-toolbar [role="combobox"]') as HTMLElement;
|
|
974
|
+
|
|
975
|
+
// Use fireEvent.mouseDown to reliably open MUI v7 Select dropdown
|
|
976
|
+
fireEvent.mouseDown(select);
|
|
977
|
+
|
|
978
|
+
// Wait for the dropdown to render (MUI uses a portal on document.body)
|
|
979
|
+
await waitFor(
|
|
980
|
+
() => {
|
|
981
|
+
const options = document.querySelectorAll('[role="option"]');
|
|
982
|
+
expect(options.length).toBeGreaterThan(0);
|
|
983
|
+
},
|
|
984
|
+
{ timeout: 5000 }
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
const options = document.querySelectorAll('[role="option"]');
|
|
922
988
|
const target = Array.from(options).find((opt) => opt.textContent === String(newSize));
|
|
923
989
|
if (!target) throw new Error(`Could not find page size option "${newSize}"`);
|
|
924
990
|
await userEvent.click(target);
|
|
@@ -937,17 +1003,18 @@ export const openColumnsPanel = async (canvasElement: HTMLElement) => {
|
|
|
937
1003
|
await userEvent.click(btn);
|
|
938
1004
|
// Wait for the panel to appear on document.body
|
|
939
1005
|
await waitFor(() => {
|
|
940
|
-
const panel = document.querySelector('.MuiDataGrid-
|
|
1006
|
+
const panel = document.querySelector('.MuiDataGrid-columnsManagement');
|
|
941
1007
|
expect(panel).toBeTruthy();
|
|
942
1008
|
});
|
|
943
1009
|
};
|
|
944
1010
|
|
|
945
1011
|
/** Toggle a column's visibility in the columns panel by its label text. */
|
|
946
1012
|
export const toggleColumnInPanel = async (fieldLabel: string) => {
|
|
947
|
-
const panel = document.querySelector('.MuiDataGrid-
|
|
1013
|
+
const panel = document.querySelector('.MuiDataGrid-columnsManagement');
|
|
948
1014
|
if (!panel) throw new Error('Columns panel is not open');
|
|
949
|
-
|
|
950
|
-
const
|
|
1015
|
+
// MUI v8 renders each column toggle as a baseCheckbox slot with class columnsManagementRow
|
|
1016
|
+
const rows = Array.from(panel.querySelectorAll('.MuiDataGrid-columnsManagementRow'));
|
|
1017
|
+
const target = rows.find((row) => row.textContent?.includes(fieldLabel));
|
|
951
1018
|
if (!target) throw new Error(`Could not find column "${fieldLabel}" in columns panel`);
|
|
952
1019
|
const checkbox = target.querySelector('input[type="checkbox"]') as HTMLElement | null;
|
|
953
1020
|
if (!checkbox) throw new Error(`Could not find checkbox for column "${fieldLabel}"`);
|
|
@@ -959,7 +1026,7 @@ export const closeColumnsPanel = async () => {
|
|
|
959
1026
|
// Press Escape to close the panel
|
|
960
1027
|
await userEvent.keyboard('{Escape}');
|
|
961
1028
|
await waitFor(() => {
|
|
962
|
-
const panel = document.querySelector('.MuiDataGrid-
|
|
1029
|
+
const panel = document.querySelector('.MuiDataGrid-columnsManagement');
|
|
963
1030
|
expect(panel).toBeFalsy();
|
|
964
1031
|
});
|
|
965
1032
|
};
|
|
@@ -1151,6 +1218,12 @@ interface SyncAssertionOptions {
|
|
|
1151
1218
|
checkDensity?: boolean;
|
|
1152
1219
|
/** Whether to check column order sync */
|
|
1153
1220
|
checkColumnOrder?: boolean;
|
|
1221
|
+
/** Whether to check row grouping sync */
|
|
1222
|
+
checkRowGrouping?: boolean;
|
|
1223
|
+
/** Whether to check aggregation sync */
|
|
1224
|
+
checkAggregation?: boolean;
|
|
1225
|
+
/** Whether to check pivot sync */
|
|
1226
|
+
checkPivot?: boolean;
|
|
1154
1227
|
}
|
|
1155
1228
|
|
|
1156
1229
|
/**
|
|
@@ -1169,6 +1242,9 @@ export const assertAllStatesInSync = async ({
|
|
|
1169
1242
|
checkPagination = true,
|
|
1170
1243
|
checkDensity = true,
|
|
1171
1244
|
checkColumnOrder = false,
|
|
1245
|
+
checkRowGrouping = false,
|
|
1246
|
+
checkAggregation = false,
|
|
1247
|
+
checkPivot = false,
|
|
1172
1248
|
}: SyncAssertionOptions) => {
|
|
1173
1249
|
await waitFor(
|
|
1174
1250
|
() => {
|
|
@@ -1275,6 +1351,53 @@ export const assertAllStatesInSync = async ({
|
|
|
1275
1351
|
expect(urlColumnOrder).toBe(inner);
|
|
1276
1352
|
}
|
|
1277
1353
|
}
|
|
1354
|
+
|
|
1355
|
+
// --- Row Grouping ---
|
|
1356
|
+
if (checkRowGrouping) {
|
|
1357
|
+
const lsKey = `${pathname}:${localStorageVersion}:rowGroupingModel`;
|
|
1358
|
+
const lsRaw = localStorage.getItem(lsKey);
|
|
1359
|
+
const lsRowGrouping = lsRaw ? JSON.parse(lsRaw) : '';
|
|
1360
|
+
|
|
1361
|
+
const urlRowGrouping = url.get('_rowGrouping');
|
|
1362
|
+
if (lsRowGrouping && urlRowGrouping) {
|
|
1363
|
+
// localStorage: `_rowGrouping=[a,b,c]`
|
|
1364
|
+
// URL: `_rowGrouping=a,b,c`
|
|
1365
|
+
const lsParams = new URLSearchParams(lsRowGrouping);
|
|
1366
|
+
const lsValue = lsParams.get('_rowGrouping') ?? '';
|
|
1367
|
+
const inner = lsValue.startsWith('[') && lsValue.endsWith(']') ? lsValue.slice(1, -1) : lsValue;
|
|
1368
|
+
expect(urlRowGrouping).toBe(inner);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// --- Aggregation ---
|
|
1373
|
+
if (checkAggregation) {
|
|
1374
|
+
const lsKey = `${pathname}:${localStorageVersion}:aggregationModel`;
|
|
1375
|
+
const lsRaw = localStorage.getItem(lsKey);
|
|
1376
|
+
const lsAggregation = lsRaw ? JSON.parse(lsRaw) : '';
|
|
1377
|
+
|
|
1378
|
+
const urlAggregation = url.get('_aggregation');
|
|
1379
|
+
if (lsAggregation && urlAggregation) {
|
|
1380
|
+
// Both use same format: `_aggregation=field.func,...`
|
|
1381
|
+
const lsParams = new URLSearchParams(lsAggregation);
|
|
1382
|
+
const lsValue = lsParams.get('_aggregation') ?? '';
|
|
1383
|
+
expect(urlAggregation).toBe(lsValue);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// --- Pivot ---
|
|
1388
|
+
if (checkPivot) {
|
|
1389
|
+
const lsKey = `${pathname}:${localStorageVersion}:pivotModel`;
|
|
1390
|
+
const lsRaw = localStorage.getItem(lsKey);
|
|
1391
|
+
const lsPivot = lsRaw ? JSON.parse(lsRaw) : '';
|
|
1392
|
+
|
|
1393
|
+
const urlPivot = url.get('_pivot');
|
|
1394
|
+
if (lsPivot && urlPivot) {
|
|
1395
|
+
// Both use same format: `_pivot=cols:f1;rows:f2;vals:f3.sum`
|
|
1396
|
+
const lsParams = new URLSearchParams(lsPivot);
|
|
1397
|
+
const lsValue = lsParams.get('_pivot') ?? '';
|
|
1398
|
+
expect(urlPivot).toBe(lsValue);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1278
1401
|
},
|
|
1279
1402
|
{ timeout: 5000 }
|
|
1280
1403
|
);
|
|
@@ -28,6 +28,13 @@ import {
|
|
|
28
28
|
clickBodyRow,
|
|
29
29
|
clickPieSlice,
|
|
30
30
|
clickBarChartBar,
|
|
31
|
+
assertChartRegion,
|
|
32
|
+
assertScreenReaderContent,
|
|
33
|
+
assertDataPointAriaLabels,
|
|
34
|
+
tabIntoChart,
|
|
35
|
+
assertScreenReaderIncludes,
|
|
36
|
+
assertScreenReaderSequence,
|
|
37
|
+
waitForChartContainers,
|
|
31
38
|
} from '../_shared/story-helpers';
|
|
32
39
|
import {
|
|
33
40
|
TOTAL,
|
|
@@ -502,3 +509,145 @@ export const WithError: Story = {
|
|
|
502
509
|
expect(retryButton).toBeTruthy();
|
|
503
510
|
},
|
|
504
511
|
};
|
|
512
|
+
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
// Accessibility — ARIA structure, chart regions, and data point labels
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
export const Accessibility: Story = {
|
|
518
|
+
render: () => <Example />,
|
|
519
|
+
play: async ({ canvasElement }) => {
|
|
520
|
+
const canvas = within(canvasElement);
|
|
521
|
+
await waitForGridToLoad(canvas);
|
|
522
|
+
await waitForChartContainers(canvasElement);
|
|
523
|
+
|
|
524
|
+
// --- Chart container regions ---
|
|
525
|
+
await assertChartRegion(canvasElement, 'Items');
|
|
526
|
+
await assertChartRegion(canvasElement, 'Tags');
|
|
527
|
+
|
|
528
|
+
// --- Screen reader hidden regions ---
|
|
529
|
+
await assertScreenReaderContent(canvasElement, 'Items', 'Pie chart showing the number of items by product');
|
|
530
|
+
await assertScreenReaderContent(canvasElement, 'Items', 'Click a slice or press Enter to filter the grid');
|
|
531
|
+
await assertScreenReaderContent(canvasElement, 'Tags', 'Bar chart showing the number of items by tag');
|
|
532
|
+
await assertScreenReaderContent(canvasElement, 'Tags', 'Click a bar or press Enter to filter the grid');
|
|
533
|
+
|
|
534
|
+
// --- Data point aria-labels ---
|
|
535
|
+
await assertDataPointAriaLabels(canvasElement, '.redsift-arc');
|
|
536
|
+
await assertDataPointAriaLabels(canvasElement, '.redsift-bar');
|
|
537
|
+
|
|
538
|
+
// --- Interactive data points (listbox options) ---
|
|
539
|
+
// Crossfiltered charts wrapped in <WithFilters> use role="listbox" with
|
|
540
|
+
// role="option" on each slice/bar (via FilterablePieChart/FilterableBarChart),
|
|
541
|
+
// rather than tabindex="0" on each data point.
|
|
542
|
+
const pieChart = canvasElement.querySelector('.redsift-piechart');
|
|
543
|
+
expect(pieChart).toBeTruthy();
|
|
544
|
+
expect(pieChart!.querySelectorAll('[role="option"]').length).toBeGreaterThanOrEqual(2);
|
|
545
|
+
|
|
546
|
+
const barChart = canvasElement.querySelector('.redsift-barchart');
|
|
547
|
+
expect(barChart).toBeTruthy();
|
|
548
|
+
expect(barChart!.querySelectorAll('[role="option"]').length).toBeGreaterThanOrEqual(2);
|
|
549
|
+
|
|
550
|
+
// --- DataCard listbox ARIA ---
|
|
551
|
+
const categoryListbox = getListbox(canvas, /filter by category/i);
|
|
552
|
+
expect(categoryListbox).toHaveAttribute('role', 'listbox');
|
|
553
|
+
const allergenListbox = getListbox(canvas, /filter by allergen/i);
|
|
554
|
+
expect(allergenListbox).toHaveAttribute('role', 'listbox');
|
|
555
|
+
|
|
556
|
+
// --- DataGrid ARIA ---
|
|
557
|
+
const grid = canvasElement.querySelector('[role="grid"]');
|
|
558
|
+
expect(grid).toBeTruthy();
|
|
559
|
+
expect(grid!.getAttribute('aria-rowcount')).toBeTruthy();
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
// Keyboard navigation — tab into charts
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
export const KeyboardNavigationCharts: Story = {
|
|
568
|
+
render: () => <Example />,
|
|
569
|
+
play: async ({ canvasElement }) => {
|
|
570
|
+
const canvas = within(canvasElement);
|
|
571
|
+
await waitForGridToLoad(canvas);
|
|
572
|
+
await waitForChartContainers(canvasElement);
|
|
573
|
+
|
|
574
|
+
// Tab into the PieChart — a slice should receive focus
|
|
575
|
+
const focusedSliceEl = await tabIntoChart(canvasElement, '.redsift-piechart');
|
|
576
|
+
expect(focusedSliceEl).toBeTruthy();
|
|
577
|
+
|
|
578
|
+
// Tab into the BarChart — a bar should receive focus
|
|
579
|
+
const focusedBarEl = await tabIntoChart(canvasElement, '.redsift-barchart');
|
|
580
|
+
expect(focusedBarEl).toBeTruthy();
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
// Virtual Screen Reader — announcement sequence verification
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
export const ScreenReaderPieChart: Story = {
|
|
589
|
+
render: () => <Example />,
|
|
590
|
+
play: async ({ canvasElement }) => {
|
|
591
|
+
const canvas = within(canvasElement);
|
|
592
|
+
await waitForGridToLoad(canvas);
|
|
593
|
+
await waitForChartContainers(canvasElement);
|
|
594
|
+
|
|
595
|
+
const pieContainer = canvasElement.querySelector('.redsift-chart-container[aria-label*="Items"]');
|
|
596
|
+
expect(pieContainer).toBeTruthy();
|
|
597
|
+
|
|
598
|
+
await assertScreenReaderSequence(pieContainer as HTMLElement, [
|
|
599
|
+
'Items distribution',
|
|
600
|
+
'Interactive chart',
|
|
601
|
+
'Pie chart showing the number of items by product. Click a slice or press Enter to filter the grid by that item.',
|
|
602
|
+
'Click a slice or press Enter to filter the grid by that item.',
|
|
603
|
+
]);
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
export const ScreenReaderBarChart: Story = {
|
|
608
|
+
render: () => <Example />,
|
|
609
|
+
play: async ({ canvasElement }) => {
|
|
610
|
+
const canvas = within(canvasElement);
|
|
611
|
+
await waitForGridToLoad(canvas);
|
|
612
|
+
await waitForChartContainers(canvasElement);
|
|
613
|
+
|
|
614
|
+
const barContainer = canvasElement.querySelector('.redsift-chart-container[aria-label*="Tags"]');
|
|
615
|
+
expect(barContainer).toBeTruthy();
|
|
616
|
+
|
|
617
|
+
await assertScreenReaderSequence(barContainer as HTMLElement, [
|
|
618
|
+
'Tags distribution',
|
|
619
|
+
'Interactive chart',
|
|
620
|
+
'Bar chart showing the number of items by tag. Click a bar or press Enter to filter the grid by that tag.',
|
|
621
|
+
'Click a bar or press Enter to filter the grid by that tag.',
|
|
622
|
+
]);
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
export const ScreenReaderEmptyState: Story = {
|
|
627
|
+
render: () => <WithEmptyStateExample />,
|
|
628
|
+
play: async ({ canvasElement }) => {
|
|
629
|
+
await assertScreenReaderIncludes(canvasElement, [
|
|
630
|
+
'No records found',
|
|
631
|
+
'Try adjusting your filters or search criteria',
|
|
632
|
+
]);
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
export const ScreenReaderErrorState: Story = {
|
|
637
|
+
render: () => <WithErrorExample />,
|
|
638
|
+
play: async ({ canvasElement }) => {
|
|
639
|
+
await assertScreenReaderIncludes(canvasElement, [
|
|
640
|
+
'Failed to load data. Please check your connection and try again.',
|
|
641
|
+
'Retry',
|
|
642
|
+
]);
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
export const ScreenReaderLoadingState: Story = {
|
|
647
|
+
render: () => <WithLoadingExample />,
|
|
648
|
+
play: async ({ canvasElement }) => {
|
|
649
|
+
const root = canvasElement.querySelector('[aria-busy="true"]');
|
|
650
|
+
expect(root).toBeTruthy();
|
|
651
|
+
expect(root!.getAttribute('aria-label')).toContain('loading');
|
|
652
|
+
},
|
|
653
|
+
};
|
|
@@ -76,6 +76,10 @@ export default () => (
|
|
|
76
76
|
>
|
|
77
77
|
<PieChart
|
|
78
78
|
title="Items"
|
|
79
|
+
aria-label="Items distribution"
|
|
80
|
+
descriptionForAssistiveTechnology="Pie chart showing the number of items by product. Click a slice or press Enter to filter the grid by that item."
|
|
81
|
+
interactionExplanation="Click a slice or press Enter to filter the grid by that item."
|
|
82
|
+
mode="interactive"
|
|
79
83
|
size="medium"
|
|
80
84
|
legendVariant="externalLabelValue"
|
|
81
85
|
colorTheme={{
|
|
@@ -100,6 +104,10 @@ export default () => (
|
|
|
100
104
|
>
|
|
101
105
|
<BarChart
|
|
102
106
|
title="Tags"
|
|
107
|
+
aria-label="Tags distribution"
|
|
108
|
+
descriptionForAssistiveTechnology="Bar chart showing the number of items by tag. Click a bar or press Enter to filter the grid by that tag."
|
|
109
|
+
interactionExplanation="Click a bar or press Enter to filter the grid by that tag."
|
|
110
|
+
mode="interactive"
|
|
103
111
|
orientation="vertical"
|
|
104
112
|
size="medium"
|
|
105
113
|
colorTheme={{
|
|
@@ -7,7 +7,7 @@ import { columns, CATEGORY_OPTIONS } from '../_shared/columns';
|
|
|
7
7
|
import { CustomToolbar } from '../_shared/helpers';
|
|
8
8
|
|
|
9
9
|
export default () => (
|
|
10
|
-
<div style={{ width: '100%' }}>
|
|
10
|
+
<div style={{ width: '100%' }} aria-busy="true" aria-label="Crossfiltered datagrid loading">
|
|
11
11
|
<Flexbox flexDirection="column" gap="16px">
|
|
12
12
|
<Flexbox gap="12px" flexWrap="wrap">
|
|
13
13
|
<DataCard color="warning" isLoading>
|