@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.
- package/CHANGELOG.md +155 -0
- package/COVERAGE.md +63 -0
- package/LICENSE +21 -0
- package/PERFORMANCE.md +59 -0
- package/REFINEMENT-NOTES.md +50 -0
- package/dist/index.esm.js +806 -134
- package/dist/src/components/Calendar/Calendar.d.ts +14 -0
- package/dist/src/components/Calendar/Calendar.d.ts.map +1 -0
- package/dist/src/components/Calendar/calendar.types.d.ts +72 -0
- package/dist/src/components/Calendar/calendar.types.d.ts.map +1 -0
- package/dist/src/components/Calendar/calendar.variants.d.ts +99 -0
- package/dist/src/components/Calendar/calendar.variants.d.ts.map +1 -0
- package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -1
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -1
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +7 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -1
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.d.ts +6 -2
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.d.ts.map +1 -1
- package/dist/src/components/DatePicker/DatePicker.d.ts +14 -0
- package/dist/src/components/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/src/components/DatePicker/datePicker.types.d.ts +88 -0
- package/dist/src/components/DatePicker/datePicker.types.d.ts.map +1 -0
- package/dist/src/components/DatePicker/datePicker.variants.d.ts +97 -0
- package/dist/src/components/DatePicker/datePicker.variants.d.ts.map +1 -0
- package/dist/src/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/src/components/Table/Table.d.ts.map +1 -1
- package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -1
- package/dist/src/components/Table/table.types.d.ts +28 -3
- package/dist/src/components/Table/table.types.d.ts.map +1 -1
- package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/eslint.config.mjs +16 -1
- package/package.json +10 -6
- package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +5 -2
- package/src/components/Calendar/Calendar.test.tsx +75 -0
- package/src/components/Calendar/Calendar.tsx +264 -0
- package/src/components/Calendar/calendar.types.ts +65 -0
- package/src/components/Calendar/calendar.variants.ts +64 -0
- package/src/components/DataGrid/DataGrid.perf.test.tsx +114 -0
- package/src/components/DataGrid/DataGrid.test.tsx +240 -0
- package/src/components/DataGrid/DataGrid.tsx +104 -16
- package/src/components/DataGrid/filters/ColumnFilters.test.tsx +330 -0
- package/src/components/DataGrid/filters/ColumnFilters.tsx +26 -2
- package/src/components/DataGrid/reorder/useColumnReorder.test.ts +95 -3
- package/src/components/DataGrid/resize/useColumnResize.test.ts +144 -0
- package/src/components/DataGrid/resize/useColumnResize.ts +30 -1
- package/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx +42 -25
- package/src/components/DatePicker/DatePicker.test.tsx +76 -0
- package/src/components/DatePicker/DatePicker.tsx +305 -0
- package/src/components/DatePicker/datePicker.types.ts +77 -0
- package/src/components/DatePicker/datePicker.variants.ts +50 -0
- package/src/components/Dialog/Dialog.tsx +9 -0
- package/src/components/Table/Table.perf.test.tsx +81 -0
- package/src/components/Table/Table.test.tsx +72 -1
- package/src/components/Table/Table.tsx +40 -4
- package/src/components/Table/cells/RenderChip.tsx +6 -9
- package/src/components/Table/cells/cells.test.tsx +247 -0
- package/src/components/Table/table.types.ts +28 -3
- package/src/components/Typography/Typography.test.tsx +15 -0
- package/src/components/Typography/typography.variants.ts +14 -1
- package/src/index.ts +21 -1
- package/test-output/vitest/coverage/base.css +224 -0
- package/test-output/vitest/coverage/block-navigation.js +87 -0
- package/test-output/vitest/coverage/clover.xml +2418 -0
- package/test-output/vitest/coverage/coverage-final.json +89 -0
- package/test-output/vitest/coverage/favicon.png +0 -0
- package/test-output/vitest/coverage/index.html +746 -0
- package/test-output/vitest/coverage/prettify.css +1 -0
- package/test-output/vitest/coverage/prettify.js +2 -0
- package/test-output/vitest/coverage/sort-arrow-sprite.png +0 -0
- package/test-output/vitest/coverage/sorter.js +210 -0
- package/test-output/vitest/coverage/src/components/Accordion/Accordion.tsx.html +376 -0
- package/test-output/vitest/coverage/src/components/Accordion/accordion.variants.ts.html +181 -0
- package/test-output/vitest/coverage/src/components/Accordion/index.html +131 -0
- package/test-output/vitest/coverage/src/components/AppShell/AppShell.tsx.html +802 -0
- package/test-output/vitest/coverage/src/components/AppShell/appShell.variants.ts.html +247 -0
- package/test-output/vitest/coverage/src/components/AppShell/index.html +131 -0
- package/test-output/vitest/coverage/src/components/AspectRatio/AspectRatio.tsx.html +304 -0
- package/test-output/vitest/coverage/src/components/AspectRatio/index.html +116 -0
- package/test-output/vitest/coverage/src/components/Autocomplete/Autocomplete.tsx.html +3292 -0
- package/test-output/vitest/coverage/src/components/Autocomplete/autocomplete.variants.ts.html +508 -0
- package/test-output/vitest/coverage/src/components/Autocomplete/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Box/Box.tsx.html +334 -0
- package/test-output/vitest/coverage/src/components/Box/box.variants.ts.html +775 -0
- package/test-output/vitest/coverage/src/components/Box/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Breadcrumbs/Breadcrumbs.tsx.html +694 -0
- package/test-output/vitest/coverage/src/components/Breadcrumbs/breadcrumbs.variants.ts.html +247 -0
- package/test-output/vitest/coverage/src/components/Breadcrumbs/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Button/Button.tsx.html +535 -0
- package/test-output/vitest/coverage/src/components/Button/button.variants.ts.html +562 -0
- package/test-output/vitest/coverage/src/components/Button/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Checkbox/Checkbox.tsx.html +943 -0
- package/test-output/vitest/coverage/src/components/Checkbox/checkbox.variants.ts.html +289 -0
- package/test-output/vitest/coverage/src/components/Checkbox/index.html +131 -0
- package/test-output/vitest/coverage/src/components/ConfirmDialog/ConfirmDialog.tsx.html +841 -0
- package/test-output/vitest/coverage/src/components/ConfirmDialog/confirmDialog.variants.ts.html +313 -0
- package/test-output/vitest/coverage/src/components/ConfirmDialog/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Container/Container.tsx.html +307 -0
- package/test-output/vitest/coverage/src/components/Container/container.variants.ts.html +328 -0
- package/test-output/vitest/coverage/src/components/Container/index.html +131 -0
- package/test-output/vitest/coverage/src/components/DataGrid/DataGrid.tsx.html +3937 -0
- package/test-output/vitest/coverage/src/components/DataGrid/dataGrid.variants.ts.html +541 -0
- package/test-output/vitest/coverage/src/components/DataGrid/filters/ColumnFilters.tsx.html +1453 -0
- package/test-output/vitest/coverage/src/components/DataGrid/filters/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DataGrid/index.html +131 -0
- package/test-output/vitest/coverage/src/components/DataGrid/reorder/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DataGrid/reorder/useColumnReorder.ts.html +517 -0
- package/test-output/vitest/coverage/src/components/DataGrid/resize/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DataGrid/resize/useColumnResize.ts.html +490 -0
- package/test-output/vitest/coverage/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx.html +592 -0
- package/test-output/vitest/coverage/src/components/DataGrid/visibility/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DateTimePicker/DateTimePicker.tsx.html +1042 -0
- package/test-output/vitest/coverage/src/components/DateTimePicker/dateTimePicker.variants.ts.html +337 -0
- package/test-output/vitest/coverage/src/components/DateTimePicker/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Dialog/Dialog.tsx.html +391 -0
- package/test-output/vitest/coverage/src/components/Dialog/dialog.variants.ts.html +283 -0
- package/test-output/vitest/coverage/src/components/Dialog/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Divider/Divider.tsx.html +505 -0
- package/test-output/vitest/coverage/src/components/Divider/divider.variants.ts.html +433 -0
- package/test-output/vitest/coverage/src/components/Divider/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Grid/Grid.tsx.html +529 -0
- package/test-output/vitest/coverage/src/components/Grid/grid.variants.ts.html +484 -0
- package/test-output/vitest/coverage/src/components/Grid/index.html +131 -0
- package/test-output/vitest/coverage/src/components/LeftNav/LeftNav.tsx.html +1075 -0
- package/test-output/vitest/coverage/src/components/LeftNav/index.html +131 -0
- package/test-output/vitest/coverage/src/components/LeftNav/leftNav.variants.ts.html +406 -0
- package/test-output/vitest/coverage/src/components/NumberField/NumberField.tsx.html +1048 -0
- package/test-output/vitest/coverage/src/components/NumberField/index.html +131 -0
- package/test-output/vitest/coverage/src/components/NumberField/numberField.variants.ts.html +367 -0
- package/test-output/vitest/coverage/src/components/OTPField/OTPField.tsx.html +835 -0
- package/test-output/vitest/coverage/src/components/OTPField/index.html +131 -0
- package/test-output/vitest/coverage/src/components/OTPField/otpField.variants.ts.html +301 -0
- package/test-output/vitest/coverage/src/components/Pagination/Pagination.tsx.html +916 -0
- package/test-output/vitest/coverage/src/components/Pagination/index.html +146 -0
- package/test-output/vitest/coverage/src/components/Pagination/pagination.helpers.ts.html +391 -0
- package/test-output/vitest/coverage/src/components/Pagination/pagination.variants.ts.html +436 -0
- package/test-output/vitest/coverage/src/components/Popover/Popover.tsx.html +250 -0
- package/test-output/vitest/coverage/src/components/Popover/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Popover/popover.variants.ts.html +157 -0
- package/test-output/vitest/coverage/src/components/RadioGroup/RadioGroup.tsx.html +934 -0
- package/test-output/vitest/coverage/src/components/RadioGroup/index.html +131 -0
- package/test-output/vitest/coverage/src/components/RadioGroup/radioGroup.variants.ts.html +346 -0
- package/test-output/vitest/coverage/src/components/Skeleton/Skeleton.tsx.html +232 -0
- package/test-output/vitest/coverage/src/components/Skeleton/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Skeleton/skeleton.variants.ts.html +346 -0
- package/test-output/vitest/coverage/src/components/Snackbar/Snackbar.tsx.html +979 -0
- package/test-output/vitest/coverage/src/components/Snackbar/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Snackbar/snackbar.variants.ts.html +307 -0
- package/test-output/vitest/coverage/src/components/Stack/Stack.tsx.html +430 -0
- package/test-output/vitest/coverage/src/components/Stack/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Stack/stack.variants.ts.html +325 -0
- package/test-output/vitest/coverage/src/components/Switch/Switch.tsx.html +559 -0
- package/test-output/vitest/coverage/src/components/Switch/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Switch/switch.variants.ts.html +295 -0
- package/test-output/vitest/coverage/src/components/Table/Table.tsx.html +2677 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderButton.tsx.html +154 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderChip.tsx.html +385 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderText.tsx.html +196 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderTwoLine.tsx.html +187 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RowActionsMenu.tsx.html +526 -0
- package/test-output/vitest/coverage/src/components/Table/cells/index.html +191 -0
- package/test-output/vitest/coverage/src/components/Table/cells/index.ts.html +127 -0
- package/test-output/vitest/coverage/src/components/Table/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Table/table.variants.ts.html +706 -0
- package/test-output/vitest/coverage/src/components/Tabs/Tabs.tsx.html +283 -0
- package/test-output/vitest/coverage/src/components/Tabs/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Tabs/tabs.variants.ts.html +292 -0
- package/test-output/vitest/coverage/src/components/TextField/TextField.tsx.html +820 -0
- package/test-output/vitest/coverage/src/components/TextField/index.html +131 -0
- package/test-output/vitest/coverage/src/components/TextField/textField.variants.ts.html +400 -0
- package/test-output/vitest/coverage/src/components/Textarea/Textarea.tsx.html +655 -0
- package/test-output/vitest/coverage/src/components/Textarea/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Textarea/textarea.variants.ts.html +340 -0
- package/test-output/vitest/coverage/src/components/Tooltip/Tooltip.tsx.html +295 -0
- package/test-output/vitest/coverage/src/components/Tooltip/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Tooltip/tooltip.variants.ts.html +169 -0
- package/test-output/vitest/coverage/src/components/TopBar/TopBar.tsx.html +283 -0
- package/test-output/vitest/coverage/src/components/TopBar/index.html +131 -0
- package/test-output/vitest/coverage/src/components/TopBar/topBar.variants.ts.html +199 -0
- package/test-output/vitest/coverage/src/components/Typography/Typography.tsx.html +421 -0
- package/test-output/vitest/coverage/src/components/Typography/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Typography/typography.variants.ts.html +436 -0
- package/test-output/vitest/coverage/src/components/VisuallyHidden/VisuallyHidden.tsx.html +262 -0
- package/test-output/vitest/coverage/src/components/VisuallyHidden/index.html +116 -0
- package/test-output/vitest/coverage/src/components/_shared/data/getNestedValue.ts.html +181 -0
- package/test-output/vitest/coverage/src/components/_shared/data/index.html +236 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useColumnAutoDetect.ts.html +385 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useControllableState.ts.html +229 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useDebouncedValue.ts.html +157 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableFilter.ts.html +439 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableSearch.ts.html +283 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableSelection.ts.html +385 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableSort.ts.html +508 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useVirtualizer.ts.html +682 -0
- package/test-output/vitest/coverage/src/components/_shared/index.html +116 -0
- package/test-output/vitest/coverage/src/components/_shared/resolveValidationState.ts.html +265 -0
- package/test-output/vitest/coverage/src/hooks/index.html +116 -0
- package/test-output/vitest/coverage/src/hooks/useAccessState.ts.html +328 -0
- package/test-output/vitest/coverage/src/index.html +116 -0
- package/test-output/vitest/coverage/src/index.ts.html +1153 -0
- package/test-output/vitest/coverage/src/utils/cn.ts.html +172 -0
- package/test-output/vitest/coverage/src/utils/index.html +116 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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}
|