@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
@@ -9,7 +9,7 @@
9
9
  padding: 0 $grid-unit-30 $grid-unit-30;
10
10
  display: flex;
11
11
  flex-direction: column;
12
- gap: $grid-unit-40;
12
+ gap: $grid-unit-30;
13
13
  container-type: inline-size;
14
14
  margin-bottom: auto;
15
15
 
@@ -17,9 +17,25 @@
17
17
  transition: padding ease-out 0.1s;
18
18
  }
19
19
 
20
+ &.has-compact-density {
21
+ gap: $grid-unit-20;
22
+
23
+ .dataviews-view-grid__row {
24
+ gap: $grid-unit-20;
25
+ }
26
+ }
27
+
28
+ &.has-comfortable-density {
29
+ gap: $grid-unit-40;
30
+
31
+ .dataviews-view-grid__row {
32
+ gap: $grid-unit-40;
33
+ }
34
+ }
35
+
20
36
  .dataviews-view-grid__row {
21
37
  display: grid;
22
- gap: $grid-unit-40;
38
+ gap: $grid-unit-30;
23
39
 
24
40
  .dataviews-view-grid__row__gridcell {
25
41
  border-radius: $radius-medium;
@@ -41,6 +57,10 @@
41
57
  }
42
58
  }
43
59
  }
60
+ }
61
+
62
+ .dataviews-view-grid,
63
+ .dataviews-view-grid-infinite-scroll {
44
64
 
45
65
  .dataviews-view-grid__card {
46
66
  height: 100%;
@@ -27,8 +27,8 @@ import {
27
27
  LAYOUT_PICKER_GRID,
28
28
  LAYOUT_PICKER_TABLE,
29
29
  } from '../../constants';
30
- import PreviewSizePicker from './utils/preview-size-picker';
31
30
  import DensityPicker from './utils/density-picker';
31
+ import GridConfigOptions from './utils/grid-config-options';
32
32
 
33
33
  export const VIEW_LAYOUTS = [
34
34
  {
@@ -43,7 +43,7 @@ export const VIEW_LAYOUTS = [
43
43
  label: __( 'Grid' ),
44
44
  component: ViewGrid,
45
45
  icon: category,
46
- viewConfigOptions: PreviewSizePicker,
46
+ viewConfigOptions: GridConfigOptions,
47
47
  },
48
48
  {
49
49
  type: LAYOUT_LIST,
@@ -64,7 +64,7 @@ export const VIEW_LAYOUTS = [
64
64
  label: __( 'Grid' ),
65
65
  component: ViewPickerGrid,
66
66
  icon: category,
67
- viewConfigOptions: PreviewSizePicker,
67
+ viewConfigOptions: GridConfigOptions,
68
68
  isPicker: true,
69
69
  },
70
70
  {
@@ -16,7 +16,7 @@ import {
16
16
  } from '@wordpress/components';
17
17
  import { __, sprintf } from '@wordpress/i18n';
18
18
  import { useInstanceId } from '@wordpress/compose';
19
- import { useContext } from '@wordpress/element';
19
+ import { useContext, useRef } from '@wordpress/element';
20
20
  import { Stack } from '@wordpress/ui';
21
21
 
22
22
  /**
@@ -35,6 +35,11 @@ import type { SetSelection } from '../../../types/private';
35
35
  import { GridItems } from '../utils/grid-items';
36
36
  const { Badge } = unlock( componentsPrivateApis );
37
37
  import getDataByGroup from '../utils/get-data-by-group';
38
+ import { useGridColumns } from '../grid/preview-size-picker';
39
+ import {
40
+ useIntersectionObserver,
41
+ usePlaceholdersNeeded,
42
+ } from '../utils/use-infinite-scroll';
38
43
 
39
44
  interface GridItemProps< Item > {
40
45
  view: ViewPickerGridType;
@@ -73,7 +78,16 @@ function GridItem< Item >( {
73
78
  }: GridItemProps< Item > ) {
74
79
  const { showTitle = true, showMedia = true, showDescription = true } = view;
75
80
  const id = getItemId( item );
81
+ const elementRef = useRef< HTMLElement | null >( null );
82
+
76
83
  const isSelected = selection.includes( id );
84
+
85
+ const setElementRef = ( element: HTMLElement | null ) => {
86
+ elementRef.current = element;
87
+ };
88
+
89
+ useIntersectionObserver( elementRef, posinset );
90
+
77
91
  const renderedMediaField = mediaField?.render ? (
78
92
  <mediaField.render
79
93
  item={ item }
@@ -88,6 +102,7 @@ function GridItem< Item >( {
88
102
 
89
103
  return (
90
104
  <Composite.Item
105
+ ref={ setElementRef }
91
106
  aria-label={
92
107
  titleField
93
108
  ? titleField.getValue( { item } ) || __( '(no title)' )
@@ -105,6 +120,7 @@ function GridItem< Item >( {
105
120
  } ) }
106
121
  aria-selected={ isSelected }
107
122
  onClick={ () => {
123
+ // Toggle in/out of selection array
108
124
  if ( isSelected ) {
109
125
  onChangeSelection(
110
126
  selection.filter( ( itemId ) => id !== itemId )
@@ -318,12 +334,21 @@ function ViewPickerGrid< Item >( {
318
334
  : null;
319
335
  const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
320
336
 
321
- const isInfiniteScroll = view.infiniteScrollEnabled && ! dataByGroup;
337
+ const isInfiniteScroll =
338
+ ( view.infiniteScrollEnabled && ! dataByGroup ) ?? false;
322
339
 
323
340
  const currentPage = view?.page ?? 1;
324
341
  const perPage = view?.perPage ?? 0;
325
342
  const setSize = isInfiniteScroll ? paginationInfo?.totalItems : undefined;
326
343
 
344
+ // Calculate placeholders needed for infinite scroll
345
+ const gridColumns = useGridColumns();
346
+ const placeholdersNeeded = usePlaceholdersNeeded(
347
+ data,
348
+ isInfiniteScroll,
349
+ gridColumns
350
+ );
351
+
327
352
  return (
328
353
  <>
329
354
  {
@@ -336,7 +361,14 @@ function ViewPickerGrid< Item >( {
336
361
  aria-multiselectable={ isMultiselect }
337
362
  className={ clsx(
338
363
  'dataviews-view-picker-grid',
339
- className
364
+ className,
365
+ {
366
+ [ `has-${ view.layout?.density }-density` ]:
367
+ view.layout?.density &&
368
+ [ 'compact', 'comfortable' ].includes(
369
+ view.layout.density
370
+ ),
371
+ }
340
372
  ) }
341
373
  aria-label={ itemListLabel }
342
374
  render={ ( { children, ...props } ) => (
@@ -371,10 +403,12 @@ function ViewPickerGrid< Item >( {
371
403
  }
372
404
  >
373
405
  { groupItems.map( ( item ) => {
406
+ // Use position from item if available (infinite scroll), otherwise calculate.
374
407
  const posInSet =
408
+ ( item as any ).position ??
375
409
  ( currentPage - 1 ) * perPage +
376
- data.indexOf( item ) +
377
- 1;
410
+ data.indexOf( item ) +
411
+ 1;
378
412
  return (
379
413
  <GridItem
380
414
  key={ getItemId( item ) }
@@ -421,7 +455,15 @@ function ViewPickerGrid< Item >( {
421
455
  <GridItems
422
456
  className={ clsx(
423
457
  'dataviews-view-picker-grid',
424
- className
458
+ className,
459
+ {
460
+ [ `has-${ view.layout?.density }-density` ]:
461
+ view.layout?.density &&
462
+ [
463
+ 'compact',
464
+ 'comfortable',
465
+ ].includes( view.layout.density ),
466
+ }
425
467
  ) }
426
468
  previewSize={ usedPreviewSize }
427
469
  aria-busy={ isLoading }
@@ -436,17 +478,28 @@ function ViewPickerGrid< Item >( {
436
478
  aria-multiselectable={ isMultiselect }
437
479
  aria-label={ itemListLabel }
438
480
  >
439
- { data.map( ( item, index ) => {
440
- let posinset = isInfiniteScroll
441
- ? index + 1
442
- : undefined;
443
-
444
- if ( ! isInfiniteScroll ) {
445
- // When infinite scroll isn't active, take pagination into account
446
- // when calculating the posinset.
447
- posinset =
448
- ( currentPage - 1 ) * perPage + index + 1;
449
- }
481
+ { /* Render placeholders for unloaded items in first row */ }
482
+ { Array.from( { length: placeholdersNeeded } ).map(
483
+ ( _, index ) => (
484
+ <Composite.Item
485
+ key={ `placeholder-${ index }` }
486
+ render={ ( { children, ...props } ) => (
487
+ <Stack
488
+ direction="column"
489
+ children={ children }
490
+ { ...props }
491
+ />
492
+ ) }
493
+ role="option"
494
+ aria-hidden
495
+ tabIndex={ -1 }
496
+ className="dataviews-view-picker-grid__card dataviews-view-picker-grid__placeholder"
497
+ />
498
+ )
499
+ ) }
500
+ { data.map( ( item ) => {
501
+ // Use position from item for accessibility in infinite scroll mode.
502
+ const posinset = ( item as any ).position;
450
503
 
451
504
  return (
452
505
  <GridItem
@@ -5,6 +5,16 @@
5
5
  @use "../utils/grid-items.scss" as *;
6
6
 
7
7
  .dataviews-view-picker-grid {
8
+ // When grouped, the density class is on the parent Composite,
9
+ // and the gap is on child .dataviews-view-grid-items elements.
10
+ &.has-compact-density .dataviews-view-grid-items {
11
+ gap: $grid-unit-20;
12
+ }
13
+
14
+ &.has-comfortable-density .dataviews-view-grid-items {
15
+ gap: $grid-unit-40;
16
+ }
17
+
8
18
  .dataviews-view-picker-grid__card {
9
19
  height: 100%;
10
20
  justify-content: flex-start;
@@ -33,6 +33,7 @@ import type { SetSelection } from '../../../types/private';
33
33
  import ColumnHeaderMenu from '../table/column-header-menu';
34
34
  import ColumnPrimary from '../table/column-primary';
35
35
  import getDataByGroup from '../utils/get-data-by-group';
36
+ import { useIntersectionObserver } from '../utils/use-infinite-scroll';
36
37
 
37
38
  interface TableColumnFieldProps< Item > {
38
39
  fields: NormalizedField< Item >[];
@@ -95,8 +96,17 @@ function TableRow< Item >( {
95
96
  posinset,
96
97
  }: TableRowProps< Item > ) {
97
98
  const { paginationInfo } = useContext( DataViewsContext );
99
+
98
100
  const isSelected = selection.includes( id );
101
+
99
102
  const [ isHovered, setIsHovered ] = useState( false );
103
+ const elementRef = useRef< HTMLElement | null >( null );
104
+
105
+ const setElementRef = ( element: HTMLElement | null ) => {
106
+ elementRef.current = element;
107
+ };
108
+
109
+ useIntersectionObserver( elementRef, posinset );
100
110
  const {
101
111
  showTitle = true,
102
112
  showMedia = true,
@@ -119,6 +129,7 @@ function TableRow< Item >( {
119
129
  return (
120
130
  <Composite.Item
121
131
  key={ id }
132
+ ref={ setElementRef }
122
133
  render={ ( { children, ...props } ) => (
123
134
  <tr
124
135
  className={ clsx( 'dataviews-view-table__row', {
@@ -136,6 +147,7 @@ function TableRow< Item >( {
136
147
  aria-posinset={ posinset }
137
148
  role={ infiniteScrollEnabled ? 'article' : 'option' }
138
149
  onClick={ () => {
150
+ // Toggle in/out of selection array
139
151
  if ( isSelected ) {
140
152
  onChangeSelection(
141
153
  selection.filter( ( itemId ) => id !== itemId )
@@ -236,6 +248,12 @@ function ViewPickerTable< Item >( {
236
248
  }
237
249
  } );
238
250
 
251
+ const groupField = view.groupBy?.field
252
+ ? fields.find( ( f ) => f.id === view.groupBy?.field )
253
+ : null;
254
+ const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
255
+ const isInfiniteScroll = view.infiniteScrollEnabled && ! dataByGroup;
256
+
239
257
  const tableNoticeId = useId();
240
258
 
241
259
  if ( nextHeaderMenuToFocus ) {
@@ -264,10 +282,6 @@ function ViewPickerTable< Item >( {
264
282
  ( field ) => field.id === view.descriptionField
265
283
  );
266
284
 
267
- const groupField = view.groupBy?.field
268
- ? fields.find( ( f ) => f.id === view.groupBy?.field )
269
- : null;
270
- const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
271
285
  const { showTitle = true, showMedia = true, showDescription = true } = view;
272
286
  const hasPrimaryColumn =
273
287
  ( titleField && showTitle ) ||
@@ -285,7 +299,6 @@ function ViewPickerTable< Item >( {
285
299
  headerMenuRefs.current.delete( column );
286
300
  }
287
301
  };
288
- const isInfiniteScroll = view.infiniteScrollEnabled && ! dataByGroup;
289
302
 
290
303
  return (
291
304
  <>
@@ -319,6 +332,7 @@ function ViewPickerTable< Item >( {
319
332
  data={ data }
320
333
  actions={ actions }
321
334
  getItemId={ getItemId }
335
+ disableSelectAll={ isInfiniteScroll }
322
336
  />
323
337
  ) }
324
338
  </th>
@@ -441,23 +455,29 @@ function ViewPickerTable< Item >( {
441
455
  orientation="vertical"
442
456
  >
443
457
  { hasData &&
444
- data.map( ( item, index ) => (
445
- <TableRow
446
- key={ getItemId( item ) }
447
- item={ item }
448
- fields={ fields }
449
- id={ getItemId( item ) || index.toString() }
450
- view={ view }
451
- titleField={ titleField }
452
- mediaField={ mediaField }
453
- descriptionField={ descriptionField }
454
- selection={ selection }
455
- getItemId={ getItemId }
456
- onChangeSelection={ onChangeSelection }
457
- multiselect={ isMultiselect }
458
- posinset={ index + 1 }
459
- />
460
- ) ) }
458
+ data.map( ( item, index ) => {
459
+ const itemId = getItemId( item );
460
+ // Use position from item for accessibility in infinite scroll mode.
461
+ const posinset = ( item as any ).position;
462
+
463
+ return (
464
+ <TableRow
465
+ key={ itemId }
466
+ item={ item }
467
+ fields={ fields }
468
+ id={ itemId || index.toString() }
469
+ view={ view }
470
+ titleField={ titleField }
471
+ mediaField={ mediaField }
472
+ descriptionField={ descriptionField }
473
+ selection={ selection }
474
+ getItemId={ getItemId }
475
+ onChangeSelection={ onChangeSelection }
476
+ multiselect={ isMultiselect }
477
+ posinset={ posinset }
478
+ />
479
+ );
480
+ } ) }
461
481
  </Composite>
462
482
  ) }
463
483
  </table>
@@ -39,7 +39,7 @@ import type {
39
39
  import type { SetSelection } from '../../../types/private';
40
40
  import ColumnHeaderMenu from './column-header-menu';
41
41
  import ColumnPrimary from './column-primary';
42
- import { useIsHorizontalScrollEnd } from './use-is-horizontal-scroll-end';
42
+ import { useScrollState } from './use-scroll-state';
43
43
  import getDataByGroup from '../utils/get-data-by-group';
44
44
  import { PropertiesSection } from '../../dataviews-view-config/properties-section';
45
45
  import { useDelayedLoading } from '../../../hooks/use-delayed-loading';
@@ -323,9 +323,9 @@ function ViewTable< Item >( {
323
323
 
324
324
  const tableNoticeId = useId();
325
325
 
326
- const isHorizontalScrollEnd = useIsHorizontalScrollEnd( {
326
+ const { isHorizontalScrollEnd, isVerticallyScrolled } = useScrollState( {
327
327
  scrollContainerRef: containerRef,
328
- enabled: !! actions?.length,
328
+ enabledHorizontal: !! actions?.length,
329
329
  } );
330
330
 
331
331
  const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data );
@@ -464,7 +464,13 @@ function ViewTable< Item >( {
464
464
  <PropertiesSection showLabel={ false } />
465
465
  </Popover>
466
466
  ) }
467
- <thead onContextMenu={ handleHeaderContextMenu }>
467
+ <thead
468
+ className={ clsx( {
469
+ 'dataviews-view-table__thead--stuck':
470
+ isVerticallyScrolled,
471
+ } ) }
472
+ onContextMenu={ handleHeaderContextMenu }
473
+ >
468
474
  <tr className="dataviews-view-table__row">
469
475
  { hasBulkActions && (
470
476
  <th
@@ -134,6 +134,19 @@
134
134
  z-index: z-index(".dataviews-view-table thead");
135
135
  background-color: inherit;
136
136
 
137
+ &.dataviews-view-table__thead--stuck {
138
+ &::after {
139
+ display: block;
140
+ content: "";
141
+ position: absolute;
142
+ bottom: 0;
143
+ left: 0;
144
+ right: 0;
145
+ height: 1px;
146
+ background-color: $gray-100;
147
+ }
148
+ }
149
+
137
150
  tr {
138
151
  border: 0;
139
152
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { MutableRefObject } from 'react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useCallback, useEffect, useState } from '@wordpress/element';
10
+ import { isRTL } from '@wordpress/i18n';
11
+
12
+ const isScrolledToEnd = ( element: Element ) => {
13
+ if ( isRTL() ) {
14
+ const scrollLeft = Math.abs( element.scrollLeft );
15
+ return scrollLeft <= 1;
16
+ }
17
+
18
+ return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
19
+ };
20
+
21
+ /**
22
+ * A hook to track the scroll state of a container element.
23
+ *
24
+ * Returns whether the container has been scrolled vertically (for sticky header styling)
25
+ * and whether it has reached the horizontal scroll end (for sticky actions column styling).
26
+ *
27
+ * The current way receives "refs" as arguments, but it lacks a mechanism to detect when a ref has changed.
28
+ * As a result, when the "ref" is updated and attached to a new div, the computation should trigger again.
29
+ * However, this isn't possible in the current setup because the hook is unaware that the ref has changed.
30
+ *
31
+ * See https://github.com/Automattic/wp-calypso/pull/103005#discussion_r2077567912.
32
+ *
33
+ * @param {Object} params The parameters for the hook.
34
+ * @param {MutableRefObject<HTMLDivElement | null>} params.scrollContainerRef The ref to the scroll container element.
35
+ * @param {boolean} [params.enabledHorizontal=false] Whether to track horizontal scroll end.
36
+ * @return {{ isHorizontalScrollEnd: boolean, isVerticallyScrolled: boolean }} The scroll state.
37
+ */
38
+ export function useScrollState( {
39
+ scrollContainerRef,
40
+ enabledHorizontal = false,
41
+ }: {
42
+ scrollContainerRef: React.MutableRefObject< HTMLDivElement | null >;
43
+ enabledHorizontal?: boolean;
44
+ } ): { isHorizontalScrollEnd: boolean; isVerticallyScrolled: boolean } {
45
+ const [ isHorizontalScrollEnd, setIsHorizontalScrollEnd ] =
46
+ useState( false );
47
+ const [ isVerticallyScrolled, setIsVerticallyScrolled ] = useState( false );
48
+
49
+ const handleScroll = useCallback( () => {
50
+ const scrollContainer = scrollContainerRef.current;
51
+ if ( ! scrollContainer ) {
52
+ return;
53
+ }
54
+
55
+ if ( enabledHorizontal ) {
56
+ setIsHorizontalScrollEnd( isScrolledToEnd( scrollContainer ) );
57
+ }
58
+
59
+ setIsVerticallyScrolled( scrollContainer.scrollTop > 0 );
60
+ }, [ scrollContainerRef, enabledHorizontal ] );
61
+ useEffect( () => {
62
+ if ( typeof window === 'undefined' || ! scrollContainerRef.current ) {
63
+ return () => {};
64
+ }
65
+
66
+ const scrollContainer = scrollContainerRef.current;
67
+
68
+ handleScroll();
69
+ scrollContainer.addEventListener( 'scroll', handleScroll );
70
+ window.addEventListener( 'resize', handleScroll );
71
+
72
+ return () => {
73
+ scrollContainer.removeEventListener( 'scroll', handleScroll );
74
+ window.removeEventListener( 'resize', handleScroll );
75
+ };
76
+ }, [ scrollContainerRef, enabledHorizontal, handleScroll ] );
77
+
78
+ return { isHorizontalScrollEnd, isVerticallyScrolled };
79
+ }
@@ -12,11 +12,21 @@ import { useContext } from '@wordpress/element';
12
12
  * Internal dependencies
13
13
  */
14
14
  import DataViewsContext from '../../dataviews-context';
15
- import type { ViewTable, ViewList, Density } from '../../../types';
15
+ import type {
16
+ ViewTable,
17
+ ViewList,
18
+ ViewGrid,
19
+ ViewPickerGrid,
20
+ Density,
21
+ } from '../../../types';
16
22
 
17
23
  export default function DensityPicker() {
18
24
  const context = useContext( DataViewsContext );
19
- const view = context.view as ViewTable | ViewList;
25
+ const view = context.view as
26
+ | ViewTable
27
+ | ViewList
28
+ | ViewGrid
29
+ | ViewPickerGrid;
20
30
  return (
21
31
  <ToggleGroupControl
22
32
  size="__unstable-large"
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import DensityPicker from './density-picker';
5
+ import PreviewSizePicker from './preview-size-picker';
6
+
7
+ export default function GridConfigOptions() {
8
+ return (
9
+ <>
10
+ <DensityPicker />
11
+ <PreviewSizePicker />
12
+ </>
13
+ );
14
+ }
@@ -3,7 +3,7 @@
3
3
  .dataviews-view-grid-items {
4
4
  margin-bottom: auto;
5
5
  display: grid;
6
- gap: $grid-unit-40;
6
+ gap: $grid-unit-30;
7
7
  grid-template-rows: max-content;
8
8
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
9
9
  padding: 0 $grid-unit-30 $grid-unit-30;
@@ -12,4 +12,12 @@
12
12
  @media not (prefers-reduced-motion) {
13
13
  transition: padding ease-out 0.1s;
14
14
  }
15
+
16
+ &.has-compact-density {
17
+ gap: $grid-unit-20;
18
+ }
19
+
20
+ &.has-comfortable-density {
21
+ gap: $grid-unit-40;
22
+ }
15
23
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useContext, useEffect } from '@wordpress/element';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import DataViewsContext from '../../dataviews-context';
10
+
11
+ /**
12
+ * Hook to set up an IntersectionObserver for infinite scroll items.
13
+ * Observes the element and triggers the callback when the item becomes visible.
14
+ *
15
+ * @param elementRef - Ref to the DOM element to observe.
16
+ * @param posinset - The position of the item in the set (1-based index).
17
+ */
18
+ export function useIntersectionObserver(
19
+ elementRef: React.RefObject< HTMLElement | null >,
20
+ posinset: number | undefined
21
+ ) {
22
+ const { intersectionObserver } = useContext( DataViewsContext );
23
+
24
+ useEffect( () => {
25
+ const element = elementRef.current;
26
+ if ( ! element || posinset === undefined || ! intersectionObserver ) {
27
+ return;
28
+ }
29
+
30
+ intersectionObserver.observe( element );
31
+
32
+ return () => {
33
+ intersectionObserver.unobserve( element );
34
+ };
35
+ }, [ elementRef, intersectionObserver, posinset ] );
36
+ }
37
+
38
+ /**
39
+ * Hook to calculate the number of placeholder items needed for the first row
40
+ * in an infinite scroll grid layout.
41
+ *
42
+ * When items are loaded starting from a position other than 1, placeholders
43
+ * are needed to maintain proper grid alignment.
44
+ *
45
+ * @param data - The array of data items.
46
+ * @param isInfiniteScroll - Whether infinite scroll is enabled.
47
+ * @param gridColumns - The number of columns in the grid.
48
+ * @return The number of placeholder items needed.
49
+ */
50
+ export function usePlaceholdersNeeded< Item >(
51
+ data: Item[],
52
+ isInfiniteScroll: boolean,
53
+ gridColumns: number
54
+ ): number {
55
+ const hasData = !! data?.length;
56
+ const firstItemPosition =
57
+ hasData && isInfiniteScroll
58
+ ? ( data[ 0 ] as { position?: number } ).position
59
+ : undefined;
60
+
61
+ return firstItemPosition && gridColumns
62
+ ? ( firstItemPosition - 1 ) % gridColumns
63
+ : 0;
64
+ }