@wordpress/dataviews 13.1.1-next.v.202603102151.0 → 14.0.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 (247) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +19 -3
  3. package/build/components/dataform-controls/datetime.cjs +8 -4
  4. package/build/components/dataform-controls/datetime.cjs.map +2 -2
  5. package/build/components/dataform-layouts/card/index.cjs +132 -128
  6. package/build/components/dataform-layouts/card/index.cjs.map +3 -3
  7. package/build/components/dataviews-bulk-actions/index.cjs +28 -5
  8. package/build/components/dataviews-bulk-actions/index.cjs.map +2 -2
  9. package/build/components/dataviews-context/index.cjs +2 -2
  10. package/build/components/dataviews-context/index.cjs.map +2 -2
  11. package/build/components/dataviews-footer/index.cjs +2 -3
  12. package/build/components/dataviews-footer/index.cjs.map +2 -2
  13. package/build/components/dataviews-layout/index.cjs +12 -3
  14. package/build/components/dataviews-layout/index.cjs.map +2 -2
  15. package/build/components/dataviews-layouts/grid/composite-grid.cjs +378 -245
  16. package/build/components/dataviews-layouts/grid/composite-grid.cjs.map +2 -2
  17. package/build/components/dataviews-layouts/index.cjs +3 -3
  18. package/build/components/dataviews-layouts/index.cjs.map +3 -3
  19. package/build/components/dataviews-layouts/picker-grid/index.cjs +76 -32
  20. package/build/components/dataviews-layouts/picker-grid/index.cjs.map +2 -2
  21. package/build/components/dataviews-layouts/picker-table/index.cjs +34 -22
  22. package/build/components/dataviews-layouts/picker-table/index.cjs.map +2 -2
  23. package/build/components/dataviews-layouts/table/index.cjs +97 -88
  24. package/build/components/dataviews-layouts/table/index.cjs.map +2 -2
  25. package/build/components/dataviews-layouts/table/{use-is-horizontal-scroll-end.cjs → use-scroll-state.cjs} +29 -33
  26. package/build/components/dataviews-layouts/table/use-scroll-state.cjs.map +7 -0
  27. package/build/components/dataviews-layouts/utils/density-picker.cjs.map +2 -2
  28. package/build/components/dataviews-layouts/utils/grid-config-options.cjs +45 -0
  29. package/build/components/dataviews-layouts/utils/grid-config-options.cjs.map +7 -0
  30. package/build/components/dataviews-layouts/utils/use-infinite-scroll.cjs +62 -0
  31. package/build/components/dataviews-layouts/utils/use-infinite-scroll.cjs.map +7 -0
  32. package/build/components/dataviews-picker-footer/index.cjs +23 -4
  33. package/build/components/dataviews-picker-footer/index.cjs.map +2 -2
  34. package/build/components/dataviews-search/index.cjs +2 -1
  35. package/build/components/dataviews-search/index.cjs.map +2 -2
  36. package/build/components/dataviews-selection-checkbox/index.cjs +3 -2
  37. package/build/components/dataviews-selection-checkbox/index.cjs.map +2 -2
  38. package/build/components/dataviews-view-config/index.cjs +0 -2
  39. package/build/components/dataviews-view-config/index.cjs.map +3 -3
  40. package/build/components/dataviews-view-config/infinite-scroll-toggle.cjs +0 -3
  41. package/build/components/dataviews-view-config/infinite-scroll-toggle.cjs.map +2 -2
  42. package/build/dataviews/index.cjs +38 -34
  43. package/build/dataviews/index.cjs.map +3 -3
  44. package/build/dataviews-picker/index.cjs +26 -25
  45. package/build/dataviews-picker/index.cjs.map +3 -3
  46. package/build/hooks/index.cjs +11 -2
  47. package/build/hooks/index.cjs.map +2 -2
  48. package/build/hooks/use-data.cjs +146 -9
  49. package/build/hooks/use-data.cjs.map +2 -2
  50. package/build/hooks/use-infinite-scroll.cjs +208 -0
  51. package/build/hooks/use-infinite-scroll.cjs.map +7 -0
  52. package/build/hooks/use-selected-items.cjs +57 -0
  53. package/build/hooks/use-selected-items.cjs.map +7 -0
  54. package/build/types/dataviews.cjs.map +1 -1
  55. package/build/types/field-api.cjs.map +1 -1
  56. package/build/utils/filter-sort-and-paginate.cjs +5 -1
  57. package/build/utils/filter-sort-and-paginate.cjs.map +2 -2
  58. package/build/utils/get-footer-message.cjs +8 -8
  59. package/build/utils/get-footer-message.cjs.map +2 -2
  60. package/build-module/components/dataform-controls/datetime.mjs +8 -4
  61. package/build-module/components/dataform-controls/datetime.mjs.map +2 -2
  62. package/build-module/components/dataform-layouts/card/index.mjs +132 -133
  63. package/build-module/components/dataform-layouts/card/index.mjs.map +2 -2
  64. package/build-module/components/dataviews-bulk-actions/index.mjs +28 -5
  65. package/build-module/components/dataviews-bulk-actions/index.mjs.map +2 -2
  66. package/build-module/components/dataviews-context/index.mjs +2 -2
  67. package/build-module/components/dataviews-context/index.mjs.map +2 -2
  68. package/build-module/components/dataviews-footer/index.mjs +2 -3
  69. package/build-module/components/dataviews-footer/index.mjs.map +2 -2
  70. package/build-module/components/dataviews-layout/index.mjs +12 -3
  71. package/build-module/components/dataviews-layout/index.mjs.map +2 -2
  72. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs +387 -246
  73. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs.map +2 -2
  74. package/build-module/components/dataviews-layouts/index.mjs +3 -3
  75. package/build-module/components/dataviews-layouts/index.mjs.map +2 -2
  76. package/build-module/components/dataviews-layouts/picker-grid/index.mjs +80 -33
  77. package/build-module/components/dataviews-layouts/picker-grid/index.mjs.map +2 -2
  78. package/build-module/components/dataviews-layouts/picker-table/index.mjs +34 -22
  79. package/build-module/components/dataviews-layouts/picker-table/index.mjs.map +2 -2
  80. package/build-module/components/dataviews-layouts/table/index.mjs +97 -88
  81. package/build-module/components/dataviews-layouts/table/index.mjs.map +2 -2
  82. package/build-module/components/dataviews-layouts/table/use-scroll-state.mjs +46 -0
  83. package/build-module/components/dataviews-layouts/table/use-scroll-state.mjs.map +7 -0
  84. package/build-module/components/dataviews-layouts/utils/density-picker.mjs.map +2 -2
  85. package/build-module/components/dataviews-layouts/utils/grid-config-options.mjs +14 -0
  86. package/build-module/components/dataviews-layouts/utils/grid-config-options.mjs.map +7 -0
  87. package/build-module/components/dataviews-layouts/utils/use-infinite-scroll.mjs +26 -0
  88. package/build-module/components/dataviews-layouts/utils/use-infinite-scroll.mjs.map +7 -0
  89. package/build-module/components/dataviews-picker-footer/index.mjs +23 -4
  90. package/build-module/components/dataviews-picker-footer/index.mjs.map +2 -2
  91. package/build-module/components/dataviews-search/index.mjs +2 -1
  92. package/build-module/components/dataviews-search/index.mjs.map +2 -2
  93. package/build-module/components/dataviews-selection-checkbox/index.mjs +3 -2
  94. package/build-module/components/dataviews-selection-checkbox/index.mjs.map +2 -2
  95. package/build-module/components/dataviews-view-config/index.mjs +0 -2
  96. package/build-module/components/dataviews-view-config/index.mjs.map +2 -2
  97. package/build-module/components/dataviews-view-config/infinite-scroll-toggle.mjs +0 -3
  98. package/build-module/components/dataviews-view-config/infinite-scroll-toggle.mjs.map +2 -2
  99. package/build-module/dataviews/index.mjs +46 -36
  100. package/build-module/dataviews/index.mjs.map +2 -2
  101. package/build-module/dataviews-picker/index.mjs +34 -27
  102. package/build-module/dataviews-picker/index.mjs.map +2 -2
  103. package/build-module/hooks/index.mjs +7 -1
  104. package/build-module/hooks/index.mjs.map +2 -2
  105. package/build-module/hooks/use-data.mjs +147 -10
  106. package/build-module/hooks/use-data.mjs.map +2 -2
  107. package/build-module/hooks/use-infinite-scroll.mjs +188 -0
  108. package/build-module/hooks/use-infinite-scroll.mjs.map +7 -0
  109. package/build-module/hooks/use-selected-items.mjs +36 -0
  110. package/build-module/hooks/use-selected-items.mjs.map +7 -0
  111. package/build-module/utils/filter-sort-and-paginate.mjs +5 -1
  112. package/build-module/utils/filter-sort-and-paginate.mjs.map +2 -2
  113. package/build-module/utils/get-footer-message.mjs +8 -8
  114. package/build-module/utils/get-footer-message.mjs.map +2 -2
  115. package/build-style/style-rtl.css +107 -41
  116. package/build-style/style.css +107 -41
  117. package/build-types/components/dataform-controls/datetime.d.ts +1 -1
  118. package/build-types/components/dataform-controls/datetime.d.ts.map +1 -1
  119. package/build-types/components/dataform-layouts/card/index.d.ts.map +1 -1
  120. package/build-types/components/dataviews-bulk-actions/index.d.ts +2 -1
  121. package/build-types/components/dataviews-bulk-actions/index.d.ts.map +1 -1
  122. package/build-types/components/dataviews-context/index.d.ts +1 -1
  123. package/build-types/components/dataviews-context/index.d.ts.map +1 -1
  124. package/build-types/components/dataviews-footer/index.d.ts.map +1 -1
  125. package/build-types/components/dataviews-layout/index.d.ts.map +1 -1
  126. package/build-types/components/dataviews-layouts/grid/composite-grid.d.ts.map +1 -1
  127. package/build-types/components/dataviews-layouts/index.d.ts +3 -3
  128. package/build-types/components/dataviews-layouts/index.d.ts.map +1 -1
  129. package/build-types/components/dataviews-layouts/picker-grid/index.d.ts.map +1 -1
  130. package/build-types/components/dataviews-layouts/picker-table/index.d.ts.map +1 -1
  131. package/build-types/components/dataviews-layouts/table/index.d.ts.map +1 -1
  132. package/build-types/components/dataviews-layouts/table/use-scroll-state.d.ts +25 -0
  133. package/build-types/components/dataviews-layouts/table/use-scroll-state.d.ts.map +1 -0
  134. package/build-types/components/dataviews-layouts/utils/density-picker.d.ts.map +1 -1
  135. package/build-types/components/dataviews-layouts/utils/grid-config-options.d.ts +2 -0
  136. package/build-types/components/dataviews-layouts/utils/grid-config-options.d.ts.map +1 -0
  137. package/build-types/components/dataviews-layouts/utils/use-infinite-scroll.d.ts +22 -0
  138. package/build-types/components/dataviews-layouts/utils/use-infinite-scroll.d.ts.map +1 -0
  139. package/build-types/components/dataviews-picker-footer/index.d.ts.map +1 -1
  140. package/build-types/components/dataviews-search/index.d.ts.map +1 -1
  141. package/build-types/components/dataviews-selection-checkbox/index.d.ts.map +1 -1
  142. package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
  143. package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts +1 -1
  144. package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts.map +1 -1
  145. package/build-types/dataviews/index.d.ts +0 -1
  146. package/build-types/dataviews/index.d.ts.map +1 -1
  147. package/build-types/dataviews/stories/empty.d.ts +1 -2
  148. package/build-types/dataviews/stories/empty.d.ts.map +1 -1
  149. package/build-types/dataviews/stories/fixtures.d.ts.map +1 -1
  150. package/build-types/dataviews/stories/free-composition.d.ts.map +1 -1
  151. package/build-types/dataviews/stories/index.story.d.ts +18 -10
  152. package/build-types/dataviews/stories/index.story.d.ts.map +1 -1
  153. package/build-types/dataviews/stories/infinite-scroll.d.ts.map +1 -1
  154. package/build-types/dataviews/stories/layout-activity.d.ts.map +1 -1
  155. package/build-types/dataviews/stories/layout-custom.d.ts +3 -1
  156. package/build-types/dataviews/stories/layout-custom.d.ts.map +1 -1
  157. package/build-types/dataviews/stories/layout-grid.d.ts.map +1 -1
  158. package/build-types/dataviews/stories/layout-list.d.ts.map +1 -1
  159. package/build-types/dataviews/stories/layout-table.d.ts.map +1 -1
  160. package/build-types/dataviews/stories/with-card.d.ts +3 -1
  161. package/build-types/dataviews/stories/with-card.d.ts.map +1 -1
  162. package/build-types/dataviews-picker/index.d.ts +0 -1
  163. package/build-types/dataviews-picker/index.d.ts.map +1 -1
  164. package/build-types/dataviews-picker/stories/fixtures.d.ts.map +1 -1
  165. package/build-types/dataviews-picker/stories/index.story.d.ts.map +1 -1
  166. package/build-types/field-types/stories/index.story.d.ts.map +1 -1
  167. package/build-types/hooks/index.d.ts +3 -0
  168. package/build-types/hooks/index.d.ts.map +1 -1
  169. package/build-types/hooks/test/use-data.d.ts +2 -0
  170. package/build-types/hooks/test/use-data.d.ts.map +1 -0
  171. package/build-types/hooks/use-data.d.ts +41 -3
  172. package/build-types/hooks/use-data.d.ts.map +1 -1
  173. package/build-types/hooks/use-infinite-scroll.d.ts +21 -0
  174. package/build-types/hooks/use-infinite-scroll.d.ts.map +1 -0
  175. package/build-types/hooks/use-selected-items.d.ts +19 -0
  176. package/build-types/hooks/use-selected-items.d.ts.map +1 -0
  177. package/build-types/types/dataviews.d.ts +15 -1
  178. package/build-types/types/dataviews.d.ts.map +1 -1
  179. package/build-types/types/field-api.d.ts +15 -4
  180. package/build-types/types/field-api.d.ts.map +1 -1
  181. package/build-types/utils/filter-sort-and-paginate.d.ts.map +1 -1
  182. package/build-types/utils/get-footer-message.d.ts +3 -2
  183. package/build-types/utils/get-footer-message.d.ts.map +1 -1
  184. package/build-wp/index.js +3202 -2761
  185. package/package.json +19 -19
  186. package/src/components/dataform-controls/datetime.tsx +19 -11
  187. package/src/components/dataform-layouts/card/index.tsx +171 -146
  188. package/src/components/dataform-layouts/card/style.scss +8 -5
  189. package/src/components/dataviews-bulk-actions/index.tsx +28 -1
  190. package/src/components/dataviews-context/index.ts +2 -2
  191. package/src/components/dataviews-footer/index.tsx +1 -6
  192. package/src/components/dataviews-layout/index.tsx +41 -19
  193. package/src/components/dataviews-layout/style.scss +8 -0
  194. package/src/components/dataviews-layouts/grid/composite-grid.tsx +433 -278
  195. package/src/components/dataviews-layouts/grid/style.scss +22 -2
  196. package/src/components/dataviews-layouts/index.ts +3 -3
  197. package/src/components/dataviews-layouts/picker-grid/index.tsx +70 -17
  198. package/src/components/dataviews-layouts/picker-grid/style.scss +10 -0
  199. package/src/components/dataviews-layouts/picker-table/index.tsx +42 -22
  200. package/src/components/dataviews-layouts/table/index.tsx +10 -4
  201. package/src/components/dataviews-layouts/table/style.scss +13 -0
  202. package/src/components/dataviews-layouts/table/use-scroll-state.ts +79 -0
  203. package/src/components/dataviews-layouts/utils/density-picker.tsx +12 -2
  204. package/src/components/dataviews-layouts/utils/grid-config-options.tsx +14 -0
  205. package/src/components/dataviews-layouts/utils/grid-items.scss +9 -1
  206. package/src/components/dataviews-layouts/utils/use-infinite-scroll.ts +64 -0
  207. package/src/components/dataviews-picker-footer/index.tsx +21 -1
  208. package/src/components/dataviews-search/index.tsx +2 -1
  209. package/src/components/dataviews-selection-checkbox/index.tsx +4 -2
  210. package/src/components/dataviews-view-config/index.tsx +0 -2
  211. package/src/components/dataviews-view-config/infinite-scroll-toggle.tsx +0 -5
  212. package/src/dataviews/index.tsx +58 -45
  213. package/src/dataviews/stories/empty.tsx +1 -3
  214. package/src/dataviews/stories/fixtures.tsx +288 -0
  215. package/src/dataviews/stories/free-composition.tsx +44 -45
  216. package/src/dataviews/stories/index.story.tsx +31 -8
  217. package/src/dataviews/stories/infinite-scroll.tsx +7 -93
  218. package/src/dataviews/stories/layout-activity.tsx +1 -0
  219. package/src/dataviews/stories/layout-custom.tsx +7 -3
  220. package/src/dataviews/stories/layout-grid.tsx +1 -0
  221. package/src/dataviews/stories/layout-list.tsx +1 -0
  222. package/src/dataviews/stories/layout-table.tsx +1 -0
  223. package/src/dataviews/stories/style.css +0 -5
  224. package/src/dataviews/stories/with-card.tsx +35 -24
  225. package/src/dataviews/style.scss +5 -8
  226. package/src/dataviews/test/dataviews.tsx +54 -1
  227. package/src/dataviews-picker/index.tsx +41 -35
  228. package/src/dataviews-picker/stories/fixtures.tsx +270 -0
  229. package/src/dataviews-picker/stories/index.story.tsx +62 -129
  230. package/src/field-types/stories/index.story.tsx +12 -0
  231. package/src/hooks/index.ts +3 -0
  232. package/src/hooks/test/use-data.ts +791 -0
  233. package/src/hooks/use-data.ts +288 -21
  234. package/src/hooks/use-infinite-scroll.ts +304 -0
  235. package/src/hooks/use-selected-items.ts +72 -0
  236. package/src/style.scss +1 -0
  237. package/src/types/dataviews.ts +18 -1
  238. package/src/types/field-api.ts +16 -3
  239. package/src/utils/filter-sort-and-paginate.ts +13 -1
  240. package/src/utils/get-footer-message.ts +12 -9
  241. package/src/utils/test/filter-sort-and-paginate.js +78 -54
  242. package/build/components/dataviews-layouts/table/use-is-horizontal-scroll-end.cjs.map +0 -7
  243. package/build-module/components/dataviews-layouts/table/use-is-horizontal-scroll-end.mjs +0 -50
  244. package/build-module/components/dataviews-layouts/table/use-is-horizontal-scroll-end.mjs.map +0 -7
  245. package/build-types/components/dataviews-layouts/table/use-is-horizontal-scroll-end.d.ts +0 -19
  246. package/build-types/components/dataviews-layouts/table/use-is-horizontal-scroll-end.d.ts.map +0 -1
  247. package/src/components/dataviews-layouts/table/use-is-horizontal-scroll-end.ts +0 -82
@@ -0,0 +1,791 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { renderHook, act } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import useData from '../use-data';
10
+ import type { View } from '../../types';
11
+
12
+ type TestItem = { id: number; name?: string };
13
+
14
+ const getItemId = ( item: TestItem ) => String( item.id );
15
+ const defaultPaginationInfo = { totalItems: 100, totalPages: 10 };
16
+
17
+ describe( 'useData', () => {
18
+ describe( 'when infinite scroll is disabled', () => {
19
+ it( 'returns provided data unchanged', () => {
20
+ const data: TestItem[] = [
21
+ { id: 1, name: 'Item 1' },
22
+ { id: 2, name: 'Item 2' },
23
+ ];
24
+ const view = {
25
+ type: 'table',
26
+ infiniteScrollEnabled: false,
27
+ } as View;
28
+
29
+ const { result } = renderHook( () =>
30
+ useData( {
31
+ view,
32
+ data,
33
+ getItemId,
34
+ paginationInfo: defaultPaginationInfo,
35
+ } )
36
+ );
37
+
38
+ expect( result.current.data ).toEqual( data );
39
+ expect( result.current.setVisibleEntries ).toBeUndefined();
40
+ } );
41
+
42
+ it( 'returns empty array when data is empty', () => {
43
+ const data: TestItem[] = [];
44
+ const view = {
45
+ type: 'table',
46
+ infiniteScrollEnabled: false,
47
+ } as View;
48
+
49
+ const { result } = renderHook( () =>
50
+ useData( {
51
+ view,
52
+ data,
53
+ getItemId,
54
+ paginationInfo: defaultPaginationInfo,
55
+ } )
56
+ );
57
+
58
+ expect( result.current.data ).toEqual( [] );
59
+ } );
60
+ } );
61
+
62
+ describe( 'when infinite scroll is enabled', () => {
63
+ describe( 'initial load', () => {
64
+ it( 'assigns positions starting from 1 by default', () => {
65
+ const data: TestItem[] = [
66
+ { id: 1, name: 'Item 1' },
67
+ { id: 2, name: 'Item 2' },
68
+ ];
69
+ const view = {
70
+ type: 'table',
71
+ infiniteScrollEnabled: true,
72
+ } as View;
73
+
74
+ const { result } = renderHook( () =>
75
+ useData( {
76
+ view,
77
+ data,
78
+ getItemId,
79
+ paginationInfo: defaultPaginationInfo,
80
+ } )
81
+ );
82
+
83
+ expect( result.current.data ).toHaveLength( 2 );
84
+ expect(
85
+ (
86
+ result.current.data[ 0 ] as TestItem & {
87
+ position: number;
88
+ }
89
+ ).position
90
+ ).toBe( 1 );
91
+ expect(
92
+ (
93
+ result.current.data[ 1 ] as TestItem & {
94
+ position: number;
95
+ }
96
+ ).position
97
+ ).toBe( 2 );
98
+ } );
99
+
100
+ it( 'assigns positions starting from view.startPosition when provided', () => {
101
+ const data: TestItem[] = [
102
+ { id: 1, name: 'Item 1' },
103
+ { id: 2, name: 'Item 2' },
104
+ ];
105
+ const view = {
106
+ type: 'table',
107
+ infiniteScrollEnabled: true,
108
+ startPosition: 5,
109
+ } as View;
110
+
111
+ const { result } = renderHook( () =>
112
+ useData( {
113
+ view,
114
+ data,
115
+ getItemId,
116
+ paginationInfo: defaultPaginationInfo,
117
+ } )
118
+ );
119
+
120
+ expect(
121
+ (
122
+ result.current.data[ 1 ] as TestItem & {
123
+ position: number;
124
+ }
125
+ ).position
126
+ ).toBe( 6 );
127
+ } );
128
+
129
+ it( 'returns setVisibleEntries callback', () => {
130
+ const data: TestItem[] = [ { id: 1 } ];
131
+ const view = {
132
+ type: 'table',
133
+ infiniteScrollEnabled: true,
134
+ } as View;
135
+
136
+ const { result } = renderHook( () =>
137
+ useData( {
138
+ view,
139
+ data,
140
+ getItemId,
141
+ paginationInfo: defaultPaginationInfo,
142
+ } )
143
+ );
144
+
145
+ expect( result.current.setVisibleEntries ).toBeDefined();
146
+ expect( typeof result.current.setVisibleEntries ).toBe(
147
+ 'function'
148
+ );
149
+ } );
150
+ } );
151
+
152
+ describe( 'loading more data when scrolling down', () => {
153
+ it( 'appends new items with positions after existing items', () => {
154
+ const initialData: TestItem[] = [
155
+ { id: 1, name: 'Item 1' },
156
+ { id: 2, name: 'Item 2' },
157
+ ];
158
+ const initialView = {
159
+ type: 'table',
160
+ infiniteScrollEnabled: true,
161
+ startPosition: 1,
162
+ } as View;
163
+
164
+ const { result, rerender } = renderHook(
165
+ ( { view, data } ) =>
166
+ useData( {
167
+ view,
168
+ data,
169
+ getItemId,
170
+ paginationInfo: defaultPaginationInfo,
171
+ } ),
172
+ { initialProps: { view: initialView, data: initialData } }
173
+ );
174
+
175
+ expect( result.current.data ).toHaveLength( 2 );
176
+
177
+ // Simulate scroll down - new page of data
178
+ const newData: TestItem[] = [
179
+ { id: 3, name: 'Item 3' },
180
+ { id: 4, name: 'Item 4' },
181
+ ];
182
+ const newView = {
183
+ ...initialView,
184
+ startPosition: 3,
185
+ } as View;
186
+
187
+ rerender( { view: newView, data: newData } );
188
+
189
+ // Should have all 4 items
190
+ expect( result.current.data ).toHaveLength( 4 );
191
+ // Items should be sorted by position
192
+ const positions = result.current.data.map(
193
+ ( d: TestItem & { position?: number } ) =>
194
+ ( d as TestItem & { position: number } ).position
195
+ );
196
+ expect( positions ).toEqual( [ 1, 2, 3, 4 ] );
197
+ } );
198
+
199
+ it( 'preserves positions for existing items', () => {
200
+ const initialData: TestItem[] = [ { id: 1 }, { id: 2 } ];
201
+ const initialView = {
202
+ type: 'table',
203
+ infiniteScrollEnabled: true,
204
+ startPosition: 1,
205
+ } as View;
206
+
207
+ const { result, rerender } = renderHook(
208
+ ( { view, data } ) =>
209
+ useData( {
210
+ view,
211
+ data,
212
+ getItemId,
213
+ paginationInfo: defaultPaginationInfo,
214
+ } ),
215
+ { initialProps: { view: initialView, data: initialData } }
216
+ );
217
+
218
+ // Scroll down with overlapping data
219
+ const newData: TestItem[] = [
220
+ { id: 2 }, // Already exists
221
+ { id: 3 }, // New
222
+ ];
223
+ const newView = { ...initialView, startPosition: 2 } as View;
224
+
225
+ rerender( { view: newView, data: newData } );
226
+
227
+ // Item 2 should keep its original position (2)
228
+ const item2 = result.current.data.find(
229
+ ( d: TestItem & { position?: number } ) => d.id === 2
230
+ ) as TestItem & { position: number };
231
+ expect( item2.position ).toBe( 2 );
232
+
233
+ // Item 3 should get position 3
234
+ const item3 = result.current.data.find(
235
+ ( d: TestItem & { position?: number } ) => d.id === 3
236
+ ) as TestItem & { position: number };
237
+ expect( item3.position ).toBe( 3 );
238
+ } );
239
+ } );
240
+
241
+ describe( 'loading more data when scrolling up', () => {
242
+ it( 'prepends new items with positions before existing items', () => {
243
+ // Start with items at positions 5, 6
244
+ const initialData: TestItem[] = [
245
+ { id: 5, name: 'Item 5' },
246
+ { id: 6, name: 'Item 6' },
247
+ ];
248
+ const initialView = {
249
+ type: 'table',
250
+ infiniteScrollEnabled: true,
251
+ startPosition: 5,
252
+ } as View;
253
+
254
+ const { result, rerender } = renderHook(
255
+ ( { view, data } ) =>
256
+ useData( {
257
+ view,
258
+ data,
259
+ getItemId,
260
+ paginationInfo: defaultPaginationInfo,
261
+ } ),
262
+ { initialProps: { view: initialView, data: initialData } }
263
+ );
264
+
265
+ expect( result.current.data ).toHaveLength( 2 );
266
+
267
+ // Simulate scroll up - load earlier data
268
+ const newData: TestItem[] = [
269
+ { id: 3, name: 'Item 3' },
270
+ { id: 4, name: 'Item 4' },
271
+ ];
272
+ const newView = {
273
+ ...initialView,
274
+ startPosition: 3, // Scrolling up
275
+ } as View;
276
+
277
+ rerender( { view: newView, data: newData } );
278
+
279
+ // Should have all 4 items
280
+ expect( result.current.data ).toHaveLength( 4 );
281
+ // Items should be sorted by position (ascending)
282
+ const ids = result.current.data.map(
283
+ ( d: TestItem & { position?: number } ) => d.id
284
+ );
285
+ expect( ids ).toEqual( [ 3, 4, 5, 6 ] );
286
+ } );
287
+ } );
288
+
289
+ describe( 'deduplication', () => {
290
+ it( 'removes duplicates when new data overlaps with existing data', () => {
291
+ const initialData: TestItem[] = [
292
+ { id: 1 },
293
+ { id: 2 },
294
+ { id: 3 },
295
+ ];
296
+ const initialView = {
297
+ type: 'table',
298
+ infiniteScrollEnabled: true,
299
+ startPosition: 1,
300
+ } as View;
301
+
302
+ const { result, rerender } = renderHook(
303
+ ( { view, data } ) =>
304
+ useData( {
305
+ view,
306
+ data,
307
+ getItemId,
308
+ paginationInfo: defaultPaginationInfo,
309
+ } ),
310
+ { initialProps: { view: initialView, data: initialData } }
311
+ );
312
+
313
+ // New data with overlapping items
314
+ const newData: TestItem[] = [ { id: 2 }, { id: 3 }, { id: 4 } ];
315
+ const newView = { ...initialView, startPosition: 2 } as View;
316
+
317
+ rerender( { view: newView, data: newData } );
318
+
319
+ // Should not have duplicate IDs
320
+ const ids = result.current.data.map(
321
+ ( d: TestItem & { position?: number } ) => d.id
322
+ );
323
+ const uniqueIds = [ ...new Set( ids ) ];
324
+ expect( ids ).toEqual( uniqueIds );
325
+ } );
326
+ } );
327
+
328
+ describe( 'buffer and unloading', () => {
329
+ it( 'keeps selected items even when outside visible buffer', () => {
330
+ const initialData: TestItem[] = Array.from(
331
+ { length: 30 },
332
+ ( _, i ) => ( { id: i + 1 } )
333
+ );
334
+ const initialView = {
335
+ type: 'table',
336
+ infiniteScrollEnabled: true,
337
+ startPosition: 1,
338
+ } as View;
339
+
340
+ const { result, rerender } = renderHook(
341
+ ( { view, data, selection } ) =>
342
+ useData( {
343
+ view,
344
+ data,
345
+ getItemId,
346
+ selection,
347
+ paginationInfo: defaultPaginationInfo,
348
+ } ),
349
+ {
350
+ initialProps: {
351
+ view: initialView,
352
+ data: initialData,
353
+ selection: [ '5' ],
354
+ },
355
+ }
356
+ );
357
+
358
+ act( () => {
359
+ result.current.setVisibleEntries?.( [ 50, 51 ] );
360
+ } );
361
+
362
+ const newData: TestItem[] = Array.from(
363
+ { length: 10 },
364
+ ( _, i ) => ( { id: 31 + i } )
365
+ );
366
+ const newView = { ...initialView, startPosition: 31 } as View;
367
+
368
+ rerender( {
369
+ view: newView,
370
+ data: newData,
371
+ selection: [ '5' ],
372
+ } );
373
+
374
+ expect(
375
+ result.current.data.some(
376
+ ( item: TestItem & { position?: number } ) =>
377
+ item.id === 5
378
+ )
379
+ ).toBe( true );
380
+ } );
381
+
382
+ it( 'keeps items within buffer range of visible entries', () => {
383
+ // Create a large dataset
384
+ const initialData: TestItem[] = Array.from(
385
+ { length: 30 },
386
+ ( _, i ) => ( { id: i + 1 } )
387
+ );
388
+ const initialView = {
389
+ type: 'table',
390
+ infiniteScrollEnabled: true,
391
+ startPosition: 1,
392
+ } as View;
393
+
394
+ const { result, rerender } = renderHook(
395
+ ( { view, data } ) =>
396
+ useData( {
397
+ view,
398
+ data,
399
+ getItemId,
400
+ paginationInfo: defaultPaginationInfo,
401
+ } ),
402
+ { initialProps: { view: initialView, data: initialData } }
403
+ );
404
+
405
+ expect( result.current.data ).toHaveLength( 30 );
406
+
407
+ // Set visible entries to middle of the list
408
+ act( () => {
409
+ result.current.setVisibleEntries?.( [ 15, 16, 17, 18 ] );
410
+ } );
411
+
412
+ // Scroll down with new data to trigger buffer logic
413
+ const newData: TestItem[] = Array.from(
414
+ { length: 10 },
415
+ ( _, i ) => ( { id: 31 + i } )
416
+ );
417
+ const newView = { ...initialView, startPosition: 31 } as View;
418
+
419
+ rerender( { view: newView, data: newData } );
420
+
421
+ // Items far above visible range should be trimmed (buffer of 20)
422
+ // Visible min is 15, so items below position (15 - 20 = -5) should be removed
423
+ // Since all items have positive positions, none should be removed in this case
424
+ // when scrolling down
425
+ const positions = result.current.data.map(
426
+ ( d: TestItem & { position?: number } ) =>
427
+ ( d as TestItem & { position: number } ).position
428
+ );
429
+ const minPosition = Math.min( ...positions );
430
+
431
+ // When scrolling down, items above visible range minus buffer should be trimmed
432
+ // visibleMin - buffer = 15 - 20 = -5, so all items >= -5 are kept
433
+ expect( minPosition ).toBeGreaterThanOrEqual( -5 );
434
+ } );
435
+
436
+ it( 'trims items from the end when scrolling up', () => {
437
+ // Start with items at high positions
438
+ const initialData: TestItem[] = Array.from(
439
+ { length: 30 },
440
+ ( _, i ) => ( { id: i + 50 } )
441
+ );
442
+ const initialView = {
443
+ type: 'table',
444
+ infiniteScrollEnabled: true,
445
+ startPosition: 50,
446
+ } as View;
447
+
448
+ const { result, rerender } = renderHook(
449
+ ( { view, data } ) =>
450
+ useData( {
451
+ view,
452
+ data,
453
+ getItemId,
454
+ paginationInfo: defaultPaginationInfo,
455
+ } ),
456
+ { initialProps: { view: initialView, data: initialData } }
457
+ );
458
+
459
+ // Set visible entries
460
+ act( () => {
461
+ result.current.setVisibleEntries?.( [ 55, 56, 57, 58 ] );
462
+ } );
463
+
464
+ // Scroll up with new data
465
+ const newData: TestItem[] = Array.from(
466
+ { length: 10 },
467
+ ( _, i ) => ( {
468
+ id: i + 40,
469
+ } )
470
+ );
471
+ const newView = { ...initialView, startPosition: 40 } as View;
472
+
473
+ rerender( { view: newView, data: newData } );
474
+
475
+ // When scrolling up, items below visible range + buffer should be trimmed
476
+ // visibleMax + buffer = 58 + 20 = 78
477
+ const positions = result.current.data.map(
478
+ ( d: TestItem & { position?: number } ) =>
479
+ ( d as TestItem & { position: number } ).position
480
+ );
481
+ const maxPosition = Math.max( ...positions );
482
+
483
+ expect( maxPosition ).toBeLessThanOrEqual( 78 );
484
+ } );
485
+ } );
486
+
487
+ describe( 'view changes', () => {
488
+ it( 'resets data when search changes', () => {
489
+ const initialData: TestItem[] = [ { id: 1 }, { id: 2 } ];
490
+ const initialView = {
491
+ type: 'table',
492
+ infiniteScrollEnabled: true,
493
+ startPosition: 1,
494
+ search: '',
495
+ } as View;
496
+
497
+ const { result, rerender } = renderHook(
498
+ ( { view, data } ) =>
499
+ useData( {
500
+ view,
501
+ data,
502
+ getItemId,
503
+ paginationInfo: defaultPaginationInfo,
504
+ } ),
505
+ { initialProps: { view: initialView, data: initialData } }
506
+ );
507
+
508
+ expect( result.current.data ).toHaveLength( 2 );
509
+
510
+ // Change search - data should be replaced, not appended
511
+ const newData: TestItem[] = [ { id: 3 } ];
512
+ const newView = { ...initialView, search: 'test' } as View;
513
+
514
+ rerender( { view: newView, data: newData } );
515
+
516
+ // Data should be reset to just the new search results
517
+ expect( result.current.data ).toHaveLength( 1 );
518
+ expect( result.current.data[ 0 ].id ).toBe( 3 );
519
+ } );
520
+
521
+ it( 'resets data when filters change', () => {
522
+ const initialData: TestItem[] = [ { id: 1 }, { id: 2 } ];
523
+ const initialView = {
524
+ type: 'table',
525
+ infiniteScrollEnabled: true,
526
+ startPosition: 1,
527
+ filters: [],
528
+ } as View;
529
+
530
+ const { result, rerender } = renderHook(
531
+ ( { view, data } ) =>
532
+ useData( {
533
+ view,
534
+ data,
535
+ getItemId,
536
+ paginationInfo: defaultPaginationInfo,
537
+ } ),
538
+ { initialProps: { view: initialView, data: initialData } }
539
+ );
540
+
541
+ expect( result.current.data ).toHaveLength( 2 );
542
+
543
+ // Change filters - data should be replaced
544
+ const newData: TestItem[] = [ { id: 5 } ];
545
+ const newView = {
546
+ ...initialView,
547
+ filters: [
548
+ { field: 'status', operator: 'is', value: 'published' },
549
+ ],
550
+ } as View;
551
+
552
+ rerender( { view: newView, data: newData } );
553
+
554
+ expect( result.current.data ).toHaveLength( 1 );
555
+ expect( result.current.data[ 0 ].id ).toBe( 5 );
556
+ } );
557
+
558
+ it( 'resets data when perPage changes', () => {
559
+ const initialData: TestItem[] = [ { id: 1 }, { id: 2 } ];
560
+ const initialView = {
561
+ type: 'table',
562
+ infiniteScrollEnabled: true,
563
+ startPosition: 1,
564
+ perPage: 10,
565
+ } as View;
566
+
567
+ const { result, rerender } = renderHook(
568
+ ( { view, data } ) =>
569
+ useData( {
570
+ view,
571
+ data,
572
+ getItemId,
573
+ paginationInfo: defaultPaginationInfo,
574
+ } ),
575
+ { initialProps: { view: initialView, data: initialData } }
576
+ );
577
+
578
+ expect( result.current.data ).toHaveLength( 2 );
579
+
580
+ // Change perPage - data should be replaced
581
+ const newData: TestItem[] = [
582
+ { id: 1 },
583
+ { id: 2 },
584
+ { id: 3 },
585
+ { id: 4 },
586
+ { id: 5 },
587
+ ];
588
+ const newView = { ...initialView, perPage: 25 } as View;
589
+
590
+ rerender( { view: newView, data: newData } );
591
+
592
+ expect( result.current.data ).toHaveLength( 5 );
593
+ } );
594
+
595
+ it( 'handles transition from infinite scroll disabled to enabled', () => {
596
+ const data: TestItem[] = [ { id: 1 }, { id: 2 } ];
597
+ const initialView = {
598
+ type: 'table',
599
+ infiniteScrollEnabled: false,
600
+ } as View;
601
+
602
+ const { result, rerender } = renderHook(
603
+ ( { view, passedData } ) =>
604
+ useData( {
605
+ view,
606
+ data: passedData,
607
+ getItemId,
608
+ paginationInfo: defaultPaginationInfo,
609
+ } ),
610
+ { initialProps: { view: initialView, passedData: data } }
611
+ );
612
+
613
+ expect( result.current.setVisibleEntries ).toBeUndefined();
614
+
615
+ // Enable infinite scroll
616
+ const newView = {
617
+ ...initialView,
618
+ infiniteScrollEnabled: true,
619
+ startPosition: 1,
620
+ } as View;
621
+
622
+ rerender( { view: newView, passedData: data } );
623
+
624
+ expect( result.current.setVisibleEntries ).toBeDefined();
625
+ expect( result.current.data ).toHaveLength( 2 );
626
+ } );
627
+
628
+ it( 'returns all data when clearing search after filtering', () => {
629
+ // This tests the scenario where:
630
+ // 1. User has full data list
631
+ // 2. User searches and gets fewer results
632
+ // 3. Visible entries are set for the filtered results
633
+ // 4. User clears search
634
+ // 5. Full data should be returned, not limited by stale visible entries
635
+ const fullData: TestItem[] = Array.from(
636
+ { length: 25 },
637
+ ( _, i ) => ( { id: i + 1 } )
638
+ );
639
+ const initialView = {
640
+ type: 'table',
641
+ infiniteScrollEnabled: true,
642
+ startPosition: 1,
643
+ search: '',
644
+ } as View;
645
+
646
+ const { result, rerender } = renderHook(
647
+ ( { view, data } ) =>
648
+ useData( {
649
+ view,
650
+ data,
651
+ getItemId,
652
+ paginationInfo: defaultPaginationInfo,
653
+ } ),
654
+ { initialProps: { view: initialView, data: fullData } }
655
+ );
656
+
657
+ // Simulate visible entries being set for the 6 items
658
+ act( () => {
659
+ result.current.setVisibleEntries?.( [ 1, 2, 3, 4, 5, 6 ] );
660
+ } );
661
+ const clearedView = { ...initialView, search: '' } as View;
662
+ rerender( { view: clearedView, data: fullData } );
663
+
664
+ // Should return all 25 items, not limited by stale visible entries
665
+ expect( result.current.data ).toHaveLength( 25 );
666
+ } );
667
+
668
+ it( 'returns all data when changing search term', () => {
669
+ const initialView = {
670
+ type: 'table',
671
+ infiniteScrollEnabled: true,
672
+ startPosition: 1,
673
+ search: 'foo',
674
+ } as View;
675
+ const fooResults: TestItem[] = [
676
+ { id: 1 },
677
+ { id: 2 },
678
+ { id: 3 },
679
+ ];
680
+
681
+ const { result, rerender } = renderHook(
682
+ ( { view, data } ) =>
683
+ useData( {
684
+ view,
685
+ data,
686
+ getItemId,
687
+ paginationInfo: defaultPaginationInfo,
688
+ } ),
689
+ { initialProps: { view: initialView, data: fooResults } }
690
+ );
691
+
692
+ expect( result.current.data ).toHaveLength( 3 );
693
+
694
+ // Set visible entries for the 3 items
695
+ act( () => {
696
+ result.current.setVisibleEntries?.( [ 1, 2, 3 ] );
697
+ } );
698
+
699
+ // Change to a different search with more results
700
+ const barResults: TestItem[] = Array.from(
701
+ { length: 15 },
702
+ ( _, i ) => ( { id: i + 10 } )
703
+ );
704
+ const barView = { ...initialView, search: 'bar' } as View;
705
+ rerender( { view: barView, data: barResults } );
706
+
707
+ // Should return all 15 items, not limited to 3
708
+ expect( result.current.data ).toHaveLength( 15 );
709
+ } );
710
+
711
+ it( 'handles single item', () => {
712
+ const data: TestItem[] = [ { id: 1 } ];
713
+ const view = {
714
+ type: 'table',
715
+ infiniteScrollEnabled: true,
716
+ startPosition: 1,
717
+ } as View;
718
+
719
+ const { result } = renderHook( () =>
720
+ useData( {
721
+ view,
722
+ data,
723
+ getItemId,
724
+ paginationInfo: defaultPaginationInfo,
725
+ } )
726
+ );
727
+
728
+ expect( result.current.data ).toHaveLength( 1 );
729
+ expect(
730
+ (
731
+ result.current.data[ 0 ] as TestItem & {
732
+ position: number;
733
+ }
734
+ ).position
735
+ ).toBe( 1 );
736
+ } );
737
+
738
+ it( 'maintains correct order after multiple scroll direction changes', () => {
739
+ const initialData: TestItem[] = [
740
+ { id: 5 },
741
+ { id: 6 },
742
+ { id: 7 },
743
+ ];
744
+ const initialView = {
745
+ type: 'table',
746
+ infiniteScrollEnabled: true,
747
+ startPosition: 5,
748
+ } as View;
749
+
750
+ const { result, rerender } = renderHook(
751
+ ( { view, data } ) =>
752
+ useData( {
753
+ view,
754
+ data,
755
+ getItemId,
756
+ paginationInfo: defaultPaginationInfo,
757
+ } ),
758
+ { initialProps: { view: initialView, data: initialData } }
759
+ );
760
+
761
+ // Scroll down
762
+ rerender( {
763
+ view: { ...initialView, startPosition: 8 } as View,
764
+ data: [ { id: 8 }, { id: 9 } ],
765
+ } );
766
+
767
+ // Scroll up
768
+ rerender( {
769
+ view: { ...initialView, startPosition: 3 } as View,
770
+ data: [ { id: 3 }, { id: 4 } ],
771
+ } );
772
+
773
+ // Scroll down again
774
+ rerender( {
775
+ view: { ...initialView, startPosition: 10 } as View,
776
+ data: [ { id: 10 } ],
777
+ } );
778
+
779
+ // All items should be in ascending order by position
780
+ const positions = result.current.data.map(
781
+ ( d: TestItem & { position?: number } ) =>
782
+ ( d as TestItem & { position: number } ).position
783
+ );
784
+ const sortedPositions = [ ...positions ].sort(
785
+ ( a, b ) => a - b
786
+ );
787
+ expect( positions ).toEqual( sortedPositions );
788
+ } );
789
+ } );
790
+ } );
791
+ } );