@wordpress/dataviews 13.1.1-next.v.202603161435.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 (183) hide show
  1. package/CHANGELOG.md +15 -6
  2. package/README.md +17 -2
  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-layouts/grid/composite-grid.cjs +378 -249
  14. package/build/components/dataviews-layouts/grid/composite-grid.cjs.map +2 -2
  15. package/build/components/dataviews-layouts/picker-grid/index.cjs +63 -30
  16. package/build/components/dataviews-layouts/picker-grid/index.cjs.map +2 -2
  17. package/build/components/dataviews-layouts/picker-table/index.cjs +34 -22
  18. package/build/components/dataviews-layouts/picker-table/index.cjs.map +2 -2
  19. package/build/components/dataviews-layouts/utils/use-infinite-scroll.cjs +62 -0
  20. package/build/components/dataviews-layouts/utils/use-infinite-scroll.cjs.map +7 -0
  21. package/build/components/dataviews-picker-footer/index.cjs +23 -4
  22. package/build/components/dataviews-picker-footer/index.cjs.map +2 -2
  23. package/build/components/dataviews-search/index.cjs +2 -1
  24. package/build/components/dataviews-search/index.cjs.map +2 -2
  25. package/build/components/dataviews-selection-checkbox/index.cjs +3 -2
  26. package/build/components/dataviews-selection-checkbox/index.cjs.map +2 -2
  27. package/build/components/dataviews-view-config/index.cjs +0 -2
  28. package/build/components/dataviews-view-config/index.cjs.map +3 -3
  29. package/build/components/dataviews-view-config/infinite-scroll-toggle.cjs +0 -3
  30. package/build/components/dataviews-view-config/infinite-scroll-toggle.cjs.map +2 -2
  31. package/build/dataviews/index.cjs +37 -37
  32. package/build/dataviews/index.cjs.map +3 -3
  33. package/build/dataviews-picker/index.cjs +25 -24
  34. package/build/dataviews-picker/index.cjs.map +3 -3
  35. package/build/hooks/index.cjs +11 -2
  36. package/build/hooks/index.cjs.map +2 -2
  37. package/build/hooks/use-data.cjs +146 -9
  38. package/build/hooks/use-data.cjs.map +2 -2
  39. package/build/hooks/use-infinite-scroll.cjs +208 -0
  40. package/build/hooks/use-infinite-scroll.cjs.map +7 -0
  41. package/build/hooks/use-selected-items.cjs +57 -0
  42. package/build/hooks/use-selected-items.cjs.map +7 -0
  43. package/build/types/dataviews.cjs.map +1 -1
  44. package/build/types/field-api.cjs.map +1 -1
  45. package/build/utils/filter-sort-and-paginate.cjs +5 -1
  46. package/build/utils/filter-sort-and-paginate.cjs.map +2 -2
  47. package/build/utils/get-footer-message.cjs +8 -8
  48. package/build/utils/get-footer-message.cjs.map +2 -2
  49. package/build-module/components/dataform-controls/datetime.mjs +8 -4
  50. package/build-module/components/dataform-controls/datetime.mjs.map +2 -2
  51. package/build-module/components/dataform-layouts/card/index.mjs +132 -133
  52. package/build-module/components/dataform-layouts/card/index.mjs.map +2 -2
  53. package/build-module/components/dataviews-bulk-actions/index.mjs +28 -5
  54. package/build-module/components/dataviews-bulk-actions/index.mjs.map +2 -2
  55. package/build-module/components/dataviews-context/index.mjs +2 -2
  56. package/build-module/components/dataviews-context/index.mjs.map +2 -2
  57. package/build-module/components/dataviews-footer/index.mjs +2 -3
  58. package/build-module/components/dataviews-footer/index.mjs.map +2 -2
  59. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs +387 -250
  60. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs.map +2 -2
  61. package/build-module/components/dataviews-layouts/picker-grid/index.mjs +67 -31
  62. package/build-module/components/dataviews-layouts/picker-grid/index.mjs.map +2 -2
  63. package/build-module/components/dataviews-layouts/picker-table/index.mjs +34 -22
  64. package/build-module/components/dataviews-layouts/picker-table/index.mjs.map +2 -2
  65. package/build-module/components/dataviews-layouts/utils/use-infinite-scroll.mjs +26 -0
  66. package/build-module/components/dataviews-layouts/utils/use-infinite-scroll.mjs.map +7 -0
  67. package/build-module/components/dataviews-picker-footer/index.mjs +23 -4
  68. package/build-module/components/dataviews-picker-footer/index.mjs.map +2 -2
  69. package/build-module/components/dataviews-search/index.mjs +2 -1
  70. package/build-module/components/dataviews-search/index.mjs.map +2 -2
  71. package/build-module/components/dataviews-selection-checkbox/index.mjs +3 -2
  72. package/build-module/components/dataviews-selection-checkbox/index.mjs.map +2 -2
  73. package/build-module/components/dataviews-view-config/index.mjs +0 -2
  74. package/build-module/components/dataviews-view-config/index.mjs.map +2 -2
  75. package/build-module/components/dataviews-view-config/infinite-scroll-toggle.mjs +0 -3
  76. package/build-module/components/dataviews-view-config/infinite-scroll-toggle.mjs.map +2 -2
  77. package/build-module/dataviews/index.mjs +45 -39
  78. package/build-module/dataviews/index.mjs.map +2 -2
  79. package/build-module/dataviews-picker/index.mjs +33 -26
  80. package/build-module/dataviews-picker/index.mjs.map +2 -2
  81. package/build-module/hooks/index.mjs +7 -1
  82. package/build-module/hooks/index.mjs.map +2 -2
  83. package/build-module/hooks/use-data.mjs +147 -10
  84. package/build-module/hooks/use-data.mjs.map +2 -2
  85. package/build-module/hooks/use-infinite-scroll.mjs +188 -0
  86. package/build-module/hooks/use-infinite-scroll.mjs.map +7 -0
  87. package/build-module/hooks/use-selected-items.mjs +36 -0
  88. package/build-module/hooks/use-selected-items.mjs.map +7 -0
  89. package/build-module/utils/filter-sort-and-paginate.mjs +5 -1
  90. package/build-module/utils/filter-sort-and-paginate.mjs.map +2 -2
  91. package/build-module/utils/get-footer-message.mjs +8 -8
  92. package/build-module/utils/get-footer-message.mjs.map +2 -2
  93. package/build-style/style-rtl.css +61 -37
  94. package/build-style/style.css +61 -37
  95. package/build-types/components/dataform-controls/datetime.d.ts +1 -1
  96. package/build-types/components/dataform-controls/datetime.d.ts.map +1 -1
  97. package/build-types/components/dataform-layouts/card/index.d.ts.map +1 -1
  98. package/build-types/components/dataviews-bulk-actions/index.d.ts +2 -1
  99. package/build-types/components/dataviews-bulk-actions/index.d.ts.map +1 -1
  100. package/build-types/components/dataviews-context/index.d.ts +1 -1
  101. package/build-types/components/dataviews-context/index.d.ts.map +1 -1
  102. package/build-types/components/dataviews-footer/index.d.ts.map +1 -1
  103. package/build-types/components/dataviews-layouts/grid/composite-grid.d.ts.map +1 -1
  104. package/build-types/components/dataviews-layouts/picker-grid/index.d.ts.map +1 -1
  105. package/build-types/components/dataviews-layouts/picker-table/index.d.ts.map +1 -1
  106. package/build-types/components/dataviews-layouts/utils/use-infinite-scroll.d.ts +22 -0
  107. package/build-types/components/dataviews-layouts/utils/use-infinite-scroll.d.ts.map +1 -0
  108. package/build-types/components/dataviews-picker-footer/index.d.ts.map +1 -1
  109. package/build-types/components/dataviews-search/index.d.ts.map +1 -1
  110. package/build-types/components/dataviews-selection-checkbox/index.d.ts.map +1 -1
  111. package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
  112. package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts +1 -1
  113. package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts.map +1 -1
  114. package/build-types/dataviews/index.d.ts +0 -1
  115. package/build-types/dataviews/index.d.ts.map +1 -1
  116. package/build-types/dataviews/stories/fixtures.d.ts.map +1 -1
  117. package/build-types/dataviews/stories/free-composition.d.ts.map +1 -1
  118. package/build-types/dataviews/stories/index.story.d.ts +11 -0
  119. package/build-types/dataviews/stories/index.story.d.ts.map +1 -1
  120. package/build-types/dataviews/stories/infinite-scroll.d.ts.map +1 -1
  121. package/build-types/dataviews/stories/with-card.d.ts.map +1 -1
  122. package/build-types/dataviews-picker/index.d.ts +0 -1
  123. package/build-types/dataviews-picker/index.d.ts.map +1 -1
  124. package/build-types/dataviews-picker/stories/fixtures.d.ts.map +1 -1
  125. package/build-types/dataviews-picker/stories/index.story.d.ts.map +1 -1
  126. package/build-types/field-types/stories/index.story.d.ts.map +1 -1
  127. package/build-types/hooks/index.d.ts +3 -0
  128. package/build-types/hooks/index.d.ts.map +1 -1
  129. package/build-types/hooks/test/use-data.d.ts +2 -0
  130. package/build-types/hooks/test/use-data.d.ts.map +1 -0
  131. package/build-types/hooks/use-data.d.ts +41 -3
  132. package/build-types/hooks/use-data.d.ts.map +1 -1
  133. package/build-types/hooks/use-infinite-scroll.d.ts +21 -0
  134. package/build-types/hooks/use-infinite-scroll.d.ts.map +1 -0
  135. package/build-types/hooks/use-selected-items.d.ts +19 -0
  136. package/build-types/hooks/use-selected-items.d.ts.map +1 -0
  137. package/build-types/types/dataviews.d.ts +7 -1
  138. package/build-types/types/dataviews.d.ts.map +1 -1
  139. package/build-types/types/field-api.d.ts +15 -4
  140. package/build-types/types/field-api.d.ts.map +1 -1
  141. package/build-types/utils/filter-sort-and-paginate.d.ts.map +1 -1
  142. package/build-types/utils/get-footer-message.d.ts +3 -2
  143. package/build-types/utils/get-footer-message.d.ts.map +1 -1
  144. package/build-wp/index.js +3013 -2613
  145. package/package.json +19 -19
  146. package/src/components/dataform-controls/datetime.tsx +19 -11
  147. package/src/components/dataform-layouts/card/index.tsx +171 -146
  148. package/src/components/dataform-layouts/card/style.scss +8 -5
  149. package/src/components/dataviews-bulk-actions/index.tsx +28 -1
  150. package/src/components/dataviews-context/index.ts +2 -2
  151. package/src/components/dataviews-footer/index.tsx +1 -6
  152. package/src/components/dataviews-layouts/grid/composite-grid.tsx +433 -284
  153. package/src/components/dataviews-layouts/grid/style.scss +4 -0
  154. package/src/components/dataviews-layouts/picker-grid/index.tsx +53 -15
  155. package/src/components/dataviews-layouts/picker-table/index.tsx +42 -22
  156. package/src/components/dataviews-layouts/utils/use-infinite-scroll.ts +64 -0
  157. package/src/components/dataviews-picker-footer/index.tsx +21 -1
  158. package/src/components/dataviews-search/index.tsx +2 -1
  159. package/src/components/dataviews-selection-checkbox/index.tsx +4 -2
  160. package/src/components/dataviews-view-config/index.tsx +0 -2
  161. package/src/components/dataviews-view-config/infinite-scroll-toggle.tsx +0 -5
  162. package/src/dataviews/index.tsx +57 -52
  163. package/src/dataviews/stories/fixtures.tsx +288 -0
  164. package/src/dataviews/stories/free-composition.tsx +12 -11
  165. package/src/dataviews/stories/index.story.tsx +19 -2
  166. package/src/dataviews/stories/infinite-scroll.tsx +12 -92
  167. package/src/dataviews/stories/with-card.tsx +30 -23
  168. package/src/dataviews/style.scss +5 -7
  169. package/src/dataviews/test/dataviews.tsx +21 -9
  170. package/src/dataviews-picker/index.tsx +40 -34
  171. package/src/dataviews-picker/stories/fixtures.tsx +270 -0
  172. package/src/dataviews-picker/stories/index.story.tsx +62 -129
  173. package/src/field-types/stories/index.story.tsx +12 -0
  174. package/src/hooks/index.ts +3 -0
  175. package/src/hooks/test/use-data.ts +791 -0
  176. package/src/hooks/use-data.ts +288 -21
  177. package/src/hooks/use-infinite-scroll.ts +304 -0
  178. package/src/hooks/use-selected-items.ts +72 -0
  179. package/src/types/dataviews.ts +8 -1
  180. package/src/types/field-api.ts +16 -3
  181. package/src/utils/filter-sort-and-paginate.ts +13 -1
  182. package/src/utils/get-footer-message.ts +12 -9
  183. package/src/utils/test/filter-sort-and-paginate.js +78 -54
@@ -57,6 +57,10 @@
57
57
  }
58
58
  }
59
59
  }
60
+ }
61
+
62
+ .dataviews-view-grid,
63
+ .dataviews-view-grid-infinite-scroll {
60
64
 
61
65
  .dataviews-view-grid__card {
62
66
  height: 100%;
@@ -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
  {
@@ -378,10 +403,12 @@ function ViewPickerGrid< Item >( {
378
403
  }
379
404
  >
380
405
  { groupItems.map( ( item ) => {
406
+ // Use position from item if available (infinite scroll), otherwise calculate.
381
407
  const posInSet =
408
+ ( item as any ).position ??
382
409
  ( currentPage - 1 ) * perPage +
383
- data.indexOf( item ) +
384
- 1;
410
+ data.indexOf( item ) +
411
+ 1;
385
412
  return (
386
413
  <GridItem
387
414
  key={ getItemId( item ) }
@@ -451,17 +478,28 @@ function ViewPickerGrid< Item >( {
451
478
  aria-multiselectable={ isMultiselect }
452
479
  aria-label={ itemListLabel }
453
480
  >
454
- { data.map( ( item, index ) => {
455
- let posinset = isInfiniteScroll
456
- ? index + 1
457
- : undefined;
458
-
459
- if ( ! isInfiniteScroll ) {
460
- // When infinite scroll isn't active, take pagination into account
461
- // when calculating the posinset.
462
- posinset =
463
- ( currentPage - 1 ) * perPage + index + 1;
464
- }
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;
465
503
 
466
504
  return (
467
505
  <GridItem
@@ -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>
@@ -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
+ }
@@ -32,15 +32,32 @@ function BulkSelectionCheckbox< Item >( {
32
32
  onChangeSelection,
33
33
  data,
34
34
  getItemId,
35
+ disableSelectAll = false,
35
36
  }: {
36
37
  selection: string[];
37
38
  selectedItems: Item[];
38
39
  onChangeSelection: SetSelection;
39
40
  data: Item[];
40
41
  getItemId: ( item: Item ) => string;
42
+ disableSelectAll?: boolean;
41
43
  } ) {
44
+ const hasSelection = selection.length > 0;
42
45
  const areAllSelected = selectedItems.length === data.length;
43
46
 
47
+ if ( disableSelectAll ) {
48
+ return (
49
+ <CheckboxControl
50
+ className="dataviews-view-table-selection-checkbox"
51
+ checked={ hasSelection }
52
+ disabled={ ! hasSelection }
53
+ onChange={ () => {
54
+ onChangeSelection( [] );
55
+ } }
56
+ aria-label={ __( 'Deselect all' ) }
57
+ />
58
+ );
59
+ }
60
+
44
61
  return (
45
62
  <CheckboxControl
46
63
  className="dataviews-view-table-selection-checkbox"
@@ -136,6 +153,7 @@ export function DataViewsPickerFooter() {
136
153
  getItemId,
137
154
  actions = EMPTY_ARRAY,
138
155
  paginationInfo,
156
+ view,
139
157
  } = useContext( DataViewsContext );
140
158
 
141
159
  const isMultiselect = useIsMultiselectPicker( actions );
@@ -143,7 +161,8 @@ export function DataViewsPickerFooter() {
143
161
  const message = getFooterMessage(
144
162
  selection.length,
145
163
  data.length,
146
- paginationInfo.totalItems
164
+ paginationInfo.totalItems,
165
+ !! view.infiniteScrollEnabled
147
166
  );
148
167
 
149
168
  const selectedItems = useMemo(
@@ -173,6 +192,7 @@ export function DataViewsPickerFooter() {
173
192
  onChangeSelection={ onChangeSelection }
174
193
  data={ data }
175
194
  getItemId={ getItemId }
195
+ disableSelectAll={ !! view.infiniteScrollEnabled }
176
196
  />
177
197
  ) }
178
198
  <span className="dataviews-bulk-actions-footer__item-count">
@@ -35,7 +35,8 @@ const DataViewsSearch = memo( function Search( { label }: SearchProps ) {
35
35
  if ( debouncedSearch !== viewRef.current?.search ) {
36
36
  onChangeViewRef.current( {
37
37
  ...viewRef.current,
38
- page: 1,
38
+ page: view.page ? 1 : undefined,
39
+ startPosition: view.startPosition ? 1 : undefined,
39
40
  search: debouncedSearch,
40
41
  } );
41
42
  }
@@ -30,7 +30,8 @@ export default function DataViewsSelectionCheckbox< Item >( {
30
30
  ...extraProps
31
31
  }: DataViewsSelectionCheckboxProps< Item > ) {
32
32
  const id = getItemId( item );
33
- const checked = ! disabled && selection.includes( id );
33
+ const isInSelectionArray = selection.includes( id );
34
+ const checked = ! disabled && isInSelectionArray;
34
35
 
35
36
  // Fallback label to ensure accessibility
36
37
  const selectionLabel =
@@ -47,8 +48,9 @@ export default function DataViewsSelectionCheckbox< Item >( {
47
48
  return;
48
49
  }
49
50
 
51
+ // Toggle in/out of selection array
50
52
  onChangeSelection(
51
- selection.includes( id )
53
+ isInSelectionArray
52
54
  ? selection.filter( ( itemId ) => id !== itemId )
53
55
  : [ ...selection, id ]
54
56
  );
@@ -31,7 +31,6 @@ import { SORTING_DIRECTIONS, sortIcons, sortLabels } from '../../constants';
31
31
  import { VIEW_LAYOUTS } from '../dataviews-layouts';
32
32
  import type { View } from '../../types';
33
33
  import DataViewsContext from '../dataviews-context';
34
- import InfiniteScrollToggle from './infinite-scroll-toggle';
35
34
  import { PropertiesSection } from './properties-section';
36
35
  import { unlock } from '../../lock-unlock';
37
36
 
@@ -346,7 +345,6 @@ export function DataviewsViewConfigDropdown() {
346
345
  { !! activeLayout?.viewConfigOptions && (
347
346
  <activeLayout.viewConfigOptions />
348
347
  ) }
349
- <InfiniteScrollToggle />
350
348
  <ItemsPerPageControl />
351
349
  <PropertiesSection />
352
350
  </Stack>
@@ -15,11 +15,6 @@ export default function InfiniteScrollToggle() {
15
15
  const { view, onChangeView } = context;
16
16
  const infiniteScrollEnabled = view.infiniteScrollEnabled ?? false;
17
17
 
18
- // Only render the toggle if an infinite scroll handler is available
19
- if ( ! context.hasInfiniteScrollHandler ) {
20
- return null;
21
- }
22
-
23
18
  return (
24
19
  <ToggleControl
25
20
  label={ __( 'Enable infinite scroll' ) }
@@ -2,12 +2,19 @@
2
2
  * External dependencies
3
3
  */
4
4
  import type { ReactNode, ComponentProps, ReactElement } from 'react';
5
+ import clsx from 'clsx';
5
6
 
6
7
  /**
7
8
  * WordPress dependencies
8
9
  */
9
- import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
10
- import { useResizeObserver, throttle } from '@wordpress/compose';
10
+ import {
11
+ useContext,
12
+ useEffect,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ } from '@wordpress/element';
17
+ import { useResizeObserver } from '@wordpress/compose';
11
18
  import { Stack } from '@wordpress/ui';
12
19
 
13
20
  /**
@@ -32,6 +39,7 @@ import DataViewsViewConfig, {
32
39
  } from '../components/dataviews-view-config';
33
40
  import normalizeFields from '../field-types';
34
41
  import useData from '../hooks/use-data';
42
+ import { useInfiniteScroll } from '../hooks/use-infinite-scroll';
35
43
  import type { Action, Field, View, SupportedLayouts } from '../types';
36
44
  import type { SelectionOrUpdater } from '../types/private';
37
45
  type ItemWithId = { id: string };
@@ -48,7 +56,6 @@ type DataViewsProps< Item > = {
48
56
  paginationInfo: {
49
57
  totalItems: number;
50
58
  totalPages: number;
51
- infiniteScrollHandler?: () => void;
52
59
  };
53
60
  defaultLayouts: SupportedLayouts;
54
61
  selection?: string[];
@@ -90,13 +97,18 @@ function DefaultUI( {
90
97
  search = true,
91
98
  searchLabel = undefined,
92
99
  }: DefaultUIProps ) {
100
+ const { view } = useContext( DataViewsContext );
101
+ const isInfiniteScroll = view.infiniteScrollEnabled;
93
102
  return (
94
103
  <>
95
104
  <Stack
96
105
  direction="row"
97
106
  align="top"
98
107
  justify="space-between"
99
- className="dataviews__view-actions"
108
+ className={ clsx( 'dataviews__view-actions', {
109
+ 'dataviews__view-actions--infinite-scroll':
110
+ isInfiniteScroll,
111
+ } ) }
100
112
  gap="xs"
101
113
  >
102
114
  <Stack
@@ -144,7 +156,31 @@ function DataViews< Item >( {
144
156
  empty,
145
157
  onReset,
146
158
  }: DataViewsProps< Item > ) {
147
- const { infiniteScrollHandler } = paginationInfo;
159
+ const [ selectionState, setSelectionState ] = useState< string[] >( [] );
160
+ const isUncontrolled =
161
+ selectionProperty === undefined || onChangeSelection === undefined;
162
+ const selection = isUncontrolled ? selectionState : selectionProperty;
163
+
164
+ // useData handles both infinite scroll and standard pagination paths,
165
+ // preserving previous data while loading and tracking initial load state.
166
+ const {
167
+ data: displayData,
168
+ paginationInfo: displayPaginationInfo,
169
+ hasInitiallyLoaded,
170
+ setVisibleEntries,
171
+ } = useData( {
172
+ view,
173
+ data: data as any,
174
+ getItemId: getItemId as any,
175
+ isLoading,
176
+ selection,
177
+ paginationInfo,
178
+ } ) as {
179
+ data: ( Item & { position?: number } )[];
180
+ paginationInfo: { totalItems: number; totalPages: number };
181
+ hasInitiallyLoaded: boolean;
182
+ setVisibleEntries?: React.Dispatch< React.SetStateAction< number[] > >;
183
+ };
148
184
  const containerRef = useRef< HTMLDivElement >( null );
149
185
  const [ containerWidth, setContainerWidth ] = useState( 0 );
150
186
  const resizeObserverRef = useResizeObserver(
@@ -155,10 +191,6 @@ function DataViews< Item >( {
155
191
  },
156
192
  { box: 'border-box' }
157
193
  );
158
- const [ selectionState, setSelectionState ] = useState< string[] >( [] );
159
- const isUncontrolled =
160
- selectionProperty === undefined || onChangeSelection === undefined;
161
- const selection = isUncontrolled ? selectionState : selectionProperty;
162
194
  const [ openedFilter, setOpenedFilter ] = useState< string | null >( null );
163
195
  function setSelectionWithChange( value: SelectionOrUpdater ) {
164
196
  const newValue =
@@ -171,11 +203,16 @@ function DataViews< Item >( {
171
203
  }
172
204
  }
173
205
  const _fields = useMemo( () => normalizeFields( fields ), [ fields ] );
206
+ // When infinite scroll is enabled, don't filter selection by current data
207
+ // because items may be scrolled out of view but still selected.
174
208
  const _selection = useMemo( () => {
209
+ if ( view.infiniteScrollEnabled ) {
210
+ return selection;
211
+ }
175
212
  return selection.filter( ( id ) =>
176
213
  data.some( ( item ) => getItemId( item ) === id )
177
214
  );
178
- }, [ selection, data, getItemId ] );
215
+ }, [ selection, data, getItemId, view.infiniteScrollEnabled ] );
179
216
 
180
217
  const filters = useFilters( _fields, view );
181
218
  const hasPrimaryOrLockedFilters = useMemo(
@@ -189,53 +226,21 @@ function DataViews< Item >( {
189
226
  hasPrimaryOrLockedFilters
190
227
  );
191
228
 
229
+ const { intersectionObserver } = useInfiniteScroll( {
230
+ view,
231
+ onChangeView,
232
+ isLoading,
233
+ paginationInfo,
234
+ containerRef,
235
+ setVisibleEntries,
236
+ } );
237
+
192
238
  useEffect( () => {
193
239
  if ( hasPrimaryOrLockedFilters && ! isShowingFilter ) {
194
240
  setIsShowingFilter( true );
195
241
  }
196
242
  }, [ hasPrimaryOrLockedFilters, isShowingFilter ] );
197
243
 
198
- const {
199
- data: displayData,
200
- paginationInfo: displayPaginationInfo,
201
- hasInitiallyLoaded,
202
- } = useData( data, isLoading, paginationInfo );
203
-
204
- // Attach scroll event listener for infinite scroll
205
- useEffect( () => {
206
- if (
207
- ! hasInitiallyLoaded ||
208
- ! view.infiniteScrollEnabled ||
209
- ! containerRef.current
210
- ) {
211
- return;
212
- }
213
-
214
- const handleScroll = throttle( ( event: unknown ) => {
215
- const target = ( event as Event ).target as HTMLElement;
216
- const scrollTop = target.scrollTop;
217
- const scrollHeight = target.scrollHeight;
218
- const clientHeight = target.clientHeight;
219
-
220
- // Check if user has scrolled near the bottom
221
- if ( scrollTop + clientHeight >= scrollHeight - 100 ) {
222
- infiniteScrollHandler?.();
223
- }
224
- }, 100 ); // Throttle to 100ms
225
-
226
- const container = containerRef.current;
227
- container.addEventListener( 'scroll', handleScroll );
228
-
229
- return () => {
230
- container.removeEventListener( 'scroll', handleScroll );
231
- handleScroll.cancel(); // Cancel any pending throttled calls
232
- };
233
- }, [
234
- hasInitiallyLoaded,
235
- infiniteScrollHandler,
236
- view.infiniteScrollEnabled,
237
- ] );
238
-
239
244
  // Filter out DataViewsPicker layouts.
240
245
  const defaultLayouts = useMemo(
241
246
  () =>
@@ -284,8 +289,8 @@ function DataViews< Item >( {
284
289
  config,
285
290
  empty,
286
291
  hasInitiallyLoaded,
287
- hasInfiniteScrollHandler: !! infiniteScrollHandler,
288
292
  onReset,
293
+ intersectionObserver,
289
294
  } }
290
295
  >
291
296
  <div className="dataviews-wrapper">