@dashforge/tw 0.8.1-beta → 0.9.1-beta

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 (204) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/COVERAGE.md +63 -0
  3. package/LICENSE +21 -0
  4. package/PERFORMANCE.md +59 -0
  5. package/REFINEMENT-NOTES.md +50 -0
  6. package/dist/index.esm.js +806 -134
  7. package/dist/src/components/Calendar/Calendar.d.ts +14 -0
  8. package/dist/src/components/Calendar/Calendar.d.ts.map +1 -0
  9. package/dist/src/components/Calendar/calendar.types.d.ts +72 -0
  10. package/dist/src/components/Calendar/calendar.types.d.ts.map +1 -0
  11. package/dist/src/components/Calendar/calendar.variants.d.ts +99 -0
  12. package/dist/src/components/Calendar/calendar.variants.d.ts.map +1 -0
  13. package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -1
  14. package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -1
  15. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +7 -0
  16. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -1
  17. package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.d.ts +6 -2
  18. package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.d.ts.map +1 -1
  19. package/dist/src/components/DatePicker/DatePicker.d.ts +14 -0
  20. package/dist/src/components/DatePicker/DatePicker.d.ts.map +1 -0
  21. package/dist/src/components/DatePicker/datePicker.types.d.ts +88 -0
  22. package/dist/src/components/DatePicker/datePicker.types.d.ts.map +1 -0
  23. package/dist/src/components/DatePicker/datePicker.variants.d.ts +97 -0
  24. package/dist/src/components/DatePicker/datePicker.variants.d.ts.map +1 -0
  25. package/dist/src/components/Dialog/Dialog.d.ts.map +1 -1
  26. package/dist/src/components/Table/Table.d.ts.map +1 -1
  27. package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -1
  28. package/dist/src/components/Table/table.types.d.ts +28 -3
  29. package/dist/src/components/Table/table.types.d.ts.map +1 -1
  30. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  31. package/dist/src/index.d.ts +6 -0
  32. package/dist/src/index.d.ts.map +1 -1
  33. package/eslint.config.mjs +16 -1
  34. package/package.json +10 -6
  35. package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +5 -2
  36. package/src/components/Calendar/Calendar.test.tsx +75 -0
  37. package/src/components/Calendar/Calendar.tsx +264 -0
  38. package/src/components/Calendar/calendar.types.ts +65 -0
  39. package/src/components/Calendar/calendar.variants.ts +64 -0
  40. package/src/components/DataGrid/DataGrid.perf.test.tsx +114 -0
  41. package/src/components/DataGrid/DataGrid.test.tsx +240 -0
  42. package/src/components/DataGrid/DataGrid.tsx +104 -16
  43. package/src/components/DataGrid/filters/ColumnFilters.test.tsx +330 -0
  44. package/src/components/DataGrid/filters/ColumnFilters.tsx +26 -2
  45. package/src/components/DataGrid/reorder/useColumnReorder.test.ts +95 -3
  46. package/src/components/DataGrid/resize/useColumnResize.test.ts +144 -0
  47. package/src/components/DataGrid/resize/useColumnResize.ts +30 -1
  48. package/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx +42 -25
  49. package/src/components/DatePicker/DatePicker.test.tsx +76 -0
  50. package/src/components/DatePicker/DatePicker.tsx +305 -0
  51. package/src/components/DatePicker/datePicker.types.ts +77 -0
  52. package/src/components/DatePicker/datePicker.variants.ts +50 -0
  53. package/src/components/Dialog/Dialog.tsx +9 -0
  54. package/src/components/Table/Table.perf.test.tsx +81 -0
  55. package/src/components/Table/Table.test.tsx +72 -1
  56. package/src/components/Table/Table.tsx +40 -4
  57. package/src/components/Table/cells/RenderChip.tsx +6 -9
  58. package/src/components/Table/cells/cells.test.tsx +247 -0
  59. package/src/components/Table/table.types.ts +28 -3
  60. package/src/components/Typography/Typography.test.tsx +15 -0
  61. package/src/components/Typography/typography.variants.ts +14 -1
  62. package/src/index.ts +21 -1
  63. package/test-output/vitest/coverage/base.css +224 -0
  64. package/test-output/vitest/coverage/block-navigation.js +87 -0
  65. package/test-output/vitest/coverage/clover.xml +2418 -0
  66. package/test-output/vitest/coverage/coverage-final.json +89 -0
  67. package/test-output/vitest/coverage/favicon.png +0 -0
  68. package/test-output/vitest/coverage/index.html +746 -0
  69. package/test-output/vitest/coverage/prettify.css +1 -0
  70. package/test-output/vitest/coverage/prettify.js +2 -0
  71. package/test-output/vitest/coverage/sort-arrow-sprite.png +0 -0
  72. package/test-output/vitest/coverage/sorter.js +210 -0
  73. package/test-output/vitest/coverage/src/components/Accordion/Accordion.tsx.html +376 -0
  74. package/test-output/vitest/coverage/src/components/Accordion/accordion.variants.ts.html +181 -0
  75. package/test-output/vitest/coverage/src/components/Accordion/index.html +131 -0
  76. package/test-output/vitest/coverage/src/components/AppShell/AppShell.tsx.html +802 -0
  77. package/test-output/vitest/coverage/src/components/AppShell/appShell.variants.ts.html +247 -0
  78. package/test-output/vitest/coverage/src/components/AppShell/index.html +131 -0
  79. package/test-output/vitest/coverage/src/components/AspectRatio/AspectRatio.tsx.html +304 -0
  80. package/test-output/vitest/coverage/src/components/AspectRatio/index.html +116 -0
  81. package/test-output/vitest/coverage/src/components/Autocomplete/Autocomplete.tsx.html +3292 -0
  82. package/test-output/vitest/coverage/src/components/Autocomplete/autocomplete.variants.ts.html +508 -0
  83. package/test-output/vitest/coverage/src/components/Autocomplete/index.html +131 -0
  84. package/test-output/vitest/coverage/src/components/Box/Box.tsx.html +334 -0
  85. package/test-output/vitest/coverage/src/components/Box/box.variants.ts.html +775 -0
  86. package/test-output/vitest/coverage/src/components/Box/index.html +131 -0
  87. package/test-output/vitest/coverage/src/components/Breadcrumbs/Breadcrumbs.tsx.html +694 -0
  88. package/test-output/vitest/coverage/src/components/Breadcrumbs/breadcrumbs.variants.ts.html +247 -0
  89. package/test-output/vitest/coverage/src/components/Breadcrumbs/index.html +131 -0
  90. package/test-output/vitest/coverage/src/components/Button/Button.tsx.html +535 -0
  91. package/test-output/vitest/coverage/src/components/Button/button.variants.ts.html +562 -0
  92. package/test-output/vitest/coverage/src/components/Button/index.html +131 -0
  93. package/test-output/vitest/coverage/src/components/Checkbox/Checkbox.tsx.html +943 -0
  94. package/test-output/vitest/coverage/src/components/Checkbox/checkbox.variants.ts.html +289 -0
  95. package/test-output/vitest/coverage/src/components/Checkbox/index.html +131 -0
  96. package/test-output/vitest/coverage/src/components/ConfirmDialog/ConfirmDialog.tsx.html +841 -0
  97. package/test-output/vitest/coverage/src/components/ConfirmDialog/confirmDialog.variants.ts.html +313 -0
  98. package/test-output/vitest/coverage/src/components/ConfirmDialog/index.html +131 -0
  99. package/test-output/vitest/coverage/src/components/Container/Container.tsx.html +307 -0
  100. package/test-output/vitest/coverage/src/components/Container/container.variants.ts.html +328 -0
  101. package/test-output/vitest/coverage/src/components/Container/index.html +131 -0
  102. package/test-output/vitest/coverage/src/components/DataGrid/DataGrid.tsx.html +3937 -0
  103. package/test-output/vitest/coverage/src/components/DataGrid/dataGrid.variants.ts.html +541 -0
  104. package/test-output/vitest/coverage/src/components/DataGrid/filters/ColumnFilters.tsx.html +1453 -0
  105. package/test-output/vitest/coverage/src/components/DataGrid/filters/index.html +116 -0
  106. package/test-output/vitest/coverage/src/components/DataGrid/index.html +131 -0
  107. package/test-output/vitest/coverage/src/components/DataGrid/reorder/index.html +116 -0
  108. package/test-output/vitest/coverage/src/components/DataGrid/reorder/useColumnReorder.ts.html +517 -0
  109. package/test-output/vitest/coverage/src/components/DataGrid/resize/index.html +116 -0
  110. package/test-output/vitest/coverage/src/components/DataGrid/resize/useColumnResize.ts.html +490 -0
  111. package/test-output/vitest/coverage/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx.html +592 -0
  112. package/test-output/vitest/coverage/src/components/DataGrid/visibility/index.html +116 -0
  113. package/test-output/vitest/coverage/src/components/DateTimePicker/DateTimePicker.tsx.html +1042 -0
  114. package/test-output/vitest/coverage/src/components/DateTimePicker/dateTimePicker.variants.ts.html +337 -0
  115. package/test-output/vitest/coverage/src/components/DateTimePicker/index.html +131 -0
  116. package/test-output/vitest/coverage/src/components/Dialog/Dialog.tsx.html +391 -0
  117. package/test-output/vitest/coverage/src/components/Dialog/dialog.variants.ts.html +283 -0
  118. package/test-output/vitest/coverage/src/components/Dialog/index.html +131 -0
  119. package/test-output/vitest/coverage/src/components/Divider/Divider.tsx.html +505 -0
  120. package/test-output/vitest/coverage/src/components/Divider/divider.variants.ts.html +433 -0
  121. package/test-output/vitest/coverage/src/components/Divider/index.html +131 -0
  122. package/test-output/vitest/coverage/src/components/Grid/Grid.tsx.html +529 -0
  123. package/test-output/vitest/coverage/src/components/Grid/grid.variants.ts.html +484 -0
  124. package/test-output/vitest/coverage/src/components/Grid/index.html +131 -0
  125. package/test-output/vitest/coverage/src/components/LeftNav/LeftNav.tsx.html +1075 -0
  126. package/test-output/vitest/coverage/src/components/LeftNav/index.html +131 -0
  127. package/test-output/vitest/coverage/src/components/LeftNav/leftNav.variants.ts.html +406 -0
  128. package/test-output/vitest/coverage/src/components/NumberField/NumberField.tsx.html +1048 -0
  129. package/test-output/vitest/coverage/src/components/NumberField/index.html +131 -0
  130. package/test-output/vitest/coverage/src/components/NumberField/numberField.variants.ts.html +367 -0
  131. package/test-output/vitest/coverage/src/components/OTPField/OTPField.tsx.html +835 -0
  132. package/test-output/vitest/coverage/src/components/OTPField/index.html +131 -0
  133. package/test-output/vitest/coverage/src/components/OTPField/otpField.variants.ts.html +301 -0
  134. package/test-output/vitest/coverage/src/components/Pagination/Pagination.tsx.html +916 -0
  135. package/test-output/vitest/coverage/src/components/Pagination/index.html +146 -0
  136. package/test-output/vitest/coverage/src/components/Pagination/pagination.helpers.ts.html +391 -0
  137. package/test-output/vitest/coverage/src/components/Pagination/pagination.variants.ts.html +436 -0
  138. package/test-output/vitest/coverage/src/components/Popover/Popover.tsx.html +250 -0
  139. package/test-output/vitest/coverage/src/components/Popover/index.html +131 -0
  140. package/test-output/vitest/coverage/src/components/Popover/popover.variants.ts.html +157 -0
  141. package/test-output/vitest/coverage/src/components/RadioGroup/RadioGroup.tsx.html +934 -0
  142. package/test-output/vitest/coverage/src/components/RadioGroup/index.html +131 -0
  143. package/test-output/vitest/coverage/src/components/RadioGroup/radioGroup.variants.ts.html +346 -0
  144. package/test-output/vitest/coverage/src/components/Skeleton/Skeleton.tsx.html +232 -0
  145. package/test-output/vitest/coverage/src/components/Skeleton/index.html +131 -0
  146. package/test-output/vitest/coverage/src/components/Skeleton/skeleton.variants.ts.html +346 -0
  147. package/test-output/vitest/coverage/src/components/Snackbar/Snackbar.tsx.html +979 -0
  148. package/test-output/vitest/coverage/src/components/Snackbar/index.html +131 -0
  149. package/test-output/vitest/coverage/src/components/Snackbar/snackbar.variants.ts.html +307 -0
  150. package/test-output/vitest/coverage/src/components/Stack/Stack.tsx.html +430 -0
  151. package/test-output/vitest/coverage/src/components/Stack/index.html +131 -0
  152. package/test-output/vitest/coverage/src/components/Stack/stack.variants.ts.html +325 -0
  153. package/test-output/vitest/coverage/src/components/Switch/Switch.tsx.html +559 -0
  154. package/test-output/vitest/coverage/src/components/Switch/index.html +131 -0
  155. package/test-output/vitest/coverage/src/components/Switch/switch.variants.ts.html +295 -0
  156. package/test-output/vitest/coverage/src/components/Table/Table.tsx.html +2677 -0
  157. package/test-output/vitest/coverage/src/components/Table/cells/RenderButton.tsx.html +154 -0
  158. package/test-output/vitest/coverage/src/components/Table/cells/RenderChip.tsx.html +385 -0
  159. package/test-output/vitest/coverage/src/components/Table/cells/RenderText.tsx.html +196 -0
  160. package/test-output/vitest/coverage/src/components/Table/cells/RenderTwoLine.tsx.html +187 -0
  161. package/test-output/vitest/coverage/src/components/Table/cells/RowActionsMenu.tsx.html +526 -0
  162. package/test-output/vitest/coverage/src/components/Table/cells/index.html +191 -0
  163. package/test-output/vitest/coverage/src/components/Table/cells/index.ts.html +127 -0
  164. package/test-output/vitest/coverage/src/components/Table/index.html +131 -0
  165. package/test-output/vitest/coverage/src/components/Table/table.variants.ts.html +706 -0
  166. package/test-output/vitest/coverage/src/components/Tabs/Tabs.tsx.html +283 -0
  167. package/test-output/vitest/coverage/src/components/Tabs/index.html +131 -0
  168. package/test-output/vitest/coverage/src/components/Tabs/tabs.variants.ts.html +292 -0
  169. package/test-output/vitest/coverage/src/components/TextField/TextField.tsx.html +820 -0
  170. package/test-output/vitest/coverage/src/components/TextField/index.html +131 -0
  171. package/test-output/vitest/coverage/src/components/TextField/textField.variants.ts.html +400 -0
  172. package/test-output/vitest/coverage/src/components/Textarea/Textarea.tsx.html +655 -0
  173. package/test-output/vitest/coverage/src/components/Textarea/index.html +131 -0
  174. package/test-output/vitest/coverage/src/components/Textarea/textarea.variants.ts.html +340 -0
  175. package/test-output/vitest/coverage/src/components/Tooltip/Tooltip.tsx.html +295 -0
  176. package/test-output/vitest/coverage/src/components/Tooltip/index.html +131 -0
  177. package/test-output/vitest/coverage/src/components/Tooltip/tooltip.variants.ts.html +169 -0
  178. package/test-output/vitest/coverage/src/components/TopBar/TopBar.tsx.html +283 -0
  179. package/test-output/vitest/coverage/src/components/TopBar/index.html +131 -0
  180. package/test-output/vitest/coverage/src/components/TopBar/topBar.variants.ts.html +199 -0
  181. package/test-output/vitest/coverage/src/components/Typography/Typography.tsx.html +421 -0
  182. package/test-output/vitest/coverage/src/components/Typography/index.html +131 -0
  183. package/test-output/vitest/coverage/src/components/Typography/typography.variants.ts.html +436 -0
  184. package/test-output/vitest/coverage/src/components/VisuallyHidden/VisuallyHidden.tsx.html +262 -0
  185. package/test-output/vitest/coverage/src/components/VisuallyHidden/index.html +116 -0
  186. package/test-output/vitest/coverage/src/components/_shared/data/getNestedValue.ts.html +181 -0
  187. package/test-output/vitest/coverage/src/components/_shared/data/index.html +236 -0
  188. package/test-output/vitest/coverage/src/components/_shared/data/useColumnAutoDetect.ts.html +385 -0
  189. package/test-output/vitest/coverage/src/components/_shared/data/useControllableState.ts.html +229 -0
  190. package/test-output/vitest/coverage/src/components/_shared/data/useDebouncedValue.ts.html +157 -0
  191. package/test-output/vitest/coverage/src/components/_shared/data/useTableFilter.ts.html +439 -0
  192. package/test-output/vitest/coverage/src/components/_shared/data/useTableSearch.ts.html +283 -0
  193. package/test-output/vitest/coverage/src/components/_shared/data/useTableSelection.ts.html +385 -0
  194. package/test-output/vitest/coverage/src/components/_shared/data/useTableSort.ts.html +508 -0
  195. package/test-output/vitest/coverage/src/components/_shared/data/useVirtualizer.ts.html +682 -0
  196. package/test-output/vitest/coverage/src/components/_shared/index.html +116 -0
  197. package/test-output/vitest/coverage/src/components/_shared/resolveValidationState.ts.html +265 -0
  198. package/test-output/vitest/coverage/src/hooks/index.html +116 -0
  199. package/test-output/vitest/coverage/src/hooks/useAccessState.ts.html +328 -0
  200. package/test-output/vitest/coverage/src/index.html +116 -0
  201. package/test-output/vitest/coverage/src/index.ts.html +1153 -0
  202. package/test-output/vitest/coverage/src/utils/cn.ts.html +172 -0
  203. package/test-output/vitest/coverage/src/utils/index.html +116 -0
  204. package/tsconfig.lib.json +7 -4
@@ -754,6 +754,80 @@ describe('DataGrid — column visibility', () => {
754
754
  expect(screen.queryByText('Email')).toBeNull();
755
755
  expect(screen.queryByText('Name')).not.toBeNull();
756
756
  });
757
+
758
+ // ───── Sprint 6 P5 — tri-state master checkbox ─────
759
+
760
+ it('renders a master toggle-all checkbox in the visibility menu', () => {
761
+ render(
762
+ <DataGrid
763
+ rows={generateUsers(5)}
764
+ cols={baseCols}
765
+ getRowId={getRowId}
766
+ rowHeight={48}
767
+ height="400px"
768
+ />,
769
+ );
770
+ fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
771
+ expect(
772
+ screen.queryByLabelText('Toggle all columns'),
773
+ ).not.toBeNull();
774
+ });
775
+
776
+ it('master checkbox is checked when every column is visible', () => {
777
+ render(
778
+ <DataGrid
779
+ rows={generateUsers(5)}
780
+ cols={baseCols}
781
+ getRowId={getRowId}
782
+ rowHeight={48}
783
+ height="400px"
784
+ />,
785
+ );
786
+ fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
787
+ const master = screen.getByLabelText('Toggle all columns') as HTMLInputElement;
788
+ expect(master.checked).toBe(true);
789
+ });
790
+
791
+ it('clicking the master when all visible hides every hideable column', () => {
792
+ const onHiddenColumnsChange = vi.fn();
793
+ render(
794
+ <DataGrid
795
+ rows={generateUsers(5)}
796
+ cols={baseCols}
797
+ getRowId={getRowId}
798
+ rowHeight={48}
799
+ height="400px"
800
+ onHiddenColumnsChange={onHiddenColumnsChange}
801
+ />,
802
+ );
803
+ fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
804
+ fireEvent.click(screen.getByLabelText('Toggle all columns'));
805
+ // baseCols = name / email / age / meta.city — all hideable.
806
+ expect(onHiddenColumnsChange).toHaveBeenCalledWith([
807
+ 'name',
808
+ 'email',
809
+ 'age',
810
+ 'meta.city',
811
+ ]);
812
+ });
813
+
814
+ it('clicking the master when some are hidden shows all', () => {
815
+ const onHiddenColumnsChange = vi.fn();
816
+ render(
817
+ <DataGrid
818
+ rows={generateUsers(5)}
819
+ cols={baseCols}
820
+ getRowId={getRowId}
821
+ rowHeight={48}
822
+ height="400px"
823
+ hiddenColumns={['email']}
824
+ onHiddenColumnsChange={onHiddenColumnsChange}
825
+ />,
826
+ );
827
+ fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
828
+ fireEvent.click(screen.getByLabelText('Toggle all columns'));
829
+ expect(onHiddenColumnsChange).toHaveBeenCalledWith([]);
830
+ });
757
831
  });
758
832
 
759
833
  describe('DataGrid — column resize', () => {
@@ -911,3 +985,169 @@ describe('DataGrid — column reorder', () => {
911
985
  expect(draggableThs[0]?.textContent).toContain('Email');
912
986
  });
913
987
  });
988
+
989
+ describe('DataGrid — Sprint 6 P2 refinements', () => {
990
+ // ───── D1: aria-rowcount / aria-rowindex ─────
991
+
992
+ it('sets aria-rowcount on the table reflecting the full dataset + header', () => {
993
+ const { container } = render(
994
+ <DataGrid
995
+ rows={generateUsers(500)}
996
+ cols={baseCols}
997
+ getRowId={getRowId}
998
+ rowHeight={48}
999
+ height="400px"
1000
+ />,
1001
+ );
1002
+ const table = container.querySelector('table');
1003
+ // 500 data rows + 1 header row
1004
+ expect(table?.getAttribute('aria-rowcount')).toBe('501');
1005
+ });
1006
+
1007
+ it('header row has aria-rowindex=1', () => {
1008
+ const { container } = render(
1009
+ <DataGrid
1010
+ rows={generateUsers(20)}
1011
+ cols={baseCols}
1012
+ getRowId={getRowId}
1013
+ rowHeight={48}
1014
+ height="400px"
1015
+ />,
1016
+ );
1017
+ expect(
1018
+ container.querySelector('thead tr')?.getAttribute('aria-rowindex'),
1019
+ ).toBe('1');
1020
+ });
1021
+
1022
+ it('data rows carry a 1-based aria-rowindex offset past the header', () => {
1023
+ const { container } = render(
1024
+ <DataGrid
1025
+ rows={generateUsers(20)}
1026
+ cols={baseCols}
1027
+ getRowId={getRowId}
1028
+ rowHeight={48}
1029
+ height="400px"
1030
+ />,
1031
+ );
1032
+ // absolute index 0 → aria-rowindex 2 (header occupies index 1)
1033
+ const firstDataRow = container.querySelector('tbody tr[data-selected]');
1034
+ expect(firstDataRow?.getAttribute('aria-rowindex')).toBe('2');
1035
+ });
1036
+
1037
+ // ───── D6: resize handle is not draggable + keyboard-focusable ─────
1038
+
1039
+ it('resize handle is keyboard-focusable and explicitly not draggable', () => {
1040
+ render(
1041
+ <DataGrid
1042
+ rows={generateUsers(5)}
1043
+ cols={baseCols}
1044
+ getRowId={getRowId}
1045
+ rowHeight={48}
1046
+ height="400px"
1047
+ />,
1048
+ );
1049
+ const handle = screen.getAllByRole('separator')[0] as HTMLElement;
1050
+ expect(handle.getAttribute('tabindex')).toBe('0');
1051
+ expect(handle.getAttribute('draggable')).toBe('false');
1052
+ });
1053
+
1054
+ // ───── D3: keyboard column resize ─────
1055
+
1056
+ it('ArrowRight on the resize handle widens the column by 16px', () => {
1057
+ const onColumnWidthsChange = vi.fn();
1058
+ const cols: TableColumn<User>[] = [
1059
+ { field: 'name', header: 'Name', width: 120 },
1060
+ ];
1061
+ render(
1062
+ <DataGrid
1063
+ rows={generateUsers(5)}
1064
+ cols={cols}
1065
+ getRowId={getRowId}
1066
+ rowHeight={48}
1067
+ height="400px"
1068
+ columnWidths={{ name: 120 }}
1069
+ onColumnWidthsChange={onColumnWidthsChange}
1070
+ />,
1071
+ );
1072
+ const handle = screen.getByRole('separator');
1073
+ fireEvent.keyDown(handle, { key: 'ArrowRight' });
1074
+ expect(onColumnWidthsChange).toHaveBeenCalledWith({ name: 136 });
1075
+ });
1076
+
1077
+ it('Shift+ArrowLeft on the resize handle narrows the column by 64px', () => {
1078
+ const onColumnWidthsChange = vi.fn();
1079
+ const cols: TableColumn<User>[] = [
1080
+ { field: 'name', header: 'Name', width: 200 },
1081
+ ];
1082
+ render(
1083
+ <DataGrid
1084
+ rows={generateUsers(5)}
1085
+ cols={cols}
1086
+ getRowId={getRowId}
1087
+ rowHeight={48}
1088
+ height="400px"
1089
+ columnWidths={{ name: 200 }}
1090
+ onColumnWidthsChange={onColumnWidthsChange}
1091
+ />,
1092
+ );
1093
+ const handle = screen.getByRole('separator');
1094
+ fireEvent.keyDown(handle, { key: 'ArrowLeft', shiftKey: true });
1095
+ expect(onColumnWidthsChange).toHaveBeenCalledWith({ name: 136 });
1096
+ });
1097
+
1098
+ // ───── D7: no-results vs no-data empty state ─────
1099
+
1100
+ it('shows the noData message when the dataset is genuinely empty', () => {
1101
+ render(
1102
+ <DataGrid
1103
+ rows={[]}
1104
+ cols={baseCols}
1105
+ getRowId={getRowId}
1106
+ rowHeight={48}
1107
+ height="400px"
1108
+ />,
1109
+ );
1110
+ expect(screen.queryByText('No data')).not.toBeNull();
1111
+ });
1112
+
1113
+ it('shows the noResults message when a filter excludes every row', () => {
1114
+ render(
1115
+ <DataGrid
1116
+ rows={generateUsers(10)}
1117
+ cols={baseCols}
1118
+ getRowId={getRowId}
1119
+ rowHeight={48}
1120
+ height="400px"
1121
+ filterModel={[{ field: 'name', op: 'contains', value: 'zzz-no-match' }]}
1122
+ />,
1123
+ );
1124
+ expect(screen.queryByText('No matching results')).not.toBeNull();
1125
+ expect(screen.queryByText('No data')).toBeNull();
1126
+ });
1127
+
1128
+ // ───── D4: Enter applies the filter ─────
1129
+
1130
+ it('pressing Enter in the filter text input applies the filter', () => {
1131
+ const onFilterChange = vi.fn();
1132
+ const cols: TableColumn<User>[] = [
1133
+ { field: 'name', header: 'Name', filterable: true },
1134
+ ];
1135
+ render(
1136
+ <DataGrid
1137
+ rows={generateUsers(5)}
1138
+ cols={cols}
1139
+ getRowId={getRowId}
1140
+ rowHeight={48}
1141
+ height="400px"
1142
+ onFilterChange={onFilterChange}
1143
+ />,
1144
+ );
1145
+ fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
1146
+ const input = screen.getByPlaceholderText('…') as HTMLInputElement;
1147
+ fireEvent.change(input, { target: { value: 'User 3' } });
1148
+ fireEvent.keyDown(input, { key: 'Enter' });
1149
+ expect(onFilterChange).toHaveBeenCalledWith([
1150
+ { field: 'name', op: 'contains', value: 'User 3' },
1151
+ ]);
1152
+ });
1153
+ });
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  useMemo,
3
+ useState,
4
+ useEffect,
3
5
  type ReactNode,
4
6
  type MouseEvent,
5
7
  type PointerEvent as RPointerEvent,
@@ -43,6 +45,7 @@ import {
43
45
  const DEFAULT_LABELS: Required<TableLabels> = {
44
46
  searchPlaceholder: 'Search...',
45
47
  noData: 'No data',
48
+ noResults: 'No matching results',
46
49
  loading: 'Loading…',
47
50
  ariaSortNone: 'Not sorted. Click to sort ascending.',
48
51
  ariaSortAscending: 'Sorted ascending. Click to sort descending.',
@@ -69,8 +72,7 @@ const DEFAULT_LABELS: Required<TableLabels> = {
69
72
  densitySpacious: 'Spacious',
70
73
  columnsButton: 'Columns',
71
74
  columnsTitle: 'Toggle columns',
72
- columnsShowAll: 'Show all',
73
- columnsHideAll: 'Hide all',
75
+ columnsToggleAll: 'Toggle all columns',
74
76
  };
75
77
 
76
78
  /**
@@ -235,7 +237,7 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
235
237
  defaultValue: defaultColumnWidths,
236
238
  onChange: onColumnWidthsChange,
237
239
  });
238
- const { startResize } = useColumnResize({
240
+ const { startResize, nudge: nudgeColumnWidth } = useColumnResize({
239
241
  widths: columnWidths,
240
242
  onChange: setColumnWidths,
241
243
  });
@@ -255,6 +257,23 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
255
257
  onChange: setColumnOrder,
256
258
  });
257
259
 
260
+ // Column reorder rides on native HTML5 drag-and-drop, whose
261
+ // `dragstart` / `drop` events DO NOT fire for touch input. On a
262
+ // coarse-pointer device the drag would be a dead affordance — so
263
+ // we detect the primary pointer and gate reorder off there. Mouse
264
+ // / trackpad / pen (fine pointer) keep the feature. A pointer-event
265
+ // based reorder that also covers touch is tracked as a follow-up
266
+ // in REFINEMENT-NOTES.md.
267
+ const [coarsePointer, setCoarsePointer] = useState(false);
268
+ useEffect(() => {
269
+ if (typeof window === 'undefined' || !window.matchMedia) return;
270
+ const mq = window.matchMedia('(pointer: coarse)');
271
+ setCoarsePointer(mq.matches);
272
+ const onChange = (e: MediaQueryListEvent) => setCoarsePointer(e.matches);
273
+ mq.addEventListener('change', onChange);
274
+ return () => mq.removeEventListener('change', onChange);
275
+ }, []);
276
+
258
277
  // Apply the active order to the columns list. Cols not in the
259
278
  // order array (e.g. newly-added columns at runtime) are appended
260
279
  // in their original position from `cols`.
@@ -420,8 +439,7 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
420
439
  labels={{
421
440
  columnsButton: labels.columnsButton,
422
441
  columnsTitle: labels.columnsTitle,
423
- columnsShowAll: labels.columnsShowAll,
424
- columnsHideAll: labels.columnsHideAll,
442
+ columnsToggleAll: labels.columnsToggleAll,
425
443
  }}
426
444
  />
427
445
  )}
@@ -434,7 +452,19 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
434
452
  className={cn(v.scroll(), slotProps?.scroll?.className)}
435
453
  style={height ? { height, maxHeight: height } : undefined}
436
454
  >
437
- <table className={cn(v.table(), slotProps?.table?.className)}>
455
+ {/*
456
+ `aria-rowcount` announces the TRUE total to assistive tech —
457
+ the virtualized DOM only holds the ~visible window, so without
458
+ it a screen reader reports "row N of <window size>" instead of
459
+ "row N of <dataset size>". `+ 1` accounts for the header row.
460
+ Each `<tr>` carries a 1-based `aria-rowindex` (header = 1,
461
+ data rows = absoluteIndex + 2). `-1` when the effective count
462
+ is unknown/zero keeps it a valid (ignored) value.
463
+ */}
464
+ <table
465
+ className={cn(v.table(), slotProps?.table?.className)}
466
+ aria-rowcount={effectiveTotalCount > 0 ? effectiveTotalCount + 1 : -1}
467
+ >
438
468
  {caption != null && (
439
469
  <caption className={cn(!showCaption && 'sr-only', 'py-2 text-sm text-neutral-500')}>
440
470
  {caption}
@@ -447,7 +477,10 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
447
477
  slotProps?.thead?.className,
448
478
  )}
449
479
  >
450
- <tr className={cn(v.headerRow(), slotProps?.headerRow?.className)}>
480
+ <tr
481
+ aria-rowindex={1}
482
+ className={cn(v.headerRow(), slotProps?.headerRow?.className)}
483
+ >
451
484
  {showSelectionColumn && (
452
485
  <th
453
486
  scope="col"
@@ -492,8 +525,11 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
492
525
  enableColumnResize && col.resizable !== false
493
526
  }
494
527
  startResize={startResize}
528
+ nudgeColumnWidth={nudgeColumnWidth}
495
529
  reorderable={
496
- enableColumnReorder && col.reorderable !== false
530
+ enableColumnReorder &&
531
+ col.reorderable !== false &&
532
+ !coarsePointer
497
533
  }
498
534
  reorderHandlers={reorderHandlers}
499
535
  labels={labels}
@@ -537,7 +573,16 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
537
573
  ? renderEmptyState({
538
574
  totalColumnCount,
539
575
  emptyState,
540
- fallback: labels.noData,
576
+ // Distinguish a genuinely empty dataset from one
577
+ // emptied by an active search / filter (works for
578
+ // server-side mode too — a query that returns 0
579
+ // rows still reads as "no matching results").
580
+ fallback:
581
+ rows.length > 0 &&
582
+ (debouncedSearchQuery.trim().length > 0 ||
583
+ filterModel.length > 0)
584
+ ? labels.noResults
585
+ : labels.noData,
541
586
  cellClass: cn(
542
587
  v.cell(),
543
588
  v.emptyState(),
@@ -648,6 +693,13 @@ function HeaderCell<T extends object>(props: {
648
693
  clampMin: number,
649
694
  clampMax: number,
650
695
  ) => (e: RPointerEvent<HTMLElement>) => void;
696
+ nudgeColumnWidth: (
697
+ field: string,
698
+ currentWidth: number,
699
+ delta: number,
700
+ clampMin: number,
701
+ clampMax: number,
702
+ ) => void;
651
703
  reorderable: boolean;
652
704
  reorderHandlers: ColumnReorderHandlers;
653
705
  labels: Required<TableLabels>;
@@ -665,6 +717,7 @@ function HeaderCell<T extends object>(props: {
665
717
  effectiveWidth,
666
718
  resizable,
667
719
  startResize,
720
+ nudgeColumnWidth,
668
721
  reorderable,
669
722
  reorderHandlers,
670
723
  labels,
@@ -720,24 +773,55 @@ function HeaderCell<T extends object>(props: {
720
773
  }
721
774
  : {};
722
775
 
776
+ // Resize bounds — shared by the pointer-drag and keyboard paths.
777
+ const resizeMin = col.minWidth ?? 40;
778
+ const resizeMax = col.maxWidth ?? 1200;
779
+ // Keyboard nudge step: 16px, or 64px with Shift held (coarse).
780
+ const measureWidth = (handle: HTMLElement): number =>
781
+ effectiveWidth ??
782
+ (handle.parentElement as HTMLElement | null)?.getBoundingClientRect()
783
+ .width ??
784
+ 120;
785
+
723
786
  const resizeHandle = resizable ? (
724
787
  <span
725
788
  role="separator"
726
789
  aria-orientation="vertical"
727
790
  aria-label={`Resize ${typeof col.header === 'string' ? col.header : col.field}`}
791
+ // Width bounds announced to assistive tech on the separator.
792
+ aria-valuemin={resizeMin}
793
+ aria-valuemax={resizeMax}
794
+ aria-valuenow={effectiveWidth ?? undefined}
795
+ // Keyboard-operable: focusable + ArrowLeft/Right resize. WCAG 2.1
796
+ // keyboard operability — the drag is no longer the only path.
797
+ tabIndex={disabled ? -1 : 0}
798
+ // D6 guard: the parent `<th>` is `draggable` when reorderable;
799
+ // marking the handle non-draggable means grabbing the handle
800
+ // starts a RESIZE, never a column-reorder drag.
801
+ draggable={false}
728
802
  onPointerDown={(e) => {
729
- const handle = e.currentTarget;
730
- const th = handle.parentElement as HTMLElement | null;
731
- const startWidth =
732
- effectiveWidth ?? th?.getBoundingClientRect().width ?? 120;
733
- const min = col.minWidth ?? 40;
734
- const max = col.maxWidth ?? 1200;
735
- startResize(col.field as string, startWidth, min, max)(e);
803
+ const startWidth = measureWidth(e.currentTarget);
804
+ startResize(col.field as string, startWidth, resizeMin, resizeMax)(e);
805
+ }}
806
+ onKeyDown={(e) => {
807
+ if (disabled) return;
808
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
809
+ e.preventDefault();
810
+ const step = (e.shiftKey ? 64 : 16) * (e.key === 'ArrowLeft' ? -1 : 1);
811
+ const current = measureWidth(e.currentTarget);
812
+ nudgeColumnWidth(
813
+ col.field as string,
814
+ current,
815
+ step,
816
+ resizeMin,
817
+ resizeMax,
818
+ );
736
819
  }}
737
820
  className={cn(
738
821
  'absolute top-0 bottom-0 right-0 w-1.5',
739
822
  'cursor-col-resize select-none touch-none',
740
823
  'hover:bg-primary-300 active:bg-primary-400',
824
+ 'focus:outline-none focus-visible:bg-primary-400',
741
825
  'transition-colors motion-reduce:transition-none',
742
826
  )}
743
827
  />
@@ -952,6 +1036,10 @@ function renderVirtualizedBody<T extends object>(args: {
952
1036
  return (
953
1037
  <tr
954
1038
  key={rowId}
1039
+ // 1-based row index in the FULL dataset (header row is 1,
1040
+ // so data rows start at 2). Lets a screen reader announce
1041
+ // the true position despite virtualization windowing.
1042
+ aria-rowindex={absoluteIdx + 2}
955
1043
  data-selected={isSelected ? 'true' : 'false'}
956
1044
  aria-selected={rowSelection !== 'none' ? isSelected : undefined}
957
1045
  className={classes.row}