@multiplatform.one/table 7.28.1 → 7.29.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.
Files changed (168) 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 +1 -1
  36. package/dist/cjs/components/TableInput/index.native.js +1 -0
  37. package/dist/cjs/components/TableInput/index.native.js.map +1 -1
  38. package/dist/esm/components/DataTable/CellContent.mjs +52 -0
  39. package/dist/esm/components/DataTable/CellContent.mjs.map +1 -0
  40. package/dist/esm/components/DataTable/CellContent.native.js +60 -0
  41. package/dist/esm/components/DataTable/CellContent.native.js.map +1 -0
  42. package/dist/esm/components/DataTable/DataTableInfiniteTail.mjs +65 -0
  43. package/dist/esm/components/DataTable/DataTableInfiniteTail.mjs.map +1 -0
  44. package/dist/esm/components/DataTable/DataTableInfiniteTail.native.js +71 -0
  45. package/dist/esm/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  46. package/dist/esm/components/DataTable/DataTableRoot.mjs +18 -48
  47. package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
  48. package/dist/esm/components/DataTable/DataTableRoot.native.js +18 -57
  49. package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
  50. package/dist/esm/components/DataTable/NativeInfiniteSentinel.mjs +36 -0
  51. package/dist/esm/components/DataTable/NativeInfiniteSentinel.mjs.map +1 -0
  52. package/dist/esm/components/DataTable/NativeInfiniteSentinel.native.js +38 -0
  53. package/dist/esm/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  54. package/dist/esm/components/DataTable/index.native.js +77 -17
  55. package/dist/esm/components/DataTable/index.native.js.map +1 -1
  56. package/dist/esm/components/DataTableBulkActions/index.mjs +71 -52
  57. package/dist/esm/components/DataTableBulkActions/index.mjs.map +1 -1
  58. package/dist/esm/components/DataTableBulkActions/index.native.js +85 -64
  59. package/dist/esm/components/DataTableBulkActions/index.native.js.map +1 -1
  60. package/dist/esm/components/DataTableCell/index.mjs +3 -1
  61. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  62. package/dist/esm/components/DataTableCell/index.native.js +3 -1
  63. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  64. package/dist/esm/components/DataTableDateFilter/index.mjs +2 -1
  65. package/dist/esm/components/DataTableDateFilter/index.mjs.map +1 -1
  66. package/dist/esm/components/DataTableDateFilter/index.native.js +2 -1
  67. package/dist/esm/components/DataTableDateFilter/index.native.js.map +1 -1
  68. package/dist/esm/components/DataTableExport/index.mjs +2 -1
  69. package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
  70. package/dist/esm/components/DataTableExport/index.native.js +2 -1
  71. package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
  72. package/dist/esm/components/DataTableGrouping/index.mjs +2 -2
  73. package/dist/esm/components/DataTableGrouping/index.native.js +2 -2
  74. package/dist/esm/components/DataTableViewOptions/index.mjs +2 -2
  75. package/dist/esm/components/DataTableViewOptions/index.mjs.map +1 -1
  76. package/dist/esm/components/DataTableViewOptions/index.native.js +2 -2
  77. package/dist/esm/components/DataTableViewOptions/index.native.js.map +1 -1
  78. package/dist/esm/components/KnobDialog.mjs +2 -1
  79. package/dist/esm/components/KnobDialog.mjs.map +1 -1
  80. package/dist/esm/components/KnobDialog.native.js +4 -2
  81. package/dist/esm/components/KnobDialog.native.js.map +1 -1
  82. package/dist/esm/components/TableInput/RowDetailEditor.mjs +2 -2
  83. package/dist/esm/components/TableInput/RowDetailEditor.mjs.map +1 -1
  84. package/dist/esm/components/TableInput/index.native.js +1 -0
  85. package/dist/esm/components/TableInput/index.native.js.map +1 -1
  86. package/dist/jsx/components/DataTable/CellContent.mjs +52 -0
  87. package/dist/jsx/components/DataTable/CellContent.mjs.map +1 -0
  88. package/dist/jsx/components/DataTable/CellContent.native.js +88 -0
  89. package/dist/jsx/components/DataTable/CellContent.native.js.map +1 -0
  90. package/dist/jsx/components/DataTable/DataTableInfiniteTail.mjs +65 -0
  91. package/dist/jsx/components/DataTable/DataTableInfiniteTail.mjs.map +1 -0
  92. package/dist/jsx/components/DataTable/DataTableInfiniteTail.native.js +99 -0
  93. package/dist/jsx/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  94. package/dist/jsx/components/DataTable/DataTableRoot.mjs +18 -48
  95. package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
  96. package/dist/jsx/components/DataTable/DataTableRoot.native.js +17 -56
  97. package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
  98. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.mjs +36 -0
  99. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.mjs.map +1 -0
  100. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.native.js +67 -0
  101. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  102. package/dist/jsx/components/DataTable/index.native.js +75 -15
  103. package/dist/jsx/components/DataTable/index.native.js.map +1 -1
  104. package/dist/jsx/components/DataTableBulkActions/index.mjs +71 -52
  105. package/dist/jsx/components/DataTableBulkActions/index.mjs.map +1 -1
  106. package/dist/jsx/components/DataTableBulkActions/index.native.js +84 -63
  107. package/dist/jsx/components/DataTableBulkActions/index.native.js.map +1 -1
  108. package/dist/jsx/components/DataTableCell/index.mjs +3 -1
  109. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  110. package/dist/jsx/components/DataTableCell/index.native.js +3 -1
  111. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  112. package/dist/jsx/components/DataTableDateFilter/index.mjs +2 -1
  113. package/dist/jsx/components/DataTableDateFilter/index.mjs.map +1 -1
  114. package/dist/jsx/components/DataTableDateFilter/index.native.js +2 -1
  115. package/dist/jsx/components/DataTableDateFilter/index.native.js.map +1 -1
  116. package/dist/jsx/components/DataTableExport/index.mjs +2 -1
  117. package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
  118. package/dist/jsx/components/DataTableExport/index.native.js +2 -1
  119. package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
  120. package/dist/jsx/components/DataTableGrouping/index.mjs +2 -2
  121. package/dist/jsx/components/DataTableGrouping/index.native.js +2 -2
  122. package/dist/jsx/components/DataTableViewOptions/index.mjs +2 -2
  123. package/dist/jsx/components/DataTableViewOptions/index.mjs.map +1 -1
  124. package/dist/jsx/components/DataTableViewOptions/index.native.js +1 -1
  125. package/dist/jsx/components/DataTableViewOptions/index.native.js.map +1 -1
  126. package/dist/jsx/components/KnobDialog.mjs +2 -1
  127. package/dist/jsx/components/KnobDialog.mjs.map +1 -1
  128. package/dist/jsx/components/KnobDialog.native.js +4 -2
  129. package/dist/jsx/components/KnobDialog.native.js.map +1 -1
  130. package/dist/jsx/components/TableInput/RowDetailEditor.mjs +2 -2
  131. package/dist/jsx/components/TableInput/RowDetailEditor.mjs.map +1 -1
  132. package/dist/jsx/components/TableInput/index.native.js +1 -0
  133. package/dist/jsx/components/TableInput/index.native.js.map +1 -1
  134. package/package.json +12 -12
  135. package/src/components/DataTable/CellContent.tsx +73 -0
  136. package/src/components/DataTable/DataTableInfiniteTail.tsx +94 -0
  137. package/src/components/DataTable/DataTableRoot.tsx +37 -81
  138. package/src/components/DataTable/NativeInfiniteSentinel.tsx +45 -0
  139. package/src/components/DataTable/cellWeight.native.spec.tsx +82 -0
  140. package/src/components/DataTable/cellWeight.spec.tsx +147 -0
  141. package/src/components/DataTable/index.native.tsx +108 -19
  142. package/src/components/DataTable/nativeRoot.spec.tsx +180 -0
  143. package/src/components/DataTableBulkActions/index.tsx +100 -71
  144. package/src/components/DataTableCell/index.tsx +3 -2
  145. package/src/components/DataTableDateFilter/index.tsx +2 -10
  146. package/src/components/DataTableExport/index.tsx +2 -1
  147. package/src/components/DataTableFilters/DataTableFilters.stories.tsx +7 -19
  148. package/src/components/DataTableGrouping/index.tsx +2 -2
  149. package/src/components/DataTablePagination/DataTablePagination.stories.tsx +12 -46
  150. package/src/components/DataTableViewOptions/index.tsx +2 -2
  151. package/src/components/KnobDialog.tsx +2 -0
  152. package/src/components/TableInput/RowDetailEditor.tsx +2 -2
  153. package/src/components/TableInput/index.native.tsx +1 -0
  154. package/types/components/DataTable/CellContent.d.ts +19 -0
  155. package/types/components/DataTable/CellContent.d.ts.map +1 -0
  156. package/types/components/DataTable/DataTableInfiniteTail.d.ts +22 -0
  157. package/types/components/DataTable/DataTableInfiniteTail.d.ts.map +1 -0
  158. package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
  159. package/types/components/DataTable/NativeInfiniteSentinel.d.ts +17 -0
  160. package/types/components/DataTable/NativeInfiniteSentinel.d.ts.map +1 -0
  161. package/types/components/DataTable/index.native.d.ts +1 -1
  162. package/types/components/DataTable/index.native.d.ts.map +1 -1
  163. package/types/components/DataTableBulkActions/index.d.ts.map +1 -1
  164. package/types/components/DataTableCell/index.d.ts.map +1 -1
  165. package/types/components/DataTableDateFilter/index.d.ts.map +1 -1
  166. package/types/components/DataTableExport/index.d.ts.map +1 -1
  167. package/types/components/KnobDialog.d.ts.map +1 -1
  168. package/types/components/TableInput/index.native.d.ts.map +1 -1
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Infinite mode tail, shared by the web and native roots: the load-more
3
+ * trigger each platform supplies, an append skeleton bounded by the actual
4
+ * load (no infinite spinner), and an honest, announced end-of-data state
5
+ * (DG-ST-01/04).
6
+ */
7
+
8
+ import { Skeleton } from "@multiplatform.one/components";
9
+ import type { ReactNode } from "react";
10
+ import { useTranslation } from "react-i18next";
11
+ import { Text, View } from "tamagui";
12
+ import type { DataTableInfiniteState } from "../../types";
13
+ import { bidiIsolate, withInterp } from "../../utils/withInterp";
14
+
15
+ export interface DataTableInfiniteTailProps {
16
+ infinite: DataTableInfiniteState;
17
+ /** Rows on screen now, for "Showing N of M". */
18
+ visibleCount: number;
19
+ /** False when the virtualizer draws its own skeleton row. */
20
+ showAppendSkeleton: boolean;
21
+ rowHeightPx: number;
22
+ fontSize: any;
23
+ gapProps?: Record<string, unknown>;
24
+ /** The platform's load-more trigger; absent once the data is exhausted. */
25
+ sentinel?: ReactNode;
26
+ }
27
+
28
+ export function DataTableInfiniteTail({
29
+ infinite,
30
+ visibleCount,
31
+ showAppendSkeleton,
32
+ rowHeightPx,
33
+ fontSize,
34
+ gapProps,
35
+ sentinel,
36
+ }: DataTableInfiniteTailProps) {
37
+ const { t } = useTranslation();
38
+ return (
39
+ <View flexDirection="column" width="100%">
40
+ {sentinel}
41
+ {showAppendSkeleton && infinite.isAppending ? (
42
+ <View
43
+ flexDirection="column"
44
+ width="100%"
45
+ {...gapProps}
46
+ paddingVertical="$2"
47
+ aria-hidden
48
+ {...({ "data-testid": "infinite-append-skeleton" } as any)}
49
+ >
50
+ {Array.from({ length: 3 }).map((_, i) => (
51
+ <Skeleton key={i} width="100%" height={rowHeightPx - 8} />
52
+ ))}
53
+ </View>
54
+ ) : null}
55
+ <Text
56
+ role="status"
57
+ aria-live="polite"
58
+ textAlign="center"
59
+ paddingVertical="$2"
60
+ fontSize={fontSize}
61
+ color="$color10"
62
+ {...({ "data-testid": "infinite-status" } as any)}
63
+ >
64
+ {/* Numeric labels are bidi-isolated so RTL contexts keep
65
+ the interpolated runs in order (FSI…PDI). */}
66
+ {infinite.isAppending
67
+ ? t("Loading more rows…")
68
+ : infinite.exhausted
69
+ ? typeof infinite.totalCount === "number" && infinite.totalCount > 0
70
+ ? bidiIsolate(
71
+ withInterp(t("All {{count}} rows loaded", { count: infinite.totalCount }), {
72
+ count: infinite.totalCount,
73
+ }),
74
+ )
75
+ : t("End of results")
76
+ : typeof infinite.totalCount === "number"
77
+ ? bidiIsolate(
78
+ withInterp(
79
+ t("Showing {{visible}} of {{total}} rows", {
80
+ visible: visibleCount,
81
+ total: infinite.totalCount,
82
+ }),
83
+ { visible: visibleCount, total: infinite.totalCount },
84
+ ),
85
+ )
86
+ : bidiIsolate(
87
+ withInterp(t("Showing {{visible}} rows", { visible: visibleCount }), {
88
+ visible: visibleCount,
89
+ }),
90
+ )}
91
+ </Text>
92
+ </View>
93
+ );
94
+ }
@@ -22,13 +22,8 @@ import { flexRender } from "@tanstack/react-table";
22
22
  import { useVirtualizer } from "@tanstack/react-virtual";
23
23
  import { ensureFocusVisibleRing, useResolvedKnobs } from "@multiplatform.one/theme";
24
24
  import React, { useEffect, useRef, useMemo, useCallback, useState } from "react";
25
- import {
26
- AsyncBoundary,
27
- EmptyState,
28
- Skeleton,
29
- Tooltip,
30
- useDirection,
31
- } from "@multiplatform.one/components";
25
+ import { AsyncBoundary, EmptyState, Tooltip, useDirection } from "@multiplatform.one/components";
26
+ import type { FontSizeTokens } from "tamagui";
32
27
  import { Text, View, isWeb, useMedia } from "tamagui";
33
28
  import { useTranslation } from "react-i18next";
34
29
  import { useDataTable } from "../../hooks/useDataTable";
@@ -59,6 +54,8 @@ import type {
59
54
  import { DataTableCardControls, DataTableCardSortControl } from "./DataTableCardControls";
60
55
  import { DataTableMobileCards } from "./DataTableMobileCards";
61
56
  import { DataTableDocumentList } from "./DataTableDocumentList";
57
+ import { DataTableInfiniteTail } from "./DataTableInfiniteTail";
58
+ import { CellContent } from "./CellContent";
62
59
  import { DataTableVirtualized } from "./DataTableVirtualized";
63
60
  import { dataTableChrome, resolveDataTableIntent, rowIntentProps } from "./dataTableChrome";
64
61
  import { tableCellKeyboardProps, tableRowStateProps } from "./rowStateChrome";
@@ -749,7 +746,11 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
749
746
  justifyContent={alignToFlex(customAlign)}
750
747
  {...({ "data-cell-align": customAlign } as any)}
751
748
  >
752
- {flexRender(columnDef.cell, cell.getContext())}
749
+ <CellContent
750
+ cellDef={columnDef.cell}
751
+ ctx={cell.getContext()}
752
+ fontSize={knobProps.label.fontSize as FontSizeTokens}
753
+ />
753
754
  </View>
754
755
  );
755
756
  }
@@ -785,7 +786,14 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
785
786
  />
786
787
  );
787
788
  },
788
- [handleCellChange, readOnly, disabled, knobProps.sizeToken, isCompact],
789
+ [
790
+ handleCellChange,
791
+ readOnly,
792
+ disabled,
793
+ knobProps.sizeToken,
794
+ knobProps.label.fontSize,
795
+ isCompact,
796
+ ],
789
797
  );
790
798
 
791
799
  const extraColumnsBefore =
@@ -2224,79 +2232,27 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
2224
2232
  </View>
2225
2233
  )}
2226
2234
 
2227
- {/* Infinite mode tail: sentinel (non-virtualized trigger), append
2228
- skeleton (bounded by the actual load — no infinite spinner),
2229
- and an honest, announced end-of-data state (DG-ST-01/04). */}
2230
2235
  {infinite ? (
2231
- <View flexDirection="column" width="100%">
2232
- {!isVirtualized && !infinite.exhausted && isWeb ? (
2233
- <View
2234
- // @ts-ignore web ref for IntersectionObserver
2235
- ref={sentinelRef as any}
2236
- height={1}
2237
- width="100%"
2238
- aria-hidden
2239
- {...({ "data-testid": "infinite-sentinel" } as any)}
2240
- />
2241
- ) : null}
2242
- {!isVirtualized && infinite.isAppending ? (
2243
- <View
2244
- flexDirection="column"
2245
- width="100%"
2246
- {...knobProps.gap}
2247
- paddingVertical="$2"
2248
- aria-hidden
2249
- {...({ "data-testid": "infinite-append-skeleton" } as any)}
2250
- >
2251
- {Array.from({ length: 3 }).map((_, i) => (
2252
- <Skeleton key={i} width="100%" height={rowHeightPx - 8} />
2253
- ))}
2254
- </View>
2255
- ) : null}
2256
- <Text
2257
- role="status"
2258
- aria-live="polite"
2259
- textAlign="center"
2260
- paddingVertical="$2"
2261
- fontSize={knobProps.sizeToken}
2262
- color="$color10"
2263
- {...({ "data-testid": "infinite-status" } as any)}
2264
- >
2265
- {/* Numeric labels are bidi-isolated so RTL contexts keep
2266
- the interpolated runs in order (FSI…PDI). */}
2267
- {infinite.isAppending
2268
- ? t("Loading more rows…")
2269
- : infinite.exhausted
2270
- ? typeof infinite.totalCount === "number" && infinite.totalCount > 0
2271
- ? bidiIsolate(
2272
- withInterp(
2273
- t("All {{count}} rows loaded", { count: infinite.totalCount }),
2274
- {
2275
- count: infinite.totalCount,
2276
- },
2277
- ),
2278
- )
2279
- : t("End of results")
2280
- : typeof infinite.totalCount === "number"
2281
- ? bidiIsolate(
2282
- withInterp(
2283
- t("Showing {{visible}} of {{total}} rows", {
2284
- visible: tableRows.length,
2285
- total: infinite.totalCount,
2286
- }),
2287
- { visible: tableRows.length, total: infinite.totalCount },
2288
- ),
2289
- )
2290
- : bidiIsolate(
2291
- withInterp(
2292
- t("Showing {{visible}} rows", { visible: tableRows.length }),
2293
- {
2294
- visible: tableRows.length,
2295
- },
2296
- ),
2297
- )}
2298
- </Text>
2299
- </View>
2236
+ <DataTableInfiniteTail
2237
+ infinite={infinite}
2238
+ visibleCount={tableRows.length}
2239
+ showAppendSkeleton={!isVirtualized}
2240
+ rowHeightPx={rowHeightPx}
2241
+ fontSize={knobProps.sizeToken}
2242
+ gapProps={knobProps.gap}
2243
+ sentinel={
2244
+ !isVirtualized && !infinite.exhausted && isWeb ? (
2245
+ <View
2246
+ // @ts-ignore web ref for IntersectionObserver
2247
+ ref={sentinelRef as any}
2248
+ height={1}
2249
+ width="100%"
2250
+ aria-hidden
2251
+ {...({ "data-testid": "infinite-sentinel" } as any)}
2252
+ />
2253
+ ) : null
2254
+ }
2255
+ />
2300
2256
  ) : null}
2301
2257
 
2302
2258
  {enableRowSelection && bulkBarPlacement === "bottom" ? (
@@ -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
+ });