@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

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.
Files changed (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -0,0 +1,21 @@
1
+ .toolbar {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--mantine-spacing-xs);
5
+ padding: 6px var(--mantine-spacing-xs);
6
+ min-height: 42px;
7
+ flex-shrink: 0;
8
+ }
9
+
10
+ /* `withBottomBorder`: the line the header draws under itself, on the toolbar. */
11
+ .toolbar[data-with-bottom-border] {
12
+ border-bottom: 1px solid var(--mantine-color-default-border);
13
+ }
14
+
15
+ .toolbarSpacer {
16
+ flex: 1;
17
+ }
18
+
19
+ .summaryCount {
20
+ white-space: nowrap;
21
+ }
@@ -0,0 +1,181 @@
1
+ import {
2
+ ActionIcon,
3
+ Box,
4
+ type BoxProps,
5
+ Loader,
6
+ Text,
7
+ Tooltip,
8
+ } from "@mantine/core";
9
+ import { useSelector } from "@tanstack/react-store";
10
+ import type { ReactNode } from "react";
11
+ import classes from "./TMDataGridToolbar.module.css";
12
+ import { useTMDataGridContext } from "../TMDataGridContext";
13
+ import { getGridCapabilities } from "../core/capabilities";
14
+ import { useSettledTableState } from "../core/useSettledTableState";
15
+ import { isFilterActive } from "../core/filterOperators";
16
+ import { FilterIcon } from "./icons";
17
+ import { seedColumnFilter } from "../useTMDataGrid";
18
+
19
+ export type TMDataGridToolbarProps = BoxProps & {
20
+ children?: ReactNode;
21
+ /**
22
+ * Draws a 1px line under the toolbar in the theme's default border colour -
23
+ * the same line the header draws under itself. Defaults to `false`.
24
+ */
25
+ withBottomBorder?: boolean;
26
+ };
27
+
28
+ /**
29
+ * Row above the grid. Compose it from the pieces below, or anything else.
30
+ * Takes Mantine's style props (`mb="sm"`, `px="md"`, `hiddenFrom="sm"`) and
31
+ * sets them on the row.
32
+ */
33
+ export function TMDataGridToolbar({
34
+ children,
35
+ withBottomBorder = false,
36
+ className,
37
+ mod,
38
+ ...others
39
+ }: TMDataGridToolbarProps) {
40
+ return (
41
+ <Box
42
+ data-dg-part="toolbar"
43
+ className={[classes.toolbar, className].filter(Boolean).join(" ")}
44
+ mod={[{ "with-bottom-border": withBottomBorder }, mod]}
45
+ {...others}
46
+ >
47
+ {children}
48
+ </Box>
49
+ );
50
+ }
51
+
52
+ /** Pushes the following toolbar items to the right. Takes Mantine's style props. */
53
+ export function TMDataGridToolbarSpacer({ className, ...others }: BoxProps) {
54
+ return (
55
+ <Box
56
+ className={[classes.toolbarSpacer, className].filter(Boolean).join(" ")}
57
+ {...others}
58
+ />
59
+ );
60
+ }
61
+
62
+ /**
63
+ * A small spinner shown while `meta.loading` is true, and nothing otherwise.
64
+ *
65
+ * The body only shows its loading state while the grid is *empty* - a
66
+ * server-driven grid refetching with rows on screen keeps showing them, which
67
+ * is right, but leaves nothing saying a fetch is running. This is that signal,
68
+ * and the consumer decides where it sits by placing it in the toolbar:
69
+ *
70
+ * ```tsx
71
+ * <TMDataGrid.Toolbar>
72
+ * <TMDataGrid.SummaryCount />
73
+ * <TMDataGrid.Spacer />
74
+ * <TMDataGrid.LoadingIndicator />
75
+ * <TMDataGrid.FilterButton />
76
+ * </TMDataGrid.Toolbar>
77
+ * ```
78
+ */
79
+ export function TMDataGridLoadingIndicator() {
80
+ const { table, labels } = useTMDataGridContext();
81
+ if (table.options.meta?.loading !== true) return null;
82
+ return <Loader size="xs" data-dg-part="loading" aria-label={labels.loading} />;
83
+ }
84
+
85
+ /**
86
+ * Visible rows over total rows. On a server-driven grid, set
87
+ * `meta.totalRowCount` for the denominator - the client never sees every row.
88
+ * Without it the count is rendered alone, since the fallback denominator would
89
+ * be the rows the server sent for the current page: a plausible-looking wrong
90
+ * total, and on an unfiltered first page the same number twice.
91
+ */
92
+ export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
93
+ const { table, controlSize } = useTMDataGridContext();
94
+ useSettledTableState(table.store);
95
+
96
+ const serverDriven =
97
+ table.options.manualFiltering === true ||
98
+ table.options.manualPagination === true;
99
+ const shown = table.getRowCount();
100
+ const total =
101
+ table.options.meta?.totalRowCount ??
102
+ (serverDriven ? undefined : table.getPreFilteredRowModel().rows.length);
103
+
104
+ return (
105
+ <Text
106
+ span
107
+ size={controlSize}
108
+ c="dimmed"
109
+ className={classes.summaryCount}
110
+ data-dg-part="summary-count"
111
+ >
112
+ {children !== undefined
113
+ ? children
114
+ : total === undefined
115
+ ? `${shown}`
116
+ : `${shown} / ${total}`}
117
+ </Text>
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Toggles the grid's filter surface - the popup or the sidebar, whichever
123
+ * `filters.surface` names - seeding a filter row on the first filterable
124
+ * column. The count of active filters tints it.
125
+ *
126
+ * Renders nothing when no column can be filtered (`enableColumnFilters:
127
+ * false`), and nothing under `filters.surface: "none"`, where there is no
128
+ * automatic surface for it to toggle. Read `ui.state.filterPanelOpen` and
129
+ * render your own control if a hand-placed panel wants one.
130
+ */
131
+ export function TMDataGridFilterButton() {
132
+ const api = useTMDataGridContext();
133
+ const { table, ui, features, filters, labels, controlSize } = api;
134
+ const opened = useSelector(ui, (state) => state.filterPanelOpen);
135
+ const columnFilters = useSelector(
136
+ table.store,
137
+ (state) => state.columnFilters,
138
+ );
139
+ const activeCount = columnFilters.filter((filter) =>
140
+ isFilterActive(filter.value),
141
+ ).length;
142
+
143
+ if (!getGridCapabilities(table, features).canFilterAny) return null;
144
+ if (filters.surface === "none") return null;
145
+
146
+ return (
147
+ <Tooltip label={labels.filters} openDelay={400}>
148
+ <ActionIcon
149
+ variant={opened || activeCount > 0 ? "light" : "subtle"}
150
+ color={activeCount > 0 ? undefined : "gray"}
151
+ size={controlSize}
152
+ aria-label={labels.filters}
153
+ // Also what the panel's click-away handler tests for, so that a click
154
+ // here reads as a toggle rather than as a click outside.
155
+ data-dg-part="filter-button"
156
+ onClick={() => {
157
+ if (opened) {
158
+ ui.actions.closeFilterPanel();
159
+ return;
160
+ }
161
+ // Not `openColumnFilter`: that one honours header filters and
162
+ // focuses the header control instead of opening anything. This
163
+ // button's whole job is to open the surface it belongs to, which a
164
+ // grid may well have alongside header filters.
165
+ //
166
+ // Seeded only when the panel would otherwise be empty: with filters
167
+ // already in state it has rows to show, and seeding would stack a
168
+ // blank row on the first filterable column on top of them.
169
+ const seedColumn =
170
+ columnFilters.length === 0
171
+ ? table.getAllLeafColumns().find((column) => column.getCanFilter())
172
+ : undefined;
173
+ if (seedColumn) seedColumnFilter(api, seedColumn.id);
174
+ ui.actions.openFilterPanel(seedColumn?.id ?? null);
175
+ }}
176
+ >
177
+ <FilterIcon size={16} stroke={1.6} />
178
+ </ActionIcon>
179
+ </Tooltip>
180
+ );
181
+ }
@@ -2,7 +2,7 @@ import { Checkbox } from "@mantine/core";
2
2
  import type { TMDataGridEditorArgs } from "../../core/editEngine";
3
3
  import { getColumnLabel } from "../../core/columnUtils";
4
4
  import { useTMDataGridContext } from "../../TMDataGridContext";
5
- import { useFieldError, useFieldValue } from "./editorShared";
5
+ import { useFieldInvalid, useFieldValue } from "./editorShared";
6
6
 
7
7
  /** The built-in editor for `meta.type: "boolean"` - a focused checkbox. */
8
8
  export function TMDataGridBooleanEditor({
@@ -12,7 +12,7 @@ export function TMDataGridBooleanEditor({
12
12
  }: TMDataGridEditorArgs) {
13
13
  const { labels } = useTMDataGridContext();
14
14
  const value = useFieldValue(field);
15
- const error = useFieldError(field);
15
+ const invalid = useFieldInvalid(field);
16
16
  return (
17
17
  <Checkbox
18
18
  size={size}
@@ -21,7 +21,7 @@ export function TMDataGridBooleanEditor({
21
21
  checked={value === true}
22
22
  onChange={(event) => field.handleChange(event.currentTarget.checked)}
23
23
  onBlur={field.handleBlur}
24
- error={error}
24
+ error={invalid}
25
25
  />
26
26
  );
27
27
  }
@@ -3,7 +3,7 @@ import { useRef } from "react";
3
3
  import type { TMDataGridEditorArgs } from "../../core/editEngine";
4
4
  import { getColumnLabel } from "../../core/columnUtils";
5
5
  import { useTMDataGridContext } from "../../TMDataGridContext";
6
- import { useFieldError, useFieldValue } from "./editorShared";
6
+ import { useFieldInvalid, useFieldValue } from "./editorShared";
7
7
 
8
8
  /**
9
9
  * The built-in editor for `meta.type: "date"` - the native
@@ -17,7 +17,7 @@ export function TMDataGridDateEditor({
17
17
  }: TMDataGridEditorArgs) {
18
18
  const { labels } = useTMDataGridContext();
19
19
  const value = useFieldValue(field);
20
- const error = useFieldError(field);
20
+ const invalid = useFieldInvalid(field);
21
21
  // The cell's original shape decides what the editor writes back: a `Date`
22
22
  // cell keeps getting Dates, an ISO-string cell keeps getting strings -
23
23
  // decided once at open, so clearing and retyping cannot flip the type.
@@ -49,7 +49,7 @@ export function TMDataGridDateEditor({
49
49
  );
50
50
  }}
51
51
  onBlur={field.handleBlur}
52
- error={error}
52
+ error={invalid}
53
53
  />
54
54
  );
55
55
  }
@@ -6,7 +6,7 @@ import {
6
6
  } from "../../core/columnOptions";
7
7
  import { getColumnLabel } from "../../core/columnUtils";
8
8
  import { useTMDataGridContext } from "../../TMDataGridContext";
9
- import { useFieldError, useFieldValue } from "./editorShared";
9
+ import { useFieldInvalid, useFieldValue } from "./editorShared";
10
10
 
11
11
  /**
12
12
  * The built-in editor for `meta.type: "multiSelect"` - cells holding string
@@ -22,7 +22,7 @@ export function TMDataGridMultiSelectEditor({
22
22
  }: TMDataGridEditorArgs) {
23
23
  const { labels } = useTMDataGridContext();
24
24
  const value = useFieldValue(field);
25
- const error = useFieldError(field);
25
+ const invalid = useFieldInvalid(field);
26
26
  return (
27
27
  <MultiSelect
28
28
  size={size}
@@ -37,7 +37,7 @@ export function TMDataGridMultiSelectEditor({
37
37
  value={Array.isArray(value) ? value.map(String) : []}
38
38
  onChange={(next) => field.handleChange(next)}
39
39
  onBlur={field.handleBlur}
40
- error={error}
40
+ error={invalid}
41
41
  />
42
42
  );
43
43
  }
@@ -0,0 +1,70 @@
1
+ import { NumberInput } from "@mantine/core";
2
+ import { useState } from "react";
3
+ import type { TMDataGridEditorArgs } from "../../core/editEngine";
4
+ import { getColumnLabel } from "../../core/columnUtils";
5
+ import { useTMDataGridContext } from "../../TMDataGridContext";
6
+ import {
7
+ selectAllOnFocus,
8
+ useCaretKeeper,
9
+ useFieldInvalid,
10
+ useFieldValue,
11
+ } from "./editorShared";
12
+
13
+ /** The built-in editor for `meta.type: "number"`. */
14
+ export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditorArgs) {
15
+ const { labels } = useTMDataGridContext();
16
+ const value = useFieldValue(field);
17
+ const invalid = useFieldInvalid(field);
18
+ const { inputRef, remember } = useCaretKeeper();
19
+ /**
20
+ * The last text Mantine reported instead of a number - "-" or "1e" on the
21
+ * way to one. Held so a half-typed number stays on screen while the field
22
+ * itself holds no value.
23
+ */
24
+ const [typed, setTyped] = useState<string | null>(null);
25
+ // The field is the source of truth: the text survives only while it still
26
+ // describes what the field holds, so a write from outside the editor -
27
+ // `edit.setCellValue`, a revert - replaces it rather than being masked.
28
+ const parsedTyped = typed === null || typed === "" ? null : Number(typed);
29
+ const textDescribesField = Number.isFinite(parsedTyped)
30
+ ? parsedTyped === value
31
+ : value === null;
32
+ return (
33
+ <NumberInput
34
+ ref={inputRef}
35
+ size={size}
36
+ w="100%"
37
+ onFocus={selectAllOnFocus}
38
+ hideControls
39
+ aria-label={labels.editCell(getColumnLabel(column))}
40
+ data-dg-part="editor-input"
41
+ value={
42
+ typed !== null && textDescribesField
43
+ ? typed
44
+ : typeof value === "number"
45
+ ? value
46
+ : ""
47
+ }
48
+ onChange={(next) => {
49
+ if (inputRef.current !== null) remember(inputRef.current);
50
+ if (typeof next === "number") {
51
+ setTyped(null);
52
+ field.handleChange(next);
53
+ return;
54
+ }
55
+ // Mantine reports a number once one can be parsed and a string while
56
+ // it cannot: "" for an empty cell, partial input on the way to a
57
+ // number. Anything that does not parse leaves the field empty rather
58
+ // than `NaN` - which is a number to every validator and would commit
59
+ // as one, with no message a user could act on.
60
+ setTyped(next);
61
+ const parsed = Number(next);
62
+ field.handleChange(
63
+ next !== "" && Number.isFinite(parsed) ? parsed : null,
64
+ );
65
+ }}
66
+ onBlur={field.handleBlur}
67
+ error={invalid}
68
+ />
69
+ );
70
+ }
@@ -6,7 +6,7 @@ import {
6
6
  } from "../../core/columnOptions";
7
7
  import { getColumnLabel } from "../../core/columnUtils";
8
8
  import { useTMDataGridContext } from "../../TMDataGridContext";
9
- import { useFieldError, useFieldValue } from "./editorShared";
9
+ import { useFieldInvalid, useFieldValue } from "./editorShared";
10
10
 
11
11
  /**
12
12
  * The built-in editor for `meta.type: "select"`. Options come from the same
@@ -23,7 +23,7 @@ export function TMDataGridSelectEditor({
23
23
  }: TMDataGridEditorArgs) {
24
24
  const { labels, features } = useTMDataGridContext();
25
25
  const value = useFieldValue(field);
26
- const error = useFieldError(field);
26
+ const invalid = useFieldInvalid(field);
27
27
  return (
28
28
  <Select
29
29
  size={size}
@@ -45,7 +45,7 @@ export function TMDataGridSelectEditor({
45
45
  if (next !== null && features.editMode === "cell") void commit();
46
46
  }}
47
47
  onBlur={field.handleBlur}
48
- error={error}
48
+ error={invalid}
49
49
  />
50
50
  );
51
51
  }
@@ -5,7 +5,7 @@ import { useTMDataGridContext } from "../../TMDataGridContext";
5
5
  import {
6
6
  selectAllOnFocus,
7
7
  useCaretKeeper,
8
- useFieldError,
8
+ useFieldInvalid,
9
9
  useFieldValue,
10
10
  } from "./editorShared";
11
11
 
@@ -13,7 +13,7 @@ import {
13
13
  export function TMDataGridStringEditor({ field, column, size }: TMDataGridEditorArgs) {
14
14
  const { labels } = useTMDataGridContext();
15
15
  const value = useFieldValue(field);
16
- const error = useFieldError(field);
16
+ const invalid = useFieldInvalid(field);
17
17
  const { inputRef, remember } = useCaretKeeper();
18
18
  return (
19
19
  <TextInput
@@ -29,7 +29,7 @@ export function TMDataGridStringEditor({ field, column, size }: TMDataGridEditor
29
29
  field.handleChange(event.currentTarget.value);
30
30
  }}
31
31
  onBlur={field.handleBlur}
32
- error={error}
32
+ error={invalid}
33
33
  />
34
34
  );
35
35
  }
@@ -1,51 +1,37 @@
1
1
  import { useStore } from "@tanstack/react-form";
2
2
  import { useLayoutEffect, useRef, type RefObject } from "react";
3
- import type { TMDataGridEditField } from "../../core/editEngine";
4
-
5
- /**
6
- * The first error as text, whatever shape the validator produced - TanStack
7
- * Form keeps errors verbatim, so a Zod issue arrives as `{ message }`, a
8
- * plain function's return arrives as it was, and a form-level schema's
9
- * pathless issues arrive keyed under an empty path (`{ "": [issues] }`).
10
- * This digs until it finds a message.
11
- */
12
- export function firstErrorText(error: unknown): string | undefined {
13
- if (error === null || error === undefined) return undefined;
14
- if (typeof error === "string") return error;
15
- if (Array.isArray(error)) {
16
- for (const entry of error) {
17
- const text = firstErrorText(entry);
18
- if (text !== undefined) return text;
19
- }
20
- return undefined;
21
- }
22
- if (typeof error === "object") {
23
- const message = (error as { message?: unknown }).message;
24
- if (typeof message === "string") return message;
25
- for (const value of Object.values(error)) {
26
- const text = firstErrorText(value);
27
- if (text !== undefined) return text;
28
- }
29
- }
30
- return undefined;
31
- }
3
+ import { isInputElement } from "../../core/dom";
4
+ import { firstErrorText, type TMDataGridEditField } from "../../core/editEngine";
32
5
 
33
6
  /** The field's live value - one subscription, on the field's own store. */
34
7
  export function useFieldValue(field: TMDataGridEditField): unknown {
35
8
  return useStore(field.store, (state) => state.value as unknown);
36
9
  }
37
10
 
38
- /** The field's first error, for the Mantine input's `error` prop. */
11
+ /** The field's first error - the message the editor's host puts in a tooltip. */
39
12
  export function useFieldError(field: TMDataGridEditField): string | undefined {
40
13
  return useStore(field.store, (state) =>
41
14
  firstErrorText(state.meta.errors as ReadonlyArray<unknown>),
42
15
  );
43
16
  }
44
17
 
18
+ /**
19
+ * Whether the field has an error, for the Mantine input's `error` prop - the
20
+ * invalid border and nothing else. The message itself would wrap inside a
21
+ * narrow cell and be clipped, so the host shows it in a tooltip instead.
22
+ */
23
+ export function useFieldInvalid(field: TMDataGridEditField): boolean {
24
+ return useStore(
25
+ field.store,
26
+ (state) =>
27
+ firstErrorText(state.meta.errors as ReadonlyArray<unknown>) !== undefined,
28
+ );
29
+ }
30
+
45
31
  /** Select-all on focus - F2/Enter open with the value ready to replace. */
46
32
  export function selectAllOnFocus(event: { currentTarget: unknown }): void {
47
33
  const target = event.currentTarget;
48
- if (target instanceof HTMLInputElement) target.select();
34
+ if (isInputElement(target)) target.select();
49
35
  }
50
36
 
51
37
  /**
@@ -6,6 +6,7 @@ import {
6
6
  operatorTakesArrayValue,
7
7
  operatorTakesRangeValue,
8
8
  } from "../../core/filterOperators";
9
+ import { filterFieldProps } from "./controlLayout";
9
10
  import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
10
11
 
11
12
  /**
@@ -31,12 +32,12 @@ export function DgAutocompleteFilter(args: TMDataGridFilterControlArgs) {
31
32
 
32
33
  return (
33
34
  <Autocomplete
34
- label={labels.filterValue}
35
+ {...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
35
36
  size={size}
36
- w={180}
37
- comboboxProps={{ withinPortal: false }}
38
37
  disabled={!needsValue}
39
- placeholder={needsValue ? labels.filterValuePlaceholder : ""}
38
+ placeholder={
39
+ needsValue && args.layout !== "header" ? labels.filterValuePlaceholder : ""
40
+ }
40
41
  data={suggestions.map((option) => option.value)}
41
42
  limit={20}
42
43
  value={needsValue && typeof value === "string" ? value : ""}
@@ -1,6 +1,7 @@
1
1
  import { Group, TextInput } from "@mantine/core";
2
2
  import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
3
3
  import { operatorTakesRangeValue } from "../../core/filterOperators";
4
+ import { filterFieldProps } from "./controlLayout";
4
5
  import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
5
6
 
6
7
  /**
@@ -16,25 +17,41 @@ export function DgDateRangeFilter(args: TMDataGridFilterControlArgs) {
16
17
  return <TMDataGridFilterValueInput {...args} />;
17
18
  }
18
19
 
20
+ const fills = args.layout !== "row";
19
21
  const pair: [string, string] = Array.isArray(value)
20
22
  ? [String(value[0] ?? ""), String(value[1] ?? "")]
21
23
  : ["", ""];
22
24
 
23
25
  return (
24
- <Group gap={4} wrap="nowrap" align="flex-start">
26
+ <Group
27
+ gap={4}
28
+ wrap="nowrap"
29
+ align="flex-start"
30
+ w={fills ? "100%" : undefined}
31
+ >
25
32
  <TextInput
26
- label={labels.filterFrom}
33
+ {...filterFieldProps(
34
+ args,
35
+ { label: labels.filterFrom, width: 88 },
36
+ labels.filterFrom,
37
+ )}
27
38
  size={size}
28
- w={88}
29
39
  type="date"
40
+ flex={fills ? 1 : undefined}
41
+ miw={0}
30
42
  value={pair[0]}
31
43
  onChange={(event) => onChange([event.currentTarget.value, pair[1]])}
32
44
  />
33
45
  <TextInput
34
- label={labels.filterTo}
46
+ {...filterFieldProps(
47
+ args,
48
+ { label: labels.filterTo, width: 88 },
49
+ labels.filterTo,
50
+ )}
35
51
  size={size}
36
- w={88}
37
52
  type="date"
53
+ flex={fills ? 1 : undefined}
54
+ miw={0}
38
55
  value={pair[1]}
39
56
  onChange={(event) => onChange([pair[0], event.currentTarget.value])}
40
57
  />
@@ -1,6 +1,7 @@
1
1
  import { Input, RangeSlider } from "@mantine/core";
2
2
  import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
3
3
  import { operatorTakesRangeValue } from "../../core/filterOperators";
4
+ import { filterFieldProps } from "./controlLayout";
4
5
  import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
5
6
 
6
7
  /**
@@ -30,7 +31,10 @@ export function DgRangeSliderFilter(args: TMDataGridFilterControlArgs) {
30
31
  const upper = pair[1] === undefined || pair[1] === "" ? max : Number(pair[1]);
31
32
 
32
33
  return (
33
- <Input.Wrapper label={labels.filterValue} size={size} w={180}>
34
+ <Input.Wrapper
35
+ {...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
36
+ size={size}
37
+ >
34
38
  <RangeSlider
35
39
  size={size}
36
40
  mt={6}
@@ -5,6 +5,7 @@ import {
5
5
  operatorTakesArrayValue,
6
6
  operatorTakesRangeValue,
7
7
  } from "../../core/filterOperators";
8
+ import { filterFieldProps } from "./controlLayout";
8
9
  import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
9
10
 
10
11
  /**
@@ -21,7 +22,10 @@ export function DgTriStateFilter(args: TMDataGridFilterControlArgs) {
21
22
  const scalar = typeof value === "string" ? value : "";
22
23
 
23
24
  return (
24
- <Input.Wrapper label={labels.filterValue} size={size}>
25
+ <Input.Wrapper
26
+ {...filterFieldProps(args, { label: labels.filterValue })}
27
+ size={size}
28
+ >
25
29
  <SegmentedControl
26
30
  size={size}
27
31
  disabled={!needsValue}