@multiplatform.one/table 7.11.0 → 7.15.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 (265) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/ChildTable/index.cjs +8 -2
  3. package/dist/cjs/components/ChildTable/index.native.js +8 -2
  4. package/dist/cjs/components/ChildTable/index.native.js.map +1 -1
  5. package/dist/cjs/components/ColumnHeaderFilter/index.cjs +77 -111
  6. package/dist/cjs/components/ColumnHeaderFilter/index.native.js +89 -129
  7. package/dist/cjs/components/ColumnHeaderFilter/index.native.js.map +1 -1
  8. package/dist/cjs/components/DataTable/DataTableRoot.cjs +17 -1
  9. package/dist/cjs/components/DataTable/DataTableRoot.native.js +25 -2
  10. package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
  11. package/dist/cjs/components/DataTableCell/CellModalEditor.cjs +72 -70
  12. package/dist/cjs/components/DataTableCell/index.cjs +2 -2
  13. package/dist/cjs/components/DataTableCell/index.native.js +2 -11
  14. package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
  15. package/dist/cjs/components/DataTableDateFilter/index.cjs +135 -158
  16. package/dist/cjs/components/DataTableDateFilter/index.native.js +153 -183
  17. package/dist/cjs/components/DataTableDateFilter/index.native.js.map +1 -1
  18. package/dist/cjs/components/DataTableExport/index.cjs +122 -139
  19. package/dist/cjs/components/DataTableExport/index.native.js +135 -157
  20. package/dist/cjs/components/DataTableExport/index.native.js.map +1 -1
  21. package/dist/cjs/components/DataTableGrouping/index.cjs +192 -212
  22. package/dist/cjs/components/DataTableGrouping/index.native.js +209 -230
  23. package/dist/cjs/components/DataTableGrouping/index.native.js.map +1 -1
  24. package/dist/cjs/components/DataTableSearch/index.cjs +103 -115
  25. package/dist/cjs/components/DataTableSearch/index.native.js +111 -123
  26. package/dist/cjs/components/DataTableSearch/index.native.js.map +1 -1
  27. package/dist/cjs/components/DataTableToolbar/index.cjs +44 -61
  28. package/dist/cjs/components/DataTableToolbar/index.native.js +46 -65
  29. package/dist/cjs/components/DataTableToolbar/index.native.js.map +1 -1
  30. package/dist/cjs/components/DataTableViewOptions/index.cjs +38 -54
  31. package/dist/cjs/components/DataTableViewOptions/index.native.js +40 -56
  32. package/dist/cjs/components/DataTableViewOptions/index.native.js.map +1 -1
  33. package/dist/cjs/components/FilterOperatorValueEditor/index.cjs +3 -11
  34. package/dist/cjs/components/FilterOperatorValueEditor/index.native.js +3 -11
  35. package/dist/cjs/components/FilterOperatorValueEditor/index.native.js.map +1 -1
  36. package/dist/cjs/components/SimpleTable/index.cjs +2 -6
  37. package/dist/cjs/components/SimpleTable/index.native.js +2 -6
  38. package/dist/cjs/components/SimpleTable/index.native.js.map +1 -1
  39. package/dist/cjs/components/TableInput/RowDetailEditor.cjs +190 -0
  40. package/dist/cjs/components/TableInput/RowDetailEditor.native.js +172 -0
  41. package/dist/cjs/components/TableInput/RowDetailEditor.native.js.map +1 -0
  42. package/dist/cjs/components/TableInput/index.cjs +42 -5
  43. package/dist/cjs/components/TableInput/index.native.js +64 -1
  44. package/dist/cjs/components/TableInput/index.native.js.map +1 -1
  45. package/dist/cjs/components/TableInput/shared.cjs +8 -0
  46. package/dist/cjs/components/TableInput/shared.native.js +10 -0
  47. package/dist/cjs/components/TableInput/shared.native.js.map +1 -1
  48. package/dist/cjs/hooks/useDataTable.cjs +11 -1
  49. package/dist/cjs/hooks/useDataTable.native.js +12 -2
  50. package/dist/cjs/hooks/useDataTable.native.js.map +1 -1
  51. package/dist/cjs/index.native.js.map +1 -1
  52. package/dist/cjs/utils/rowPress.cjs +4 -1
  53. package/dist/cjs/utils/rowPress.native.js +4 -1
  54. package/dist/cjs/utils/rowPress.native.js.map +1 -1
  55. package/dist/esm/components/ChildTable/index.mjs +8 -2
  56. package/dist/esm/components/ChildTable/index.mjs.map +1 -1
  57. package/dist/esm/components/ChildTable/index.native.js +8 -2
  58. package/dist/esm/components/ChildTable/index.native.js.map +1 -1
  59. package/dist/esm/components/ColumnHeaderFilter/index.mjs +80 -114
  60. package/dist/esm/components/ColumnHeaderFilter/index.mjs.map +1 -1
  61. package/dist/esm/components/ColumnHeaderFilter/index.native.js +92 -132
  62. package/dist/esm/components/ColumnHeaderFilter/index.native.js.map +1 -1
  63. package/dist/esm/components/DataTable/DataTableRoot.mjs +17 -1
  64. package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
  65. package/dist/esm/components/DataTable/DataTableRoot.native.js +25 -2
  66. package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
  67. package/dist/esm/components/DataTableCell/CellModalEditor.mjs +76 -74
  68. package/dist/esm/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  69. package/dist/esm/components/DataTableCell/index.mjs +3 -3
  70. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  71. package/dist/esm/components/DataTableCell/index.native.js +3 -12
  72. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  73. package/dist/esm/components/DataTableDateFilter/index.mjs +137 -160
  74. package/dist/esm/components/DataTableDateFilter/index.mjs.map +1 -1
  75. package/dist/esm/components/DataTableDateFilter/index.native.js +155 -185
  76. package/dist/esm/components/DataTableDateFilter/index.native.js.map +1 -1
  77. package/dist/esm/components/DataTableExport/index.mjs +124 -141
  78. package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
  79. package/dist/esm/components/DataTableExport/index.native.js +137 -159
  80. package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
  81. package/dist/esm/components/DataTableGrouping/index.mjs +194 -214
  82. package/dist/esm/components/DataTableGrouping/index.mjs.map +1 -1
  83. package/dist/esm/components/DataTableGrouping/index.native.js +211 -232
  84. package/dist/esm/components/DataTableGrouping/index.native.js.map +1 -1
  85. package/dist/esm/components/DataTableSearch/index.mjs +105 -117
  86. package/dist/esm/components/DataTableSearch/index.mjs.map +1 -1
  87. package/dist/esm/components/DataTableSearch/index.native.js +113 -125
  88. package/dist/esm/components/DataTableSearch/index.native.js.map +1 -1
  89. package/dist/esm/components/DataTableToolbar/index.mjs +46 -63
  90. package/dist/esm/components/DataTableToolbar/index.mjs.map +1 -1
  91. package/dist/esm/components/DataTableToolbar/index.native.js +48 -67
  92. package/dist/esm/components/DataTableToolbar/index.native.js.map +1 -1
  93. package/dist/esm/components/DataTableViewOptions/index.mjs +40 -56
  94. package/dist/esm/components/DataTableViewOptions/index.mjs.map +1 -1
  95. package/dist/esm/components/DataTableViewOptions/index.native.js +42 -58
  96. package/dist/esm/components/DataTableViewOptions/index.native.js.map +1 -1
  97. package/dist/esm/components/FilterOperatorValueEditor/index.mjs +3 -11
  98. package/dist/esm/components/FilterOperatorValueEditor/index.mjs.map +1 -1
  99. package/dist/esm/components/FilterOperatorValueEditor/index.native.js +3 -11
  100. package/dist/esm/components/FilterOperatorValueEditor/index.native.js.map +1 -1
  101. package/dist/esm/components/SimpleTable/index.mjs +3 -7
  102. package/dist/esm/components/SimpleTable/index.mjs.map +1 -1
  103. package/dist/esm/components/SimpleTable/index.native.js +3 -7
  104. package/dist/esm/components/SimpleTable/index.native.js.map +1 -1
  105. package/dist/esm/components/TableInput/RowDetailEditor.mjs +165 -0
  106. package/dist/esm/components/TableInput/RowDetailEditor.mjs.map +1 -0
  107. package/dist/esm/components/TableInput/RowDetailEditor.native.js +144 -0
  108. package/dist/esm/components/TableInput/RowDetailEditor.native.js.map +1 -0
  109. package/dist/esm/components/TableInput/index.mjs +42 -5
  110. package/dist/esm/components/TableInput/index.mjs.map +1 -1
  111. package/dist/esm/components/TableInput/index.native.js +64 -1
  112. package/dist/esm/components/TableInput/index.native.js.map +1 -1
  113. package/dist/esm/components/TableInput/shared.mjs +7 -1
  114. package/dist/esm/components/TableInput/shared.mjs.map +1 -1
  115. package/dist/esm/components/TableInput/shared.native.js +9 -1
  116. package/dist/esm/components/TableInput/shared.native.js.map +1 -1
  117. package/dist/esm/hooks/useDataTable.mjs +11 -1
  118. package/dist/esm/hooks/useDataTable.mjs.map +1 -1
  119. package/dist/esm/hooks/useDataTable.native.js +12 -2
  120. package/dist/esm/hooks/useDataTable.native.js.map +1 -1
  121. package/dist/esm/index.mjs.map +1 -1
  122. package/dist/esm/index.native.js.map +1 -1
  123. package/dist/esm/utils/rowPress.mjs +4 -1
  124. package/dist/esm/utils/rowPress.mjs.map +1 -1
  125. package/dist/esm/utils/rowPress.native.js +4 -1
  126. package/dist/esm/utils/rowPress.native.js.map +1 -1
  127. package/dist/jsx/components/ChildTable/index.mjs +8 -2
  128. package/dist/jsx/components/ChildTable/index.mjs.map +1 -1
  129. package/dist/jsx/components/ChildTable/index.native.js +8 -2
  130. package/dist/jsx/components/ChildTable/index.native.js.map +1 -1
  131. package/dist/jsx/components/ColumnHeaderFilter/index.mjs +80 -114
  132. package/dist/jsx/components/ColumnHeaderFilter/index.mjs.map +1 -1
  133. package/dist/jsx/components/ColumnHeaderFilter/index.native.js +89 -129
  134. package/dist/jsx/components/ColumnHeaderFilter/index.native.js.map +1 -1
  135. package/dist/jsx/components/DataTable/DataTableRoot.mjs +17 -1
  136. package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
  137. package/dist/jsx/components/DataTable/DataTableRoot.native.js +25 -2
  138. package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
  139. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs +76 -74
  140. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  141. package/dist/jsx/components/DataTableCell/index.mjs +3 -3
  142. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  143. package/dist/jsx/components/DataTableCell/index.native.js +2 -11
  144. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  145. package/dist/jsx/components/DataTableDateFilter/index.mjs +137 -160
  146. package/dist/jsx/components/DataTableDateFilter/index.mjs.map +1 -1
  147. package/dist/jsx/components/DataTableDateFilter/index.native.js +153 -183
  148. package/dist/jsx/components/DataTableDateFilter/index.native.js.map +1 -1
  149. package/dist/jsx/components/DataTableExport/index.mjs +124 -141
  150. package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
  151. package/dist/jsx/components/DataTableExport/index.native.js +135 -157
  152. package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
  153. package/dist/jsx/components/DataTableGrouping/index.mjs +194 -214
  154. package/dist/jsx/components/DataTableGrouping/index.mjs.map +1 -1
  155. package/dist/jsx/components/DataTableGrouping/index.native.js +209 -230
  156. package/dist/jsx/components/DataTableGrouping/index.native.js.map +1 -1
  157. package/dist/jsx/components/DataTableSearch/index.mjs +105 -117
  158. package/dist/jsx/components/DataTableSearch/index.mjs.map +1 -1
  159. package/dist/jsx/components/DataTableSearch/index.native.js +111 -123
  160. package/dist/jsx/components/DataTableSearch/index.native.js.map +1 -1
  161. package/dist/jsx/components/DataTableToolbar/index.mjs +46 -63
  162. package/dist/jsx/components/DataTableToolbar/index.mjs.map +1 -1
  163. package/dist/jsx/components/DataTableToolbar/index.native.js +46 -65
  164. package/dist/jsx/components/DataTableToolbar/index.native.js.map +1 -1
  165. package/dist/jsx/components/DataTableViewOptions/index.mjs +40 -56
  166. package/dist/jsx/components/DataTableViewOptions/index.mjs.map +1 -1
  167. package/dist/jsx/components/DataTableViewOptions/index.native.js +40 -56
  168. package/dist/jsx/components/DataTableViewOptions/index.native.js.map +1 -1
  169. package/dist/jsx/components/FilterOperatorValueEditor/index.mjs +3 -11
  170. package/dist/jsx/components/FilterOperatorValueEditor/index.mjs.map +1 -1
  171. package/dist/jsx/components/FilterOperatorValueEditor/index.native.js +3 -11
  172. package/dist/jsx/components/FilterOperatorValueEditor/index.native.js.map +1 -1
  173. package/dist/jsx/components/SimpleTable/index.mjs +3 -7
  174. package/dist/jsx/components/SimpleTable/index.mjs.map +1 -1
  175. package/dist/jsx/components/SimpleTable/index.native.js +2 -6
  176. package/dist/jsx/components/SimpleTable/index.native.js.map +1 -1
  177. package/dist/jsx/components/TableInput/RowDetailEditor.mjs +165 -0
  178. package/dist/jsx/components/TableInput/RowDetailEditor.mjs.map +1 -0
  179. package/dist/jsx/components/TableInput/RowDetailEditor.native.js +172 -0
  180. package/dist/jsx/components/TableInput/RowDetailEditor.native.js.map +1 -0
  181. package/dist/jsx/components/TableInput/index.mjs +42 -5
  182. package/dist/jsx/components/TableInput/index.mjs.map +1 -1
  183. package/dist/jsx/components/TableInput/index.native.js +64 -1
  184. package/dist/jsx/components/TableInput/index.native.js.map +1 -1
  185. package/dist/jsx/components/TableInput/shared.mjs +7 -1
  186. package/dist/jsx/components/TableInput/shared.mjs.map +1 -1
  187. package/dist/jsx/components/TableInput/shared.native.js +10 -0
  188. package/dist/jsx/components/TableInput/shared.native.js.map +1 -1
  189. package/dist/jsx/hooks/useDataTable.mjs +11 -1
  190. package/dist/jsx/hooks/useDataTable.mjs.map +1 -1
  191. package/dist/jsx/hooks/useDataTable.native.js +12 -2
  192. package/dist/jsx/hooks/useDataTable.native.js.map +1 -1
  193. package/dist/jsx/index.js.map +1 -1
  194. package/dist/jsx/index.mjs.map +1 -1
  195. package/dist/jsx/index.native.js.map +1 -1
  196. package/dist/jsx/utils/rowPress.mjs +4 -1
  197. package/dist/jsx/utils/rowPress.mjs.map +1 -1
  198. package/dist/jsx/utils/rowPress.native.js +4 -1
  199. package/dist/jsx/utils/rowPress.native.js.map +1 -1
  200. package/package.json +14 -14
  201. package/src/components/ChildTable/childTable.cellHidden.spec.tsx +57 -0
  202. package/src/components/ChildTable/childTable.rowDetail.spec.tsx +120 -0
  203. package/src/components/ChildTable/index.tsx +16 -0
  204. package/src/components/ColumnHeaderFilter/index.tsx +89 -116
  205. package/src/components/DataTable/DataTable.stories.tsx +71 -6
  206. package/src/components/DataTable/DataTableRoot.tsx +27 -4
  207. package/src/components/DataTable/dataTableMobileCards.spec.tsx +1 -4
  208. package/src/components/DataTable/headerContainment.spec.tsx +91 -0
  209. package/src/components/DataTable/rowPress.spec.tsx +2 -6
  210. package/src/components/DataTableBulkActions/DataTableBulkActions.stories.tsx +5 -4
  211. package/src/components/DataTableCell/CellModalEditor.tsx +95 -75
  212. package/src/components/DataTableCell/cellDraftPolicy.spec.tsx +12 -2
  213. package/src/components/DataTableCell/cellModalEditor.spec.tsx +75 -49
  214. package/src/components/DataTableCell/dataTableCell.spec.tsx +31 -14
  215. package/src/components/DataTableCell/index.tsx +3 -2
  216. package/src/components/DataTableDateFilter/DataTableDateFilter.spec.tsx +6 -3
  217. package/src/components/DataTableDateFilter/index.tsx +151 -180
  218. package/src/components/DataTableExport/index.tsx +135 -142
  219. package/src/components/DataTableGrouping/index.tsx +193 -195
  220. package/src/components/DataTableSearch/index.tsx +111 -109
  221. package/src/components/DataTableToolbar/index.tsx +39 -46
  222. package/src/components/DataTableViewOptions/DataTableViewOptions.stories.tsx +52 -0
  223. package/src/components/DataTableViewOptions/index.tsx +32 -40
  224. package/src/components/FilterOperatorValueEditor/FilterOperatorValueEditor.spec.tsx +64 -2
  225. package/src/components/FilterOperatorValueEditor/index.tsx +0 -8
  226. package/src/components/SimpleTable/index.tsx +3 -2
  227. package/src/components/SimpleTable/simpleTable.spec.tsx +15 -0
  228. package/src/components/Table/accessibility.spec.tsx +7 -19
  229. package/src/components/TableInput/RowDetailEditor.native.tsx +134 -0
  230. package/src/components/TableInput/RowDetailEditor.tsx +173 -0
  231. package/src/components/TableInput/index.native.tsx +49 -1
  232. package/src/components/TableInput/index.tsx +65 -10
  233. package/src/components/TableInput/shared.ts +29 -0
  234. package/src/components/tableDropdowns.spec.tsx +172 -0
  235. package/src/hooks/useDataTable.spec.ts +39 -0
  236. package/src/hooks/useDataTable.ts +15 -1
  237. package/src/index.ts +1 -0
  238. package/src/utils/rowPress.ts +3 -0
  239. package/types/components/ChildTable/index.d.ts +10 -0
  240. package/types/components/ChildTable/index.d.ts.map +1 -1
  241. package/types/components/ColumnHeaderFilter/index.d.ts +1 -1
  242. package/types/components/ColumnHeaderFilter/index.d.ts.map +1 -1
  243. package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
  244. package/types/components/DataTableCell/CellModalEditor.d.ts +10 -7
  245. package/types/components/DataTableCell/CellModalEditor.d.ts.map +1 -1
  246. package/types/components/DataTableCell/index.d.ts.map +1 -1
  247. package/types/components/DataTableDateFilter/index.d.ts.map +1 -1
  248. package/types/components/DataTableGrouping/index.d.ts.map +1 -1
  249. package/types/components/DataTableSearch/index.d.ts.map +1 -1
  250. package/types/components/FilterOperatorValueEditor/index.d.ts.map +1 -1
  251. package/types/components/SimpleTable/index.d.ts.map +1 -1
  252. package/types/components/TableInput/RowDetailEditor.d.ts +24 -0
  253. package/types/components/TableInput/RowDetailEditor.d.ts.map +1 -0
  254. package/types/components/TableInput/RowDetailEditor.native.d.ts +11 -0
  255. package/types/components/TableInput/RowDetailEditor.native.d.ts.map +1 -0
  256. package/types/components/TableInput/index.d.ts +11 -2
  257. package/types/components/TableInput/index.d.ts.map +1 -1
  258. package/types/components/TableInput/index.native.d.ts +8 -1
  259. package/types/components/TableInput/index.native.d.ts.map +1 -1
  260. package/types/components/TableInput/shared.d.ts +11 -0
  261. package/types/components/TableInput/shared.d.ts.map +1 -1
  262. package/types/hooks/useDataTable.d.ts.map +1 -1
  263. package/types/index.d.ts +1 -0
  264. package/types/index.d.ts.map +1 -1
  265. package/types/utils/rowPress.d.ts.map +1 -1
@@ -14,6 +14,7 @@ import {
14
14
  ensureFocusVisibleRing,
15
15
  useResolvedKnobs,
16
16
  type DisabledRecipe,
17
+ transitionProps,
17
18
  } from "@multiplatform.one/theme";
18
19
  import { useCallback, useState, type ReactNode } from "react";
19
20
  import { useTranslation } from "react-i18next";
@@ -331,7 +332,7 @@ function SimpleTableMobileCards({
331
332
  const tableZebraOn = knobProps.tableZebra === "on";
332
333
  const [titleCol, ...detailCols] = columns;
333
334
  const typeSize = knobProps.label.fontSize;
334
- const motion = knobProps.transition ? { transition: knobProps.transition } : {};
335
+ const motion = transitionProps(knobProps.transition);
335
336
 
336
337
  if (data.length === 0) {
337
338
  return (
@@ -478,7 +479,7 @@ export function SimpleTableDesktop({
478
479
  const tableZebraOn = knobProps.tableZebra === "on";
479
480
  const ariaLabel = caption ? undefined : props["aria-label"];
480
481
  const typeSize = knobProps.label.fontSize;
481
- const motion = knobProps.transition ? { transition: knobProps.transition } : {};
482
+ const motion = transitionProps(knobProps.transition);
482
483
  const pad = cellPad(knobProps.gap.gap);
483
484
  const frameBorder = knobProps.surface.borderWidth;
484
485
 
@@ -417,4 +417,19 @@ describe("SimpleTable", () => {
417
417
  fireEvent.click(container.querySelector("tbody tr, [role='row']") as Element);
418
418
  expect(onRowPress).not.toHaveBeenCalled();
419
419
  });
420
+ it("survives the animation knob flipping live from none to bouncy (MPO-327)", () => {
421
+ const columns: SimpleTableColumn[] = [{ accessorKey: "name", header: "Name" }];
422
+ const data = [{ name: "Alice" }, { name: "Bob" }];
423
+ const Flip = ({ animation }: { animation: "none" | "bouncy" }) => (
424
+ <Preset overrides={{ animation }}>
425
+ <SimpleTableDesktop data={data} columns={columns} onRowPress={() => {}} />
426
+ </Preset>
427
+ );
428
+ const { container, rerender } = renderWithProviders(<Flip animation="none" />);
429
+ expect(container.querySelector("table")?.getAttribute("data-animation")).toBe("none");
430
+
431
+ expect(() => rerender(<Flip animation="bouncy" />)).not.toThrow();
432
+ expect(container.querySelector("table")?.getAttribute("data-animation")).toBe("on");
433
+ expect(container.textContent).toContain("Bob");
434
+ });
420
435
  });
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { renderWithProviders, fireEvent } from "@multiplatform.one/test-utils";
11
+ import { waitFor } from "@testing-library/react";
11
12
  import { describe, expect, it, vi } from "vitest";
12
13
  import { Table } from "@multiplatform.one/table-primitives";
13
14
  import { useTableKeyboard } from "../../hooks/useTableKeyboard";
@@ -111,14 +112,7 @@ describe("Table Accessibility", () => {
111
112
  expect(true).toBe(true);
112
113
  });
113
114
 
114
- it("Escape key behavior: Tamagui Popover handles Escape natively", () => {
115
- // Tamagui's Popover component handles Escape key natively to close.
116
- // We verify the popover can be opened (controlled state) and that
117
- // the Escape key handler exists in the DOM event chain.
118
- //
119
- // Note: In happy-dom test environment, Tamagui popover portals may
120
- // not fully render, so we test the controlled open/close pattern.
121
-
115
+ it("Escape closes the column filter panel", async () => {
122
116
  const onFilterChange = vi.fn();
123
117
 
124
118
  const { container } = renderWithProviders(
@@ -130,22 +124,16 @@ describe("Table Accessibility", () => {
130
124
  />,
131
125
  );
132
126
 
133
- // The filter trigger should be present and clickable
134
127
  const trigger = container.querySelector('[aria-label="Filter column Status"]');
135
128
  expect(trigger).not.toBeNull();
136
129
 
137
- // Click to open the popover
138
130
  fireEvent.click(trigger!);
131
+ const panel = () => document.querySelector('[data-testid="floating-panel-viewport"]');
132
+ await waitFor(() => expect(panel()).not.toBeNull());
139
133
 
140
- // The popover content should now be controlled by internal state.
141
- // In happy-dom, the Popover.Content may or may not portal into the DOM.
142
- // The key verification is that the component structure supports Escape:
143
- // ColumnHeaderFilter uses Tamagui Popover with controlled open/onOpenChange,
144
- // which natively handles Escape key to call onOpenChange(false).
145
- //
146
- // We verify the pattern is correct by checking the trigger exists
147
- // and the component renders without error after click interaction.
148
- expect(trigger).not.toBeNull();
134
+ fireEvent.keyDown(document.activeElement ?? document.body, { key: "Escape" });
135
+ await waitFor(() => expect(panel()).toBeNull());
136
+ expect(onFilterChange).not.toHaveBeenCalled();
149
137
  });
150
138
 
151
139
  it("semantic HTML and ARIA roles on table primitives", () => {
@@ -0,0 +1,134 @@
1
+ /**
2
+ * RowDetailEditor (native) — the row form as a FloatingPanel bottom sheet,
3
+ * the same dialog/sheet split CellModalEditor.native makes. Props, staging
4
+ * and footer match ./RowDetailEditor.tsx; backdrop tap and Android back
5
+ * commit like "Done".
6
+ */
7
+
8
+ import { Button, FloatingPanel } from "@multiplatform.one/forms";
9
+ import { useResolvedKnobs } from "@multiplatform.one/theme";
10
+ import { PencilSimpleIcon } from "@phosphor-icons/react";
11
+ import { useCallback, useRef, useState } from "react";
12
+ import { Text, View, XStack, YStack } from "tamagui";
13
+ import type { DataTableData, DataTableField, DataTableFieldProps } from "../../types";
14
+ import { t } from "../../shared/t";
15
+ import { withInterp } from "../../utils/withInterp";
16
+ import { DataTableCell } from "../DataTableCell";
17
+ import type { RowDetailEditorProps } from "./RowDetailEditor";
18
+ import { changedColumns, columnKey } from "./shared";
19
+
20
+ export type { RowDetailEditorProps } from "./RowDetailEditor";
21
+
22
+ export function RowDetailEditor<TData extends DataTableData>({
23
+ row,
24
+ rowIndex,
25
+ columns,
26
+ readOnly,
27
+ compact,
28
+ isCellHidden,
29
+ onCommit,
30
+ }: RowDetailEditorProps<TData>) {
31
+ const { knobProps } = useResolvedKnobs({ compact });
32
+ const [draft, setDraft] = useState<TData | null>(null);
33
+ const draftRef = useRef(draft);
34
+ draftRef.current = draft;
35
+ const rowRef = useRef(row);
36
+ rowRef.current = row;
37
+ const onCommitRef = useRef(onCommit);
38
+ onCommitRef.current = onCommit;
39
+ const discardRef = useRef(false);
40
+ const rowNumber = rowIndex + 1;
41
+
42
+ const handleOpenChange = useCallback(
43
+ (next: boolean) => {
44
+ if (next) {
45
+ discardRef.current = false;
46
+ const seeded = { ...rowRef.current };
47
+ draftRef.current = seeded;
48
+ setDraft(seeded);
49
+ return;
50
+ }
51
+ const active = draftRef.current;
52
+ if (!discardRef.current && !readOnly && active) {
53
+ const changed = changedColumns(rowRef.current, active, columns);
54
+ if (changed.length > 0) onCommitRef.current(active, changed);
55
+ }
56
+ discardRef.current = false;
57
+ draftRef.current = null;
58
+ setDraft(null);
59
+ },
60
+ [columns, readOnly],
61
+ );
62
+
63
+ const handleCancel = useCallback(() => {
64
+ discardRef.current = true;
65
+ handleOpenChange(false);
66
+ }, [handleOpenChange]);
67
+
68
+ const setDraftValue = useCallback((columnId: string, value: unknown) => {
69
+ const current = draftRef.current;
70
+ if (!current) return;
71
+ const next = { ...current, [columnId]: value } as TData;
72
+ draftRef.current = next;
73
+ setDraft(next);
74
+ }, []);
75
+
76
+ const triggerLabel = withInterp(
77
+ t(readOnly ? "View row {{row}}" : "Edit row {{row}}", { row: rowNumber }),
78
+ { row: rowNumber },
79
+ );
80
+ const current = draft ?? row;
81
+
82
+ return (
83
+ <FloatingPanel
84
+ open={draft !== null}
85
+ onOpenChange={handleOpenChange}
86
+ scrollable
87
+ triggerSizing="content"
88
+ triggerA11y={{ label: triggerLabel }}
89
+ trigger={
90
+ <View alignItems="center" justifyContent="center">
91
+ <PencilSimpleIcon size={16} />
92
+ </View>
93
+ }
94
+ >
95
+ <YStack {...knobProps.gapLg}>
96
+ <Text {...knobProps.heading}>
97
+ {withInterp(t("Row {{row}}", { row: rowNumber }), { row: rowNumber })}
98
+ </Text>
99
+ {columns.map((col) => {
100
+ const columnId = columnKey(col);
101
+ if (isCellHidden?.(current, columnId, rowIndex)) return null;
102
+ const label = typeof col.header === "string" ? col.header : columnId;
103
+ return (
104
+ <YStack key={columnId} {...knobProps.gap}>
105
+ <Text {...knobProps.label}>{label}</Text>
106
+ <DataTableCell
107
+ value={current[columnId as keyof TData]}
108
+ onChange={(value) => setDraftValue(columnId, value)}
109
+ field={(col.field || "input") as DataTableField}
110
+ fieldProps={col.fieldProps as DataTableFieldProps | undefined}
111
+ readOnly={readOnly}
112
+ compact={false}
113
+ columnId={columnId}
114
+ columnLabel={label}
115
+ rowIndex={rowIndex}
116
+ rowData={current}
117
+ />
118
+ </YStack>
119
+ );
120
+ })}
121
+ <XStack {...knobProps.gap} justifyContent="flex-end">
122
+ {readOnly ? (
123
+ <Button onPress={handleCancel}>{t("Close")}</Button>
124
+ ) : (
125
+ <>
126
+ <Button onPress={handleCancel}>{t("Cancel")}</Button>
127
+ <Button onPress={() => handleOpenChange(false)}>{t("Done")}</Button>
128
+ </>
129
+ )}
130
+ </XStack>
131
+ </YStack>
132
+ </FloatingPanel>
133
+ );
134
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * RowDetailEditor — the child-table row opened as a form (Frappe's grid
3
+ * row "Edit" form). The grid shows the list-view columns; the detail editor
4
+ * shows every `columns` entry for one row, full-size, in a dialog.
5
+ *
6
+ * Same staging contract as CellModalEditor (UX-023): the fields edit a draft
7
+ * copy of the row seeded on open. "Done" and an outside click commit the
8
+ * draft once; "Cancel" and Escape discard it. A read-only row opens for
9
+ * viewing with a single "Close".
10
+ */
11
+
12
+ import { DialogContent, DialogOverlay } from "@multiplatform.one/components";
13
+ import { Button } from "@multiplatform.one/forms";
14
+ import { TableCellContext } from "@multiplatform.one/table-primitives";
15
+ import { useResolvedKnobs } from "@multiplatform.one/theme";
16
+ import { PencilSimpleIcon } from "@phosphor-icons/react";
17
+ import { useCallback, useRef, useState } from "react";
18
+ import { Dialog, ScrollView, Text, XStack, YStack } from "tamagui";
19
+ import type {
20
+ DataTableColumn,
21
+ DataTableData,
22
+ DataTableField,
23
+ DataTableFieldProps,
24
+ } from "../../types";
25
+ import { t } from "../../shared/t";
26
+ import { withInterp } from "../../utils/withInterp";
27
+ import { DataTableCell } from "../DataTableCell";
28
+ import { changedColumns, columnKey, type ChildCellHidden } from "./shared";
29
+
30
+ export interface RowDetailEditorProps<TData extends DataTableData> {
31
+ row: TData;
32
+ rowIndex: number;
33
+ columns: DataTableColumn<TData>[];
34
+ readOnly: boolean;
35
+ compact: boolean;
36
+ isCellHidden?: ChildCellHidden<TData>;
37
+ /** Receives the edited row once per session, only when a value changed. */
38
+ onCommit: (row: TData, changed: string[]) => void;
39
+ }
40
+
41
+ export function RowDetailEditor<TData extends DataTableData>({
42
+ row,
43
+ rowIndex,
44
+ columns,
45
+ readOnly,
46
+ compact,
47
+ isCellHidden,
48
+ onCommit,
49
+ }: RowDetailEditorProps<TData>) {
50
+ const { knobProps } = useResolvedKnobs({ compact });
51
+ const [draft, setDraft] = useState<TData | null>(null);
52
+ const draftRef = useRef(draft);
53
+ draftRef.current = draft;
54
+ const rowRef = useRef(row);
55
+ rowRef.current = row;
56
+ const onCommitRef = useRef(onCommit);
57
+ onCommitRef.current = onCommit;
58
+ const discardRef = useRef(false);
59
+ const rowNumber = rowIndex + 1;
60
+
61
+ const handleOpenChange = useCallback(
62
+ (next: boolean) => {
63
+ if (next) {
64
+ discardRef.current = false;
65
+ const seeded = { ...rowRef.current };
66
+ draftRef.current = seeded;
67
+ setDraft(seeded);
68
+ return;
69
+ }
70
+ const active = draftRef.current;
71
+ if (!discardRef.current && !readOnly && active) {
72
+ const changed = changedColumns(rowRef.current, active, columns);
73
+ if (changed.length > 0) onCommitRef.current(active, changed);
74
+ }
75
+ discardRef.current = false;
76
+ draftRef.current = null;
77
+ setDraft(null);
78
+ },
79
+ [columns, readOnly],
80
+ );
81
+
82
+ const handleCancel = useCallback(() => {
83
+ discardRef.current = true;
84
+ handleOpenChange(false);
85
+ }, [handleOpenChange]);
86
+
87
+ const setDraftValue = useCallback((columnId: string, value: unknown) => {
88
+ const current = draftRef.current;
89
+ if (!current) return;
90
+ const next = { ...current, [columnId]: value } as TData;
91
+ draftRef.current = next;
92
+ setDraft(next);
93
+ }, []);
94
+
95
+ const triggerLabel = withInterp(
96
+ t(readOnly ? "View row {{row}}" : "Edit row {{row}}", { row: rowNumber }),
97
+ { row: rowNumber },
98
+ );
99
+ const current = draft ?? row;
100
+
101
+ return (
102
+ <>
103
+ <Button
104
+ chromeless
105
+ compact={compact}
106
+ onPress={() => handleOpenChange(true)}
107
+ hitSlop={knobProps.nestedControl.hitSlop}
108
+ aria-label={triggerLabel}
109
+ aria-haspopup="dialog"
110
+ aria-expanded={draft !== null}
111
+ >
112
+ <PencilSimpleIcon size={16} />
113
+ </Button>
114
+ <Dialog open={draft !== null} onOpenChange={handleOpenChange} modal>
115
+ <Dialog.Portal>
116
+ <DialogOverlay opacity={0.5} />
117
+ <DialogContent
118
+ maxWidth={720}
119
+ maxHeight="80vh"
120
+ width="90vw"
121
+ onEscapeKeyDown={() => {
122
+ discardRef.current = true;
123
+ }}
124
+ >
125
+ <Dialog.Title {...knobProps.heading}>
126
+ {withInterp(t("Row {{row}}", { row: rowNumber }), { row: rowNumber })}
127
+ </Dialog.Title>
128
+ <ScrollView flexShrink={1} minHeight={0}>
129
+ <TableCellContext.Provider value={{ inTableCell: false }}>
130
+ <YStack {...knobProps.gapLg}>
131
+ {columns.map((col) => {
132
+ const columnId = columnKey(col);
133
+ if (isCellHidden?.(current, columnId, rowIndex)) return null;
134
+ const label = typeof col.header === "string" ? col.header : columnId;
135
+ return (
136
+ <YStack key={columnId} {...knobProps.gap}>
137
+ <Text {...knobProps.label}>{label}</Text>
138
+ <DataTableCell
139
+ value={current[columnId as keyof TData]}
140
+ onChange={(value) => setDraftValue(columnId, value)}
141
+ field={(col.field || "input") as DataTableField}
142
+ fieldProps={col.fieldProps as DataTableFieldProps | undefined}
143
+ readOnly={readOnly}
144
+ compact={false}
145
+ columnId={columnId}
146
+ columnLabel={label}
147
+ rowIndex={rowIndex}
148
+ rowData={current}
149
+ />
150
+ </YStack>
151
+ );
152
+ })}
153
+ </YStack>
154
+ </TableCellContext.Provider>
155
+ </ScrollView>
156
+ <XStack {...knobProps.gap} justifyContent="flex-end">
157
+ {readOnly ? (
158
+ <Button onPress={handleCancel}>{t("Close")}</Button>
159
+ ) : (
160
+ <>
161
+ <Button onPress={handleCancel}>{t("Cancel")}</Button>
162
+ <Button theme="accent" onPress={() => handleOpenChange(false)}>
163
+ {t("Done")}
164
+ </Button>
165
+ </>
166
+ )}
167
+ </XStack>
168
+ </DialogContent>
169
+ </Dialog.Portal>
170
+ </Dialog>
171
+ </>
172
+ );
173
+ }
@@ -32,7 +32,8 @@ import type {
32
32
  } from "../../types";
33
33
  import { DataTableCell } from "../DataTableCell";
34
34
  import { withInterp } from "../../utils/withInterp";
35
- import { generateRowId, isEmptyRows, SKELETON_ROW_COUNT } from "./shared";
35
+ import { RowDetailEditor } from "./RowDetailEditor";
36
+ import { generateRowId, isEmptyRows, SKELETON_ROW_COUNT, type ChildCellHidden } from "./shared";
36
37
 
37
38
  export interface TableInputProps<TData extends DataTableData = DataTableData> {
38
39
  /** Data array for the table (ignored in favor of form state in the form branch) */
@@ -80,6 +81,12 @@ export interface TableInputProps<TData extends DataTableData = DataTableData> {
80
81
  compact?: boolean;
81
82
  /** Leading grab handle + keyboard reorder via kanbanDnd (web implements). */
82
83
  enableReorder?: boolean;
84
+ /** Per-row button that opens the row in a detail editor (MPO-22). */
85
+ rowDetail?: boolean;
86
+ /** Fields the detail editor shows. Defaults to `columns`. */
87
+ detailColumns?: DataTableColumn<TData>[];
88
+ /** Hides one row's cell, in the grid and in the detail editor. */
89
+ isCellHidden?: ChildCellHidden<TData>;
83
90
  }
84
91
 
85
92
  export function TableInput<TData extends DataTableData = DataTableData>({
@@ -103,6 +110,9 @@ export function TableInput<TData extends DataTableData = DataTableData>({
103
110
  emptyMessage,
104
111
  "aria-label": ariaLabel,
105
112
  compact = true,
113
+ rowDetail = false,
114
+ detailColumns,
115
+ isCellHidden,
106
116
  }: TableInputProps<TData>) {
107
117
  const { t } = useTranslation();
108
118
  const { resolvedForm } = useFormField({ form: formProp });
@@ -138,6 +148,9 @@ export function TableInput<TData extends DataTableData = DataTableData>({
138
148
  emptyMessage: emptyMessage ?? t("No rows"),
139
149
  ariaLabel: resolvedAriaLabel,
140
150
  compact,
151
+ rowDetail,
152
+ detailColumns: detailColumns ?? columns,
153
+ isCellHidden,
141
154
  t,
142
155
  };
143
156
 
@@ -191,6 +204,9 @@ interface TableInputInnerProps<TData extends DataTableData> {
191
204
  emptyMessage: string;
192
205
  ariaLabel: string;
193
206
  compact: boolean;
207
+ rowDetail: boolean;
208
+ detailColumns: DataTableColumn<TData>[];
209
+ isCellHidden?: ChildCellHidden<TData>;
194
210
  t: (key: string, options?: Record<string, unknown>) => string;
195
211
  }
196
212
 
@@ -210,6 +226,9 @@ function TableInputInner<TData extends DataTableData>({
210
226
  emptyMessage,
211
227
  ariaLabel,
212
228
  compact,
229
+ rowDetail,
230
+ detailColumns,
231
+ isCellHidden,
213
232
  t,
214
233
  }: TableInputInnerProps<TData>) {
215
234
  const theme = useTheme();
@@ -219,6 +238,7 @@ function TableInputInner<TData extends DataTableData>({
219
238
  const indexWidth = Math.max(36, chromePx);
220
239
  const actionWidth = chromePx + 16;
221
240
  const [activeRowId, setActiveRowId] = useState<string | null>(null);
241
+ const Detail = RowDetailEditor<TData>;
222
242
 
223
243
  const handleCellChange = useCallback(
224
244
  (rowIndex: number, columnId: string, value: unknown) => {
@@ -250,6 +270,16 @@ function TableInputInner<TData extends DataTableData>({
250
270
  [rows, commit, minRows],
251
271
  );
252
272
 
273
+ const handleRowDetailCommit = useCallback(
274
+ (rowIndex: number, next: TData, changed: string[]) => {
275
+ for (const columnId of changed) {
276
+ onCellChange?.(rowIndex, columnId, next[columnId as keyof TData]);
277
+ }
278
+ commit(rows.map((row, index) => (index === rowIndex ? next : row)));
279
+ },
280
+ [rows, commit, onCellChange],
281
+ );
282
+
253
283
  const canAddRow = !maxRows || rows.length < maxRows;
254
284
  const canDelete = !minRows || rows.length > (minRows || 0);
255
285
 
@@ -277,6 +307,7 @@ function TableInputInner<TData extends DataTableData>({
277
307
  </Text>
278
308
  </View>
279
309
  ))}
310
+ {rowDetail && <View width={actionWidth} />}
280
311
  {showDeleteRow && !readOnly && <View width={actionWidth} />}
281
312
  </XStack>
282
313
  );
@@ -304,6 +335,9 @@ function TableInputInner<TData extends DataTableData>({
304
335
  const field = (col.field || "input") as DataTableField;
305
336
  const fieldProps = col.fieldProps as DataTableFieldProps | undefined;
306
337
 
338
+ if (isCellHidden?.(item, columnId, index)) {
339
+ return <View key={columnId} flex={1} paddingHorizontal="$1" />;
340
+ }
307
341
  return (
308
342
  <View key={columnId} flex={1} paddingHorizontal="$1">
309
343
  <DataTableCell
@@ -321,6 +355,19 @@ function TableInputInner<TData extends DataTableData>({
321
355
  </View>
322
356
  );
323
357
  })}
358
+ {rowDetail && (
359
+ <View width={actionWidth} justifyContent="center" alignItems="center">
360
+ <Detail
361
+ row={item}
362
+ rowIndex={index}
363
+ columns={detailColumns}
364
+ readOnly={readOnly}
365
+ compact={compact}
366
+ isCellHidden={isCellHidden}
367
+ onCommit={(next, changed) => handleRowDetailCommit(index, next, changed)}
368
+ />
369
+ </View>
370
+ )}
324
371
  {showDeleteRow && !readOnly && (
325
372
  <View width={actionWidth} justifyContent="center" alignItems="center">
326
373
  <Button
@@ -355,6 +402,7 @@ function TableInputInner<TData extends DataTableData>({
355
402
  <Skeleton variant="rounded" width="70%" height={16} />
356
403
  </View>
357
404
  ))}
405
+ {rowDetail && <View width={actionWidth} />}
358
406
  {showDeleteRow && !readOnly && <View width={actionWidth} />}
359
407
  </XStack>
360
408
  ))}