@multiplatform.one/table 7.8.0 → 7.11.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 +60 -0
- package/dist/cjs/components/DataTable/DataTableCardControls.cjs +90 -54
- package/dist/cjs/components/DataTable/DataTableCardControls.native.js +93 -56
- package/dist/cjs/components/DataTable/DataTableCardControls.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableMobileCards.cjs +100 -33
- package/dist/cjs/components/DataTable/DataTableMobileCards.native.js +139 -38
- package/dist/cjs/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableRoot.cjs +51 -8
- package/dist/cjs/components/DataTable/DataTableRoot.native.js +53 -9
- package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/cjs/components/DataTable/index.native.js +59 -93
- package/dist/cjs/components/DataTable/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableFilterButton/index.cjs +14 -0
- package/dist/cjs/components/DataTableFilterButton/index.native.js +15 -0
- package/dist/cjs/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableToolbar/index.cjs +178 -101
- package/dist/cjs/components/DataTableToolbar/index.native.js +187 -109
- package/dist/cjs/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/cjs/hooks/useDataTable.cjs +32 -1
- package/dist/cjs/hooks/useDataTable.native.js +37 -1
- package/dist/cjs/hooks/useDataTable.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableCardControls.mjs +89 -54
- package/dist/esm/components/DataTable/DataTableCardControls.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableCardControls.native.js +92 -56
- package/dist/esm/components/DataTable/DataTableCardControls.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableMobileCards.mjs +99 -34
- package/dist/esm/components/DataTable/DataTableMobileCards.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableMobileCards.native.js +138 -39
- package/dist/esm/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableRoot.mjs +53 -10
- package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableRoot.native.js +55 -11
- package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/esm/components/DataTable/index.native.js +60 -94
- package/dist/esm/components/DataTable/index.native.js.map +1 -1
- package/dist/esm/components/DataTableFilterButton/index.mjs +14 -1
- package/dist/esm/components/DataTableFilterButton/index.mjs.map +1 -1
- package/dist/esm/components/DataTableFilterButton/index.native.js +15 -1
- package/dist/esm/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/esm/components/DataTableToolbar/index.mjs +180 -104
- package/dist/esm/components/DataTableToolbar/index.mjs.map +1 -1
- package/dist/esm/components/DataTableToolbar/index.native.js +189 -112
- package/dist/esm/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/esm/hooks/useDataTable.mjs +32 -1
- package/dist/esm/hooks/useDataTable.mjs.map +1 -1
- package/dist/esm/hooks/useDataTable.native.js +37 -1
- package/dist/esm/hooks/useDataTable.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableCardControls.mjs +89 -54
- package/dist/jsx/components/DataTable/DataTableCardControls.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableCardControls.native.js +93 -56
- package/dist/jsx/components/DataTable/DataTableCardControls.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableMobileCards.mjs +99 -34
- package/dist/jsx/components/DataTable/DataTableMobileCards.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableMobileCards.native.js +139 -38
- package/dist/jsx/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableRoot.mjs +53 -10
- package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableRoot.native.js +53 -9
- package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/jsx/components/DataTable/index.native.js +59 -93
- package/dist/jsx/components/DataTable/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableFilterButton/index.mjs +14 -1
- package/dist/jsx/components/DataTableFilterButton/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableFilterButton/index.native.js +15 -0
- package/dist/jsx/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableToolbar/index.mjs +180 -104
- package/dist/jsx/components/DataTableToolbar/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableToolbar/index.native.js +187 -109
- package/dist/jsx/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/jsx/hooks/useDataTable.mjs +32 -1
- package/dist/jsx/hooks/useDataTable.mjs.map +1 -1
- package/dist/jsx/hooks/useDataTable.native.js +37 -1
- package/dist/jsx/hooks/useDataTable.native.js.map +1 -1
- package/package.json +14 -14
- package/src/components/DataTable/DataTable.spec.tsx +5 -3
- package/src/components/DataTable/DataTable.stories.tsx +59 -0
- package/src/components/DataTable/DataTableCardControls.stories.tsx +61 -2
- package/src/components/DataTable/DataTableCardControls.tsx +92 -66
- package/src/components/DataTable/DataTableMobileCards.tsx +162 -39
- package/src/components/DataTable/DataTableRoot.tsx +82 -14
- package/src/components/DataTable/cardLayout.spec.tsx +23 -11
- package/src/components/DataTable/cardToolbar.spec.tsx +238 -0
- package/src/components/DataTable/chromeInterplay.spec.tsx +7 -5
- package/src/components/DataTable/columnWidthLayout.spec.tsx +189 -0
- package/src/components/DataTable/dataTableCardControls.spec.tsx +3 -2
- package/src/components/DataTable/dataTableMobileCards.spec.tsx +50 -2
- package/src/components/DataTable/footerLayout.spec.tsx +250 -0
- package/src/components/DataTable/index.native.tsx +80 -124
- package/src/components/DataTable/nativeCardParity.spec.tsx +95 -0
- package/src/components/DataTable/pinningGeometry.spec.tsx +66 -1
- package/src/components/DataTableExport/DataTableExport.spec.tsx +42 -18
- package/src/components/DataTableFilterButton/index.tsx +9 -0
- package/src/components/DataTableToolbar/index.tsx +184 -105
- package/src/hooks/useDataTable.spec.ts +114 -0
- package/src/hooks/useDataTable.ts +23 -1
- package/src/types.ts +14 -12
- package/types/components/DataTable/DataTableCardControls.d.ts +3 -0
- package/types/components/DataTable/DataTableCardControls.d.ts.map +1 -1
- package/types/components/DataTable/DataTableMobileCards.d.ts +42 -8
- package/types/components/DataTable/DataTableMobileCards.d.ts.map +1 -1
- package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
- package/types/components/DataTable/index.native.d.ts +1 -1
- package/types/components/DataTable/index.native.d.ts.map +1 -1
- package/types/components/DataTableFilterButton/index.d.ts +2 -1
- package/types/components/DataTableFilterButton/index.d.ts.map +1 -1
- package/types/components/DataTableToolbar/index.d.ts +1 -0
- package/types/components/DataTableToolbar/index.d.ts.map +1 -1
- package/types/hooks/useDataTable.d.ts.map +1 -1
- package/types/types.d.ts +14 -12
- package/types/types.d.ts.map +1 -1
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
* resolved layout is "cards" (below the `$sm` breakpoint with
|
|
6
6
|
* layout="auto", or forced via layout="cards").
|
|
7
7
|
*
|
|
8
|
-
* Card anatomy
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* Card anatomy: the first visible column is the card title in the header
|
|
9
|
+
* row (with selection checkbox / row number / delete). The remaining
|
|
10
|
+
* visible columns are NOT a stack of label/value rows any more — see
|
|
11
|
+
* `classifyCardField`. Column visibility and order from table state apply
|
|
12
|
+
* unchanged — cells come from `row.getVisibleCells()`. Values render
|
|
13
|
+
* through the same cell renderers as the grid, inside `TableCellContext`
|
|
14
|
+
* so fields stay chromeless (useIsInTableCell).
|
|
15
|
+
*
|
|
16
|
+
* The card is FLAT: no border, no elevation. A card list is drawn inside
|
|
17
|
+
* a panel that already has its own frame, and an outlined, shadowed card
|
|
18
|
+
* inside it reads as a box inside a box. The `cardSurface` fill and the
|
|
19
|
+
* gap between cards are what separate one row from the next.
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { TrashIcon } from "@phosphor-icons/react";
|
|
@@ -21,10 +25,10 @@ import { Button, Checkbox } from "@multiplatform.one/forms";
|
|
|
21
25
|
import { hairline, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
22
26
|
import type React from "react";
|
|
23
27
|
import { useCallback } from "react";
|
|
24
|
-
import { Text, View, XStack } from "tamagui";
|
|
28
|
+
import { Text, View, VisuallyHidden, XStack } from "tamagui";
|
|
25
29
|
import { useTranslation } from "react-i18next";
|
|
26
30
|
import { TableCellContext } from "@multiplatform.one/table-primitives";
|
|
27
|
-
import type { DataTableColumn } from "../../types";
|
|
31
|
+
import type { DataTableColumn, DataTableField } from "../../types";
|
|
28
32
|
import { withInterp } from "../../utils/withInterp";
|
|
29
33
|
import { rowIntentProps } from "./dataTableChrome";
|
|
30
34
|
|
|
@@ -34,13 +38,74 @@ import { rowIntentProps } from "./dataTableChrome";
|
|
|
34
38
|
* header is a render function or a bare glyph (the liked heart) — without
|
|
35
39
|
* it a card field row would print the raw column id (`_liked_by`).
|
|
36
40
|
*/
|
|
37
|
-
function getColumnLabel(cell: any): string {
|
|
41
|
+
export function getColumnLabel(cell: any): string {
|
|
38
42
|
const columnDef = cell.column.columnDef as DataTableColumn;
|
|
39
43
|
const metaLabel = columnDef.meta?.label;
|
|
40
44
|
if (typeof metaLabel === "string" && metaLabel) return metaLabel;
|
|
41
45
|
return typeof columnDef.header === "string" ? columnDef.header : columnDef.id || cell.column.id;
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
/** Fields that draw their own control, which names itself and needs a target. */
|
|
49
|
+
const selfNamingFields = new Set<DataTableField>([
|
|
50
|
+
"checkbox",
|
|
51
|
+
"switch",
|
|
52
|
+
"rating",
|
|
53
|
+
"progress",
|
|
54
|
+
"image",
|
|
55
|
+
"signature",
|
|
56
|
+
"fileupload",
|
|
57
|
+
"colorpicker",
|
|
58
|
+
"barcode",
|
|
59
|
+
"geolocation",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** Column ids whose value is a state badge. A badge names itself. */
|
|
63
|
+
const indicatorIds = new Set(["status", "priority", "workflow_state", "state"]);
|
|
64
|
+
|
|
65
|
+
const dateLike = /^\d{4}-\d{2}-\d{2}([T ]|$)|^\d{1,2}\/\d{1,2}\/\d{2,4}$/;
|
|
66
|
+
const numberLike = /^[+\-−]?[\d,\s]*\d(\.\d+)?%?$/;
|
|
67
|
+
|
|
68
|
+
export type CardFieldKind = "skip" | "control" | "bare" | "figure";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether a card field prints the column's name, and where it goes.
|
|
72
|
+
*
|
|
73
|
+
* A card is not the grid with its header repeated down the page. In the
|
|
74
|
+
* grid the header is written ONCE for a whole column of values; on a card
|
|
75
|
+
* it is written once per value, so a 12-column table prints 12 labels per
|
|
76
|
+
* record. That is what a label costs here, and most labels do not earn it.
|
|
77
|
+
*
|
|
78
|
+
* skip the raw value is absent. A field with nothing in it is not a
|
|
79
|
+
* fact — "Rejected —" spends a row saying nothing.
|
|
80
|
+
* control a button, a switch, a rating. The control names itself and
|
|
81
|
+
* needs its own tap target, so it keeps a row and takes no label.
|
|
82
|
+
* bare a string that is neither a number nor a date. "DIVIDEND
|
|
83
|
+
* CREDIT" and "Income:Dividends" say what they are; "Payee" and
|
|
84
|
+
* "Account" in front of them are the header printed twice. The
|
|
85
|
+
* name is kept for assistive tech via VisuallyHidden, which
|
|
86
|
+
* costs no layout.
|
|
87
|
+
* figure a number, a date, a boolean. "1,204" alone cannot be read, so
|
|
88
|
+
* these DO keep their label — inline beside the figure, not in a
|
|
89
|
+
* 40%-wide column of its own.
|
|
90
|
+
*/
|
|
91
|
+
export function classifyCardField(cell: any): CardFieldKind {
|
|
92
|
+
const def = cell.column.columnDef as DataTableColumn;
|
|
93
|
+
const id = String(cell.column.id ?? "");
|
|
94
|
+
const role = def.meta?.role;
|
|
95
|
+
if (role === "hidden" || role === "skip") return "skip";
|
|
96
|
+
if (role === "actions" || id.startsWith("_") || (def.field && selfNamingFields.has(def.field))) {
|
|
97
|
+
return "control";
|
|
98
|
+
}
|
|
99
|
+
const raw = typeof cell.getValue === "function" ? cell.getValue() : undefined;
|
|
100
|
+
if (raw == null || (typeof raw === "string" && raw.trim() === "")) return "skip";
|
|
101
|
+
if (role === "indicator" || indicatorIds.has(id)) return "bare";
|
|
102
|
+
if (typeof raw === "string") {
|
|
103
|
+
const value = raw.trim();
|
|
104
|
+
return numberLike.test(value) || dateLike.test(value) ? "figure" : "bare";
|
|
105
|
+
}
|
|
106
|
+
return "figure";
|
|
107
|
+
}
|
|
108
|
+
|
|
44
109
|
export interface DataTableMobileCardsProps<TData> {
|
|
45
110
|
tableRows: any[];
|
|
46
111
|
enableRowSelection?: boolean;
|
|
@@ -98,6 +163,15 @@ export function DataTableMobileCards<TData extends Record<string, any>>({
|
|
|
98
163
|
const { knobProps } = useResolvedKnobs({ component: "DataTable", compact });
|
|
99
164
|
const tableZebraOn = knobProps.tableZebra === "on";
|
|
100
165
|
|
|
166
|
+
const renderCell = useCallback(
|
|
167
|
+
(cell: any, index: number, rowData: TData) => (
|
|
168
|
+
<TableCellContext.Provider value={{ inTableCell: true, isHeader: false }}>
|
|
169
|
+
{renderCellContent(cell, index, rowData)}
|
|
170
|
+
</TableCellContext.Provider>
|
|
171
|
+
),
|
|
172
|
+
[renderCellContent],
|
|
173
|
+
);
|
|
174
|
+
|
|
101
175
|
const renderMobileCard = useCallback(
|
|
102
176
|
(row: any, index: number) => {
|
|
103
177
|
const rowData = row.original as TData;
|
|
@@ -105,14 +179,22 @@ export function DataTableMobileCards<TData extends Record<string, any>>({
|
|
|
105
179
|
const [titleCell, ...detailCells] = visibleCells;
|
|
106
180
|
const showDelete = showDeleteRow && !readOnly;
|
|
107
181
|
|
|
182
|
+
const bare: any[] = [];
|
|
183
|
+
const figures: any[] = [];
|
|
184
|
+
const controls: any[] = [];
|
|
185
|
+
for (const cell of detailCells) {
|
|
186
|
+
const kind = classifyCardField(cell);
|
|
187
|
+
if (kind === "bare") bare.push(cell);
|
|
188
|
+
else if (kind === "figure") figures.push(cell);
|
|
189
|
+
else if (kind === "control") controls.push(cell);
|
|
190
|
+
}
|
|
191
|
+
const hasBody = bare.length > 0 || figures.length > 0 || controls.length > 0;
|
|
192
|
+
|
|
108
193
|
return (
|
|
109
194
|
<View
|
|
110
195
|
key={`${row.id}-${index}`}
|
|
111
196
|
testID="data-table-card"
|
|
112
|
-
{...knobProps.surface}
|
|
113
197
|
{...knobProps.cardSurface}
|
|
114
|
-
elevation={knobProps.elevation}
|
|
115
|
-
borderColor="$color6"
|
|
116
198
|
{...(tableZebraOn && index % 2 === 1 ? { backgroundColor: "$color2" } : {})}
|
|
117
199
|
{...({
|
|
118
200
|
"data-table-zebra": tableZebraOn ? (index % 2 === 0 ? "even" : "odd") : "off",
|
|
@@ -124,7 +206,7 @@ export function DataTableMobileCards<TData extends Record<string, any>>({
|
|
|
124
206
|
<XStack
|
|
125
207
|
alignItems="center"
|
|
126
208
|
gap="$2"
|
|
127
|
-
{...(
|
|
209
|
+
{...(hasBody
|
|
128
210
|
? {
|
|
129
211
|
paddingBottom: "$2",
|
|
130
212
|
marginBottom: "$2",
|
|
@@ -156,11 +238,10 @@ export function DataTableMobileCards<TData extends Record<string, any>>({
|
|
|
156
238
|
flex={1}
|
|
157
239
|
minWidth={0}
|
|
158
240
|
aria-label={getColumnLabel(titleCell)}
|
|
241
|
+
accessibilityLabel={getColumnLabel(titleCell)}
|
|
159
242
|
{...({ "data-card-slot": "title" } as any)}
|
|
160
243
|
>
|
|
161
|
-
|
|
162
|
-
{renderCellContent(titleCell, index, rowData)}
|
|
163
|
-
</TableCellContext.Provider>
|
|
244
|
+
{renderCell(titleCell, index, rowData)}
|
|
164
245
|
</View>
|
|
165
246
|
) : (
|
|
166
247
|
<View flex={1} />
|
|
@@ -185,29 +266,71 @@ export function DataTableMobileCards<TData extends Record<string, any>>({
|
|
|
185
266
|
)}
|
|
186
267
|
</XStack>
|
|
187
268
|
|
|
188
|
-
{/*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
269
|
+
{/* Self-naming values, wrapped into one flow. No printed labels:
|
|
270
|
+
the name goes to assistive tech only, at no layout cost. */}
|
|
271
|
+
{bare.length > 0 ? (
|
|
272
|
+
<XStack
|
|
273
|
+
flexWrap="wrap"
|
|
193
274
|
alignItems="center"
|
|
194
|
-
paddingVertical="$1.5"
|
|
195
275
|
gap="$2"
|
|
276
|
+
{...({ "data-card-slot": "meta" } as any)}
|
|
196
277
|
>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
278
|
+
{bare.map((cell: any, i: number) => (
|
|
279
|
+
<XStack key={cell.id} alignItems="center" gap="$2" flexShrink={1} minWidth={0}>
|
|
280
|
+
<VisuallyHidden>{`${getColumnLabel(cell)}: `}</VisuallyHidden>
|
|
281
|
+
<View flexShrink={1} minWidth={0}>
|
|
282
|
+
{renderCell(cell, index, rowData)}
|
|
283
|
+
</View>
|
|
284
|
+
{/* The separator TRAILS its value, so a line that wraps
|
|
285
|
+
starts with content instead of a stray dot. */}
|
|
286
|
+
{i < bare.length - 1 ? (
|
|
287
|
+
<Text
|
|
288
|
+
fontSize={knobProps.sizeToken}
|
|
289
|
+
color="$color8"
|
|
290
|
+
aria-hidden
|
|
291
|
+
accessibilityElementsHidden
|
|
292
|
+
importantForAccessibility="no-hide-descendants"
|
|
293
|
+
>
|
|
294
|
+
·
|
|
295
|
+
</Text>
|
|
296
|
+
) : null}
|
|
297
|
+
</XStack>
|
|
298
|
+
))}
|
|
299
|
+
</XStack>
|
|
300
|
+
) : null}
|
|
301
|
+
|
|
302
|
+
{/* Figures keep their name, inline beside the value. */}
|
|
303
|
+
{figures.length > 0 ? (
|
|
304
|
+
<XStack
|
|
305
|
+
flexWrap="wrap"
|
|
306
|
+
alignItems="center"
|
|
307
|
+
gap="$3"
|
|
308
|
+
paddingTop={bare.length > 0 ? "$1.5" : 0}
|
|
309
|
+
{...({ "data-card-slot": "figure" } as any)}
|
|
310
|
+
>
|
|
311
|
+
{figures.map((cell: any) => (
|
|
312
|
+
<XStack key={cell.id} alignItems="center" gap="$1.5" flexShrink={1} minWidth={0}>
|
|
313
|
+
<Text fontSize={knobProps.sizeToken} color="$color10" fontWeight="500">
|
|
314
|
+
{getColumnLabel(cell)}
|
|
315
|
+
</Text>
|
|
316
|
+
<View flexShrink={1} minWidth={0}>
|
|
317
|
+
{renderCell(cell, index, rowData)}
|
|
318
|
+
</View>
|
|
319
|
+
</XStack>
|
|
320
|
+
))}
|
|
321
|
+
</XStack>
|
|
322
|
+
) : null}
|
|
323
|
+
|
|
324
|
+
{/* Controls keep a row of their own — they are tap targets. */}
|
|
325
|
+
{controls.map((cell: any) => (
|
|
326
|
+
<View
|
|
327
|
+
key={cell.id}
|
|
328
|
+
paddingTop="$2"
|
|
329
|
+
minWidth={0}
|
|
330
|
+
{...({ "data-card-slot": "control" } as any)}
|
|
331
|
+
>
|
|
332
|
+
<VisuallyHidden>{`${getColumnLabel(cell)}: `}</VisuallyHidden>
|
|
333
|
+
{renderCell(cell, index, rowData)}
|
|
211
334
|
</View>
|
|
212
335
|
))}
|
|
213
336
|
</View>
|
|
@@ -221,7 +344,7 @@ export function DataTableMobileCards<TData extends Record<string, any>>({
|
|
|
221
344
|
disabled,
|
|
222
345
|
dataLength,
|
|
223
346
|
minRows,
|
|
224
|
-
|
|
347
|
+
renderCell,
|
|
225
348
|
onDeleteRow,
|
|
226
349
|
compact,
|
|
227
350
|
knobProps,
|
|
@@ -43,7 +43,7 @@ import type {
|
|
|
43
43
|
} from "../../types";
|
|
44
44
|
import { DataTableColumnPinning } from "../DataTableColumnPinning";
|
|
45
45
|
import { DataTablePagination } from "../DataTablePagination";
|
|
46
|
-
import { DataTableToolbar } from "../DataTableToolbar";
|
|
46
|
+
import { DataTableCardToolbar, DataTableToolbar } from "../DataTableToolbar";
|
|
47
47
|
import { DataTableBulkActions } from "../DataTableBulkActions";
|
|
48
48
|
import { DataTableCell } from "../DataTableCell";
|
|
49
49
|
import { ColumnHeaderFilter } from "../ColumnHeaderFilter";
|
|
@@ -56,7 +56,7 @@ import type {
|
|
|
56
56
|
DataTableStateContextValue,
|
|
57
57
|
FilterEditTarget,
|
|
58
58
|
} from "./DataTableStateContext";
|
|
59
|
-
import { DataTableCardControls } from "./DataTableCardControls";
|
|
59
|
+
import { DataTableCardControls, DataTableCardSortControl } from "./DataTableCardControls";
|
|
60
60
|
import { DataTableMobileCards } from "./DataTableMobileCards";
|
|
61
61
|
import { DataTableDocumentList } from "./DataTableDocumentList";
|
|
62
62
|
import { DataTableVirtualized } from "./DataTableVirtualized";
|
|
@@ -334,6 +334,7 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
334
334
|
flexBasis: "auto" as const,
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
|
+
if (columnLayout === "equal" && !enableColumnResizing) return {};
|
|
337
338
|
// Scroll layout needs one concrete width per column (from the column
|
|
338
339
|
// def, TanStack default otherwise) so independent flex rows stay
|
|
339
340
|
// aligned while the table grows past its container.
|
|
@@ -461,13 +462,13 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
461
462
|
// flexBasis auto, not the `flex: 0` shorthand: RN expands `flex: 0` to
|
|
462
463
|
// basis 0, which overrides per-column widths and crushes every cell to
|
|
463
464
|
// the minWidth floor (contents then overflow and get clipped).
|
|
464
|
-
return { flexGrow: 0, flexShrink: 0, flexBasis: "auto" as const, minWidth:
|
|
465
|
+
return { flexGrow: 0, flexShrink: 0, flexBasis: "auto" as const, minWidth: 0 };
|
|
465
466
|
}
|
|
466
467
|
// Same flexBasis trap as the scroll branch above: `flex: 0` expands to
|
|
467
468
|
// basis 0 and the per-column width from getColSizeProps loses, crushing
|
|
468
469
|
// every resizable cell to the min-width floor (content then overlaps).
|
|
469
470
|
if (enableColumnResizing) {
|
|
470
|
-
return { flexGrow: 0, flexShrink: 0, flexBasis: "auto" as const };
|
|
471
|
+
return { flexGrow: 0, flexShrink: 0, flexBasis: "auto" as const, minWidth: 0 };
|
|
471
472
|
}
|
|
472
473
|
return { flex: 1, flexShrink: 1, flexGrow: 1, flexBasis: 0, minWidth: 0 };
|
|
473
474
|
}, [isFlexLayout, columnLayout, enableColumnResizing]);
|
|
@@ -1124,6 +1125,12 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
1124
1125
|
[table, state, actions, computed, selectAllScope, filterUi],
|
|
1125
1126
|
);
|
|
1126
1127
|
|
|
1128
|
+
const hasCardSort =
|
|
1129
|
+
showCards &&
|
|
1130
|
+
enableSorting &&
|
|
1131
|
+
table.getVisibleLeafColumns().some((column) => column.getCanSort());
|
|
1132
|
+
const defaultCardToolbar = showCards && !renderToolbar;
|
|
1133
|
+
const ToolbarComponent = defaultCardToolbar ? DataTableCardToolbar : DataTableToolbar;
|
|
1127
1134
|
const renderToolbarComponent = () => {
|
|
1128
1135
|
const activeFilterCount = table.getState().columnFilters?.length || 0;
|
|
1129
1136
|
|
|
@@ -1143,10 +1150,20 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
1143
1150
|
}
|
|
1144
1151
|
|
|
1145
1152
|
return (
|
|
1146
|
-
<
|
|
1153
|
+
<ToolbarComponent
|
|
1147
1154
|
globalFilter={state.globalFilter || ""}
|
|
1148
1155
|
onGlobalFilterChange={actions.setGlobalFilter}
|
|
1149
1156
|
onClearFilters={actions.clearFilters}
|
|
1157
|
+
customActions={
|
|
1158
|
+
defaultCardToolbar && hasCardSort ? (
|
|
1159
|
+
<DataTableCardSortControl
|
|
1160
|
+
table={table}
|
|
1161
|
+
compact={isCompact}
|
|
1162
|
+
disabled={disabled}
|
|
1163
|
+
contained
|
|
1164
|
+
/>
|
|
1165
|
+
) : undefined
|
|
1166
|
+
}
|
|
1150
1167
|
activeFilterCount={activeFilterCount}
|
|
1151
1168
|
onSaveView={actions.saveView}
|
|
1152
1169
|
onLoadView={actions.loadView}
|
|
@@ -1192,7 +1209,8 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
1192
1209
|
newValue: any,
|
|
1193
1210
|
) => Promise<void>,
|
|
1194
1211
|
) => {
|
|
1195
|
-
const hasToolbar =
|
|
1212
|
+
const hasToolbar =
|
|
1213
|
+
enableGlobalSearch || enableFiltering || enableViews || (defaultCardToolbar && hasCardSort);
|
|
1196
1214
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
1197
1215
|
|
|
1198
1216
|
// DF-12 / Axiom 6 HONEST STATE — a height-constrained body clips rows
|
|
@@ -1347,14 +1365,16 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
1347
1365
|
<>
|
|
1348
1366
|
{/* Header affordances (sort / per-column filter) live in the
|
|
1349
1367
|
grid headers; the card list restores them here. */}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1368
|
+
{!defaultCardToolbar && (
|
|
1369
|
+
<DataTableCardControls
|
|
1370
|
+
table={table}
|
|
1371
|
+
enableSorting={enableSorting}
|
|
1372
|
+
enableFiltering={enableFiltering}
|
|
1373
|
+
availableColumns={cardFilterColumns}
|
|
1374
|
+
compact={isCompact}
|
|
1375
|
+
disabled={disabled}
|
|
1376
|
+
/>
|
|
1377
|
+
)}
|
|
1358
1378
|
<DataTableMobileCards
|
|
1359
1379
|
tableRows={displayRows}
|
|
1360
1380
|
enableRowSelection={enableRowSelection}
|
|
@@ -2085,7 +2105,31 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
2085
2105
|
<Table.Foot>
|
|
2086
2106
|
{footerGroups.map((footerGroup: any) => (
|
|
2087
2107
|
<Table.Row key={footerGroup.id}>
|
|
2108
|
+
{enableRowSelection && (
|
|
2109
|
+
<Table.Cell
|
|
2110
|
+
cellWidth={selectionColWidth}
|
|
2111
|
+
cellHeight={cellHeight as any}
|
|
2112
|
+
cellLocation="first"
|
|
2113
|
+
/>
|
|
2114
|
+
)}
|
|
2115
|
+
{showRowNumbers && (
|
|
2116
|
+
<Table.Cell
|
|
2117
|
+
cellWidth={chrome.rowNumberCol}
|
|
2118
|
+
cellHeight={cellHeight as any}
|
|
2119
|
+
cellLocation={enableRowSelection ? "middle" : "first"}
|
|
2120
|
+
/>
|
|
2121
|
+
)}
|
|
2122
|
+
{enableRowExpansion && (
|
|
2123
|
+
<Table.Cell
|
|
2124
|
+
cellWidth={chrome.expandCol}
|
|
2125
|
+
cellHeight={cellHeight as any}
|
|
2126
|
+
cellLocation={
|
|
2127
|
+
enableRowSelection || showRowNumbers ? "middle" : "first"
|
|
2128
|
+
}
|
|
2129
|
+
/>
|
|
2130
|
+
)}
|
|
2088
2131
|
{footerGroup.headers.map((header: any) => {
|
|
2132
|
+
const stickyStyle = getStickyStyle(header.column.id);
|
|
2089
2133
|
const columnAlign = resolveColumnAlign({
|
|
2090
2134
|
...(header.column.columnDef as DataTableColumn),
|
|
2091
2135
|
id: header.column.id,
|
|
@@ -2093,6 +2137,23 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
2093
2137
|
return (
|
|
2094
2138
|
<Table.Cell
|
|
2095
2139
|
key={header.id}
|
|
2140
|
+
{...cellFlexProps}
|
|
2141
|
+
{...getColSizeProps(header.column)}
|
|
2142
|
+
{...(isWeb && stickyStyle
|
|
2143
|
+
? {
|
|
2144
|
+
style: {
|
|
2145
|
+
position: "sticky",
|
|
2146
|
+
...("insetInlineStart" in stickyStyle && {
|
|
2147
|
+
insetInlineStart: stickyStyle.insetInlineStart,
|
|
2148
|
+
}),
|
|
2149
|
+
...("insetInlineEnd" in stickyStyle && {
|
|
2150
|
+
insetInlineEnd: stickyStyle.insetInlineEnd,
|
|
2151
|
+
}),
|
|
2152
|
+
zIndex: 2,
|
|
2153
|
+
backgroundColor: "var(--color2)",
|
|
2154
|
+
} as React.CSSProperties,
|
|
2155
|
+
}
|
|
2156
|
+
: {})}
|
|
2096
2157
|
justifyContent={alignToFlex(columnAlign)}
|
|
2097
2158
|
data-align={columnAlign}
|
|
2098
2159
|
>
|
|
@@ -2115,6 +2176,13 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
|
|
|
2115
2176
|
</Table.Cell>
|
|
2116
2177
|
);
|
|
2117
2178
|
})}
|
|
2179
|
+
{showDeleteRow && !readOnly && (
|
|
2180
|
+
<Table.Cell
|
|
2181
|
+
cellWidth={chrome.deleteCol}
|
|
2182
|
+
cellHeight={cellHeight as any}
|
|
2183
|
+
cellLocation="last"
|
|
2184
|
+
/>
|
|
2185
|
+
)}
|
|
2118
2186
|
</Table.Row>
|
|
2119
2187
|
))}
|
|
2120
2188
|
</Table.Foot>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* breakpoint switch are exercised without resizing a window.
|
|
8
8
|
*
|
|
9
9
|
* Covers: breakpoint/override switching, card anatomy (title slot +
|
|
10
|
-
*
|
|
10
|
+
* classified fields), column visibility, sorting order, global search,
|
|
11
11
|
* column filters, the card sort/filter controls, the SimpleTable layout
|
|
12
12
|
* override, and the Table primitives card layout.
|
|
13
13
|
*/
|
|
@@ -127,7 +127,7 @@ describe("DataTable card layout", () => {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
describe("card anatomy", () => {
|
|
130
|
-
it("renders the first visible column as the card title and the rest
|
|
130
|
+
it("renders the first visible column as the card title and classifies the rest", () => {
|
|
131
131
|
const { container } = renderWithProviders(
|
|
132
132
|
<DataTable data={sampleData} columns={columns} onStateChange={vi.fn()} />,
|
|
133
133
|
);
|
|
@@ -140,15 +140,21 @@ describe("DataTable card layout", () => {
|
|
|
140
140
|
expect(title!.textContent).toContain("John");
|
|
141
141
|
expect(within(firstCard).queryByText("First Name")).toBeNull();
|
|
142
142
|
|
|
143
|
-
//
|
|
144
|
-
|
|
143
|
+
// A self-naming string goes bare. Its column name still reaches
|
|
144
|
+
// assistive tech, through a hidden node that costs no layout.
|
|
145
|
+
expect(within(firstCard).queryByText("Last Name")).toBeNull();
|
|
146
|
+
expect(within(firstCard).queryByText("Email")).toBeNull();
|
|
147
|
+
expect(firstCard.textContent).toContain("Last Name:");
|
|
148
|
+
expect(firstCard.textContent).toContain("Email:");
|
|
149
|
+
|
|
150
|
+
// A figure cannot be read without its name, so that one prints.
|
|
145
151
|
expect(within(firstCard).getByText("Age")).toBeDefined();
|
|
146
|
-
|
|
152
|
+
|
|
147
153
|
expect(firstCard.textContent).toContain("Doe");
|
|
148
154
|
expect(firstCard.textContent).toContain("28");
|
|
149
155
|
});
|
|
150
156
|
|
|
151
|
-
it("
|
|
157
|
+
it("names a control from meta.label when the header is a render function", () => {
|
|
152
158
|
const glyphColumns: DataTableColumn<Person>[] = [
|
|
153
159
|
{ id: "firstName", accessorKey: "firstName", header: "First Name" },
|
|
154
160
|
{
|
|
@@ -162,7 +168,9 @@ describe("DataTable card layout", () => {
|
|
|
162
168
|
<DataTable data={sampleData} columns={glyphColumns} onStateChange={vi.fn()} />,
|
|
163
169
|
);
|
|
164
170
|
const [firstCard] = getCards(container);
|
|
165
|
-
|
|
171
|
+
// A leading underscore marks a control: its own row, no printed label.
|
|
172
|
+
expect(within(firstCard).queryByText("Liked")).toBeNull();
|
|
173
|
+
expect(firstCard.textContent).toContain("Liked:");
|
|
166
174
|
expect(firstCard.textContent).not.toContain("_liked_by");
|
|
167
175
|
});
|
|
168
176
|
|
|
@@ -180,9 +188,11 @@ describe("DataTable card layout", () => {
|
|
|
180
188
|
<DataTable data={sampleData} columns={mixedColumns} onStateChange={vi.fn()} />,
|
|
181
189
|
);
|
|
182
190
|
const [firstCard] = getCards(container);
|
|
191
|
+
// A figure prints its name, a bare string hides it. meta.label wins
|
|
192
|
+
// either way, and the id fallback stays honest.
|
|
183
193
|
expect(within(firstCard).getByText("Age (years)")).toBeDefined();
|
|
184
194
|
expect(within(firstCard).queryByText("♥")).toBeNull();
|
|
185
|
-
expect(
|
|
195
|
+
expect(firstCard.textContent).toContain("email_addr:");
|
|
186
196
|
});
|
|
187
197
|
|
|
188
198
|
it("respects column visibility (hidden columns stay hidden in cards)", () => {
|
|
@@ -196,6 +206,7 @@ describe("DataTable card layout", () => {
|
|
|
196
206
|
);
|
|
197
207
|
const [firstCard] = getCards(container);
|
|
198
208
|
expect(within(firstCard).queryByText("Email")).toBeNull();
|
|
209
|
+
expect(firstCard.textContent).not.toContain("Email:");
|
|
199
210
|
expect(firstCard.textContent).not.toContain("john@example.com");
|
|
200
211
|
});
|
|
201
212
|
});
|
|
@@ -272,11 +283,12 @@ describe("DataTable card layout", () => {
|
|
|
272
283
|
expect(titles).toEqual(["John"]);
|
|
273
284
|
});
|
|
274
285
|
|
|
275
|
-
it("renders the
|
|
276
|
-
const {
|
|
286
|
+
it("renders the canonical Filter in card layout", () => {
|
|
287
|
+
const { getByRole, queryByText } = renderWithProviders(
|
|
277
288
|
<DataTable data={sampleData} columns={columns} onStateChange={vi.fn()} />,
|
|
278
289
|
);
|
|
279
|
-
expect(
|
|
290
|
+
expect(getByRole("button", { name: "Filter" })).toBeDefined();
|
|
291
|
+
expect(queryByText("Add Filter")).toBeNull();
|
|
280
292
|
});
|
|
281
293
|
|
|
282
294
|
it("hides sort/filter chrome when sorting and filtering are disabled", () => {
|