@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
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the per-column filter UI (`ColumnFilterTrigger`).
|
|
4
|
+
*
|
|
5
|
+
* Sprint 6 P4 — the filter UI shipped in 0.8.0-beta with only
|
|
6
|
+
* DataGrid-integration coverage. This file exercises each filter
|
|
7
|
+
* type (text / number / boolean / date) directly, including the
|
|
8
|
+
* apply / clear branches + edge cases.
|
|
9
|
+
*/
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
import { describe, it, expect, afterEach, beforeAll, vi } from 'vitest';
|
|
12
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
13
|
+
import { ColumnFilterTrigger } from './ColumnFilters.js';
|
|
14
|
+
import type { TableFilterModel } from '../../Table/table.types.js';
|
|
15
|
+
|
|
16
|
+
void React;
|
|
17
|
+
afterEach(() => cleanup());
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
21
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
22
|
+
class {
|
|
23
|
+
observe() {}
|
|
24
|
+
unobserve() {}
|
|
25
|
+
disconnect() {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const EMPTY: TableFilterModel = [];
|
|
31
|
+
|
|
32
|
+
describe('ColumnFilterTrigger — trigger button', () => {
|
|
33
|
+
it('renders a filter button', () => {
|
|
34
|
+
render(
|
|
35
|
+
<ColumnFilterTrigger
|
|
36
|
+
field="name"
|
|
37
|
+
filterType="text"
|
|
38
|
+
filterModel={EMPTY}
|
|
39
|
+
onFilterChange={() => {}}
|
|
40
|
+
/>,
|
|
41
|
+
);
|
|
42
|
+
expect(screen.getByRole('button', { name: 'Filter' })).not.toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('shows aria-pressed=false when no filter is active', () => {
|
|
46
|
+
render(
|
|
47
|
+
<ColumnFilterTrigger
|
|
48
|
+
field="name"
|
|
49
|
+
filterType="text"
|
|
50
|
+
filterModel={EMPTY}
|
|
51
|
+
onFilterChange={() => {}}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByRole('button', { name: 'Filter' }).getAttribute('aria-pressed'),
|
|
56
|
+
).toBe('false');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('shows aria-pressed=true when a filter is active on the field', () => {
|
|
60
|
+
render(
|
|
61
|
+
<ColumnFilterTrigger
|
|
62
|
+
field="name"
|
|
63
|
+
filterType="text"
|
|
64
|
+
filterModel={[{ field: 'name', op: 'contains', value: 'x' }]}
|
|
65
|
+
onFilterChange={() => {}}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
expect(
|
|
69
|
+
screen.getByRole('button', { name: 'Filter' }).getAttribute('aria-pressed'),
|
|
70
|
+
).toBe('true');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('translates the trigger label via labels', () => {
|
|
74
|
+
render(
|
|
75
|
+
<ColumnFilterTrigger
|
|
76
|
+
field="name"
|
|
77
|
+
filterType="text"
|
|
78
|
+
filterModel={EMPTY}
|
|
79
|
+
onFilterChange={() => {}}
|
|
80
|
+
labels={{ filterColumn: 'Filtra' }}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
expect(screen.getByRole('button', { name: 'Filtra' })).not.toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('is disabled when disabled is set', () => {
|
|
87
|
+
render(
|
|
88
|
+
<ColumnFilterTrigger
|
|
89
|
+
field="name"
|
|
90
|
+
filterType="text"
|
|
91
|
+
filterModel={EMPTY}
|
|
92
|
+
onFilterChange={() => {}}
|
|
93
|
+
disabled
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
expect(
|
|
97
|
+
(screen.getByRole('button', { name: 'Filter' }) as HTMLButtonElement)
|
|
98
|
+
.disabled,
|
|
99
|
+
).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('ColumnFilterTrigger — text filter', () => {
|
|
104
|
+
it('Apply commits a contains filter', () => {
|
|
105
|
+
const onFilterChange = vi.fn();
|
|
106
|
+
render(
|
|
107
|
+
<ColumnFilterTrigger
|
|
108
|
+
field="name"
|
|
109
|
+
filterType="text"
|
|
110
|
+
filterModel={EMPTY}
|
|
111
|
+
onFilterChange={onFilterChange}
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
115
|
+
fireEvent.change(screen.getByPlaceholderText('…'), {
|
|
116
|
+
target: { value: 'abc' },
|
|
117
|
+
});
|
|
118
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
119
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
120
|
+
{ field: 'name', op: 'contains', value: 'abc' },
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('Apply with an empty value commits a removal (null → no filter)', () => {
|
|
125
|
+
const onFilterChange = vi.fn();
|
|
126
|
+
render(
|
|
127
|
+
<ColumnFilterTrigger
|
|
128
|
+
field="name"
|
|
129
|
+
filterType="text"
|
|
130
|
+
filterModel={[{ field: 'name', op: 'contains', value: 'old' }]}
|
|
131
|
+
onFilterChange={onFilterChange}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
135
|
+
fireEvent.change(screen.getByDisplayValue('old'), {
|
|
136
|
+
target: { value: '' },
|
|
137
|
+
});
|
|
138
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
139
|
+
expect(onFilterChange).toHaveBeenCalledWith([]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('Clear removes the filter for the field', () => {
|
|
143
|
+
const onFilterChange = vi.fn();
|
|
144
|
+
render(
|
|
145
|
+
<ColumnFilterTrigger
|
|
146
|
+
field="name"
|
|
147
|
+
filterType="text"
|
|
148
|
+
filterModel={[{ field: 'name', op: 'contains', value: 'x' }]}
|
|
149
|
+
onFilterChange={onFilterChange}
|
|
150
|
+
/>,
|
|
151
|
+
);
|
|
152
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
153
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
|
|
154
|
+
expect(onFilterChange).toHaveBeenCalledWith([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('Enter in the text input applies the filter', () => {
|
|
158
|
+
const onFilterChange = vi.fn();
|
|
159
|
+
render(
|
|
160
|
+
<ColumnFilterTrigger
|
|
161
|
+
field="name"
|
|
162
|
+
filterType="text"
|
|
163
|
+
filterModel={EMPTY}
|
|
164
|
+
onFilterChange={onFilterChange}
|
|
165
|
+
/>,
|
|
166
|
+
);
|
|
167
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
168
|
+
const input = screen.getByPlaceholderText('…');
|
|
169
|
+
fireEvent.change(input, { target: { value: 'typed' } });
|
|
170
|
+
fireEvent.keyDown(input, { key: 'Enter' });
|
|
171
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
172
|
+
{ field: 'name', op: 'contains', value: 'typed' },
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('ColumnFilterTrigger — number range filter', () => {
|
|
178
|
+
it('Apply with min + max commits a between filter', () => {
|
|
179
|
+
const onFilterChange = vi.fn();
|
|
180
|
+
render(
|
|
181
|
+
<ColumnFilterTrigger
|
|
182
|
+
field="age"
|
|
183
|
+
filterType="number"
|
|
184
|
+
filterModel={EMPTY}
|
|
185
|
+
onFilterChange={onFilterChange}
|
|
186
|
+
/>,
|
|
187
|
+
);
|
|
188
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
189
|
+
const inputs = screen.getAllByRole('spinbutton');
|
|
190
|
+
fireEvent.change(inputs[0]!, { target: { value: '10' } });
|
|
191
|
+
fireEvent.change(inputs[1]!, { target: { value: '50' } });
|
|
192
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
193
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
194
|
+
{ field: 'age', op: 'between', value: [10, 50] },
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('Apply with only a min commits an open-ended range', () => {
|
|
199
|
+
const onFilterChange = vi.fn();
|
|
200
|
+
render(
|
|
201
|
+
<ColumnFilterTrigger
|
|
202
|
+
field="age"
|
|
203
|
+
filterType="number"
|
|
204
|
+
filterModel={EMPTY}
|
|
205
|
+
onFilterChange={onFilterChange}
|
|
206
|
+
/>,
|
|
207
|
+
);
|
|
208
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
209
|
+
const inputs = screen.getAllByRole('spinbutton');
|
|
210
|
+
fireEvent.change(inputs[0]!, { target: { value: '18' } });
|
|
211
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
212
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
213
|
+
{ field: 'age', op: 'between', value: [18, null] },
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('Apply with both ends empty removes the filter', () => {
|
|
218
|
+
const onFilterChange = vi.fn();
|
|
219
|
+
render(
|
|
220
|
+
<ColumnFilterTrigger
|
|
221
|
+
field="age"
|
|
222
|
+
filterType="number"
|
|
223
|
+
filterModel={[{ field: 'age', op: 'between', value: [1, 2] }]}
|
|
224
|
+
onFilterChange={onFilterChange}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
228
|
+
// The two inputs are seeded from the current value — clear them.
|
|
229
|
+
const inputs = screen.getAllByRole('spinbutton');
|
|
230
|
+
fireEvent.change(inputs[0]!, { target: { value: '' } });
|
|
231
|
+
fireEvent.change(inputs[1]!, { target: { value: '' } });
|
|
232
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
233
|
+
expect(onFilterChange).toHaveBeenCalledWith([]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('ColumnFilterTrigger — boolean filter', () => {
|
|
238
|
+
it('Apply with True selected commits an equals=true filter', () => {
|
|
239
|
+
const onFilterChange = vi.fn();
|
|
240
|
+
render(
|
|
241
|
+
<ColumnFilterTrigger
|
|
242
|
+
field="active"
|
|
243
|
+
filterType="boolean"
|
|
244
|
+
filterModel={EMPTY}
|
|
245
|
+
onFilterChange={onFilterChange}
|
|
246
|
+
/>,
|
|
247
|
+
);
|
|
248
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
249
|
+
fireEvent.click(screen.getByLabelText('True'));
|
|
250
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
251
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
252
|
+
{ field: 'active', op: 'equals', value: true },
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('Apply with False selected commits an equals=false filter', () => {
|
|
257
|
+
const onFilterChange = vi.fn();
|
|
258
|
+
render(
|
|
259
|
+
<ColumnFilterTrigger
|
|
260
|
+
field="active"
|
|
261
|
+
filterType="boolean"
|
|
262
|
+
filterModel={EMPTY}
|
|
263
|
+
onFilterChange={onFilterChange}
|
|
264
|
+
/>,
|
|
265
|
+
);
|
|
266
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
267
|
+
fireEvent.click(screen.getByLabelText('False'));
|
|
268
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
269
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
270
|
+
{ field: 'active', op: 'equals', value: false },
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('Apply with All selected removes the filter', () => {
|
|
275
|
+
const onFilterChange = vi.fn();
|
|
276
|
+
render(
|
|
277
|
+
<ColumnFilterTrigger
|
|
278
|
+
field="active"
|
|
279
|
+
filterType="boolean"
|
|
280
|
+
filterModel={[{ field: 'active', op: 'equals', value: true }]}
|
|
281
|
+
onFilterChange={onFilterChange}
|
|
282
|
+
/>,
|
|
283
|
+
);
|
|
284
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
285
|
+
fireEvent.click(screen.getByLabelText('All'));
|
|
286
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
287
|
+
expect(onFilterChange).toHaveBeenCalledWith([]);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('ColumnFilterTrigger — date range filter', () => {
|
|
292
|
+
it('Apply with from + to commits a between filter (ISO strings)', () => {
|
|
293
|
+
const onFilterChange = vi.fn();
|
|
294
|
+
render(
|
|
295
|
+
<ColumnFilterTrigger
|
|
296
|
+
field="joined"
|
|
297
|
+
filterType="date"
|
|
298
|
+
filterModel={EMPTY}
|
|
299
|
+
onFilterChange={onFilterChange}
|
|
300
|
+
/>,
|
|
301
|
+
);
|
|
302
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
303
|
+
const dateInputs = document.querySelectorAll('input[type="date"]');
|
|
304
|
+
fireEvent.change(dateInputs[0]!, { target: { value: '2024-01-01' } });
|
|
305
|
+
fireEvent.change(dateInputs[1]!, { target: { value: '2024-12-31' } });
|
|
306
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
307
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
308
|
+
{ field: 'joined', op: 'between', value: ['2024-01-01', '2024-12-31'] },
|
|
309
|
+
]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('Apply with both date ends empty removes the filter', () => {
|
|
313
|
+
const onFilterChange = vi.fn();
|
|
314
|
+
render(
|
|
315
|
+
<ColumnFilterTrigger
|
|
316
|
+
field="joined"
|
|
317
|
+
filterType="date"
|
|
318
|
+
filterModel={[
|
|
319
|
+
{ field: 'joined', op: 'between', value: ['2024-01-01', null] },
|
|
320
|
+
]}
|
|
321
|
+
onFilterChange={onFilterChange}
|
|
322
|
+
/>,
|
|
323
|
+
);
|
|
324
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
325
|
+
const dateInputs = document.querySelectorAll('input[type="date"]');
|
|
326
|
+
fireEvent.change(dateInputs[0]!, { target: { value: '' } });
|
|
327
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
328
|
+
expect(onFilterChange).toHaveBeenCalledWith([]);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, type ReactNode } from 'react';
|
|
1
|
+
import { useState, type ReactNode, type KeyboardEvent } from 'react';
|
|
2
2
|
import { Popover } from '../../Popover/Popover.js';
|
|
3
3
|
import { cn } from '../../../utils/cn.js';
|
|
4
4
|
import type {
|
|
@@ -40,6 +40,22 @@ function setFilter(
|
|
|
40
40
|
return [...rest, next];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Shared `onKeyDown` for filter inputs — pressing Enter commits the
|
|
45
|
+
* filter (equivalent to clicking Apply). Without this the user has to
|
|
46
|
+
* leave the keyboard and click the button, which reads as broken for
|
|
47
|
+
* a single-field text filter.
|
|
48
|
+
*/
|
|
49
|
+
function onEnterApply(
|
|
50
|
+
e: KeyboardEvent<HTMLInputElement>,
|
|
51
|
+
apply: () => void,
|
|
52
|
+
): void {
|
|
53
|
+
if (e.key === 'Enter') {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
apply();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
// ───── Filter labels (i18n) ─────
|
|
44
60
|
|
|
45
61
|
export interface ColumnFilterLabels {
|
|
@@ -173,18 +189,22 @@ function TextFilter(props: {
|
|
|
173
189
|
typeof current?.value === 'string' ? current.value : '',
|
|
174
190
|
);
|
|
175
191
|
|
|
192
|
+
const apply = () =>
|
|
193
|
+
onCommit(value.trim() ? { field, op: 'contains', value } : null);
|
|
194
|
+
|
|
176
195
|
return (
|
|
177
196
|
<div className="flex flex-col gap-2 min-w-[200px]">
|
|
178
197
|
<input
|
|
179
198
|
type="text"
|
|
180
199
|
value={value}
|
|
181
200
|
onChange={(e) => setValue(e.target.value)}
|
|
201
|
+
onKeyDown={(e) => onEnterApply(e, apply)}
|
|
182
202
|
placeholder="…"
|
|
183
203
|
autoFocus
|
|
184
204
|
className="rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
185
205
|
/>
|
|
186
206
|
<FilterActions
|
|
187
|
-
onApply={
|
|
207
|
+
onApply={apply}
|
|
188
208
|
onClear={() => {
|
|
189
209
|
setValue('');
|
|
190
210
|
onCommit(null);
|
|
@@ -236,6 +256,7 @@ function NumberRangeFilter(props: {
|
|
|
236
256
|
type="number"
|
|
237
257
|
value={min}
|
|
238
258
|
onChange={(e) => setMin(e.target.value)}
|
|
259
|
+
onKeyDown={(e) => onEnterApply(e, apply)}
|
|
239
260
|
autoFocus
|
|
240
261
|
className="rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
241
262
|
/>
|
|
@@ -246,6 +267,7 @@ function NumberRangeFilter(props: {
|
|
|
246
267
|
type="number"
|
|
247
268
|
value={max}
|
|
248
269
|
onChange={(e) => setMax(e.target.value)}
|
|
270
|
+
onKeyDown={(e) => onEnterApply(e, apply)}
|
|
249
271
|
className="rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
250
272
|
/>
|
|
251
273
|
</label>
|
|
@@ -357,6 +379,7 @@ function DateRangeFilter(props: {
|
|
|
357
379
|
type="date"
|
|
358
380
|
value={from}
|
|
359
381
|
onChange={(e) => setFrom(e.target.value)}
|
|
382
|
+
onKeyDown={(e) => onEnterApply(e, apply)}
|
|
360
383
|
autoFocus
|
|
361
384
|
className="rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
362
385
|
/>
|
|
@@ -367,6 +390,7 @@ function DateRangeFilter(props: {
|
|
|
367
390
|
type="date"
|
|
368
391
|
value={to}
|
|
369
392
|
onChange={(e) => setTo(e.target.value)}
|
|
393
|
+
onKeyDown={(e) => onEnterApply(e, apply)}
|
|
370
394
|
className="rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
371
395
|
/>
|
|
372
396
|
</label>
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
// @vitest-environment
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import {
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { renderHook, act } from '@testing-library/react';
|
|
4
|
+
import { moveColumn, useColumnReorder } from './useColumnReorder.js';
|
|
5
|
+
|
|
6
|
+
/** Minimal fake of the bits of a drag event the hook reads. */
|
|
7
|
+
function fakeDragStart() {
|
|
8
|
+
return {
|
|
9
|
+
dataTransfer: { effectAllowed: '', setData: vi.fn() },
|
|
10
|
+
} as unknown as React.DragEvent<HTMLElement>;
|
|
11
|
+
}
|
|
12
|
+
function fakeDragOver(clientX: number) {
|
|
13
|
+
return {
|
|
14
|
+
preventDefault: vi.fn(),
|
|
15
|
+
dataTransfer: { dropEffect: '' },
|
|
16
|
+
currentTarget: {
|
|
17
|
+
getBoundingClientRect: () => ({ left: 0, width: 100 }),
|
|
18
|
+
},
|
|
19
|
+
clientX,
|
|
20
|
+
} as unknown as React.DragEvent<HTMLElement>;
|
|
21
|
+
}
|
|
22
|
+
function fakeDrop() {
|
|
23
|
+
return { preventDefault: vi.fn() } as unknown as React.DragEvent<HTMLElement>;
|
|
24
|
+
}
|
|
4
25
|
|
|
5
26
|
describe('moveColumn', () => {
|
|
6
27
|
const order = ['a', 'b', 'c', 'd'];
|
|
@@ -36,3 +57,74 @@ describe('moveColumn', () => {
|
|
|
36
57
|
expect(result.sort()).toEqual(['a', 'b', 'c', 'd']);
|
|
37
58
|
});
|
|
38
59
|
});
|
|
60
|
+
|
|
61
|
+
describe('useColumnReorder — drag handlers', () => {
|
|
62
|
+
const order = ['a', 'b', 'c', 'd'];
|
|
63
|
+
|
|
64
|
+
it('dragStart records the source field + dragging state', () => {
|
|
65
|
+
const { result } = renderHook(() =>
|
|
66
|
+
useColumnReorder({ order, onChange: vi.fn() }),
|
|
67
|
+
);
|
|
68
|
+
const e = fakeDragStart();
|
|
69
|
+
act(() => result.current.dragStart('a')(e));
|
|
70
|
+
expect(result.current.draggingField).toBe('a');
|
|
71
|
+
expect(e.dataTransfer.setData).toHaveBeenCalledWith('text/plain', 'a');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('dragOver on the RIGHT half marks a right-side drop target', () => {
|
|
75
|
+
const { result } = renderHook(() =>
|
|
76
|
+
useColumnReorder({ order, onChange: vi.fn() }),
|
|
77
|
+
);
|
|
78
|
+
act(() => result.current.dragStart('a')(fakeDragStart()));
|
|
79
|
+
// clientX 80 of a 0..100 cell → past the midpoint → 'right'.
|
|
80
|
+
act(() => result.current.dragOver('c')(fakeDragOver(80)));
|
|
81
|
+
expect(result.current.dropTarget).toEqual({ field: 'c', side: 'right' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('dragOver on the LEFT half marks a left-side drop target', () => {
|
|
85
|
+
const { result } = renderHook(() =>
|
|
86
|
+
useColumnReorder({ order, onChange: vi.fn() }),
|
|
87
|
+
);
|
|
88
|
+
act(() => result.current.dragStart('a')(fakeDragStart()));
|
|
89
|
+
act(() => result.current.dragOver('c')(fakeDragOver(20)));
|
|
90
|
+
expect(result.current.dropTarget).toEqual({ field: 'c', side: 'left' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('dragOver onto the source column itself is a no-op', () => {
|
|
94
|
+
const { result } = renderHook(() =>
|
|
95
|
+
useColumnReorder({ order, onChange: vi.fn() }),
|
|
96
|
+
);
|
|
97
|
+
act(() => result.current.dragStart('a')(fakeDragStart()));
|
|
98
|
+
act(() => result.current.dragOver('a')(fakeDragOver(80)));
|
|
99
|
+
expect(result.current.dropTarget).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('drop commits the reordered list via onChange', () => {
|
|
103
|
+
const onChange = vi.fn();
|
|
104
|
+
const { result } = renderHook(() => useColumnReorder({ order, onChange }));
|
|
105
|
+
act(() => result.current.dragStart('a')(fakeDragStart()));
|
|
106
|
+
act(() => result.current.dragOver('c')(fakeDragOver(80)));
|
|
107
|
+
act(() => result.current.drop('c')(fakeDrop()));
|
|
108
|
+
// 'a' dropped on the RIGHT of 'c' → ['b','c','a','d'].
|
|
109
|
+
expect(onChange).toHaveBeenCalledWith(['b', 'c', 'a', 'd']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('drop on the source column does not call onChange', () => {
|
|
113
|
+
const onChange = vi.fn();
|
|
114
|
+
const { result } = renderHook(() => useColumnReorder({ order, onChange }));
|
|
115
|
+
act(() => result.current.dragStart('a')(fakeDragStart()));
|
|
116
|
+
act(() => result.current.drop('a')(fakeDrop()));
|
|
117
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('dragEnd clears the dragging + drop-target state', () => {
|
|
121
|
+
const { result } = renderHook(() =>
|
|
122
|
+
useColumnReorder({ order, onChange: vi.fn() }),
|
|
123
|
+
);
|
|
124
|
+
act(() => result.current.dragStart('a')(fakeDragStart()));
|
|
125
|
+
act(() => result.current.dragOver('c')(fakeDragOver(80)));
|
|
126
|
+
act(() => result.current.dragEnd());
|
|
127
|
+
expect(result.current.draggingField).toBeNull();
|
|
128
|
+
expect(result.current.dropTarget).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for `useColumnResize` — the pointer-drag + keyboard
|
|
4
|
+
* column-resize hook (Sprint 4.2-bis, keyboard `nudge` added in
|
|
5
|
+
* Sprint 6 P2). Sprint 6 P4 — direct coverage of the clamp logic
|
|
6
|
+
* and the drag commit path.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import { renderHook } from '@testing-library/react';
|
|
10
|
+
import { useColumnResize } from './useColumnResize.js';
|
|
11
|
+
|
|
12
|
+
describe('useColumnResize — nudge (keyboard path)', () => {
|
|
13
|
+
it('commits currentWidth + delta', () => {
|
|
14
|
+
const onChange = vi.fn();
|
|
15
|
+
const { result } = renderHook(() =>
|
|
16
|
+
useColumnResize({ widths: { a: 100 }, onChange }),
|
|
17
|
+
);
|
|
18
|
+
result.current.nudge('a', 100, 16, 40, 400);
|
|
19
|
+
expect(onChange).toHaveBeenCalledWith({ a: 116 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('clamps to the effective minimum', () => {
|
|
23
|
+
const onChange = vi.fn();
|
|
24
|
+
const { result } = renderHook(() =>
|
|
25
|
+
useColumnResize({ widths: { a: 50 }, onChange }),
|
|
26
|
+
);
|
|
27
|
+
// currentWidth 50, delta -64 → 14, clamped up to min 40.
|
|
28
|
+
result.current.nudge('a', 50, -64, 40, 400);
|
|
29
|
+
expect(onChange).toHaveBeenCalledWith({ a: 40 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('clamps to the effective maximum', () => {
|
|
33
|
+
const onChange = vi.fn();
|
|
34
|
+
const { result } = renderHook(() =>
|
|
35
|
+
useColumnResize({ widths: { a: 380 }, onChange }),
|
|
36
|
+
);
|
|
37
|
+
// currentWidth 380, delta +64 → 444, clamped down to max 400.
|
|
38
|
+
result.current.nudge('a', 380, 64, 40, 400);
|
|
39
|
+
expect(onChange).toHaveBeenCalledWith({ a: 400 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('honors the hook-level minWidth/maxWidth floor/ceiling', () => {
|
|
43
|
+
const onChange = vi.fn();
|
|
44
|
+
const { result } = renderHook(() =>
|
|
45
|
+
useColumnResize({
|
|
46
|
+
widths: {},
|
|
47
|
+
onChange,
|
|
48
|
+
minWidth: 80,
|
|
49
|
+
maxWidth: 200,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
// clampMin 40 < hook min 80 → effective min is 80.
|
|
53
|
+
result.current.nudge('a', 100, -64, 40, 1000);
|
|
54
|
+
expect(onChange).toHaveBeenCalledWith({ a: 80 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('preserves the other columns in the width map', () => {
|
|
58
|
+
const onChange = vi.fn();
|
|
59
|
+
const { result } = renderHook(() =>
|
|
60
|
+
useColumnResize({ widths: { a: 100, b: 200 }, onChange }),
|
|
61
|
+
);
|
|
62
|
+
result.current.nudge('a', 100, 16, 40, 400);
|
|
63
|
+
expect(onChange).toHaveBeenCalledWith({ a: 116, b: 200 });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('useColumnResize — startResize (pointer-drag path)', () => {
|
|
68
|
+
it('returns an event handler from the factory', () => {
|
|
69
|
+
const { result } = renderHook(() =>
|
|
70
|
+
useColumnResize({ widths: {}, onChange: () => {} }),
|
|
71
|
+
);
|
|
72
|
+
const handler = result.current.startResize('a', 100, 40, 400);
|
|
73
|
+
expect(typeof handler).toBe('function');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('a pointer drag commits a clamped new width via pointermove', () => {
|
|
77
|
+
const onChange = vi.fn();
|
|
78
|
+
const { result } = renderHook(() =>
|
|
79
|
+
useColumnResize({ widths: { a: 100 }, onChange }),
|
|
80
|
+
);
|
|
81
|
+
const el = document.createElement('span');
|
|
82
|
+
document.body.appendChild(el);
|
|
83
|
+
|
|
84
|
+
// Synthetic pointerdown — shape mirrors React's PointerEvent.
|
|
85
|
+
const downEvent = {
|
|
86
|
+
pointerType: 'mouse',
|
|
87
|
+
button: 0,
|
|
88
|
+
clientX: 500,
|
|
89
|
+
pointerId: 1,
|
|
90
|
+
currentTarget: el,
|
|
91
|
+
preventDefault: () => {},
|
|
92
|
+
stopPropagation: () => {},
|
|
93
|
+
} as unknown as React.PointerEvent<HTMLElement>;
|
|
94
|
+
|
|
95
|
+
result.current.startResize('a', 100, 40, 400)(downEvent);
|
|
96
|
+
|
|
97
|
+
// Drag 30px to the right → 100 + 30 = 130.
|
|
98
|
+
const move = new Event('pointermove') as Event & { clientX: number };
|
|
99
|
+
move.clientX = 530;
|
|
100
|
+
el.dispatchEvent(move);
|
|
101
|
+
expect(onChange).toHaveBeenLastCalledWith({ a: 130 });
|
|
102
|
+
|
|
103
|
+
// Drag far left → clamps to min 40.
|
|
104
|
+
const move2 = new Event('pointermove') as Event & { clientX: number };
|
|
105
|
+
move2.clientX = 0;
|
|
106
|
+
el.dispatchEvent(move2);
|
|
107
|
+
expect(onChange).toHaveBeenLastCalledWith({ a: 40 });
|
|
108
|
+
|
|
109
|
+
// pointerup tears the listeners down — a later move is a no-op.
|
|
110
|
+
const up = new Event('pointerup') as Event & { pointerId: number };
|
|
111
|
+
up.pointerId = 1;
|
|
112
|
+
el.dispatchEvent(up);
|
|
113
|
+
onChange.mockClear();
|
|
114
|
+
const move3 = new Event('pointermove') as Event & { clientX: number };
|
|
115
|
+
move3.clientX = 999;
|
|
116
|
+
el.dispatchEvent(move3);
|
|
117
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
118
|
+
|
|
119
|
+
document.body.removeChild(el);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('ignores a non-primary mouse button', () => {
|
|
123
|
+
const onChange = vi.fn();
|
|
124
|
+
const { result } = renderHook(() =>
|
|
125
|
+
useColumnResize({ widths: {}, onChange }),
|
|
126
|
+
);
|
|
127
|
+
const el = document.createElement('span');
|
|
128
|
+
const downEvent = {
|
|
129
|
+
pointerType: 'mouse',
|
|
130
|
+
button: 2, // right-click
|
|
131
|
+
clientX: 0,
|
|
132
|
+
pointerId: 1,
|
|
133
|
+
currentTarget: el,
|
|
134
|
+
preventDefault: () => {},
|
|
135
|
+
stopPropagation: () => {},
|
|
136
|
+
} as unknown as React.PointerEvent<HTMLElement>;
|
|
137
|
+
result.current.startResize('a', 100, 40, 400)(downEvent);
|
|
138
|
+
const move = new Event('pointermove') as Event & { clientX: number };
|
|
139
|
+
move.clientX = 200;
|
|
140
|
+
el.dispatchEvent(move);
|
|
141
|
+
// No listener was attached → no commit.
|
|
142
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
});
|