@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.
Files changed (58) hide show
  1. package/data/demos/patterns/_shared/StateDebugPanel.tsx +2 -2
  2. package/data/demos/patterns/_shared/columns.tsx +12 -3
  3. package/data/demos/patterns/_shared/defaults.ts +1 -1
  4. package/data/demos/patterns/_shared/filter-helpers.ts +1 -1
  5. package/data/demos/patterns/_shared/helpers.tsx +4 -2
  6. package/data/demos/patterns/_shared/server-logic.ts +1 -1
  7. package/data/demos/patterns/_shared/story-helpers.ts +154 -31
  8. package/data/demos/patterns/crossfiltered-datagrid-client-side/CrossfilteredDatagridClientSide.interaction.stories.tsx +149 -0
  9. package/data/demos/patterns/crossfiltered-datagrid-client-side/example.tsx +8 -0
  10. package/data/demos/patterns/crossfiltered-datagrid-client-side/with-loading.tsx +1 -1
  11. package/data/demos/patterns/crossfiltered-datagrid-server-side/CrossfilteredDatagridServerSide.interaction.stories.tsx +140 -0
  12. package/data/demos/patterns/crossfiltered-datagrid-server-side/example.tsx +18 -10
  13. package/data/demos/patterns/crossfiltered-datagrid-server-side/with-loading.tsx +1 -1
  14. package/data/demos/patterns/drilldowned-datagrid-client-side/DrilldownedDatagridClientSide.interaction.stories.tsx +89 -0
  15. package/data/demos/patterns/drilldowned-datagrid-client-side/example.tsx +1 -1
  16. package/data/demos/patterns/drilldowned-datagrid-client-side/with-loading.tsx +1 -1
  17. package/data/demos/patterns/drilldowned-datagrid-server-side/DrilldownedDatagridServerSide.interaction.stories.tsx +83 -0
  18. package/data/demos/patterns/drilldowned-datagrid-server-side/example.tsx +1 -1
  19. package/data/demos/patterns/drilldowned-datagrid-server-side/with-loading.tsx +1 -1
  20. package/data/demos/patterns/single-datagrid-client-side/SingleDatagridClientSide.interaction.stories.tsx +105 -1
  21. package/data/demos/patterns/single-datagrid-client-side/example.tsx +4 -4
  22. package/data/demos/patterns/single-datagrid-client-side/with-loading.tsx +1 -1
  23. package/data/demos/patterns/single-datagrid-server-side/SingleDatagridServerSide.interaction.stories.tsx +99 -2
  24. package/data/demos/patterns/single-datagrid-server-side/example.tsx +4 -4
  25. package/data/demos/patterns/single-datagrid-server-side/with-loading.tsx +1 -1
  26. package/data/demos/patterns/stateful-single-datagrid-client-side/StatefulSingleDatagridClientSide.interaction.stories.tsx +227 -1
  27. package/data/demos/patterns/stateful-single-datagrid-client-side/example.tsx +6 -5
  28. package/data/demos/patterns/stateful-single-datagrid-client-side/with-loading.tsx +1 -1
  29. package/data/demos/patterns/stateful-single-datagrid-server-side/StatefulSingleDatagridServerSide.interaction.stories.tsx +237 -1
  30. package/data/demos/patterns/stateful-single-datagrid-server-side/example.tsx +6 -5
  31. package/data/demos/patterns/stateful-single-datagrid-server-side/with-loading.tsx +1 -1
  32. package/data/demos/patterns/tabbed-datagrid-client-side/TabbedDatagridClientSide.interaction.stories.tsx +133 -0
  33. package/data/demos/patterns/tabbed-datagrid-client-side/with-loading.tsx +1 -1
  34. package/data/demos/patterns/tabbed-datagrid-server-side/TabbedDatagridServerSide.interaction.stories.tsx +131 -0
  35. package/data/demos/patterns/tabbed-datagrid-server-side/example.tsx +1 -1
  36. package/data/demos/patterns/tabbed-datagrid-server-side/with-loading.tsx +1 -1
  37. package/data/docs/components/dashboard/Dashboard.json +2 -2
  38. package/data/docs/components/products/DmarcSummaryBoxes.json +1 -1
  39. package/data/docs/components/table/DataGrid.json +7 -7
  40. package/data/docs/components/table/GridToolbarFilterSemanticField.json +1 -1
  41. package/data/docs/components/table/StatefulDataGrid.json +7 -7
  42. package/data/docs/components/table/Toolbar.json +8 -2
  43. package/data/docs/components-index.json +1 -1
  44. package/data/docs/components.json +28 -22
  45. package/data/docs/llms-full.txt +56 -46
  46. package/data/docs/llms.txt +5 -5
  47. package/data/docs/patterns-catalog.md +24 -25
  48. package/data/docs/patterns.json +4 -4
  49. package/data/metadata.json +2 -2
  50. package/data/patterns/crossfiltered-datagrid-server-side.mdx +1 -1
  51. package/data/patterns/drilldowned-datagrid-client-side.mdx +1 -1
  52. package/data/patterns/drilldowned-datagrid-server-side.mdx +1 -1
  53. package/data/patterns/single-datagrid-client-side.mdx +7 -7
  54. package/data/patterns/single-datagrid-server-side.mdx +4 -4
  55. package/data/patterns/stateful-single-datagrid-client-side.mdx +36 -21
  56. package/data/patterns/stateful-single-datagrid-server-side.mdx +46 -18
  57. package/data/patterns/tabbed-datagrid-server-side.mdx +1 -1
  58. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
- import type { GridApiPro } from '@mui/x-data-grid-pro/models/gridApiPro';
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<GridApiPro>;
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-pro';
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: (params: any) => parseDate(params.value),
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: (params: any) => parseDate(params.value),
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,
@@ -1,4 +1,4 @@
1
- import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
1
+ import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-premium';
2
2
 
3
3
  /**
4
4
  * Default filter model applied to all pattern examples.
@@ -1,4 +1,4 @@
1
- import { GridFilterModel } from '@mui/x-data-grid-pro';
1
+ import { GridFilterModel } from '@mui/x-data-grid-premium';
2
2
  import { Row } from './data';
3
3
 
4
4
  // -- Types ------------------------------------------------------------------
@@ -8,7 +8,7 @@ import {
8
8
  GridToolbarExport,
9
9
  GridToolbarFilterButton,
10
10
  GridToolbarQuickFilter,
11
- } from '@mui/x-data-grid-pro';
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-pro';
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: 5000 }
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: 3000 }
346
+ { timeout: 10000 }
324
347
  );
325
- // Small yield to let React finish any pending microtask re-renders
326
- await new Promise((resolve) => setTimeout(resolve, 200));
327
- const headerCheckbox = canvasElement.querySelector('.MuiDataGrid-columnHeaderCheckbox input[type="checkbox"]')!;
328
- fireEvent.click(headerCheckbox);
329
- // Wait for React and MUI DataGrid to process the selection change
330
- await new Promise((resolve) => setTimeout(resolve, 200));
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
- await waitFor(() => {
336
- const rows = canvasElement.querySelectorAll('.MuiDataGrid-row');
337
- expect(rows.length).toBeGreaterThan(rowIndex);
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
- /** Get all chart container regions (elements with role="region" inside .redsift-chart-container). */
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[role="region"]'));
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
- * The dropdown menu renders as a portal on document.body.
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
- const select = canvasElement.querySelector('.MuiTablePagination-select') as HTMLElement | null;
914
- if (!select) throw new Error('Could not find page size select');
915
- await userEvent.click(select);
916
- // The menu renders as a MUI Popper on document.body
917
- await waitFor(() => {
918
- const options = document.querySelectorAll('li[role="option"]');
919
- expect(options.length).toBeGreaterThan(0);
920
- });
921
- const options = document.querySelectorAll('li[role="option"]');
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-columnsPanel');
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-columnsPanel');
1013
+ const panel = document.querySelector('.MuiDataGrid-columnsManagement');
948
1014
  if (!panel) throw new Error('Columns panel is not open');
949
- const labels = Array.from(panel.querySelectorAll('.MuiFormControlLabel-root'));
950
- const target = labels.find((label) => label.textContent?.includes(fieldLabel));
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-columnsPanel');
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>