@multiplatform.one/table 7.8.0 → 7.10.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/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/index.native.js +59 -93
- package/dist/cjs/components/DataTable/index.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/index.native.js +60 -94
- package/dist/esm/components/DataTable/index.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/index.native.js +59 -93
- package/dist/jsx/components/DataTable/index.native.js.map +1 -1
- package/package.json +12 -12
- package/src/components/DataTable/DataTable.spec.tsx +5 -3
- package/src/components/DataTable/DataTableMobileCards.tsx +162 -39
- package/src/components/DataTable/cardLayout.spec.tsx +19 -8
- package/src/components/DataTable/dataTableMobileCards.spec.tsx +50 -2
- package/src/components/DataTable/index.native.tsx +80 -124
- package/src/components/DataTable/nativeCardParity.spec.tsx +95 -0
- package/types/components/DataTable/DataTableMobileCards.d.ts +42 -8
- package/types/components/DataTable/DataTableMobileCards.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
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* delete cluster beside the title, the delete floor at `minRows`, and the two
|
|
9
9
|
* distinct empty-state copies (no data vs no results) — plus the rule that an
|
|
10
10
|
* in-flight refetch paints NO empty state over a momentarily blank body.
|
|
11
|
+
*
|
|
12
|
+
* It also pins `classifyCardField`, which decides whether a field prints its
|
|
13
|
+
* column name at all. In the grid a header is written once for a whole
|
|
14
|
+
* column; on a card it would be written once per value.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
14
|
-
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
18
|
+
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
|
15
19
|
import { describe, expect, it, vi } from "vitest";
|
|
16
20
|
import type { DataTableColumn, TableFilter } from "../../types";
|
|
17
21
|
import { DataTable } from "./index";
|
|
@@ -52,7 +56,7 @@ describe("DataTableMobileCards", () => {
|
|
|
52
56
|
expect(cards(container).length).toBe(people.length);
|
|
53
57
|
});
|
|
54
58
|
|
|
55
|
-
it("the first visible column is the card title, the rest are
|
|
59
|
+
it("the first visible column is the card title, the rest are classified fields", () => {
|
|
56
60
|
const { container } = renderCards();
|
|
57
61
|
const first = cards(container)[0];
|
|
58
62
|
expect(first.querySelector('[data-card-slot="title"]')?.textContent).toBe("John");
|
|
@@ -69,6 +73,50 @@ describe("DataTableMobileCards", () => {
|
|
|
69
73
|
expect(first.textContent).not.toContain("Last Name");
|
|
70
74
|
});
|
|
71
75
|
|
|
76
|
+
it("a self-naming string goes bare, into one wrapped flow with its name hidden", () => {
|
|
77
|
+
const { container } = renderCards();
|
|
78
|
+
const first = cards(container)[0];
|
|
79
|
+
const meta = first.querySelector('[data-card-slot="meta"]');
|
|
80
|
+
expect(meta).not.toBeNull();
|
|
81
|
+
expect(meta!.textContent).toContain("Doe");
|
|
82
|
+
expect(within(first).queryByText("Last Name")).toBeNull();
|
|
83
|
+
expect(first.textContent).toContain("Last Name:");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("a figure keeps its column name, inline beside the value", () => {
|
|
87
|
+
const { container } = renderCards();
|
|
88
|
+
const first = cards(container)[0];
|
|
89
|
+
const figure = first.querySelector('[data-card-slot="figure"]');
|
|
90
|
+
expect(figure).not.toBeNull();
|
|
91
|
+
expect(figure!.textContent).toContain("28");
|
|
92
|
+
expect(within(first).getByText("Age")).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("a field with no value spends no row at all", () => {
|
|
96
|
+
const { container } = renderCards({
|
|
97
|
+
data: [{ id: 1, firstName: "John", lastName: "", age: 28 }],
|
|
98
|
+
});
|
|
99
|
+
const first = cards(container)[0];
|
|
100
|
+
expect(first.querySelector('[data-card-slot="meta"]')).toBeNull();
|
|
101
|
+
expect(first.textContent).not.toContain("Last Name");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("a control keeps a row of its own and prints no label", () => {
|
|
105
|
+
// Explicit id on BOTH columns: DataTable seeds columnOrder from
|
|
106
|
+
// `col.id || ""`, so an id-less accessor column beside an id'd one would
|
|
107
|
+
// put `_actions` first and make the control the card title instead.
|
|
108
|
+
const { container } = renderCards({
|
|
109
|
+
columns: [
|
|
110
|
+
{ id: "firstName", accessorKey: "firstName", header: "First Name" },
|
|
111
|
+
{ id: "_actions", accessorKey: "lastName", header: "Actions", meta: { role: "actions" } },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
const first = cards(container)[0];
|
|
115
|
+
expect(first.querySelector('[data-card-slot="control"]')).not.toBeNull();
|
|
116
|
+
expect(within(first).queryByText("Actions")).toBeNull();
|
|
117
|
+
expect(first.textContent).toContain("Actions:");
|
|
118
|
+
});
|
|
119
|
+
|
|
72
120
|
it("selection adds a select-all bar above the stack and a checkbox per card", () => {
|
|
73
121
|
const { container } = renderCards({ enableRowSelection: true });
|
|
74
122
|
expect(screen.getByLabelText("Select all rows")).toBeTruthy();
|
|
@@ -17,12 +17,12 @@ import { FlashList, type FlashListRef } from "@shopify/flash-list";
|
|
|
17
17
|
import { AsyncBoundary, EmptyState, useDirection } from "@multiplatform.one/components";
|
|
18
18
|
import { Button, Field, useFormField } from "@multiplatform.one/forms";
|
|
19
19
|
import { flexRender } from "@tanstack/react-table";
|
|
20
|
-
import {
|
|
20
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
21
21
|
import React, { useRef, useMemo, useCallback } from "react";
|
|
22
22
|
import { ScrollView, Text, View, useMedia, withStaticProperties } from "tamagui";
|
|
23
23
|
import { useTranslation } from "react-i18next";
|
|
24
24
|
import { useDataTable } from "../../hooks/useDataTable";
|
|
25
|
-
import type { DataTableColumn, DataTableField } from "../../types";
|
|
25
|
+
import type { DataTableColumn, DataTableField, SelectAllScope } from "../../types";
|
|
26
26
|
import { DEFAULT_PAGE_SIZE_OPTIONS } from "../../utils/paginationDefaults";
|
|
27
27
|
import { withInterp } from "../../utils/withInterp";
|
|
28
28
|
import { DataTableColumnPinning } from "../DataTableColumnPinning";
|
|
@@ -38,6 +38,7 @@ import { DataTableCell } from "../DataTableCell";
|
|
|
38
38
|
import { Table } from "@multiplatform.one/table-primitives";
|
|
39
39
|
import { DataTableContext } from "./DataTableContext";
|
|
40
40
|
import { DataTableDocumentList } from "./DataTableDocumentList";
|
|
41
|
+
import { DataTableMobileCards } from "./DataTableMobileCards";
|
|
41
42
|
import { dataTableChrome, resolveDataTableIntent, rowIntentProps } from "./dataTableChrome";
|
|
42
43
|
import { tableRowStateProps } from "./rowStateChrome";
|
|
43
44
|
import { DataTableStateContext } from "./DataTableStateContext";
|
|
@@ -134,6 +135,8 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
134
135
|
virtualizationOverscan = 5,
|
|
135
136
|
enableRowExpansion = false,
|
|
136
137
|
renderSubComponent,
|
|
138
|
+
enableRowSelection = false,
|
|
139
|
+
selectAllScope: selectAllScopeProp,
|
|
137
140
|
enableColumnPinning = false,
|
|
138
141
|
maxPinnedColumns = 3,
|
|
139
142
|
enableKeyboardNavigation: _enableKeyboardNavigation = false,
|
|
@@ -181,6 +184,9 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
181
184
|
compact,
|
|
182
185
|
});
|
|
183
186
|
const chrome = dataTableChrome(String(knobProps.sizeToken));
|
|
187
|
+
// House selectAllScope knob (DG-TBL-03): prop overrides knob; default page.
|
|
188
|
+
const selectAllScope: SelectAllScope =
|
|
189
|
+
selectAllScopeProp ?? (knobProps.selectAllScope === "filtered" ? "filtered" : "page");
|
|
184
190
|
const rowHeightPx = chrome.rowPx;
|
|
185
191
|
// Collapsed expand-caret points toward inline-end, so it mirrors in RTL via
|
|
186
192
|
// glyph swap (useDirection, Axiom 15); rotation sign flips so expanded
|
|
@@ -216,6 +222,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
216
222
|
virtualizationOverscan,
|
|
217
223
|
enableRowExpansion,
|
|
218
224
|
renderSubComponent,
|
|
225
|
+
enableRowSelection,
|
|
219
226
|
enableColumnPinning,
|
|
220
227
|
maxPinnedColumns,
|
|
221
228
|
enableKeyboardNavigation: false,
|
|
@@ -225,6 +232,25 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
225
232
|
isFetching: isFetchingProp,
|
|
226
233
|
});
|
|
227
234
|
|
|
235
|
+
const selectAllChecked =
|
|
236
|
+
enableRowSelection &&
|
|
237
|
+
(selectAllScope === "filtered"
|
|
238
|
+
? table.getIsAllRowsSelected()
|
|
239
|
+
: table.getIsAllPageRowsSelected());
|
|
240
|
+
const selectAllIndeterminate =
|
|
241
|
+
enableRowSelection &&
|
|
242
|
+
(selectAllScope === "filtered"
|
|
243
|
+
? table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()
|
|
244
|
+
: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected());
|
|
245
|
+
|
|
246
|
+
const toggleSelectAll = useCallback(() => {
|
|
247
|
+
if (selectAllScope === "filtered") {
|
|
248
|
+
table.toggleAllRowsSelected();
|
|
249
|
+
} else {
|
|
250
|
+
table.toggleAllPageRowsSelected();
|
|
251
|
+
}
|
|
252
|
+
}, [selectAllScope, table]);
|
|
253
|
+
|
|
228
254
|
const resolvedSize: "sm" | "md" | "lg" =
|
|
229
255
|
size ?? (knobProps.sizeToken === "$3" ? "sm" : knobProps.sizeToken === "$5" ? "lg" : "md");
|
|
230
256
|
|
|
@@ -493,8 +519,8 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
493
519
|
// (see utils/rowPress.ts). Roving tabindex is a web-only concern.
|
|
494
520
|
const rowPressActive = Boolean(onRowPress) && !disabled;
|
|
495
521
|
|
|
496
|
-
const
|
|
497
|
-
(row: (typeof tableRows)[0]
|
|
522
|
+
const deriveRowIdentity = useCallback(
|
|
523
|
+
(row: (typeof tableRows)[0]): string | undefined => {
|
|
498
524
|
const fromAccessor = rowLabel?.(row.original as TData);
|
|
499
525
|
if (fromAccessor) return fromAccessor;
|
|
500
526
|
let firstCellValue: unknown;
|
|
@@ -509,13 +535,33 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
509
535
|
) {
|
|
510
536
|
return String(firstCellValue);
|
|
511
537
|
}
|
|
512
|
-
return
|
|
538
|
+
return undefined;
|
|
513
539
|
},
|
|
514
|
-
[rowLabel
|
|
540
|
+
[rowLabel],
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const deriveRowLabel = useCallback(
|
|
544
|
+
(row: (typeof tableRows)[0], index: number): string =>
|
|
545
|
+
deriveRowIdentity(row) ??
|
|
546
|
+
withInterp(t("Row {{row}}", { row: index + 1 }), { row: index + 1 }),
|
|
547
|
+
[deriveRowIdentity, t],
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// Selection-toggle name: row identity beats a bare index ("Select row
|
|
551
|
+
// Bulbasaur", falling back to "Select row 3"), the same source the grid
|
|
552
|
+
// and the card list both read.
|
|
553
|
+
const deriveSelectRowLabel = useCallback(
|
|
554
|
+
(row: (typeof tableRows)[0], index: number): string => {
|
|
555
|
+
const identity = deriveRowIdentity(row) ?? String(index + 1);
|
|
556
|
+
return withInterp(t("Select row {{row}}", { row: identity }), { row: identity });
|
|
557
|
+
},
|
|
558
|
+
[deriveRowIdentity, t],
|
|
515
559
|
);
|
|
516
560
|
|
|
517
561
|
const getRowPressProps = useCallback(
|
|
518
|
-
|
|
562
|
+
// The card list asks for the "card" variant. Native carries no DOM row
|
|
563
|
+
// role for it to override, so both variants land on role="button".
|
|
564
|
+
(row: (typeof tableRows)[0], index: number, _variant: "row" | "card" = "row") => {
|
|
519
565
|
if (!rowPressActive) return {};
|
|
520
566
|
return {
|
|
521
567
|
onPress: () => onRowPress?.(row.original as TData, index),
|
|
@@ -551,101 +597,6 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
551
597
|
[renderRowContent, tableZebraOn, t, getRowPressProps],
|
|
552
598
|
);
|
|
553
599
|
|
|
554
|
-
const renderMobileCard = useCallback(
|
|
555
|
-
(row: (typeof tableRows)[0], index: number, onDelete?: (idx: number) => void) => {
|
|
556
|
-
const rowData = row.original as TData;
|
|
557
|
-
const visibleCells = row.getVisibleCells();
|
|
558
|
-
return (
|
|
559
|
-
<View
|
|
560
|
-
key={`${row.id}-${index}`}
|
|
561
|
-
backgroundColor="$color1"
|
|
562
|
-
borderRadius="$4"
|
|
563
|
-
borderWidth={1}
|
|
564
|
-
borderColor="$borderColor"
|
|
565
|
-
padding="$3"
|
|
566
|
-
marginBottom="$2"
|
|
567
|
-
{...(getRowPressProps(row, index) as any)}
|
|
568
|
-
{...(rowIntentProps(rowData) as any)}
|
|
569
|
-
>
|
|
570
|
-
{showRowNumbers && (
|
|
571
|
-
<View
|
|
572
|
-
flexDirection="row"
|
|
573
|
-
justifyContent="space-between"
|
|
574
|
-
paddingBottom="$2"
|
|
575
|
-
{...hairline.bottom}
|
|
576
|
-
borderBottomColor="$borderColor"
|
|
577
|
-
marginBottom="$2"
|
|
578
|
-
>
|
|
579
|
-
<Text fontSize="$3" color="$color10" fontWeight="600">
|
|
580
|
-
#{index + 1}
|
|
581
|
-
</Text>
|
|
582
|
-
{showDeleteRow && !readOnly && (
|
|
583
|
-
<Button
|
|
584
|
-
chromeless
|
|
585
|
-
circular
|
|
586
|
-
size="$2"
|
|
587
|
-
icon={TrashIcon}
|
|
588
|
-
disabled={disabled || tableRows.length <= minRows}
|
|
589
|
-
onPress={() => onDelete?.(index)}
|
|
590
|
-
aria-label={withInterp(t("Delete row {{row}}", { row: index + 1 }), {
|
|
591
|
-
row: index + 1,
|
|
592
|
-
})}
|
|
593
|
-
/>
|
|
594
|
-
)}
|
|
595
|
-
</View>
|
|
596
|
-
)}
|
|
597
|
-
{visibleCells.map((cell: any) => {
|
|
598
|
-
const columnDef = cell.column.columnDef as DataTableColumn;
|
|
599
|
-
const headerLabel =
|
|
600
|
-
typeof columnDef.header === "string"
|
|
601
|
-
? columnDef.header
|
|
602
|
-
: columnDef.id || cell.column.id;
|
|
603
|
-
return (
|
|
604
|
-
<View
|
|
605
|
-
key={cell.id}
|
|
606
|
-
flexDirection="row"
|
|
607
|
-
alignItems="center"
|
|
608
|
-
paddingVertical="$1.5"
|
|
609
|
-
gap="$2"
|
|
610
|
-
>
|
|
611
|
-
<Text fontSize="$3" color="$color10" fontWeight="500" width="40%" flexShrink={0}>
|
|
612
|
-
{headerLabel}
|
|
613
|
-
</Text>
|
|
614
|
-
<View flex={1}>{renderCellContent(cell, index, rowData)}</View>
|
|
615
|
-
</View>
|
|
616
|
-
);
|
|
617
|
-
})}
|
|
618
|
-
{!showRowNumbers && showDeleteRow && !readOnly && (
|
|
619
|
-
<View paddingTop="$2" alignItems="flex-end">
|
|
620
|
-
<Button
|
|
621
|
-
chromeless
|
|
622
|
-
circular
|
|
623
|
-
size="$2"
|
|
624
|
-
icon={TrashIcon}
|
|
625
|
-
disabled={disabled || tableRows.length <= minRows}
|
|
626
|
-
onPress={() => onDelete?.(index)}
|
|
627
|
-
aria-label={withInterp(t("Delete row {{row}}", { row: index + 1 }), {
|
|
628
|
-
row: index + 1,
|
|
629
|
-
})}
|
|
630
|
-
/>
|
|
631
|
-
</View>
|
|
632
|
-
)}
|
|
633
|
-
</View>
|
|
634
|
-
);
|
|
635
|
-
},
|
|
636
|
-
[
|
|
637
|
-
showRowNumbers,
|
|
638
|
-
showDeleteRow,
|
|
639
|
-
readOnly,
|
|
640
|
-
disabled,
|
|
641
|
-
tableRows.length,
|
|
642
|
-
minRows,
|
|
643
|
-
renderCellContent,
|
|
644
|
-
t,
|
|
645
|
-
getRowPressProps,
|
|
646
|
-
],
|
|
647
|
-
);
|
|
648
|
-
|
|
649
600
|
const renderToolbarComponent = () => {
|
|
650
601
|
if (renderToolbar) {
|
|
651
602
|
return renderToolbar({
|
|
@@ -777,36 +728,37 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
777
728
|
{showList ? (
|
|
778
729
|
<DataTableDocumentList
|
|
779
730
|
tableRows={displayRows}
|
|
731
|
+
enableRowSelection={enableRowSelection}
|
|
780
732
|
disabled={disabled}
|
|
781
733
|
hasActiveFilters={computed.hasActiveFilters}
|
|
782
734
|
renderCellContent={renderCellContent}
|
|
783
735
|
compact={isCompact}
|
|
736
|
+
selectRowLabel={deriveSelectRowLabel}
|
|
784
737
|
queryInFlight={queryInFlight}
|
|
785
738
|
{...(rowPressActive ? { getRowPressProps } : {})}
|
|
786
739
|
/>
|
|
787
740
|
) : showCards ? (
|
|
788
741
|
<View flexDirection="column" gap="$2">
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
)}
|
|
742
|
+
<DataTableMobileCards
|
|
743
|
+
tableRows={displayRows}
|
|
744
|
+
enableRowSelection={enableRowSelection}
|
|
745
|
+
showRowNumbers={showRowNumbers}
|
|
746
|
+
showDeleteRow={showDeleteRow}
|
|
747
|
+
readOnly={readOnly}
|
|
748
|
+
disabled={disabled}
|
|
749
|
+
dataLength={tableData.length}
|
|
750
|
+
minRows={minRows}
|
|
751
|
+
hasActiveFilters={computed.hasActiveFilters}
|
|
752
|
+
renderCellContent={renderCellContent}
|
|
753
|
+
onDeleteRow={isFormMode ? effectiveHandleDeleteRow : handleDeleteRow}
|
|
754
|
+
selectAllChecked={selectAllChecked}
|
|
755
|
+
selectAllIndeterminate={selectAllIndeterminate}
|
|
756
|
+
onToggleSelectAll={toggleSelectAll}
|
|
757
|
+
compact={isCompact}
|
|
758
|
+
selectRowLabel={deriveSelectRowLabel}
|
|
759
|
+
queryInFlight={queryInFlight}
|
|
760
|
+
{...(rowPressActive ? { getRowPressProps } : {})}
|
|
761
|
+
/>
|
|
810
762
|
{enablePagination && (
|
|
811
763
|
<View width="100%" {...(disabled ? disabledState.chromeKnobProps : {})}>
|
|
812
764
|
{renderPaginationComponent()}
|
|
@@ -1099,7 +1051,6 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
1099
1051
|
virtualizationHeight,
|
|
1100
1052
|
virtualizationOverscan,
|
|
1101
1053
|
renderRow,
|
|
1102
|
-
renderMobileCard,
|
|
1103
1054
|
renderSubComponent,
|
|
1104
1055
|
footerGroups,
|
|
1105
1056
|
sm,
|
|
@@ -1118,6 +1069,11 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
1118
1069
|
renderCellContent,
|
|
1119
1070
|
rowPressActive,
|
|
1120
1071
|
getRowPressProps,
|
|
1072
|
+
enableRowSelection,
|
|
1073
|
+
selectAllChecked,
|
|
1074
|
+
selectAllIndeterminate,
|
|
1075
|
+
toggleSelectAll,
|
|
1076
|
+
deriveSelectRowLabel,
|
|
1121
1077
|
displayRows,
|
|
1122
1078
|
queryInFlight,
|
|
1123
1079
|
t,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One card renderer, two platforms.
|
|
3
|
+
*
|
|
4
|
+
* DataTableRoot.tsx (web) and index.native.tsx (native) are separate roots
|
|
5
|
+
* because native virtualizes through FlashList. The CARD LIST is not part
|
|
6
|
+
* of that split — a card has no platform-specific anatomy — so both roots
|
|
7
|
+
* render the same DataTableMobileCards rather than each drawing a card.
|
|
8
|
+
*
|
|
9
|
+
* MPO-285 shared the field RULE (classifyCardField, getColumnLabel) while
|
|
10
|
+
* leaving two copies of the JSX, and the two had already drifted: the
|
|
11
|
+
* native copy hardcoded its surface, and shipped without zebra striping,
|
|
12
|
+
* the selection checkbox, the empty states, and the TableCellContext that
|
|
13
|
+
* makes a field render chromeless. Sharing the rule alone does not stop
|
|
14
|
+
* that, because a new slot still has to be added twice.
|
|
15
|
+
*
|
|
16
|
+
* These assertions read source rather than render: the native root pulls in
|
|
17
|
+
* FlashList and the suite runs under happy-dom, so index.native.tsx is
|
|
18
|
+
* never the module a spec resolves. What the card DOES on native is covered
|
|
19
|
+
* by dataTableMobileCards.spec.tsx, which now describes both platforms.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
23
|
+
import { resolve } from "node:path";
|
|
24
|
+
import { describe, expect, it } from "vitest";
|
|
25
|
+
|
|
26
|
+
const DIR = resolve(__dirname);
|
|
27
|
+
|
|
28
|
+
function read(file: string): string {
|
|
29
|
+
return readFileSync(resolve(DIR, file), "utf-8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A `<Component ... />` element at a call site, as text. */
|
|
33
|
+
function callSite(source: string, component: string): string {
|
|
34
|
+
const start = source.indexOf(`<${component}`);
|
|
35
|
+
if (start === -1) return "";
|
|
36
|
+
const end = source.indexOf("/>", start);
|
|
37
|
+
return end === -1 ? "" : source.slice(start, end);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Prop names that call site passes. */
|
|
41
|
+
function propNames(source: string, component: string): Set<string> {
|
|
42
|
+
const names = new Set<string>();
|
|
43
|
+
for (const match of callSite(source, component).matchAll(/(?:^|\s)([A-Za-z][A-Za-z0-9]*)=\{/g)) {
|
|
44
|
+
names.add(match[1]!);
|
|
45
|
+
}
|
|
46
|
+
return names;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cardCallSite = (source: string) => callSite(source, "DataTableMobileCards");
|
|
50
|
+
|
|
51
|
+
describe("DataTable card renderer parity", () => {
|
|
52
|
+
it("the native root renders DataTableMobileCards and carries no card of its own", () => {
|
|
53
|
+
const native = read("index.native.tsx");
|
|
54
|
+
expect(native).toMatch(
|
|
55
|
+
/import \{[^}]*DataTableMobileCards[^}]*\} from "\.\/DataTableMobileCards"/,
|
|
56
|
+
);
|
|
57
|
+
expect(cardCallSite(native)).not.toBe("");
|
|
58
|
+
expect(native).not.toMatch(/renderMobileCard/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("the native root reaches the card field rule through the component, not directly", () => {
|
|
62
|
+
// MPO-285 left index.native.tsx importing classifyCardField/getColumnLabel
|
|
63
|
+
// to feed its own JSX. With the JSX shared there is nothing left to feed:
|
|
64
|
+
// an import here again would mean a second card had grown back.
|
|
65
|
+
const native = read("index.native.tsx");
|
|
66
|
+
expect(native).not.toMatch(/\bclassifyCardField\b/);
|
|
67
|
+
expect(native).not.toMatch(/\bgetColumnLabel\b/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("exactly one file in the component draws a card", () => {
|
|
71
|
+
const drawers = readdirSync(DIR)
|
|
72
|
+
.filter(
|
|
73
|
+
(file) => file.endsWith(".tsx") && !file.includes(".spec.") && !file.includes(".stories."),
|
|
74
|
+
)
|
|
75
|
+
.filter((file) => read(file).includes('testID="data-table-card"'));
|
|
76
|
+
expect(drawers).toEqual(["DataTableMobileCards.tsx"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Both roots hand their row views the same contract. A prop web passes and
|
|
80
|
+
// native does not is how the card lost selection and the empty states in
|
|
81
|
+
// the first place, so it is the drift worth failing a build over.
|
|
82
|
+
it.each(["DataTableMobileCards", "DataTableDocumentList"])(
|
|
83
|
+
"native passes every %s prop web passes",
|
|
84
|
+
(component) => {
|
|
85
|
+
const web = propNames(read("DataTableRoot.tsx"), component);
|
|
86
|
+
const native = propNames(read("index.native.tsx"), component);
|
|
87
|
+
expect(web.size).toBeGreaterThan(0);
|
|
88
|
+
expect([...web].filter((prop) => !native.has(prop)).sort()).toEqual([]);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it("native hands the card list its press props, so a card is pressable there too", () => {
|
|
93
|
+
expect(cardCallSite(read("index.native.tsx"))).toMatch(/getRowPressProps/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -5,16 +5,50 @@
|
|
|
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
|
import type React from "react";
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the human label for a column: `meta.label` → string header →
|
|
24
|
+
* column id. `meta.label` is the explicit label channel for columns whose
|
|
25
|
+
* header is a render function or a bare glyph (the liked heart) — without
|
|
26
|
+
* it a card field row would print the raw column id (`_liked_by`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function getColumnLabel(cell: any): string;
|
|
29
|
+
export type CardFieldKind = "skip" | "control" | "bare" | "figure";
|
|
30
|
+
/**
|
|
31
|
+
* Whether a card field prints the column's name, and where it goes.
|
|
32
|
+
*
|
|
33
|
+
* A card is not the grid with its header repeated down the page. In the
|
|
34
|
+
* grid the header is written ONCE for a whole column of values; on a card
|
|
35
|
+
* it is written once per value, so a 12-column table prints 12 labels per
|
|
36
|
+
* record. That is what a label costs here, and most labels do not earn it.
|
|
37
|
+
*
|
|
38
|
+
* skip the raw value is absent. A field with nothing in it is not a
|
|
39
|
+
* fact — "Rejected —" spends a row saying nothing.
|
|
40
|
+
* control a button, a switch, a rating. The control names itself and
|
|
41
|
+
* needs its own tap target, so it keeps a row and takes no label.
|
|
42
|
+
* bare a string that is neither a number nor a date. "DIVIDEND
|
|
43
|
+
* CREDIT" and "Income:Dividends" say what they are; "Payee" and
|
|
44
|
+
* "Account" in front of them are the header printed twice. The
|
|
45
|
+
* name is kept for assistive tech via VisuallyHidden, which
|
|
46
|
+
* costs no layout.
|
|
47
|
+
* figure a number, a date, a boolean. "1,204" alone cannot be read, so
|
|
48
|
+
* these DO keep their label — inline beside the figure, not in a
|
|
49
|
+
* 40%-wide column of its own.
|
|
50
|
+
*/
|
|
51
|
+
export declare function classifyCardField(cell: any): CardFieldKind;
|
|
18
52
|
export interface DataTableMobileCardsProps<TData> {
|
|
19
53
|
tableRows: any[];
|
|
20
54
|
enableRowSelection?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTableMobileCards.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/DataTableMobileCards.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"DataTableMobileCards.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/DataTableMobileCards.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,CAKhD;AAsBD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,GAAG,aAAa,CAgB1D;AAED,MAAM,WAAW,yBAAyB,CAAC,KAAK;IAC9C,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,KAAK,KAAK,CAAC,SAAS,CAAC;IACpF,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjG,qEAAqE;IACrE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACtE,SAAS,EACT,kBAA0B,EAC1B,cAAc,EACd,aAAa,EACb,QAAQ,EACR,QAAgB,EAChB,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,gBAAwB,EACxB,sBAA8B,EAC9B,iBAAiB,EACjB,OAAe,EACf,cAAc,EACd,gBAAgB,EAChB,aAAqB,GACtB,EAAE,yBAAyB,CAAC,KAAK,CAAC,2CA+OlC"}
|
|
@@ -17,7 +17,7 @@ export { DataTableContext } from "./DataTableContext";
|
|
|
17
17
|
export { DataTableStateContext, useDataTableState } from "./DataTableStateContext";
|
|
18
18
|
export type { DataTableStateContextValue } from "./DataTableStateContext";
|
|
19
19
|
export type { DataTableRootProps } from "./DataTableRoot";
|
|
20
|
-
declare function DataTableRoot<TData extends Record<string, any> = Record<string, any>>({ data, columns, initialState, onStateChange, onRetry, isLoading: isLoadingProp, isFetching: isFetchingProp, syncWithUrl, urlDebounceMs, urlStrategy, manualFiltering, manualPagination, manualSorting, enableFiltering, enableSorting, enablePagination, pageSizeOptions, enableColumnVisibility, enableColumnOrdering, enableColumnResizing, enableGlobalSearch, enableViews, layout, enableVirtualization, virtualizationHeight, virtualizationOverscan, enableRowExpansion, renderSubComponent, enableColumnPinning, maxPinnedColumns, enableKeyboardNavigation: _enableKeyboardNavigation, onRowPress, rowLabel, readOnly, onCellChange, name, form: formProp, showRowNumbers, showAddRow, showDeleteRow, minRows, maxRows, defaultRow, onDataChange, rowCount, pageCount, renderToolbar, renderPagination, renderFilters, className: _className, size, compact, disabled, accent, error, warning, success, children, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props }: DataTableRootProps<TData>): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
declare function DataTableRoot<TData extends Record<string, any> = Record<string, any>>({ data, columns, initialState, onStateChange, onRetry, isLoading: isLoadingProp, isFetching: isFetchingProp, syncWithUrl, urlDebounceMs, urlStrategy, manualFiltering, manualPagination, manualSorting, enableFiltering, enableSorting, enablePagination, pageSizeOptions, enableColumnVisibility, enableColumnOrdering, enableColumnResizing, enableGlobalSearch, enableViews, layout, enableVirtualization, virtualizationHeight, virtualizationOverscan, enableRowExpansion, renderSubComponent, enableRowSelection, selectAllScope: selectAllScopeProp, enableColumnPinning, maxPinnedColumns, enableKeyboardNavigation: _enableKeyboardNavigation, onRowPress, rowLabel, readOnly, onCellChange, name, form: formProp, showRowNumbers, showAddRow, showDeleteRow, minRows, maxRows, defaultRow, onDataChange, rowCount, pageCount, renderToolbar, renderPagination, renderFilters, className: _className, size, compact, disabled, accent, error, warning, success, children, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props }: DataTableRootProps<TData>): import("react/jsx-runtime").JSX.Element;
|
|
21
21
|
export declare const DataTable: typeof DataTableRoot & {
|
|
22
22
|
Toolbar: typeof DataTableToolbar;
|
|
23
23
|
Filters: typeof DataTableFilters;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.native.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/index.native.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.native.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/index.native.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAUzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACnF,YAAY,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAC1E,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AA0D1D,iBAAS,aAAa,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAC9E,IAAI,EACJ,OAAO,EACP,YAAY,EACZ,aAAa,EACb,OAAO,EACP,SAAS,EAAE,aAAa,EACxB,UAAU,EAAE,cAAc,EAC1B,WAAmB,EACnB,aAAmB,EACnB,WAAW,EACX,eAAuB,EACvB,gBAAwB,EACxB,aAAqB,EACrB,eAAsB,EACtB,aAAoB,EACpB,gBAAuB,EACvB,eAAe,EACf,sBAA6B,EAC7B,oBAA4B,EAC5B,oBAA4B,EAC5B,kBAAyB,EACzB,WAAmB,EACnB,MAAe,EACf,oBAA4B,EAC5B,oBAAoB,EACpB,sBAA0B,EAC1B,kBAA0B,EAC1B,kBAAkB,EAClB,kBAA0B,EAC1B,cAAc,EAAE,kBAAkB,EAClC,mBAA2B,EAC3B,gBAAoB,EACpB,wBAAwB,EAAE,yBAAiC,EAC3D,UAAU,EACV,QAAQ,EACR,QAAgB,EAChB,YAAY,EACZ,IAAI,EACJ,IAAI,EAAE,QAAQ,EACd,cAAsB,EACtB,UAAkB,EAClB,aAAqB,EACrB,OAAW,EACX,OAAO,EACP,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,SAAS,EAAE,UAAU,EACrB,IAAI,EACJ,OAAe,EACf,QAAgB,EAChB,MAAM,EACN,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAQ,EACR,YAAY,EAAE,SAAS,EACvB,kBAAkB,EAAE,eAAe,EACnC,GAAG,KAAK,EACT,EAAE,kBAAkB,CAAC,KAAK,CAAC,2CA25B3B;AAED,eAAO,MAAM,SAAS;;;;;;;;;;CAUpB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,OAAO,SAAS,CAAC"}
|