@dashforge/tw 0.8.0-beta → 0.9.0-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 +188 -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 +235 -160
- 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 +44 -0
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.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 +29 -6
- 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/eslint.config.mjs +16 -1
- package/package.json +11 -8
- package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +5 -2
- package/src/components/DataGrid/DataGrid.perf.test.tsx +114 -0
- package/src/components/DataGrid/DataGrid.test.tsx +253 -17
- package/src/components/DataGrid/DataGrid.tsx +107 -24
- 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 +186 -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 +41 -8
- 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 +29 -6
- package/src/components/Typography/Typography.test.tsx +15 -0
- package/src/components/Typography/typography.variants.ts +14 -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/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts +0 -33
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts.map +0 -1
- package/src/components/DataGrid/visibility/ColumnVisibilityDialog.tsx +0 -200
|
@@ -667,11 +667,11 @@ describe('DataGrid — column visibility', () => {
|
|
|
667
667
|
expect(screen.queryByRole('button', { name: 'Columns' })).toBeNull();
|
|
668
668
|
});
|
|
669
669
|
|
|
670
|
-
it('opens
|
|
670
|
+
it('opens a popover menu with one checkbox per hideable column', () => {
|
|
671
671
|
const cols: TableColumn<User>[] = [
|
|
672
672
|
{ field: 'name', header: 'Name' },
|
|
673
673
|
{ field: 'email', header: 'Email' },
|
|
674
|
-
// hideable: false → not shown in the
|
|
674
|
+
// hideable: false → not shown in the menu (structurally required)
|
|
675
675
|
{ field: 'age', header: 'Age', hideable: false },
|
|
676
676
|
];
|
|
677
677
|
render(
|
|
@@ -684,16 +684,13 @@ describe('DataGrid — column visibility', () => {
|
|
|
684
684
|
/>,
|
|
685
685
|
);
|
|
686
686
|
fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
|
|
687
|
+
// Radix Popover content has role="dialog" too.
|
|
687
688
|
expect(screen.queryByRole('dialog')).not.toBeNull();
|
|
688
|
-
// The
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
expect(dialog.textContent).toContain('Name');
|
|
694
|
-
expect(dialog.textContent).toContain('Email');
|
|
695
|
-
expect(dialog.textContent).not.toContain('Age');
|
|
696
|
-
void labels;
|
|
689
|
+
// The menu should expose checkboxes for Name + Email (not Age).
|
|
690
|
+
const menu = screen.getByRole('dialog');
|
|
691
|
+
expect(menu.textContent).toContain('Name');
|
|
692
|
+
expect(menu.textContent).toContain('Email');
|
|
693
|
+
expect(menu.textContent).not.toContain('Age');
|
|
697
694
|
});
|
|
698
695
|
|
|
699
696
|
it('respects col.defaultHidden on first render', () => {
|
|
@@ -716,7 +713,7 @@ describe('DataGrid — column visibility', () => {
|
|
|
716
713
|
expect(screen.queryByText('Name')).not.toBeNull();
|
|
717
714
|
});
|
|
718
715
|
|
|
719
|
-
it('toggling a column
|
|
716
|
+
it('toggling a column auto-commits via onHiddenColumnsChange (no Done button)', () => {
|
|
720
717
|
const onHiddenColumnsChange = vi.fn();
|
|
721
718
|
render(
|
|
722
719
|
<DataGrid
|
|
@@ -729,18 +726,17 @@ describe('DataGrid — column visibility', () => {
|
|
|
729
726
|
/>,
|
|
730
727
|
);
|
|
731
728
|
fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
|
|
732
|
-
// Uncheck "Email" inside the
|
|
733
|
-
const
|
|
729
|
+
// Uncheck "Email" inside the popover menu
|
|
730
|
+
const menu = screen.getByRole('dialog');
|
|
734
731
|
const emailCheckbox = Array.from(
|
|
735
|
-
|
|
732
|
+
menu.querySelectorAll('input[type="checkbox"]'),
|
|
736
733
|
).find((el) => {
|
|
737
734
|
const li = el.closest('label');
|
|
738
735
|
return li?.textContent?.includes('Email');
|
|
739
736
|
}) as HTMLInputElement | undefined;
|
|
740
737
|
expect(emailCheckbox).toBeDefined();
|
|
741
738
|
fireEvent.click(emailCheckbox!);
|
|
742
|
-
//
|
|
743
|
-
fireEvent.click(screen.getByRole('button', { name: 'Done' }));
|
|
739
|
+
// Auto-commit on toggle — no Done button anymore
|
|
744
740
|
expect(onHiddenColumnsChange).toHaveBeenCalledWith(['email']);
|
|
745
741
|
});
|
|
746
742
|
|
|
@@ -758,6 +754,80 @@ describe('DataGrid — column visibility', () => {
|
|
|
758
754
|
expect(screen.queryByText('Email')).toBeNull();
|
|
759
755
|
expect(screen.queryByText('Name')).not.toBeNull();
|
|
760
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
|
+
});
|
|
761
831
|
});
|
|
762
832
|
|
|
763
833
|
describe('DataGrid — column resize', () => {
|
|
@@ -915,3 +985,169 @@ describe('DataGrid — column reorder', () => {
|
|
|
915
985
|
expect(draggableThs[0]?.textContent).toContain('Email');
|
|
916
986
|
});
|
|
917
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,
|
|
@@ -33,7 +35,7 @@ import type {
|
|
|
33
35
|
} from '../Table/table.types.js';
|
|
34
36
|
import type { DataGridProps } from './dataGrid.types.js';
|
|
35
37
|
import { ColumnFilterTrigger } from './filters/ColumnFilters.js';
|
|
36
|
-
import { ColumnVisibilityTrigger } from './visibility/
|
|
38
|
+
import { ColumnVisibilityTrigger } from './visibility/ColumnVisibilityMenu.js';
|
|
37
39
|
import { useColumnResize } from './resize/useColumnResize.js';
|
|
38
40
|
import {
|
|
39
41
|
useColumnReorder,
|
|
@@ -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.',
|
|
@@ -68,12 +71,8 @@ const DEFAULT_LABELS: Required<TableLabels> = {
|
|
|
68
71
|
densityComfortable: 'Comfortable',
|
|
69
72
|
densitySpacious: 'Spacious',
|
|
70
73
|
columnsButton: 'Columns',
|
|
71
|
-
columnsTitle: '
|
|
72
|
-
|
|
73
|
-
'Show or hide columns. Required columns cannot be toggled.',
|
|
74
|
-
columnsShowAll: 'Show all',
|
|
75
|
-
columnsHideAll: 'Hide all',
|
|
76
|
-
columnsDone: 'Done',
|
|
74
|
+
columnsTitle: 'Toggle columns',
|
|
75
|
+
columnsToggleAll: 'Toggle all columns',
|
|
77
76
|
};
|
|
78
77
|
|
|
79
78
|
/**
|
|
@@ -238,7 +237,7 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
238
237
|
defaultValue: defaultColumnWidths,
|
|
239
238
|
onChange: onColumnWidthsChange,
|
|
240
239
|
});
|
|
241
|
-
const { startResize } = useColumnResize({
|
|
240
|
+
const { startResize, nudge: nudgeColumnWidth } = useColumnResize({
|
|
242
241
|
widths: columnWidths,
|
|
243
242
|
onChange: setColumnWidths,
|
|
244
243
|
});
|
|
@@ -258,6 +257,23 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
258
257
|
onChange: setColumnOrder,
|
|
259
258
|
});
|
|
260
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
|
+
|
|
261
277
|
// Apply the active order to the columns list. Cols not in the
|
|
262
278
|
// order array (e.g. newly-added columns at runtime) are appended
|
|
263
279
|
// in their original position from `cols`.
|
|
@@ -414,7 +430,7 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
414
430
|
<ColumnVisibilityTrigger
|
|
415
431
|
cols={cols.filter(
|
|
416
432
|
(c) =>
|
|
417
|
-
// RBAC-hidden columns are never offered in the
|
|
433
|
+
// RBAC-hidden columns are never offered in the menu.
|
|
418
434
|
!c.access || c.access.onUnauthorized !== 'hide',
|
|
419
435
|
)}
|
|
420
436
|
hiddenColumns={hiddenColumns}
|
|
@@ -423,10 +439,7 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
423
439
|
labels={{
|
|
424
440
|
columnsButton: labels.columnsButton,
|
|
425
441
|
columnsTitle: labels.columnsTitle,
|
|
426
|
-
|
|
427
|
-
columnsShowAll: labels.columnsShowAll,
|
|
428
|
-
columnsHideAll: labels.columnsHideAll,
|
|
429
|
-
columnsDone: labels.columnsDone,
|
|
442
|
+
columnsToggleAll: labels.columnsToggleAll,
|
|
430
443
|
}}
|
|
431
444
|
/>
|
|
432
445
|
)}
|
|
@@ -439,7 +452,19 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
439
452
|
className={cn(v.scroll(), slotProps?.scroll?.className)}
|
|
440
453
|
style={height ? { height, maxHeight: height } : undefined}
|
|
441
454
|
>
|
|
442
|
-
|
|
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
|
+
>
|
|
443
468
|
{caption != null && (
|
|
444
469
|
<caption className={cn(!showCaption && 'sr-only', 'py-2 text-sm text-neutral-500')}>
|
|
445
470
|
{caption}
|
|
@@ -452,7 +477,10 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
452
477
|
slotProps?.thead?.className,
|
|
453
478
|
)}
|
|
454
479
|
>
|
|
455
|
-
<tr
|
|
480
|
+
<tr
|
|
481
|
+
aria-rowindex={1}
|
|
482
|
+
className={cn(v.headerRow(), slotProps?.headerRow?.className)}
|
|
483
|
+
>
|
|
456
484
|
{showSelectionColumn && (
|
|
457
485
|
<th
|
|
458
486
|
scope="col"
|
|
@@ -497,8 +525,11 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
497
525
|
enableColumnResize && col.resizable !== false
|
|
498
526
|
}
|
|
499
527
|
startResize={startResize}
|
|
528
|
+
nudgeColumnWidth={nudgeColumnWidth}
|
|
500
529
|
reorderable={
|
|
501
|
-
enableColumnReorder &&
|
|
530
|
+
enableColumnReorder &&
|
|
531
|
+
col.reorderable !== false &&
|
|
532
|
+
!coarsePointer
|
|
502
533
|
}
|
|
503
534
|
reorderHandlers={reorderHandlers}
|
|
504
535
|
labels={labels}
|
|
@@ -542,7 +573,16 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
542
573
|
? renderEmptyState({
|
|
543
574
|
totalColumnCount,
|
|
544
575
|
emptyState,
|
|
545
|
-
|
|
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,
|
|
546
586
|
cellClass: cn(
|
|
547
587
|
v.cell(),
|
|
548
588
|
v.emptyState(),
|
|
@@ -653,6 +693,13 @@ function HeaderCell<T extends object>(props: {
|
|
|
653
693
|
clampMin: number,
|
|
654
694
|
clampMax: number,
|
|
655
695
|
) => (e: RPointerEvent<HTMLElement>) => void;
|
|
696
|
+
nudgeColumnWidth: (
|
|
697
|
+
field: string,
|
|
698
|
+
currentWidth: number,
|
|
699
|
+
delta: number,
|
|
700
|
+
clampMin: number,
|
|
701
|
+
clampMax: number,
|
|
702
|
+
) => void;
|
|
656
703
|
reorderable: boolean;
|
|
657
704
|
reorderHandlers: ColumnReorderHandlers;
|
|
658
705
|
labels: Required<TableLabels>;
|
|
@@ -670,6 +717,7 @@ function HeaderCell<T extends object>(props: {
|
|
|
670
717
|
effectiveWidth,
|
|
671
718
|
resizable,
|
|
672
719
|
startResize,
|
|
720
|
+
nudgeColumnWidth,
|
|
673
721
|
reorderable,
|
|
674
722
|
reorderHandlers,
|
|
675
723
|
labels,
|
|
@@ -725,24 +773,55 @@ function HeaderCell<T extends object>(props: {
|
|
|
725
773
|
}
|
|
726
774
|
: {};
|
|
727
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
|
+
|
|
728
786
|
const resizeHandle = resizable ? (
|
|
729
787
|
<span
|
|
730
788
|
role="separator"
|
|
731
789
|
aria-orientation="vertical"
|
|
732
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}
|
|
733
802
|
onPointerDown={(e) => {
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
);
|
|
741
819
|
}}
|
|
742
820
|
className={cn(
|
|
743
821
|
'absolute top-0 bottom-0 right-0 w-1.5',
|
|
744
822
|
'cursor-col-resize select-none touch-none',
|
|
745
823
|
'hover:bg-primary-300 active:bg-primary-400',
|
|
824
|
+
'focus:outline-none focus-visible:bg-primary-400',
|
|
746
825
|
'transition-colors motion-reduce:transition-none',
|
|
747
826
|
)}
|
|
748
827
|
/>
|
|
@@ -957,6 +1036,10 @@ function renderVirtualizedBody<T extends object>(args: {
|
|
|
957
1036
|
return (
|
|
958
1037
|
<tr
|
|
959
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}
|
|
960
1043
|
data-selected={isSelected ? 'true' : 'false'}
|
|
961
1044
|
aria-selected={rowSelection !== 'none' ? isSelected : undefined}
|
|
962
1045
|
className={classes.row}
|