@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0
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/README.md +5 -212
- package/dist/index.d.ts +1323 -796
- package/dist/index.js +4719 -3193
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +8 -8
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/components/generatedColumns.tsx +187 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +704 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -12,12 +12,14 @@ import {
|
|
|
12
12
|
import type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
13
13
|
import { getColumnCapabilities, getGridCapabilities } from "../core/capabilities";
|
|
14
14
|
import { autosizeColumn } from "../core/autosize";
|
|
15
|
+
import { isNode } from "../core/dom";
|
|
15
16
|
import {
|
|
16
17
|
getColumnRegion,
|
|
17
18
|
getStepTargetColumn,
|
|
18
19
|
keepGeneratedColumnsOutermost,
|
|
19
20
|
moveColumn,
|
|
20
21
|
moveColumnByStep,
|
|
22
|
+
pinningPositionOfSide,
|
|
21
23
|
type TMDataGridDropSide,
|
|
22
24
|
} from "../core/columnOrdering";
|
|
23
25
|
import {
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
UngroupIcon,
|
|
47
49
|
} from "./icons";
|
|
48
50
|
import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
|
|
51
|
+
import { TMDataGridMenuColumns } from "./TMDataGridMenu";
|
|
49
52
|
import { type TMDataGridFeatures, openColumnFilter } from "../useTMDataGrid";
|
|
50
53
|
|
|
51
54
|
/** The table as the header cell sees it, after the context type erasure. */
|
|
@@ -89,7 +92,7 @@ export function TMDataGridHeaderCell({
|
|
|
89
92
|
renderColumnMenuItems?: TMDataGridColumnMenuItemsRenderer;
|
|
90
93
|
}) {
|
|
91
94
|
const api = useTMDataGridContext();
|
|
92
|
-
const { table, features, labels } = api;
|
|
95
|
+
const { table, features, filters, labels } = api;
|
|
93
96
|
const column = header.column;
|
|
94
97
|
const [menuOpened, setMenuOpened] = useState(false);
|
|
95
98
|
const [contextMenuOpened, setContextMenuOpened] = useState(false);
|
|
@@ -191,7 +194,7 @@ export function TMDataGridHeaderCell({
|
|
|
191
194
|
function handleDragLeave(event: DragEvent<HTMLDivElement>) {
|
|
192
195
|
// dragleave also fires when the pointer crosses into a child element.
|
|
193
196
|
const next = event.relatedTarget;
|
|
194
|
-
if (next
|
|
197
|
+
if (isNode(next) && event.currentTarget.contains(next)) return;
|
|
195
198
|
setDropSide(null);
|
|
196
199
|
}
|
|
197
200
|
|
|
@@ -270,8 +273,8 @@ export function TMDataGridHeaderCell({
|
|
|
270
273
|
*/
|
|
271
274
|
function setPinned(position: "left" | "right" | false) {
|
|
272
275
|
if (position) freezeRenderedWidths();
|
|
273
|
-
column.pin(position);
|
|
274
|
-
// `pin("
|
|
276
|
+
column.pin(pinningPositionOfSide(position));
|
|
277
|
+
// `pin("end")` appends, which would leave the column outside the edit
|
|
275
278
|
// lane. Normalising after the pin rather than writing the lane by hand
|
|
276
279
|
// keeps TanStack's own handling of a header group's leaves.
|
|
277
280
|
table.setColumnPinning(keepGeneratedColumnsOutermost);
|
|
@@ -297,7 +300,9 @@ export function TMDataGridHeaderCell({
|
|
|
297
300
|
<Menu.Divider key="sort-divider" />,
|
|
298
301
|
);
|
|
299
302
|
}
|
|
300
|
-
|
|
303
|
+
// Not under header filters: the item's only job is to reveal a control that
|
|
304
|
+
// is already on screen, one row below this menu.
|
|
305
|
+
if (canFilter && !filters.inHeader) {
|
|
301
306
|
menuItems.push(
|
|
302
307
|
<Menu.Item
|
|
303
308
|
key="filter"
|
|
@@ -476,13 +481,19 @@ export function TMDataGridHeaderCell({
|
|
|
476
481
|
}
|
|
477
482
|
if (canManageColumns) {
|
|
478
483
|
menuItems.push(
|
|
479
|
-
<Menu.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
<Menu.Sub key="manage">
|
|
485
|
+
<Menu.Sub.Target>
|
|
486
|
+
<Menu.Sub.Item leftSection={<ColumnsIcon size={16} stroke={1.6} />}>
|
|
487
|
+
{labels.manageColumns}
|
|
488
|
+
</Menu.Sub.Item>
|
|
489
|
+
</Menu.Sub.Target>
|
|
490
|
+
<Menu.Sub.Dropdown>
|
|
491
|
+
{/* No search box: `Menu.Search` registers on the root menu context,
|
|
492
|
+
which switches off type-ahead and the arrow-key handling of every
|
|
493
|
+
dropdown of this menu - the column menu's included. */}
|
|
494
|
+
<TMDataGridMenuColumns searchable={false} />
|
|
495
|
+
</Menu.Sub.Dropdown>
|
|
496
|
+
</Menu.Sub>,
|
|
486
497
|
);
|
|
487
498
|
}
|
|
488
499
|
// A trailing divider reads as a dropped item - drop the divider instead.
|
|
@@ -529,11 +540,11 @@ export function TMDataGridHeaderCell({
|
|
|
529
540
|
// The same coordinate the body, summary and entry cells carry, so one
|
|
530
541
|
// selector reaches a column's header and its cells alike.
|
|
531
542
|
data-column-id={column.id}
|
|
532
|
-
data-active={isSorted || isFiltered}
|
|
543
|
+
data-active={isSorted || isFiltered || undefined}
|
|
533
544
|
data-align={align}
|
|
534
545
|
// A control lane is a fixed track, so it cannot take the cell padding the
|
|
535
546
|
// scale grows for text. See isControlColumn.
|
|
536
|
-
data-control-column={isControlColumn(column.id)}
|
|
547
|
+
data-control-column={isControlColumn(column.id) || undefined}
|
|
537
548
|
// Only meaningful on a sortable column: "none" announces that this
|
|
538
549
|
// header sorts, which is wrong on one that does not.
|
|
539
550
|
aria-sort={
|
|
@@ -558,6 +569,11 @@ export function TMDataGridHeaderCell({
|
|
|
558
569
|
onDragLeave={isDropTarget ? handleDragLeave : undefined}
|
|
559
570
|
onDrop={isDropTarget ? handleDrop : undefined}
|
|
560
571
|
style={{
|
|
572
|
+
// A group header covers the tracks of the leaves under it. Without the
|
|
573
|
+
// span it would sit in one track and leave the rest of its width with
|
|
574
|
+
// no cell at all - a gap the body rows scroll up through, since the
|
|
575
|
+
// header rows above the body are what hides them.
|
|
576
|
+
gridColumn: header.colSpan > 1 ? `span ${header.colSpan}` : undefined,
|
|
561
577
|
left: pinnedAt === "left" ? layout.offset : undefined,
|
|
562
578
|
right: pinnedAt === "right" ? layout.offset : undefined,
|
|
563
579
|
position: pinnedAt ? "sticky" : undefined,
|
|
@@ -599,7 +615,9 @@ export function TMDataGridHeaderCell({
|
|
|
599
615
|
|
|
600
616
|
{isLeaf && (
|
|
601
617
|
<div className={classes.headerActions}>
|
|
602
|
-
{
|
|
618
|
+
{/* Dropped under header filters, where the filled-in control below
|
|
619
|
+
is both the indicator and the way in. */}
|
|
620
|
+
{isFiltered && !filters.inHeader && (
|
|
603
621
|
<ActionIcon
|
|
604
622
|
className={classes.headerAction}
|
|
605
623
|
data-pinned-visible="true"
|
|
@@ -621,19 +639,24 @@ export function TMDataGridHeaderCell({
|
|
|
621
639
|
{canSort && (
|
|
622
640
|
<ActionIcon
|
|
623
641
|
className={`${classes.headerAction} ${classes.sortAction}`}
|
|
624
|
-
data-pinned-visible={isSorted}
|
|
642
|
+
data-pinned-visible={isSorted || undefined}
|
|
625
643
|
// The arrow is the only sort indicator now that the title no
|
|
626
644
|
// longer tints, so it takes the colour while the sort holds. On
|
|
627
645
|
// an unsorted column it is only the hover affordance, and stays a
|
|
628
646
|
// faded grey so the two are not confused.
|
|
629
|
-
data-sorted={isSorted}
|
|
647
|
+
data-sorted={isSorted || undefined}
|
|
630
648
|
variant="subtle"
|
|
631
649
|
color={isSorted ? undefined : "gray"}
|
|
632
650
|
size="xs"
|
|
633
651
|
aria-label={labels.sortColumn(label)}
|
|
634
652
|
data-dg-part="header-sort"
|
|
635
653
|
data-column-id={column.id}
|
|
636
|
-
|
|
654
|
+
// Stopped here: the header cell around it sorts on click too, and
|
|
655
|
+
// letting the click reach it would advance the sort twice.
|
|
656
|
+
onClick={(event) => {
|
|
657
|
+
event.stopPropagation();
|
|
658
|
+
column.getToggleSortingHandler()?.(event);
|
|
659
|
+
}}
|
|
637
660
|
>
|
|
638
661
|
{sortDirection ? (
|
|
639
662
|
<ArrowDownIcon size={14} stroke={1.6} />
|
|
@@ -667,7 +690,7 @@ export function TMDataGridHeaderCell({
|
|
|
667
690
|
// Held visible for the right-click menu too: that one opens at
|
|
668
691
|
// the pointer and is portaled, so the hover that revealed the
|
|
669
692
|
// actions is lost the moment the pointer enters the dropdown.
|
|
670
|
-
data-pinned-visible={menuOpened || contextMenuOpened}
|
|
693
|
+
data-pinned-visible={menuOpened || contextMenuOpened || undefined}
|
|
671
694
|
variant="subtle"
|
|
672
695
|
color="gray"
|
|
673
696
|
size="xs"
|
|
@@ -702,18 +725,30 @@ export function TMDataGridHeaderCell({
|
|
|
702
725
|
]
|
|
703
726
|
.filter(Boolean)
|
|
704
727
|
.join(" ")}
|
|
728
|
+
// A part only where it is a handle: on a fixed-width column the
|
|
729
|
+
// separator is a plain divider, and a test that finds it would be
|
|
730
|
+
// dragging something that does nothing.
|
|
731
|
+
data-dg-part={canResize ? "header-resize" : undefined}
|
|
732
|
+
data-column-id={canResize ? column.id : undefined}
|
|
705
733
|
// Keeps a drag that starts on the separator from being picked up by
|
|
706
734
|
// the draggable header around it.
|
|
707
735
|
draggable={false}
|
|
708
736
|
// The drag starts from `column.getSize()`, so the rendered width has
|
|
709
737
|
// to be in `columnSizing` before the handler reads it - otherwise
|
|
710
738
|
// the first pointer move snaps a fluid column to its declared size.
|
|
739
|
+
//
|
|
740
|
+
// The handler listens for the moves and the release on the document
|
|
741
|
+
// it is given - the separator's own, not the global one, which is
|
|
742
|
+
// the opener's when the grid is rendered through a portal into a
|
|
743
|
+
// window opened with `window.open`.
|
|
711
744
|
onMouseDown={
|
|
712
745
|
canResize
|
|
713
746
|
? (event) => {
|
|
714
747
|
suppressSortRef.current = true;
|
|
715
748
|
freezeRenderedWidths();
|
|
716
|
-
header.getResizeHandler()(
|
|
749
|
+
header.getResizeHandler(event.currentTarget.ownerDocument)(
|
|
750
|
+
event,
|
|
751
|
+
);
|
|
717
752
|
}
|
|
718
753
|
: undefined
|
|
719
754
|
}
|
|
@@ -721,7 +756,9 @@ export function TMDataGridHeaderCell({
|
|
|
721
756
|
canResize
|
|
722
757
|
? (event) => {
|
|
723
758
|
freezeRenderedWidths();
|
|
724
|
-
header.getResizeHandler()(
|
|
759
|
+
header.getResizeHandler(event.currentTarget.ownerDocument)(
|
|
760
|
+
event,
|
|
761
|
+
);
|
|
725
762
|
}
|
|
726
763
|
: undefined
|
|
727
764
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* The second header row, under the group rows and above the body. Same column
|
|
2
|
+
tracks as every other row, through subgrid, so it follows resizing,
|
|
3
|
+
reordering and pinning without knowing about any of them.
|
|
4
|
+
|
|
5
|
+
Sticky like the header rows above it, and stacked under them by the same
|
|
6
|
+
measurement: the table writes every header row's `top` inline once it knows
|
|
7
|
+
how tall the rows before it are. This is the pre-measurement fallback, and
|
|
8
|
+
it is right for the common case of one header row. */
|
|
9
|
+
.headerFilterRow {
|
|
10
|
+
display: grid;
|
|
11
|
+
grid-column: 1 / -1;
|
|
12
|
+
grid-template-columns: subgrid;
|
|
13
|
+
position: sticky;
|
|
14
|
+
top: var(--dg-header-height);
|
|
15
|
+
z-index: var(--dg-z-header, 6);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.headerFilterCell {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 2px;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
/* A 60px column has room for the input and nothing beside it. Clipping is
|
|
24
|
+
what keeps the control from spilling over its neighbour; the operator
|
|
25
|
+
button is the first thing to go. */
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
padding: calc(var(--dg-padding) / 2);
|
|
28
|
+
/* Opaque: the body scrolls underneath it. */
|
|
29
|
+
background-color: var(--mantine-color-body);
|
|
30
|
+
border-bottom: 1px solid var(--mantine-color-default-border);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* The generated lanes - checkbox, details, row number - hold no filter, so
|
|
34
|
+
they keep the header's own padding rather than a control's. */
|
|
35
|
+
.headerFilterCell[data-control-column="true"] {
|
|
36
|
+
padding: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.headerFilterValue {
|
|
40
|
+
flex: 1;
|
|
41
|
+
min-width: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.headerFilterOperator {
|
|
45
|
+
flex: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.operatorItemActive {
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
color: var(--mantine-primary-color-filled);
|
|
51
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
|
2
|
+
import { useSelector } from "@tanstack/react-store";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import classes from "./TMDataGridHeaderFilterRow.module.css";
|
|
5
|
+
import sticky from "./sticky.module.css";
|
|
6
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
7
|
+
import {
|
|
8
|
+
getColumnDefaultOperator,
|
|
9
|
+
getColumnLabel,
|
|
10
|
+
getColumnOperators,
|
|
11
|
+
isControlColumn,
|
|
12
|
+
} from "../core/columnUtils";
|
|
13
|
+
import {
|
|
14
|
+
type TMDataGridFilterOperator,
|
|
15
|
+
type TMDataGridFilterValue,
|
|
16
|
+
emptyValueForOperator,
|
|
17
|
+
filterValueShape,
|
|
18
|
+
isTMDataGridFilterValue,
|
|
19
|
+
} from "../core/filterOperators";
|
|
20
|
+
import { FilterIcon } from "./icons";
|
|
21
|
+
import {
|
|
22
|
+
filterControlFor,
|
|
23
|
+
filterOptionsUseFacets,
|
|
24
|
+
} from "./filters/filterControlFor";
|
|
25
|
+
import type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
26
|
+
import type { TMDataGridHeader } from "./TMDataGridHeaderCell";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether a value says nothing at all - untrimmed, unlike `isFilterActive`,
|
|
30
|
+
* which is about whether a filter *narrows* anything. A lone space narrows
|
|
31
|
+
* nothing but is very much something the user typed.
|
|
32
|
+
*/
|
|
33
|
+
function isEmptyValue(value: string | ReadonlyArray<string>): boolean {
|
|
34
|
+
return Array.isArray(value)
|
|
35
|
+
? value.every((entry) => entry === "")
|
|
36
|
+
: value === "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One column's filter control, as it appears in the header row: the value
|
|
41
|
+
* control the column would get in the panel, plus a button for the operator
|
|
42
|
+
* when the column's type offers more than one.
|
|
43
|
+
*
|
|
44
|
+
* The column and operator dropdowns of a panel row have no place here - the
|
|
45
|
+
* column is the one the cell sits over, and a header cell has room for a value
|
|
46
|
+
* and not much else. Everything else is the same filter: the same operators,
|
|
47
|
+
* the same `meta.filter.control`, the same `columnFilters` state.
|
|
48
|
+
*/
|
|
49
|
+
function HeaderFilterControl({ column }: { column: TMDataGridHeader["column"] }) {
|
|
50
|
+
const { table, labels, controlSize } = useTMDataGridContext();
|
|
51
|
+
const filterValue = useSelector(
|
|
52
|
+
table.store,
|
|
53
|
+
(state) => state.columnFilters.find((filter) => filter.id === column.id)?.value,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const defaultOperator = getColumnDefaultOperator(column);
|
|
57
|
+
const current = isTMDataGridFilterValue(filterValue) ? filterValue : undefined;
|
|
58
|
+
const operator = current?.operator ?? defaultOperator;
|
|
59
|
+
const value = current?.value ?? emptyValueForOperator(operator);
|
|
60
|
+
const label = getColumnLabel(column);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Writes the filter, or drops it entirely once nothing is left to say. A
|
|
64
|
+
* header control is always on screen, so an empty one has no row to keep
|
|
65
|
+
* alive the way a panel row does, and leaving the entry behind would put a
|
|
66
|
+
* filter in `columnFilters` that says nothing.
|
|
67
|
+
*
|
|
68
|
+
* Emptiness here is the literal one, not `isFilterActive`'s: that one trims,
|
|
69
|
+
* and dropping the filter on a value of `" "` would take the space back out
|
|
70
|
+
* of the input the moment it was typed. An operator the user picked is worth
|
|
71
|
+
* keeping even with an empty value; the type's own default is not.
|
|
72
|
+
*/
|
|
73
|
+
function write(next: TMDataGridFilterValue) {
|
|
74
|
+
const worthKeeping =
|
|
75
|
+
!isEmptyValue(next.value) || next.operator !== defaultOperator;
|
|
76
|
+
column.setFilterValue(worthKeeping ? next : undefined);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function changeOperator(next: TMDataGridFilterOperator) {
|
|
80
|
+
// Same arity rule as the panel: keep the value across operators of the
|
|
81
|
+
// same shape, reset it across any shape boundary.
|
|
82
|
+
write({
|
|
83
|
+
operator: next,
|
|
84
|
+
value:
|
|
85
|
+
filterValueShape(next) === filterValueShape(operator)
|
|
86
|
+
? value
|
|
87
|
+
: emptyValueForOperator(next),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Memoized because this row re-renders with the table - on every scroll
|
|
92
|
+
// frame, for every column - while resolving faceted options walks the
|
|
93
|
+
// faceted map into a sorted set. TanStack swaps that map's identity whenever
|
|
94
|
+
// the values behind it change, so it is exactly the right key.
|
|
95
|
+
const facetKey = filterOptionsUseFacets(column)
|
|
96
|
+
? column.getFacetedUniqueValues()
|
|
97
|
+
: undefined;
|
|
98
|
+
const { options, ValueControl } = useMemo(
|
|
99
|
+
() => filterControlFor(table, column),
|
|
100
|
+
// `facetKey` is a cache key, not something the body reads: the resolution
|
|
101
|
+
// reaches the faceted map through the column, so nothing here names it.
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
[table, column, facetKey],
|
|
104
|
+
);
|
|
105
|
+
const operators = getColumnOperators(column);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<div className={classes.headerFilterValue} data-dg-filter-value-slot>
|
|
110
|
+
<ValueControl
|
|
111
|
+
column={column}
|
|
112
|
+
table={table}
|
|
113
|
+
operator={operator}
|
|
114
|
+
value={value}
|
|
115
|
+
onChange={(next) => write({ operator, value: next })}
|
|
116
|
+
options={options}
|
|
117
|
+
size={controlSize}
|
|
118
|
+
labels={labels}
|
|
119
|
+
layout="header"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<OperatorMenu
|
|
124
|
+
columnId={column.id}
|
|
125
|
+
columnLabel={label}
|
|
126
|
+
operator={operator}
|
|
127
|
+
isDefault={operator === defaultOperator}
|
|
128
|
+
operators={operators}
|
|
129
|
+
onChange={changeOperator}
|
|
130
|
+
/>
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The operator picker beside a header filter's value: which comparison the
|
|
137
|
+
* column is on, and the list to change it to.
|
|
138
|
+
*
|
|
139
|
+
* The tooltip is the only thing saying what the current operator is while the
|
|
140
|
+
* menu is shut, and the only thing in the way once it is open - so it is held
|
|
141
|
+
* back while the dropdown shows.
|
|
142
|
+
*/
|
|
143
|
+
function OperatorMenu({
|
|
144
|
+
columnId,
|
|
145
|
+
columnLabel,
|
|
146
|
+
operator,
|
|
147
|
+
isDefault,
|
|
148
|
+
operators,
|
|
149
|
+
onChange,
|
|
150
|
+
}: {
|
|
151
|
+
columnId: string;
|
|
152
|
+
columnLabel: string;
|
|
153
|
+
operator: TMDataGridFilterOperator;
|
|
154
|
+
isDefault: boolean;
|
|
155
|
+
operators: ReadonlyArray<TMDataGridFilterOperator>;
|
|
156
|
+
onChange: (next: TMDataGridFilterOperator) => void;
|
|
157
|
+
}) {
|
|
158
|
+
const { labels } = useTMDataGridContext();
|
|
159
|
+
const [opened, setOpened] = useState(false);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Menu
|
|
163
|
+
opened={opened}
|
|
164
|
+
onChange={setOpened}
|
|
165
|
+
position="bottom-end"
|
|
166
|
+
shadow="md"
|
|
167
|
+
width={220}
|
|
168
|
+
withinPortal
|
|
169
|
+
>
|
|
170
|
+
<Menu.Target>
|
|
171
|
+
<Tooltip
|
|
172
|
+
label={labels.operators[operator]}
|
|
173
|
+
openDelay={400}
|
|
174
|
+
disabled={opened}
|
|
175
|
+
>
|
|
176
|
+
<ActionIcon
|
|
177
|
+
className={classes.headerFilterOperator}
|
|
178
|
+
variant="subtle"
|
|
179
|
+
// Tinted while the column is on anything but its default operator -
|
|
180
|
+
// the one part of the filter the value cannot show.
|
|
181
|
+
color={isDefault ? "gray" : undefined}
|
|
182
|
+
size="sm"
|
|
183
|
+
aria-label={labels.filterOperatorFor(columnLabel)}
|
|
184
|
+
data-dg-part="header-filter-operator"
|
|
185
|
+
data-column-id={columnId}
|
|
186
|
+
>
|
|
187
|
+
<FilterIcon size={14} stroke={1.6} />
|
|
188
|
+
</ActionIcon>
|
|
189
|
+
</Tooltip>
|
|
190
|
+
</Menu.Target>
|
|
191
|
+
<Menu.Dropdown>
|
|
192
|
+
{operators.map((candidate) => (
|
|
193
|
+
<Menu.Item
|
|
194
|
+
key={candidate}
|
|
195
|
+
className={
|
|
196
|
+
candidate === operator ? classes.operatorItemActive : undefined
|
|
197
|
+
}
|
|
198
|
+
onClick={() => onChange(candidate)}
|
|
199
|
+
>
|
|
200
|
+
{labels.operators[candidate]}
|
|
201
|
+
</Menu.Item>
|
|
202
|
+
))}
|
|
203
|
+
</Menu.Dropdown>
|
|
204
|
+
</Menu>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `filters.inHeader` - a second header row holding one value control per
|
|
210
|
+
* filterable column, always visible.
|
|
211
|
+
*
|
|
212
|
+
* A header row like the group rows above it, on the same column tracks and
|
|
213
|
+
* with the same pinned lanes, so resizing, reordering and pinning move the
|
|
214
|
+
* controls with their columns. It sticks below the group rows rather than at
|
|
215
|
+
* the top, and carries `data-dg-header-row` so the body's scroll offset counts
|
|
216
|
+
* it in.
|
|
217
|
+
*
|
|
218
|
+
* @internal Rendered by `TMDataGrid.Table`.
|
|
219
|
+
*/
|
|
220
|
+
export function TMDataGridHeaderFilterRow({
|
|
221
|
+
leafHeaders,
|
|
222
|
+
layoutFor,
|
|
223
|
+
ariaRowIndex,
|
|
224
|
+
}: {
|
|
225
|
+
leafHeaders: ReadonlyArray<TMDataGridHeader>;
|
|
226
|
+
layoutFor: (columnId: string) => TMDataGridColumnLayout;
|
|
227
|
+
ariaRowIndex: number;
|
|
228
|
+
}) {
|
|
229
|
+
const { ui, labels } = useTMDataGridContext();
|
|
230
|
+
const focusColumnId = useSelector(ui, (state) => state.headerFilterColumnId);
|
|
231
|
+
const rowRef = useRef<HTMLDivElement>(null);
|
|
232
|
+
|
|
233
|
+
// `openColumnFilter` under header filters means "put me in that column's
|
|
234
|
+
// control" - there is no panel to open. The column may be scrolled out
|
|
235
|
+
// sideways, so it is brought into view first.
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (focusColumnId === null) return;
|
|
238
|
+
const cell = rowRef.current?.querySelector<HTMLElement>(
|
|
239
|
+
`[data-dg-part="header-filter-cell"][data-column-id="${CSS.escape(focusColumnId)}"]`,
|
|
240
|
+
);
|
|
241
|
+
cell?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
242
|
+
cell
|
|
243
|
+
?.querySelector<HTMLElement>(
|
|
244
|
+
"input:not([type='hidden']), textarea, select, [tabindex]:not([tabindex='-1'])",
|
|
245
|
+
)
|
|
246
|
+
?.focus();
|
|
247
|
+
ui.actions.focusHeaderFilter(null);
|
|
248
|
+
}, [focusColumnId, ui]);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div
|
|
252
|
+
ref={rowRef}
|
|
253
|
+
role="row"
|
|
254
|
+
aria-rowindex={ariaRowIndex}
|
|
255
|
+
// Counted by the same measurement the group rows are: it is part of what
|
|
256
|
+
// the body scrolls under.
|
|
257
|
+
data-dg-header-row
|
|
258
|
+
data-dg-header-filter-row
|
|
259
|
+
data-dg-part="header-filter-row"
|
|
260
|
+
// The header/body boundary is this row once it exists, so it wears the
|
|
261
|
+
// scrolled-under shadow the last group row wears otherwise.
|
|
262
|
+
className={`${classes.headerFilterRow} ${sticky.headerBoundary}`}
|
|
263
|
+
>
|
|
264
|
+
{leafHeaders.map((header) => {
|
|
265
|
+
const column = header.column;
|
|
266
|
+
const layout = layoutFor(column.id);
|
|
267
|
+
return (
|
|
268
|
+
<div
|
|
269
|
+
key={header.id}
|
|
270
|
+
role="columnheader"
|
|
271
|
+
aria-label={labels.filterOn(getColumnLabel(column))}
|
|
272
|
+
data-dg-part="header-filter-cell"
|
|
273
|
+
data-column-id={column.id}
|
|
274
|
+
data-control-column={isControlColumn(column.id) || undefined}
|
|
275
|
+
className={[
|
|
276
|
+
classes.headerFilterCell,
|
|
277
|
+
layout.isBoundary && layout.pinnedAt === "left"
|
|
278
|
+
? sticky.stickyLeft
|
|
279
|
+
: "",
|
|
280
|
+
layout.isBoundary && layout.pinnedAt === "right"
|
|
281
|
+
? sticky.stickyRight
|
|
282
|
+
: "",
|
|
283
|
+
]
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join(" ")}
|
|
286
|
+
style={{
|
|
287
|
+
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
288
|
+
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
289
|
+
position: layout.pinnedAt ? "sticky" : undefined,
|
|
290
|
+
zIndex: layout.pinnedAt
|
|
291
|
+
? "var(--dg-z-header-pinned-cell, 7)"
|
|
292
|
+
: undefined,
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
{column.getCanFilter() && <HeaderFilterControl column={column} />}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|