@dashforge/tw 0.8.1-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 +126 -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 +177 -67
- 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/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/eslint.config.mjs +16 -1
- package/package.json +9 -6
- 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 +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/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/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
|
@@ -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}
|
|
@@ -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>
|