@multiplatform.one/table 7.28.1 → 7.29.1

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 (178) hide show
  1. package/dist/cjs/components/DataTable/CellContent.cjs +77 -0
  2. package/dist/cjs/components/DataTable/CellContent.native.js +88 -0
  3. package/dist/cjs/components/DataTable/CellContent.native.js.map +1 -0
  4. package/dist/cjs/components/DataTable/DataTableInfiniteTail.cjs +90 -0
  5. package/dist/cjs/components/DataTable/DataTableInfiniteTail.native.js +99 -0
  6. package/dist/cjs/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  7. package/dist/cjs/components/DataTable/DataTableRoot.cjs +17 -47
  8. package/dist/cjs/components/DataTable/DataTableRoot.native.js +17 -56
  9. package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
  10. package/dist/cjs/components/DataTable/NativeInfiniteSentinel.cjs +62 -0
  11. package/dist/cjs/components/DataTable/NativeInfiniteSentinel.native.js +67 -0
  12. package/dist/cjs/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  13. package/dist/cjs/components/DataTable/index.native.js +75 -15
  14. package/dist/cjs/components/DataTable/index.native.js.map +1 -1
  15. package/dist/cjs/components/DataTableBulkActions/index.cjs +70 -51
  16. package/dist/cjs/components/DataTableBulkActions/index.native.js +84 -63
  17. package/dist/cjs/components/DataTableBulkActions/index.native.js.map +1 -1
  18. package/dist/cjs/components/DataTableCell/index.cjs +3 -1
  19. package/dist/cjs/components/DataTableCell/index.native.js +3 -1
  20. package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
  21. package/dist/cjs/components/DataTableDateFilter/index.cjs +2 -1
  22. package/dist/cjs/components/DataTableDateFilter/index.native.js +2 -1
  23. package/dist/cjs/components/DataTableDateFilter/index.native.js.map +1 -1
  24. package/dist/cjs/components/DataTableExport/index.cjs +2 -1
  25. package/dist/cjs/components/DataTableExport/index.native.js +2 -1
  26. package/dist/cjs/components/DataTableExport/index.native.js.map +1 -1
  27. package/dist/cjs/components/DataTableGrouping/index.cjs +2 -2
  28. package/dist/cjs/components/DataTableGrouping/index.native.js +2 -2
  29. package/dist/cjs/components/DataTableViewOptions/index.cjs +1 -1
  30. package/dist/cjs/components/DataTableViewOptions/index.native.js +1 -1
  31. package/dist/cjs/components/DataTableViewOptions/index.native.js.map +1 -1
  32. package/dist/cjs/components/KnobDialog.cjs +2 -1
  33. package/dist/cjs/components/KnobDialog.native.js +4 -2
  34. package/dist/cjs/components/KnobDialog.native.js.map +1 -1
  35. package/dist/cjs/components/TableInput/RowDetailEditor.cjs +79 -81
  36. package/dist/cjs/components/TableInput/index.cjs +20 -19
  37. package/dist/cjs/components/TableInput/index.native.js +1 -0
  38. package/dist/cjs/components/TableInput/index.native.js.map +1 -1
  39. package/dist/esm/components/DataTable/CellContent.mjs +52 -0
  40. package/dist/esm/components/DataTable/CellContent.mjs.map +1 -0
  41. package/dist/esm/components/DataTable/CellContent.native.js +60 -0
  42. package/dist/esm/components/DataTable/CellContent.native.js.map +1 -0
  43. package/dist/esm/components/DataTable/DataTableInfiniteTail.mjs +65 -0
  44. package/dist/esm/components/DataTable/DataTableInfiniteTail.mjs.map +1 -0
  45. package/dist/esm/components/DataTable/DataTableInfiniteTail.native.js +71 -0
  46. package/dist/esm/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  47. package/dist/esm/components/DataTable/DataTableRoot.mjs +18 -48
  48. package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
  49. package/dist/esm/components/DataTable/DataTableRoot.native.js +18 -57
  50. package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
  51. package/dist/esm/components/DataTable/NativeInfiniteSentinel.mjs +36 -0
  52. package/dist/esm/components/DataTable/NativeInfiniteSentinel.mjs.map +1 -0
  53. package/dist/esm/components/DataTable/NativeInfiniteSentinel.native.js +38 -0
  54. package/dist/esm/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  55. package/dist/esm/components/DataTable/index.native.js +77 -17
  56. package/dist/esm/components/DataTable/index.native.js.map +1 -1
  57. package/dist/esm/components/DataTableBulkActions/index.mjs +71 -52
  58. package/dist/esm/components/DataTableBulkActions/index.mjs.map +1 -1
  59. package/dist/esm/components/DataTableBulkActions/index.native.js +85 -64
  60. package/dist/esm/components/DataTableBulkActions/index.native.js.map +1 -1
  61. package/dist/esm/components/DataTableCell/index.mjs +3 -1
  62. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  63. package/dist/esm/components/DataTableCell/index.native.js +3 -1
  64. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  65. package/dist/esm/components/DataTableDateFilter/index.mjs +2 -1
  66. package/dist/esm/components/DataTableDateFilter/index.mjs.map +1 -1
  67. package/dist/esm/components/DataTableDateFilter/index.native.js +2 -1
  68. package/dist/esm/components/DataTableDateFilter/index.native.js.map +1 -1
  69. package/dist/esm/components/DataTableExport/index.mjs +2 -1
  70. package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
  71. package/dist/esm/components/DataTableExport/index.native.js +2 -1
  72. package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
  73. package/dist/esm/components/DataTableGrouping/index.mjs +2 -2
  74. package/dist/esm/components/DataTableGrouping/index.native.js +2 -2
  75. package/dist/esm/components/DataTableViewOptions/index.mjs +2 -2
  76. package/dist/esm/components/DataTableViewOptions/index.mjs.map +1 -1
  77. package/dist/esm/components/DataTableViewOptions/index.native.js +2 -2
  78. package/dist/esm/components/DataTableViewOptions/index.native.js.map +1 -1
  79. package/dist/esm/components/KnobDialog.mjs +2 -1
  80. package/dist/esm/components/KnobDialog.mjs.map +1 -1
  81. package/dist/esm/components/KnobDialog.native.js +4 -2
  82. package/dist/esm/components/KnobDialog.native.js.map +1 -1
  83. package/dist/esm/components/TableInput/RowDetailEditor.mjs +81 -83
  84. package/dist/esm/components/TableInput/RowDetailEditor.mjs.map +1 -1
  85. package/dist/esm/components/TableInput/index.mjs +20 -19
  86. package/dist/esm/components/TableInput/index.mjs.map +1 -1
  87. package/dist/esm/components/TableInput/index.native.js +1 -0
  88. package/dist/esm/components/TableInput/index.native.js.map +1 -1
  89. package/dist/jsx/components/DataTable/CellContent.mjs +52 -0
  90. package/dist/jsx/components/DataTable/CellContent.mjs.map +1 -0
  91. package/dist/jsx/components/DataTable/CellContent.native.js +88 -0
  92. package/dist/jsx/components/DataTable/CellContent.native.js.map +1 -0
  93. package/dist/jsx/components/DataTable/DataTableInfiniteTail.mjs +65 -0
  94. package/dist/jsx/components/DataTable/DataTableInfiniteTail.mjs.map +1 -0
  95. package/dist/jsx/components/DataTable/DataTableInfiniteTail.native.js +99 -0
  96. package/dist/jsx/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  97. package/dist/jsx/components/DataTable/DataTableRoot.mjs +18 -48
  98. package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
  99. package/dist/jsx/components/DataTable/DataTableRoot.native.js +17 -56
  100. package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
  101. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.mjs +36 -0
  102. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.mjs.map +1 -0
  103. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.native.js +67 -0
  104. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  105. package/dist/jsx/components/DataTable/index.native.js +75 -15
  106. package/dist/jsx/components/DataTable/index.native.js.map +1 -1
  107. package/dist/jsx/components/DataTableBulkActions/index.mjs +71 -52
  108. package/dist/jsx/components/DataTableBulkActions/index.mjs.map +1 -1
  109. package/dist/jsx/components/DataTableBulkActions/index.native.js +84 -63
  110. package/dist/jsx/components/DataTableBulkActions/index.native.js.map +1 -1
  111. package/dist/jsx/components/DataTableCell/index.mjs +3 -1
  112. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  113. package/dist/jsx/components/DataTableCell/index.native.js +3 -1
  114. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  115. package/dist/jsx/components/DataTableDateFilter/index.mjs +2 -1
  116. package/dist/jsx/components/DataTableDateFilter/index.mjs.map +1 -1
  117. package/dist/jsx/components/DataTableDateFilter/index.native.js +2 -1
  118. package/dist/jsx/components/DataTableDateFilter/index.native.js.map +1 -1
  119. package/dist/jsx/components/DataTableExport/index.mjs +2 -1
  120. package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
  121. package/dist/jsx/components/DataTableExport/index.native.js +2 -1
  122. package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
  123. package/dist/jsx/components/DataTableGrouping/index.mjs +2 -2
  124. package/dist/jsx/components/DataTableGrouping/index.native.js +2 -2
  125. package/dist/jsx/components/DataTableViewOptions/index.mjs +2 -2
  126. package/dist/jsx/components/DataTableViewOptions/index.mjs.map +1 -1
  127. package/dist/jsx/components/DataTableViewOptions/index.native.js +1 -1
  128. package/dist/jsx/components/DataTableViewOptions/index.native.js.map +1 -1
  129. package/dist/jsx/components/KnobDialog.mjs +2 -1
  130. package/dist/jsx/components/KnobDialog.mjs.map +1 -1
  131. package/dist/jsx/components/KnobDialog.native.js +4 -2
  132. package/dist/jsx/components/KnobDialog.native.js.map +1 -1
  133. package/dist/jsx/components/TableInput/RowDetailEditor.mjs +81 -83
  134. package/dist/jsx/components/TableInput/RowDetailEditor.mjs.map +1 -1
  135. package/dist/jsx/components/TableInput/index.mjs +20 -19
  136. package/dist/jsx/components/TableInput/index.mjs.map +1 -1
  137. package/dist/jsx/components/TableInput/index.native.js +1 -0
  138. package/dist/jsx/components/TableInput/index.native.js.map +1 -1
  139. package/package.json +12 -12
  140. package/src/components/ChildTable/childTable.rowDetail.spec.tsx +69 -0
  141. package/src/components/DataTable/CellContent.tsx +73 -0
  142. package/src/components/DataTable/DataTableInfiniteTail.tsx +94 -0
  143. package/src/components/DataTable/DataTableRoot.tsx +37 -81
  144. package/src/components/DataTable/NativeInfiniteSentinel.tsx +45 -0
  145. package/src/components/DataTable/cellWeight.native.spec.tsx +82 -0
  146. package/src/components/DataTable/cellWeight.spec.tsx +147 -0
  147. package/src/components/DataTable/index.native.tsx +108 -19
  148. package/src/components/DataTable/nativeRoot.spec.tsx +180 -0
  149. package/src/components/DataTableBulkActions/index.tsx +100 -71
  150. package/src/components/DataTableCell/index.tsx +3 -2
  151. package/src/components/DataTableDateFilter/index.tsx +2 -10
  152. package/src/components/DataTableExport/index.tsx +2 -1
  153. package/src/components/DataTableFilters/DataTableFilters.stories.tsx +7 -19
  154. package/src/components/DataTableGrouping/index.tsx +2 -2
  155. package/src/components/DataTablePagination/DataTablePagination.stories.tsx +12 -46
  156. package/src/components/DataTableViewOptions/index.tsx +2 -2
  157. package/src/components/KnobDialog.tsx +2 -0
  158. package/src/components/TableInput/RowDetailEditor.tsx +73 -72
  159. package/src/components/TableInput/index.native.tsx +1 -0
  160. package/src/components/TableInput/index.tsx +30 -21
  161. package/types/components/DataTable/CellContent.d.ts +19 -0
  162. package/types/components/DataTable/CellContent.d.ts.map +1 -0
  163. package/types/components/DataTable/DataTableInfiniteTail.d.ts +22 -0
  164. package/types/components/DataTable/DataTableInfiniteTail.d.ts.map +1 -0
  165. package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
  166. package/types/components/DataTable/NativeInfiniteSentinel.d.ts +17 -0
  167. package/types/components/DataTable/NativeInfiniteSentinel.d.ts.map +1 -0
  168. package/types/components/DataTable/index.native.d.ts +1 -1
  169. package/types/components/DataTable/index.native.d.ts.map +1 -1
  170. package/types/components/DataTableBulkActions/index.d.ts.map +1 -1
  171. package/types/components/DataTableCell/index.d.ts.map +1 -1
  172. package/types/components/DataTableDateFilter/index.d.ts.map +1 -1
  173. package/types/components/DataTableExport/index.d.ts.map +1 -1
  174. package/types/components/KnobDialog.d.ts.map +1 -1
  175. package/types/components/TableInput/RowDetailEditor.d.ts +4 -0
  176. package/types/components/TableInput/RowDetailEditor.d.ts.map +1 -1
  177. package/types/components/TableInput/index.d.ts.map +1 -1
  178. package/types/components/TableInput/index.native.d.ts.map +1 -1
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Native load-more trigger for DataTable's infinite mode.
3
+ *
4
+ * Web watches its sentinel with an IntersectionObserver. React Native has
5
+ * none, and the table never sees its host's scroll events: the backoffice
6
+ * list scrolls the whole screen in a ScrollView the table sits inside. So
7
+ * the sentinel asks where it is on screen, on a short interval while more
8
+ * pages exist, and fires once it comes within `leadPx` of the window's
9
+ * bottom edge, the same lead web gives its observer.
10
+ */
11
+
12
+ import { type ComponentRef, useEffect, useRef } from "react";
13
+ import { Dimensions, View } from "react-native";
14
+
15
+ export const NATIVE_SENTINEL_POLL_MS = 250;
16
+
17
+ export interface NativeInfiniteSentinelProps {
18
+ onVisible: () => void;
19
+ leadPx: number;
20
+ }
21
+
22
+ export function NativeInfiniteSentinel({ onVisible, leadPx }: NativeInfiniteSentinelProps) {
23
+ const ref = useRef<ComponentRef<typeof View>>(null);
24
+ const onVisibleRef = useRef(onVisible);
25
+ onVisibleRef.current = onVisible;
26
+
27
+ useEffect(() => {
28
+ let active = true;
29
+ const timer = setInterval(() => {
30
+ ref.current?.measureInWindow((_x, y, width) => {
31
+ // An unmeasured view reports all zeros; width 0 means not laid out.
32
+ if (!active || width <= 0) return;
33
+ if (y <= Dimensions.get("window").height + leadPx) onVisibleRef.current();
34
+ });
35
+ }, NATIVE_SENTINEL_POLL_MS);
36
+ return () => {
37
+ active = false;
38
+ clearInterval(timer);
39
+ };
40
+ }, [leadPx]);
41
+
42
+ return (
43
+ <View ref={ref} style={{ height: 1, width: "100%" }} aria-hidden testID="infinite-sentinel" />
44
+ );
45
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * MPO-446 table arm (native): the native root renders body cells through the
3
+ * same CellContent as web, so a primitive value rides house Text and takes
4
+ * the fontWeight and bodyFont knobs. index.native.tsx pulls in FlashList and
5
+ * the suite runs under happy-dom, so the root is read as source (the
6
+ * ./nativeCardParity.spec.tsx precedent) and CellContent renders under the
7
+ * native platform flags.
8
+ */
9
+
10
+ import { readFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
13
+ import { Preset } from "@multiplatform.one/theme";
14
+ import { cleanup, screen } from "@testing-library/react";
15
+ import { Text as RawText } from "tamagui";
16
+ import { afterEach, describe, expect, it, vi } from "vitest";
17
+ import { CellContent } from "./CellContent";
18
+
19
+ vi.mock("@multiplatform.one/platform", async (importOriginal) => ({
20
+ ...(await importOriginal<typeof import("@multiplatform.one/platform")>()),
21
+ isWeb: false,
22
+ isNative: true,
23
+ }));
24
+
25
+ afterEach(cleanup);
26
+
27
+ function classes(el: Element, prefix: string): string[] {
28
+ return String(el.className || "")
29
+ .split(" ")
30
+ .filter((c) => c.startsWith(prefix))
31
+ .sort();
32
+ }
33
+
34
+ function explicit(prefix: string, props: Record<string, string>): string[] {
35
+ renderWithProviders(<RawText {...props}>probe</RawText>);
36
+ const got = classes(screen.getByText("probe"), prefix);
37
+ cleanup();
38
+ return got;
39
+ }
40
+
41
+ describe("DataTable custom cell values follow the text knobs (native, MPO-446)", () => {
42
+ it("really is running with the native platform flags", async () => {
43
+ const platform = await import("@multiplatform.one/platform");
44
+ expect(platform.isWeb).toBe(false);
45
+ expect(platform.isNative).toBe(true);
46
+ });
47
+
48
+ it("the native root renders body cells through CellContent with the value voice size", () => {
49
+ const native = readFileSync(resolve(__dirname, "index.native.tsx"), "utf-8");
50
+ const start = native.indexOf("<CellContent");
51
+ expect(start).toBeGreaterThan(-1);
52
+ const site = native.slice(start, native.indexOf("/>", start));
53
+ expect(site).toMatch(/cellDef=\{columnDef\.cell\}/);
54
+ expect(site).toMatch(/fontSize=\{knobProps\.label\.fontSize/);
55
+ });
56
+
57
+ it.each([
58
+ ["regular", "400"],
59
+ ["bold", "700"],
60
+ ] as const)("string and number values paint the knob weight at %s", (fontWeight, want) => {
61
+ const expected = explicit("_fow-", { fontWeight: want });
62
+ for (const value of ["Bulbasaur", 4461]) {
63
+ renderWithProviders(
64
+ <Preset overrides={{ fontWeight }}>
65
+ <CellContent cellDef={() => value} ctx={{}} fontSize="$3" />
66
+ </Preset>,
67
+ );
68
+ expect(classes(screen.getByText(String(value)), "_fow-"), String(value)).toEqual(expected);
69
+ cleanup();
70
+ }
71
+ });
72
+
73
+ it("string values take the bodyFont family", () => {
74
+ const expected = explicit("_ff-", { fontFamily: "$mono" });
75
+ renderWithProviders(
76
+ <Preset overrides={{ bodyFont: "mono" }}>
77
+ <CellContent cellDef={() => "Bulbasaur"} ctx={{}} fontSize="$3" />
78
+ </Preset>,
79
+ );
80
+ expect(classes(screen.getByText("Bulbasaur"), "_ff-")).toEqual(expected);
81
+ });
82
+ });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * MPO-446 table arm (web): a DataTable custom cell's value takes the
3
+ * fontWeight and bodyFont knobs.
4
+ *
5
+ * `cell: (info) => info.getValue()` returns a primitive, and flexRender
6
+ * dropped it as a bare text node into the cell's alignment View: a div with
7
+ * no text voice, so the value painted the page's 400 under Weight bold. The
8
+ * value now rides house Text (./CellContent.tsx). ./cellWeight.native.spec.tsx
9
+ * is the native arm.
10
+ */
11
+
12
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
13
+ import { Preset } from "@multiplatform.one/theme";
14
+ import { act, cleanup, screen } from "@testing-library/react";
15
+ import { useState, type ReactNode } from "react";
16
+ import { Text as RawText } from "tamagui";
17
+ import { afterEach, describe, expect, it } from "vitest";
18
+ import type { DataTableColumn } from "../../types";
19
+ import { CellContent } from "./CellContent";
20
+ import { DataTable } from "./index";
21
+
22
+ type Row = { id: number; name: string; status: string; amount: string };
23
+
24
+ const rows: Row[] = [{ id: 4461, name: "Bulbasaur", status: "sleeping", amount: "12.50" }];
25
+
26
+ const columns: DataTableColumn<Row>[] = [
27
+ { id: "id", accessorKey: "id", header: "ID", cell: (info) => info.getValue() },
28
+ { id: "name", accessorKey: "name", header: "Name", cell: (info) => info.getValue() },
29
+ {
30
+ id: "status",
31
+ accessorKey: "status",
32
+ header: "Status",
33
+ cell: (info) => <RawText fontWeight="300">{String(info.getValue())}</RawText>,
34
+ },
35
+ {
36
+ id: "amount",
37
+ accessorKey: "amount",
38
+ header: "Amount",
39
+ field: "money",
40
+ fieldProps: { currency: "USD", locale: "en-US" },
41
+ },
42
+ ];
43
+
44
+ const weights = [
45
+ ["regular", "400"],
46
+ ["bold", "700"],
47
+ ] as const;
48
+
49
+ afterEach(cleanup);
50
+
51
+ function classes(el: Element, prefix: string): string[] {
52
+ return String(el.className || "")
53
+ .split(" ")
54
+ .filter((c) => c.startsWith(prefix))
55
+ .sort();
56
+ }
57
+
58
+ /** The class a prop paints, read off a raw node so no house wiring is involved. */
59
+ function explicit(prefix: string, props: Record<string, string>): string[] {
60
+ renderWithProviders(<RawText {...props}>probe</RawText>);
61
+ const got = classes(screen.getByText("probe"), prefix);
62
+ cleanup();
63
+ return got;
64
+ }
65
+
66
+ function renderTable(overrides: Record<string, string>) {
67
+ return renderWithProviders(
68
+ <Preset overrides={overrides as never}>
69
+ <DataTable
70
+ data={rows}
71
+ columns={columns as DataTableColumn<Record<string, any>>[]}
72
+ layout="table"
73
+ enablePagination={false}
74
+ readOnly
75
+ />
76
+ </Preset>,
77
+ );
78
+ }
79
+
80
+ describe("DataTable custom cell values follow the text knobs (web, MPO-446)", () => {
81
+ it.each(weights)("string and number values paint the knob weight at %s", (fontWeight, want) => {
82
+ const expected = explicit("_fow-", { fontWeight: want });
83
+ renderTable({ fontWeight });
84
+ for (const text of ["Bulbasaur", "4461"]) {
85
+ const node = screen.getByText(text);
86
+ expect(node.closest("[data-cell-align]")).not.toBe(node);
87
+ expect(classes(node, "_fow-"), text).toEqual(expected);
88
+ }
89
+ });
90
+
91
+ it("string values take the bodyFont family", () => {
92
+ const expected = explicit("_ff-", { fontFamily: "$mono" });
93
+ renderTable({ bodyFont: "mono" });
94
+ expect(classes(screen.getByText("Bulbasaur"), "_ff-")).toEqual(expected);
95
+ });
96
+
97
+ it.each(weights)("a money value paints the knob weight at %s", (fontWeight, want) => {
98
+ const expected = explicit("_fow-", { fontWeight: want });
99
+ renderTable({ fontWeight });
100
+ expect(classes(screen.getByText(/12\.50/), "_fow-")).toEqual(expected);
101
+ });
102
+
103
+ it("a renderer that returns an element keeps its own weight", () => {
104
+ const expected = explicit("_fow-", { fontWeight: "300" });
105
+ renderTable({ fontWeight: "bold" });
106
+ expect(classes(screen.getByText("sleeping"), "_fow-")).toEqual(expected);
107
+ });
108
+ });
109
+
110
+ describe("CellContent keeps flexRender's component identity per renderer", () => {
111
+ function Counter({ start }: { start: number }): ReactNode {
112
+ const [n, setN] = useState(start);
113
+ return (
114
+ <RawText onPress={() => setN((v) => v + 1)} data-testid="counter">
115
+ {n}
116
+ </RawText>
117
+ );
118
+ }
119
+
120
+ it("a renderer's hook state survives a re-render with the same renderer", () => {
121
+ const render = (ctx: unknown) => <Counter start={(ctx as { start: number }).start} />;
122
+ const { rerender } = renderWithProviders(
123
+ <CellContent cellDef={render} ctx={{ start: 1 }} fontSize="$3" />,
124
+ );
125
+ act(() => screen.getByTestId("counter").click());
126
+ expect(screen.getByTestId("counter").textContent).toBe("2");
127
+ rerender(<CellContent cellDef={render} ctx={{ start: 1 }} fontSize="$3" />);
128
+ expect(screen.getByTestId("counter").textContent).toBe("2");
129
+ });
130
+
131
+ it("a new renderer mounts its own hooks instead of reading the old renderer's slots", () => {
132
+ const first = () => {
133
+ const [label] = useState("first renderer");
134
+ return label;
135
+ };
136
+ const second = () => {
137
+ const [label] = useState("second renderer");
138
+ return label;
139
+ };
140
+ const { rerender } = renderWithProviders(
141
+ <CellContent cellDef={first} ctx={{}} fontSize="$3" />,
142
+ );
143
+ expect(screen.getByText("first renderer")).toBeInTheDocument();
144
+ rerender(<CellContent cellDef={second} ctx={{}} fontSize="$3" />);
145
+ expect(screen.getByText("second renderer")).toBeInTheDocument();
146
+ });
147
+ });
@@ -14,19 +14,21 @@ import {
14
14
  TrashIcon,
15
15
  } from "@phosphor-icons/react";
16
16
  import { FlashList, type FlashListRef } from "@shopify/flash-list";
17
- import { AsyncBoundary, EmptyState, useDirection } from "@multiplatform.one/components";
17
+ import { AsyncBoundary, EmptyState, useDirection, ScrollView } from "@multiplatform.one/components";
18
18
  import { Button, Field, useFormField } from "@multiplatform.one/forms";
19
19
  import { flexRender } from "@tanstack/react-table";
20
20
  import { useResolvedKnobs } from "@multiplatform.one/theme";
21
21
  import React, { useRef, useMemo, useCallback } from "react";
22
- import { ScrollView, Text, View, useMedia, withStaticProperties } from "tamagui";
22
+ import type { FontSizeTokens } from "tamagui";
23
+ import { Text, View, useMedia, withStaticProperties } from "tamagui";
23
24
  import { useTranslation } from "react-i18next";
24
25
  import { useDataTable } from "../../hooks/useDataTable";
25
- import type { DataTableColumn, DataTableField, SelectAllScope } from "../../types";
26
- import { DEFAULT_PAGE_SIZE_OPTIONS } from "../../utils/paginationDefaults";
26
+ import type { BulkAction, DataTableColumn, DataTableField, SelectAllScope } from "../../types";
27
+ import { DEFAULT_PAGE_SIZE_OPTIONS, INFINITE_TRIGGER_ROWS } from "../../utils/paginationDefaults";
27
28
  import { withInterp } from "../../utils/withInterp";
28
29
  import { DataTableColumnPinning } from "../DataTableColumnPinning";
29
30
  import { DataTablePagination } from "../DataTablePagination";
31
+ import { CellContent } from "./CellContent";
30
32
  import { DataTableToolbar } from "../DataTableToolbar";
31
33
  import { DataTableFilters } from "../DataTableFilters";
32
34
  import { DataTableBulkActions } from "../DataTableBulkActions";
@@ -38,8 +40,10 @@ import { DataTableCell } from "../DataTableCell";
38
40
  import { Table } from "@multiplatform.one/table-primitives";
39
41
  import { DataTableContext } from "./DataTableContext";
40
42
  import { DataTableDocumentList } from "./DataTableDocumentList";
43
+ import { DataTableInfiniteTail } from "./DataTableInfiniteTail";
41
44
  import { DataTableMobileCards } from "./DataTableMobileCards";
42
45
  import { dataTableChrome, resolveDataTableIntent, rowIntentProps } from "./dataTableChrome";
46
+ import { NativeInfiniteSentinel } from "./NativeInfiniteSentinel";
43
47
  import { tableRowStateProps } from "./rowStateChrome";
44
48
  import { DataTableStateContext } from "./DataTableStateContext";
45
49
  import type { DataTableStateContextValue } from "./DataTableStateContext";
@@ -110,6 +114,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
110
114
  data,
111
115
  columns,
112
116
  initialState,
117
+ controlledState,
113
118
  onStateChange,
114
119
  onRetry,
115
120
  isLoading: isLoadingProp,
@@ -121,24 +126,32 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
121
126
  manualPagination = false,
122
127
  manualSorting = false,
123
128
  enableFiltering = true,
129
+ filterableFields,
124
130
  enableSorting = true,
125
131
  enablePagination = true,
126
132
  pageSizeOptions,
133
+ paginationMode = "pages",
134
+ loadPage,
135
+ rowAccumulator,
127
136
  enableColumnVisibility = true,
128
137
  enableColumnOrdering = false,
129
138
  enableColumnResizing = false,
130
139
  enableGlobalSearch = true,
131
140
  enableViews = false,
141
+ columnLayout = "equal",
132
142
  layout = "auto",
133
143
  enableVirtualization = false,
134
144
  virtualizationHeight,
135
145
  virtualizationOverscan = 5,
136
146
  enableRowExpansion = false,
137
147
  renderSubComponent,
148
+ getSubRows,
138
149
  enableRowSelection = false,
139
150
  selectAllScope: selectAllScopeProp,
151
+ bulkActions,
140
152
  enableColumnPinning = false,
141
153
  maxPinnedColumns = 3,
154
+ enableGrouping = false,
142
155
  enableKeyboardNavigation: _enableKeyboardNavigation = false,
143
156
  onRowPress,
144
157
  rowLabel,
@@ -184,6 +197,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
184
197
  compact,
185
198
  });
186
199
  const chrome = dataTableChrome(String(knobProps.sizeToken));
200
+ const bulkBarPlacement = knobProps.bulkBarPlacement === "bottom" ? "bottom" : "top";
187
201
  // House selectAllScope knob (DG-TBL-03): prop overrides knob; default page.
188
202
  const selectAllScope: SelectAllScope =
189
203
  selectAllScopeProp ?? (knobProps.selectAllScope === "filtered" ? "filtered" : "page");
@@ -202,6 +216,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
202
216
  data,
203
217
  columns,
204
218
  initialState,
219
+ controlledState,
205
220
  onStateChange,
206
221
  syncWithUrl,
207
222
  urlDebounceMs,
@@ -210,21 +225,28 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
210
225
  manualPagination,
211
226
  manualSorting,
212
227
  enableFiltering,
228
+ filterableFields,
213
229
  enableSorting,
214
230
  enablePagination,
231
+ paginationMode,
232
+ loadPage,
233
+ rowAccumulator,
215
234
  enableColumnVisibility,
216
235
  enableColumnOrdering,
217
236
  enableColumnResizing,
237
+ columnLayout,
218
238
  enableGlobalSearch,
219
239
  enableViews,
220
240
  enableVirtualization,
221
241
  virtualizationHeight,
222
242
  virtualizationOverscan,
223
243
  enableRowExpansion,
244
+ getSubRows,
224
245
  renderSubComponent,
225
246
  enableRowSelection,
226
247
  enableColumnPinning,
227
248
  maxPinnedColumns,
249
+ enableGrouping,
228
250
  enableKeyboardNavigation: false,
229
251
  rowCount,
230
252
  pageCount,
@@ -232,6 +254,8 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
232
254
  isFetching: isFetchingProp,
233
255
  });
234
256
 
257
+ const resolvedBulkActions = (bulkActions ?? []) as BulkAction<TData>[];
258
+
235
259
  const selectAllChecked =
236
260
  enableRowSelection &&
237
261
  (selectAllScope === "filtered"
@@ -257,12 +281,22 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
257
281
  const cellHeight = resolvedSize === "sm" ? "$6" : resolvedSize === "lg" ? "$10" : "$8";
258
282
 
259
283
  const headerGroups = table.getHeaderGroups();
260
- const tableRows = table.getRowModel().rows;
284
+ const rowModelRows = table.getRowModel().rows;
285
+ // Infinite mode reveals accumulated pages only; loader mode's row model is
286
+ // already just the accumulated rows, so the slice is a no-op there.
287
+ const infinite = computed.infinite;
288
+ const tableRows = infinite ? rowModelRows.slice(0, infinite.visibleRowCount) : rowModelRows;
261
289
  const heldRowsRef = useRef(tableRows);
262
290
  if (tableRows.length > 0) {
263
291
  heldRowsRef.current = tableRows;
264
292
  }
265
- const queryInFlight = Boolean(state.isFetching || state.isPlaceholderData || state.isLoading);
293
+ const queryInFlight = Boolean(
294
+ state.isFetching ||
295
+ state.isPlaceholderData ||
296
+ infinite?.isAppending ||
297
+ state.isLoading ||
298
+ infinite?.isInitialLoading,
299
+ );
266
300
  const displayRows =
267
301
  tableRows.length > 0
268
302
  ? tableRows
@@ -307,6 +341,8 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
307
341
 
308
342
  const isVirtualized =
309
343
  enableVirtualization && virtualizationHeight != null && virtualizationHeight > 0;
344
+ // Only the grid virtualizes on native; list and cards render in flow.
345
+ const isVirtualGrid = isVirtualized && !showList && !showCards;
310
346
 
311
347
  // DataTableColumnPinning is a hook-style helper (it calls useState/useMemo/
312
348
  // useCallback internally), so it must run unconditionally at the top level —
@@ -369,16 +405,11 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
369
405
  const value = cell.getValue();
370
406
 
371
407
  if (columnDef.cell && !columnDef.field) {
372
- // TanStack injects a default cell renderer that returns the raw value,
373
- // and story/consumer columns commonly do `cell: (info) => info.getValue()`;
374
- // raw strings/numbers outside <Text> are dropped on native. flexRender
375
- // hides function-cell primitives inside an element, so NativeCellContent
376
- // calls the renderer itself and wraps primitive output.
377
408
  return (
378
- <NativeCellContent
409
+ <CellContent
379
410
  cellDef={columnDef.cell}
380
411
  ctx={cell.getContext()}
381
- fontSize={knobProps.sizeToken}
412
+ fontSize={knobProps.label.fontSize as FontSizeTokens}
382
413
  />
383
414
  );
384
415
  }
@@ -398,7 +429,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
398
429
  />
399
430
  );
400
431
  },
401
- [handleCellChange, readOnly, disabled, knobProps.sizeToken],
432
+ [handleCellChange, readOnly, disabled, knobProps.label.fontSize],
402
433
  );
403
434
 
404
435
  const renderRowContent = useCallback(
@@ -647,8 +678,8 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
647
678
  };
648
679
 
649
680
  const stateContextValue = useMemo<DataTableStateContextValue<TData>>(
650
- () => ({ table, state, actions, computed }),
651
- [table, state, actions, computed],
681
+ () => ({ table, state, actions, computed, selectAllScope }),
682
+ [table, state, actions, computed, selectAllScope],
652
683
  );
653
684
 
654
685
  const renderTableInner = useCallback(
@@ -676,7 +707,12 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
676
707
  />
677
708
  );
678
709
  }
679
- if (state.isLoading && displayRows.length === 0 && !state.isFetching) {
710
+ if (
711
+ (state.isLoading || infinite?.isInitialLoading) &&
712
+ displayRows.length === 0 &&
713
+ !state.isFetching &&
714
+ !state.isPlaceholderData
715
+ ) {
680
716
  return <AsyncBoundary loading layout="table" aria-label={ariaLabel || "Data table"} />;
681
717
  }
682
718
 
@@ -711,6 +747,16 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
711
747
  {renderToolbarComponent()}
712
748
  </View>
713
749
  )}
750
+ {enableRowSelection && bulkBarPlacement === "top" ? (
751
+ <View width="100%" {...(disabled ? disabledState.chromeKnobProps : {})}>
752
+ <DataTableBulkActions
753
+ actions={resolvedBulkActions}
754
+ maxVisibleActions={5}
755
+ showSelectionCount
756
+ placement="top"
757
+ />
758
+ </View>
759
+ ) : null}
714
760
  {children}
715
761
  {renderFilters?.({
716
762
  filters: state.filters || [],
@@ -759,7 +805,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
759
805
  queryInFlight={queryInFlight}
760
806
  {...(rowPressActive ? { getRowPressProps } : {})}
761
807
  />
762
- {enablePagination && (
808
+ {enablePagination && !infinite && (
763
809
  <View width="100%" {...(disabled ? disabledState.chromeKnobProps : {})}>
764
810
  {renderPaginationComponent()}
765
811
  </View>
@@ -932,11 +978,19 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
932
978
  ) : isVirtualized ? (
933
979
  <View style={{ height: virtualizationHeight }}>
934
980
  <FlashList
981
+ keyboardShouldPersistTaps="handled"
935
982
  ref={flashListRef}
936
983
  data={displayRows}
937
984
  drawDistance={rowHeightPx * virtualizationOverscan}
938
985
  keyExtractor={(row) => row.id}
939
986
  renderItem={({ item, index }) => renderRow(item, index)}
987
+ {...(infinite && !infinite.exhausted
988
+ ? {
989
+ onEndReached: infinite.loadMore,
990
+ onEndReachedThreshold:
991
+ (INFINITE_TRIGGER_ROWS * rowHeightPx) / virtualizationHeight!,
992
+ }
993
+ : {})}
940
994
  />
941
995
  </View>
942
996
  ) : (
@@ -993,6 +1047,36 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
993
1047
  </ScrollView>
994
1048
  )}
995
1049
 
1050
+ {infinite ? (
1051
+ <DataTableInfiniteTail
1052
+ infinite={infinite}
1053
+ visibleCount={tableRows.length}
1054
+ showAppendSkeleton={!isVirtualGrid}
1055
+ rowHeightPx={rowHeightPx}
1056
+ fontSize={knobProps.sizeToken}
1057
+ gapProps={knobProps.gap}
1058
+ sentinel={
1059
+ !isVirtualGrid && !infinite.exhausted ? (
1060
+ <NativeInfiniteSentinel
1061
+ onVisible={infinite.loadMore}
1062
+ leadPx={INFINITE_TRIGGER_ROWS * rowHeightPx}
1063
+ />
1064
+ ) : null
1065
+ }
1066
+ />
1067
+ ) : null}
1068
+
1069
+ {enableRowSelection && bulkBarPlacement === "bottom" ? (
1070
+ <View width="100%" {...(disabled ? disabledState.chromeKnobProps : {})}>
1071
+ <DataTableBulkActions
1072
+ actions={resolvedBulkActions}
1073
+ maxVisibleActions={5}
1074
+ showSelectionCount
1075
+ placement="bottom"
1076
+ />
1077
+ </View>
1078
+ ) : null}
1079
+
996
1080
  {showAddRow && !readOnly && (
997
1081
  <View>
998
1082
  <Button
@@ -1006,7 +1090,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
1006
1090
  </View>
1007
1091
  )}
1008
1092
 
1009
- {!showCards && enablePagination && (
1093
+ {!showCards && enablePagination && !infinite && (
1010
1094
  <View width="100%" {...(disabled ? disabledState.chromeKnobProps : {})}>
1011
1095
  {renderPaginationComponent()}
1012
1096
  </View>
@@ -1076,6 +1160,11 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
1076
1160
  deriveSelectRowLabel,
1077
1161
  displayRows,
1078
1162
  queryInFlight,
1163
+ infinite,
1164
+ isVirtualGrid,
1165
+ rowHeightPx,
1166
+ bulkBarPlacement,
1167
+ resolvedBulkActions,
1079
1168
  t,
1080
1169
  ],
1081
1170
  );