@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.
- package/CHANGELOG.md +18 -1
- package/README.md +19 -3
- package/build/components/dataform-controls/datetime.cjs +8 -4
- package/build/components/dataform-controls/datetime.cjs.map +2 -2
- package/build/components/dataform-layouts/card/index.cjs +132 -128
- package/build/components/dataform-layouts/card/index.cjs.map +3 -3
- package/build/components/dataviews-bulk-actions/index.cjs +28 -5
- package/build/components/dataviews-bulk-actions/index.cjs.map +2 -2
- package/build/components/dataviews-context/index.cjs +2 -2
- package/build/components/dataviews-context/index.cjs.map +2 -2
- package/build/components/dataviews-footer/index.cjs +2 -3
- package/build/components/dataviews-footer/index.cjs.map +2 -2
- package/build/components/dataviews-layout/index.cjs +12 -3
- package/build/components/dataviews-layout/index.cjs.map +2 -2
- package/build/components/dataviews-layouts/grid/composite-grid.cjs +378 -245
- package/build/components/dataviews-layouts/grid/composite-grid.cjs.map +2 -2
- package/build/components/dataviews-layouts/index.cjs +3 -3
- package/build/components/dataviews-layouts/index.cjs.map +3 -3
- package/build/components/dataviews-layouts/picker-grid/index.cjs +76 -32
- package/build/components/dataviews-layouts/picker-grid/index.cjs.map +2 -2
- package/build/components/dataviews-layouts/picker-table/index.cjs +34 -22
- package/build/components/dataviews-layouts/picker-table/index.cjs.map +2 -2
- package/build/components/dataviews-layouts/table/index.cjs +97 -88
- package/build/components/dataviews-layouts/table/index.cjs.map +2 -2
- package/build/components/dataviews-layouts/table/{use-is-horizontal-scroll-end.cjs → use-scroll-state.cjs} +29 -33
- package/build/components/dataviews-layouts/table/use-scroll-state.cjs.map +7 -0
- package/build/components/dataviews-layouts/utils/density-picker.cjs.map +2 -2
- package/build/components/dataviews-layouts/utils/grid-config-options.cjs +45 -0
- package/build/components/dataviews-layouts/utils/grid-config-options.cjs.map +7 -0
- package/build/components/dataviews-layouts/utils/use-infinite-scroll.cjs +62 -0
- package/build/components/dataviews-layouts/utils/use-infinite-scroll.cjs.map +7 -0
- package/build/components/dataviews-picker-footer/index.cjs +23 -4
- package/build/components/dataviews-picker-footer/index.cjs.map +2 -2
- package/build/components/dataviews-search/index.cjs +2 -1
- package/build/components/dataviews-search/index.cjs.map +2 -2
- package/build/components/dataviews-selection-checkbox/index.cjs +3 -2
- package/build/components/dataviews-selection-checkbox/index.cjs.map +2 -2
- package/build/components/dataviews-view-config/index.cjs +0 -2
- package/build/components/dataviews-view-config/index.cjs.map +3 -3
- package/build/components/dataviews-view-config/infinite-scroll-toggle.cjs +0 -3
- package/build/components/dataviews-view-config/infinite-scroll-toggle.cjs.map +2 -2
- package/build/dataviews/index.cjs +38 -34
- package/build/dataviews/index.cjs.map +3 -3
- package/build/dataviews-picker/index.cjs +26 -25
- package/build/dataviews-picker/index.cjs.map +3 -3
- package/build/hooks/index.cjs +11 -2
- package/build/hooks/index.cjs.map +2 -2
- package/build/hooks/use-data.cjs +146 -9
- package/build/hooks/use-data.cjs.map +2 -2
- package/build/hooks/use-infinite-scroll.cjs +208 -0
- package/build/hooks/use-infinite-scroll.cjs.map +7 -0
- package/build/hooks/use-selected-items.cjs +57 -0
- package/build/hooks/use-selected-items.cjs.map +7 -0
- package/build/types/dataviews.cjs.map +1 -1
- package/build/types/field-api.cjs.map +1 -1
- package/build/utils/filter-sort-and-paginate.cjs +5 -1
- package/build/utils/filter-sort-and-paginate.cjs.map +2 -2
- package/build/utils/get-footer-message.cjs +8 -8
- package/build/utils/get-footer-message.cjs.map +2 -2
- package/build-module/components/dataform-controls/datetime.mjs +8 -4
- package/build-module/components/dataform-controls/datetime.mjs.map +2 -2
- package/build-module/components/dataform-layouts/card/index.mjs +132 -133
- package/build-module/components/dataform-layouts/card/index.mjs.map +2 -2
- package/build-module/components/dataviews-bulk-actions/index.mjs +28 -5
- package/build-module/components/dataviews-bulk-actions/index.mjs.map +2 -2
- package/build-module/components/dataviews-context/index.mjs +2 -2
- package/build-module/components/dataviews-context/index.mjs.map +2 -2
- package/build-module/components/dataviews-footer/index.mjs +2 -3
- package/build-module/components/dataviews-footer/index.mjs.map +2 -2
- package/build-module/components/dataviews-layout/index.mjs +12 -3
- package/build-module/components/dataviews-layout/index.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/grid/composite-grid.mjs +387 -246
- package/build-module/components/dataviews-layouts/grid/composite-grid.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/index.mjs +3 -3
- package/build-module/components/dataviews-layouts/index.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/picker-grid/index.mjs +80 -33
- package/build-module/components/dataviews-layouts/picker-grid/index.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/picker-table/index.mjs +34 -22
- package/build-module/components/dataviews-layouts/picker-table/index.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/table/index.mjs +97 -88
- package/build-module/components/dataviews-layouts/table/index.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/table/use-scroll-state.mjs +46 -0
- package/build-module/components/dataviews-layouts/table/use-scroll-state.mjs.map +7 -0
- package/build-module/components/dataviews-layouts/utils/density-picker.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/utils/grid-config-options.mjs +14 -0
- package/build-module/components/dataviews-layouts/utils/grid-config-options.mjs.map +7 -0
- package/build-module/components/dataviews-layouts/utils/use-infinite-scroll.mjs +26 -0
- package/build-module/components/dataviews-layouts/utils/use-infinite-scroll.mjs.map +7 -0
- package/build-module/components/dataviews-picker-footer/index.mjs +23 -4
- package/build-module/components/dataviews-picker-footer/index.mjs.map +2 -2
- package/build-module/components/dataviews-search/index.mjs +2 -1
- package/build-module/components/dataviews-search/index.mjs.map +2 -2
- package/build-module/components/dataviews-selection-checkbox/index.mjs +3 -2
- package/build-module/components/dataviews-selection-checkbox/index.mjs.map +2 -2
- package/build-module/components/dataviews-view-config/index.mjs +0 -2
- package/build-module/components/dataviews-view-config/index.mjs.map +2 -2
- package/build-module/components/dataviews-view-config/infinite-scroll-toggle.mjs +0 -3
- package/build-module/components/dataviews-view-config/infinite-scroll-toggle.mjs.map +2 -2
- package/build-module/dataviews/index.mjs +46 -36
- package/build-module/dataviews/index.mjs.map +2 -2
- package/build-module/dataviews-picker/index.mjs +34 -27
- package/build-module/dataviews-picker/index.mjs.map +2 -2
- package/build-module/hooks/index.mjs +7 -1
- package/build-module/hooks/index.mjs.map +2 -2
- package/build-module/hooks/use-data.mjs +147 -10
- package/build-module/hooks/use-data.mjs.map +2 -2
- package/build-module/hooks/use-infinite-scroll.mjs +188 -0
- package/build-module/hooks/use-infinite-scroll.mjs.map +7 -0
- package/build-module/hooks/use-selected-items.mjs +36 -0
- package/build-module/hooks/use-selected-items.mjs.map +7 -0
- package/build-module/utils/filter-sort-and-paginate.mjs +5 -1
- package/build-module/utils/filter-sort-and-paginate.mjs.map +2 -2
- package/build-module/utils/get-footer-message.mjs +8 -8
- package/build-module/utils/get-footer-message.mjs.map +2 -2
- package/build-style/style-rtl.css +107 -41
- package/build-style/style.css +107 -41
- package/build-types/components/dataform-controls/datetime.d.ts +1 -1
- package/build-types/components/dataform-controls/datetime.d.ts.map +1 -1
- package/build-types/components/dataform-layouts/card/index.d.ts.map +1 -1
- package/build-types/components/dataviews-bulk-actions/index.d.ts +2 -1
- package/build-types/components/dataviews-bulk-actions/index.d.ts.map +1 -1
- package/build-types/components/dataviews-context/index.d.ts +1 -1
- package/build-types/components/dataviews-context/index.d.ts.map +1 -1
- package/build-types/components/dataviews-footer/index.d.ts.map +1 -1
- package/build-types/components/dataviews-layout/index.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/grid/composite-grid.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/index.d.ts +3 -3
- package/build-types/components/dataviews-layouts/index.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/picker-grid/index.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/picker-table/index.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/table/index.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/table/use-scroll-state.d.ts +25 -0
- package/build-types/components/dataviews-layouts/table/use-scroll-state.d.ts.map +1 -0
- package/build-types/components/dataviews-layouts/utils/density-picker.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/utils/grid-config-options.d.ts +2 -0
- package/build-types/components/dataviews-layouts/utils/grid-config-options.d.ts.map +1 -0
- package/build-types/components/dataviews-layouts/utils/use-infinite-scroll.d.ts +22 -0
- package/build-types/components/dataviews-layouts/utils/use-infinite-scroll.d.ts.map +1 -0
- package/build-types/components/dataviews-picker-footer/index.d.ts.map +1 -1
- package/build-types/components/dataviews-search/index.d.ts.map +1 -1
- package/build-types/components/dataviews-selection-checkbox/index.d.ts.map +1 -1
- package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
- package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts +1 -1
- package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts.map +1 -1
- package/build-types/dataviews/index.d.ts +0 -1
- package/build-types/dataviews/index.d.ts.map +1 -1
- package/build-types/dataviews/stories/empty.d.ts +1 -2
- package/build-types/dataviews/stories/empty.d.ts.map +1 -1
- package/build-types/dataviews/stories/fixtures.d.ts.map +1 -1
- package/build-types/dataviews/stories/free-composition.d.ts.map +1 -1
- package/build-types/dataviews/stories/index.story.d.ts +18 -10
- package/build-types/dataviews/stories/index.story.d.ts.map +1 -1
- package/build-types/dataviews/stories/infinite-scroll.d.ts.map +1 -1
- package/build-types/dataviews/stories/layout-activity.d.ts.map +1 -1
- package/build-types/dataviews/stories/layout-custom.d.ts +3 -1
- package/build-types/dataviews/stories/layout-custom.d.ts.map +1 -1
- package/build-types/dataviews/stories/layout-grid.d.ts.map +1 -1
- package/build-types/dataviews/stories/layout-list.d.ts.map +1 -1
- package/build-types/dataviews/stories/layout-table.d.ts.map +1 -1
- package/build-types/dataviews/stories/with-card.d.ts +3 -1
- package/build-types/dataviews/stories/with-card.d.ts.map +1 -1
- package/build-types/dataviews-picker/index.d.ts +0 -1
- package/build-types/dataviews-picker/index.d.ts.map +1 -1
- package/build-types/dataviews-picker/stories/fixtures.d.ts.map +1 -1
- package/build-types/dataviews-picker/stories/index.story.d.ts.map +1 -1
- package/build-types/field-types/stories/index.story.d.ts.map +1 -1
- package/build-types/hooks/index.d.ts +3 -0
- package/build-types/hooks/index.d.ts.map +1 -1
- package/build-types/hooks/test/use-data.d.ts +2 -0
- package/build-types/hooks/test/use-data.d.ts.map +1 -0
- package/build-types/hooks/use-data.d.ts +41 -3
- package/build-types/hooks/use-data.d.ts.map +1 -1
- package/build-types/hooks/use-infinite-scroll.d.ts +21 -0
- package/build-types/hooks/use-infinite-scroll.d.ts.map +1 -0
- package/build-types/hooks/use-selected-items.d.ts +19 -0
- package/build-types/hooks/use-selected-items.d.ts.map +1 -0
- package/build-types/types/dataviews.d.ts +15 -1
- package/build-types/types/dataviews.d.ts.map +1 -1
- package/build-types/types/field-api.d.ts +15 -4
- package/build-types/types/field-api.d.ts.map +1 -1
- package/build-types/utils/filter-sort-and-paginate.d.ts.map +1 -1
- package/build-types/utils/get-footer-message.d.ts +3 -2
- package/build-types/utils/get-footer-message.d.ts.map +1 -1
- package/build-wp/index.js +3202 -2761
- package/package.json +19 -19
- package/src/components/dataform-controls/datetime.tsx +19 -11
- package/src/components/dataform-layouts/card/index.tsx +171 -146
- package/src/components/dataform-layouts/card/style.scss +8 -5
- package/src/components/dataviews-bulk-actions/index.tsx +28 -1
- package/src/components/dataviews-context/index.ts +2 -2
- package/src/components/dataviews-footer/index.tsx +1 -6
- package/src/components/dataviews-layout/index.tsx +41 -19
- package/src/components/dataviews-layout/style.scss +8 -0
- package/src/components/dataviews-layouts/grid/composite-grid.tsx +433 -278
- package/src/components/dataviews-layouts/grid/style.scss +22 -2
- package/src/components/dataviews-layouts/index.ts +3 -3
- package/src/components/dataviews-layouts/picker-grid/index.tsx +70 -17
- package/src/components/dataviews-layouts/picker-grid/style.scss +10 -0
- package/src/components/dataviews-layouts/picker-table/index.tsx +42 -22
- package/src/components/dataviews-layouts/table/index.tsx +10 -4
- package/src/components/dataviews-layouts/table/style.scss +13 -0
- package/src/components/dataviews-layouts/table/use-scroll-state.ts +79 -0
- package/src/components/dataviews-layouts/utils/density-picker.tsx +12 -2
- package/src/components/dataviews-layouts/utils/grid-config-options.tsx +14 -0
- package/src/components/dataviews-layouts/utils/grid-items.scss +9 -1
- package/src/components/dataviews-layouts/utils/use-infinite-scroll.ts +64 -0
- package/src/components/dataviews-picker-footer/index.tsx +21 -1
- package/src/components/dataviews-search/index.tsx +2 -1
- package/src/components/dataviews-selection-checkbox/index.tsx +4 -2
- package/src/components/dataviews-view-config/index.tsx +0 -2
- package/src/components/dataviews-view-config/infinite-scroll-toggle.tsx +0 -5
- package/src/dataviews/index.tsx +58 -45
- package/src/dataviews/stories/empty.tsx +1 -3
- package/src/dataviews/stories/fixtures.tsx +288 -0
- package/src/dataviews/stories/free-composition.tsx +44 -45
- package/src/dataviews/stories/index.story.tsx +31 -8
- package/src/dataviews/stories/infinite-scroll.tsx +7 -93
- package/src/dataviews/stories/layout-activity.tsx +1 -0
- package/src/dataviews/stories/layout-custom.tsx +7 -3
- package/src/dataviews/stories/layout-grid.tsx +1 -0
- package/src/dataviews/stories/layout-list.tsx +1 -0
- package/src/dataviews/stories/layout-table.tsx +1 -0
- package/src/dataviews/stories/style.css +0 -5
- package/src/dataviews/stories/with-card.tsx +35 -24
- package/src/dataviews/style.scss +5 -8
- package/src/dataviews/test/dataviews.tsx +54 -1
- package/src/dataviews-picker/index.tsx +41 -35
- package/src/dataviews-picker/stories/fixtures.tsx +270 -0
- package/src/dataviews-picker/stories/index.story.tsx +62 -129
- package/src/field-types/stories/index.story.tsx +12 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/test/use-data.ts +791 -0
- package/src/hooks/use-data.ts +288 -21
- package/src/hooks/use-infinite-scroll.ts +304 -0
- package/src/hooks/use-selected-items.ts +72 -0
- package/src/style.scss +1 -0
- package/src/types/dataviews.ts +18 -1
- package/src/types/field-api.ts +16 -3
- package/src/utils/filter-sort-and-paginate.ts +13 -1
- package/src/utils/get-footer-message.ts +12 -9
- package/src/utils/test/filter-sort-and-paginate.js +78 -54
- package/build/components/dataviews-layouts/table/use-is-horizontal-scroll-end.cjs.map +0 -7
- package/build-module/components/dataviews-layouts/table/use-is-horizontal-scroll-end.mjs +0 -50
- package/build-module/components/dataviews-layouts/table/use-is-horizontal-scroll-end.mjs.map +0 -7
- package/build-types/components/dataviews-layouts/table/use-is-horizontal-scroll-end.d.ts +0 -19
- package/build-types/components/dataviews-layouts/table/use-is-horizontal-scroll-end.d.ts.map +0 -1
- package/src/components/dataviews-layouts/table/use-is-horizontal-scroll-end.ts +0 -82
package/src/hooks/use-data.ts
CHANGED
|
@@ -1,45 +1,312 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { useEffect,
|
|
4
|
+
import { useState, useEffect, useMemo, useRef } from '@wordpress/element';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import type { View } from '../types';
|
|
5
10
|
|
|
6
11
|
type PaginationInfo = {
|
|
7
12
|
totalItems: number;
|
|
8
13
|
totalPages: number;
|
|
9
|
-
infiniteScrollHandler?: () => void;
|
|
10
14
|
};
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
isLoading: boolean | undefined,
|
|
15
|
-
paginationInfo: PaginationInfo
|
|
16
|
-
): {
|
|
16
|
+
interface UseDataParams< Item > {
|
|
17
|
+
view: View;
|
|
17
18
|
data: Item[];
|
|
19
|
+
getItemId: ( item: Item ) => string;
|
|
20
|
+
isLoading?: boolean;
|
|
21
|
+
paginationInfo: PaginationInfo;
|
|
22
|
+
selection?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface UseDataResult< Item > {
|
|
26
|
+
data: ( Item & { position?: number } )[];
|
|
18
27
|
paginationInfo: PaginationInfo;
|
|
19
28
|
hasInitiallyLoaded: boolean;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
setVisibleEntries?: React.Dispatch< React.SetStateAction< number[] > >;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hook to manage data for DataViews.
|
|
34
|
+
*
|
|
35
|
+
* When infinite scroll is enabled, this hook handles:
|
|
36
|
+
* - Loading more data when scrolling up or down
|
|
37
|
+
* - Maintaining stable positions for items
|
|
38
|
+
* - Unloading items that are no longer visible (with a buffer)
|
|
39
|
+
*
|
|
40
|
+
* When infinite scroll is disabled, it preserves the previous data and
|
|
41
|
+
* pagination info while loading, so the UI doesn't flash empty.
|
|
42
|
+
*
|
|
43
|
+
* In both cases, it tracks whether data has initially loaded.
|
|
44
|
+
*
|
|
45
|
+
* @param params - Configuration parameters
|
|
46
|
+
* @param params.view - Current view configuration
|
|
47
|
+
* @param params.data - Current page of data
|
|
48
|
+
* @param params.getItemId - Function to extract item ID
|
|
49
|
+
* @param params.isLoading - Whether data is currently loading
|
|
50
|
+
* @param params.paginationInfo - Pagination info (totalItems, totalPages)
|
|
51
|
+
* @param params.selection - Currently selected item IDs
|
|
52
|
+
* @return Object containing data, paginationInfo, hasInitiallyLoaded,
|
|
53
|
+
* and optional setVisibleEntries callback
|
|
54
|
+
*/
|
|
55
|
+
export default function useData< Item >( {
|
|
56
|
+
view,
|
|
57
|
+
data: shownData,
|
|
58
|
+
getItemId,
|
|
59
|
+
isLoading,
|
|
60
|
+
paginationInfo,
|
|
61
|
+
selection,
|
|
62
|
+
}: UseDataParams< Item > ): UseDataResult< Item > {
|
|
63
|
+
const isInfiniteScrollEnabled = view.infiniteScrollEnabled;
|
|
64
|
+
|
|
24
65
|
const [ hasInitiallyLoaded, setHasInitiallyLoaded ] = useState(
|
|
25
66
|
! isLoading
|
|
26
67
|
);
|
|
27
68
|
useEffect( () => {
|
|
28
69
|
if ( ! isLoading ) {
|
|
29
|
-
previousDataRef.current = data;
|
|
30
|
-
previousPaginationInfoRef.current = paginationInfo;
|
|
31
70
|
setHasInitiallyLoaded( true );
|
|
32
71
|
}
|
|
33
|
-
}, [
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
}, [ isLoading ] );
|
|
73
|
+
|
|
74
|
+
const previousDataRef = useRef< Item[] >( shownData );
|
|
75
|
+
const previousPaginationInfoRef =
|
|
76
|
+
useRef< PaginationInfo >( paginationInfo );
|
|
77
|
+
useEffect( () => {
|
|
78
|
+
if ( ! isLoading ) {
|
|
79
|
+
previousDataRef.current = shownData;
|
|
80
|
+
previousPaginationInfoRef.current = paginationInfo;
|
|
81
|
+
}
|
|
82
|
+
}, [ shownData, isLoading, paginationInfo ] );
|
|
83
|
+
|
|
84
|
+
// Infinite scroll state.
|
|
85
|
+
const [ visibleEntries, setVisibleEntries ] = useState< number[] >( [] );
|
|
86
|
+
|
|
87
|
+
// Track the mapping of item IDs to their positions in the full dataset
|
|
88
|
+
const positionMapRef = useRef< Map< string, number > >( new Map() );
|
|
89
|
+
|
|
90
|
+
// Store accumulated records in a ref for persistence across renders
|
|
91
|
+
const allLoadedRecordsRef = useRef< Item[] >( [] );
|
|
92
|
+
|
|
93
|
+
// Track previous view parameters to detect when we need to reset
|
|
94
|
+
const prevViewParamsRef = useRef< {
|
|
95
|
+
search: string | undefined;
|
|
96
|
+
filters: string | undefined;
|
|
97
|
+
perPage: number | undefined;
|
|
98
|
+
} >( {
|
|
99
|
+
search: undefined,
|
|
100
|
+
filters: undefined,
|
|
101
|
+
perPage: undefined,
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
// Determine scroll direction based on position changes
|
|
105
|
+
const scrollDirectionRef = useRef< 'up' | 'down' | undefined >( undefined );
|
|
106
|
+
const prevStartPositionRef = useRef< number | undefined >( undefined );
|
|
107
|
+
|
|
108
|
+
// Track whether we've done initial load
|
|
109
|
+
const hasInitializedRef = useRef( false );
|
|
110
|
+
|
|
111
|
+
// Compute data synchronously during render using useMemo
|
|
112
|
+
// This ensures the returned data is always in sync with shownData
|
|
113
|
+
const allLoadedRecords = useMemo( () => {
|
|
114
|
+
// Update scroll direction based on position changes
|
|
115
|
+
if (
|
|
116
|
+
view.startPosition !== undefined &&
|
|
117
|
+
prevStartPositionRef.current !== undefined
|
|
118
|
+
) {
|
|
119
|
+
if ( view.startPosition < prevStartPositionRef.current ) {
|
|
120
|
+
scrollDirectionRef.current = 'up';
|
|
121
|
+
} else if ( view.startPosition > prevStartPositionRef.current ) {
|
|
122
|
+
scrollDirectionRef.current = 'down';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
prevStartPositionRef.current = view.startPosition;
|
|
126
|
+
|
|
127
|
+
// Serialize filters for comparison
|
|
128
|
+
const currentFiltersKey = JSON.stringify( view.filters ?? [] );
|
|
129
|
+
const prevFiltersKey = prevViewParamsRef.current.filters;
|
|
130
|
+
|
|
131
|
+
// Check if view parameters that require a reset have changed
|
|
132
|
+
const shouldReset =
|
|
133
|
+
! hasInitializedRef.current ||
|
|
134
|
+
! view.infiniteScrollEnabled ||
|
|
135
|
+
view.search !== prevViewParamsRef.current.search ||
|
|
136
|
+
currentFiltersKey !== prevFiltersKey ||
|
|
137
|
+
view.perPage !== prevViewParamsRef.current.perPage;
|
|
138
|
+
hasInitializedRef.current = true;
|
|
139
|
+
// Update tracked view parameters
|
|
140
|
+
prevViewParamsRef.current = {
|
|
141
|
+
search: view.search,
|
|
142
|
+
filters: currentFiltersKey,
|
|
143
|
+
perPage: view.perPage,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if ( shouldReset ) {
|
|
147
|
+
// Reset - clear position map and replace all data
|
|
148
|
+
positionMapRef.current.clear();
|
|
149
|
+
// Reset scroll direction to prevent stale directional filtering
|
|
150
|
+
scrollDirectionRef.current = undefined;
|
|
151
|
+
// Use the view's startPosition if defined, otherwise default to 1
|
|
152
|
+
const startPosition = view.search ? 1 : view.startPosition ?? 1;
|
|
153
|
+
const records = shownData.map( ( record, index ) => {
|
|
154
|
+
const position = startPosition + index;
|
|
155
|
+
positionMapRef.current.set( getItemId( record ), position );
|
|
156
|
+
return {
|
|
157
|
+
...record,
|
|
158
|
+
position,
|
|
159
|
+
};
|
|
160
|
+
} );
|
|
161
|
+
allLoadedRecordsRef.current = records;
|
|
162
|
+
return records;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Subsequent pages - merge with existing data
|
|
166
|
+
const prev = allLoadedRecordsRef.current;
|
|
167
|
+
const shownDataIds = new Set( shownData.map( getItemId ) );
|
|
168
|
+
const scrollDirection = scrollDirectionRef.current;
|
|
169
|
+
|
|
170
|
+
// The position for each item in shownData should be based on the
|
|
171
|
+
// current startPosition from the view, which reflects the actual
|
|
172
|
+
// offset in the dataset. This ensures aria-posinset values are
|
|
173
|
+
// semantically correct - if startPosition is 40, there are exactly
|
|
174
|
+
// 39 items before the first item in shownData.
|
|
175
|
+
// When there's an active search, always start from position 1 since
|
|
176
|
+
// search results are a filtered subset, not a paginated view.
|
|
177
|
+
const basePosition = view.search ? 1 : view.startPosition ?? 1;
|
|
178
|
+
const newRecords = shownData.map( ( record, index ) => {
|
|
179
|
+
const itemId = getItemId( record );
|
|
180
|
+
const position = view.infiniteScrollEnabled
|
|
181
|
+
? basePosition + index
|
|
182
|
+
: undefined;
|
|
183
|
+
|
|
184
|
+
// Always update the position map with the correct position
|
|
185
|
+
// based on the current query's startPosition
|
|
186
|
+
if ( position !== undefined ) {
|
|
187
|
+
positionMapRef.current.set( itemId, position );
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...record,
|
|
192
|
+
position,
|
|
193
|
+
};
|
|
194
|
+
} );
|
|
195
|
+
|
|
196
|
+
if ( newRecords.length === 0 ) {
|
|
197
|
+
return prev;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Remove duplicates from prev, keeping only records not in shownData
|
|
201
|
+
const prevWithoutDuplicates = prev.filter(
|
|
202
|
+
( record ) => ! shownDataIds.has( getItemId( record ) )
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Update the loaded range
|
|
206
|
+
const allRecords =
|
|
207
|
+
scrollDirection === 'up'
|
|
208
|
+
? [ ...newRecords, ...prevWithoutDuplicates ]
|
|
209
|
+
: [ ...prevWithoutDuplicates, ...newRecords ];
|
|
210
|
+
|
|
211
|
+
// Sort all records by position to ensure correct order
|
|
212
|
+
// This is crucial when items are reloaded after scrolling in different directions
|
|
213
|
+
allRecords.sort( ( a, b ) => {
|
|
214
|
+
const posA = ( a as Item & { position: number } ).position;
|
|
215
|
+
const posB = ( b as Item & { position: number } ).position;
|
|
216
|
+
return posA - posB;
|
|
217
|
+
} );
|
|
218
|
+
|
|
219
|
+
let result = allRecords;
|
|
220
|
+
|
|
221
|
+
if ( visibleEntries.length > 0 ) {
|
|
222
|
+
const visibleMin = Math.min( ...visibleEntries );
|
|
223
|
+
const visibleMax = Math.max( ...visibleEntries );
|
|
224
|
+
// Buffer size balances allowing new items to render (when prepended
|
|
225
|
+
// during scroll up) while unloading items no longer on screen.
|
|
226
|
+
// Use a larger buffer to prevent scrollbar from jumping when items
|
|
227
|
+
// are unloaded, which could trigger unwanted scroll events.
|
|
228
|
+
const buffer = 20;
|
|
229
|
+
|
|
230
|
+
const recordPositions = allRecords.map(
|
|
231
|
+
( r ) => ( r as Item & { position: number } ).position
|
|
232
|
+
);
|
|
233
|
+
const minRecordPos = Math.min( ...recordPositions );
|
|
234
|
+
const maxRecordPos = Math.max( ...recordPositions );
|
|
235
|
+
|
|
236
|
+
// Check if there's any overlap between visible range and actual record positions
|
|
237
|
+
// to avoid filtering when visibleEntries are stale (e.g., after search/filter reset)
|
|
238
|
+
const hasOverlap = ! (
|
|
239
|
+
maxRecordPos < visibleMin - buffer ||
|
|
240
|
+
minRecordPos > visibleMax + buffer
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if ( hasOverlap ) {
|
|
244
|
+
result = allRecords.filter( ( record ) => {
|
|
245
|
+
const itemId = getItemId( record );
|
|
246
|
+
const isSelected = selection?.includes( itemId );
|
|
247
|
+
// Never unload selected items, even if outside visible range
|
|
248
|
+
if ( isSelected ) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const itemPosition = (
|
|
253
|
+
record as Item & { position: number }
|
|
254
|
+
).position;
|
|
255
|
+
// When scrolling, only trim items from the end we're scrolling away from
|
|
256
|
+
if ( scrollDirection === 'up' ) {
|
|
257
|
+
// When scrolling up, only trim items below the visible range
|
|
258
|
+
return itemPosition <= visibleMax + buffer;
|
|
259
|
+
} else if ( scrollDirection === 'down' ) {
|
|
260
|
+
// When scrolling down, only trim items above the visible range
|
|
261
|
+
return itemPosition >= visibleMin - buffer;
|
|
262
|
+
}
|
|
263
|
+
// When not scrolling or first load, keep items within buffer range
|
|
264
|
+
return (
|
|
265
|
+
itemPosition >= visibleMin - buffer &&
|
|
266
|
+
itemPosition <= visibleMax + buffer
|
|
267
|
+
);
|
|
268
|
+
} );
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
allLoadedRecordsRef.current = result;
|
|
273
|
+
return result;
|
|
274
|
+
}, [
|
|
275
|
+
shownData,
|
|
276
|
+
view.search,
|
|
277
|
+
view.filters,
|
|
278
|
+
view.perPage,
|
|
279
|
+
view.startPosition,
|
|
280
|
+
view.infiniteScrollEnabled,
|
|
281
|
+
visibleEntries,
|
|
282
|
+
selection,
|
|
283
|
+
getItemId,
|
|
284
|
+
] );
|
|
285
|
+
|
|
286
|
+
// When infinite scroll is disabled, preserve previous data while loading
|
|
287
|
+
if ( ! isInfiniteScrollEnabled ) {
|
|
288
|
+
const dataToReturn =
|
|
36
289
|
isLoading && previousDataRef.current?.length
|
|
37
290
|
? previousDataRef.current
|
|
38
|
-
:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
:
|
|
291
|
+
: shownData;
|
|
292
|
+
return {
|
|
293
|
+
data: dataToReturn.map( ( item ) => ( {
|
|
294
|
+
...item,
|
|
295
|
+
position: undefined,
|
|
296
|
+
} ) ) as ( Item & { position?: number } )[],
|
|
297
|
+
paginationInfo:
|
|
298
|
+
isLoading && previousDataRef.current?.length
|
|
299
|
+
? previousPaginationInfoRef.current
|
|
300
|
+
: paginationInfo,
|
|
301
|
+
hasInitiallyLoaded,
|
|
302
|
+
setVisibleEntries: undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
data: allLoadedRecords as ( Item & { position?: number } )[],
|
|
308
|
+
paginationInfo,
|
|
43
309
|
hasInitiallyLoaded,
|
|
310
|
+
setVisibleEntries,
|
|
44
311
|
};
|
|
45
312
|
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
} from '@wordpress/element';
|
|
10
|
+
import { throttle } from '@wordpress/compose';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies
|
|
14
|
+
*/
|
|
15
|
+
import type { View } from '../types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Captures an anchor element for scroll position preservation.
|
|
19
|
+
* Finds the element closest to the center of the viewport and stores its position.
|
|
20
|
+
*
|
|
21
|
+
* @param container The scrollable container element.
|
|
22
|
+
* @param anchorElementRef Ref to store the anchor element data.
|
|
23
|
+
* @param direction The scroll direction ('up' or 'down').
|
|
24
|
+
* @return Whether an anchor element was successfully captured.
|
|
25
|
+
*/
|
|
26
|
+
function captureAnchorElement(
|
|
27
|
+
container: HTMLElement,
|
|
28
|
+
anchorElementRef: React.MutableRefObject< {
|
|
29
|
+
posinset: number;
|
|
30
|
+
viewportOffset: number;
|
|
31
|
+
direction: 'up' | 'down' | null;
|
|
32
|
+
} | null >,
|
|
33
|
+
direction: 'up' | 'down'
|
|
34
|
+
): boolean {
|
|
35
|
+
// Find a visible element to use as anchor - prefer one in the middle of the viewport
|
|
36
|
+
const containerRect = container.getBoundingClientRect();
|
|
37
|
+
const centerY = containerRect.top + containerRect.height / 2;
|
|
38
|
+
|
|
39
|
+
// Query all items with aria-posinset and find the one closest to center
|
|
40
|
+
const items = Array.from( container.querySelectorAll( '[aria-posinset]' ) );
|
|
41
|
+
|
|
42
|
+
if ( items.length === 0 ) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Find the item closest to the center of the viewport
|
|
47
|
+
const bestAnchor = items.reduce( ( best, item ) => {
|
|
48
|
+
const itemRect = item.getBoundingClientRect();
|
|
49
|
+
const itemCenterY = itemRect.top + itemRect.height / 2;
|
|
50
|
+
const distance = Math.abs( itemCenterY - centerY );
|
|
51
|
+
|
|
52
|
+
const bestRect = best.getBoundingClientRect();
|
|
53
|
+
const bestCenterY = bestRect.top + bestRect.height / 2;
|
|
54
|
+
const bestDistance = Math.abs( bestCenterY - centerY );
|
|
55
|
+
|
|
56
|
+
return distance < bestDistance ? item : best;
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
const posinset = Number( bestAnchor.getAttribute( 'aria-posinset' ) );
|
|
60
|
+
const anchorRect = bestAnchor.getBoundingClientRect();
|
|
61
|
+
anchorElementRef.current = {
|
|
62
|
+
posinset,
|
|
63
|
+
viewportOffset: anchorRect.top - containerRect.top,
|
|
64
|
+
direction,
|
|
65
|
+
};
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type UseInfiniteScrollProps = {
|
|
70
|
+
view: View;
|
|
71
|
+
onChangeView: ( view: View ) => void;
|
|
72
|
+
isLoading: boolean;
|
|
73
|
+
paginationInfo: {
|
|
74
|
+
totalItems: number;
|
|
75
|
+
totalPages: number;
|
|
76
|
+
};
|
|
77
|
+
containerRef: React.MutableRefObject< HTMLDivElement | null >;
|
|
78
|
+
setVisibleEntries?: React.Dispatch< React.SetStateAction< number[] > >;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type UseInfiniteScrollResult = {
|
|
82
|
+
intersectionObserver?: IntersectionObserver | null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function useInfiniteScroll( {
|
|
86
|
+
view,
|
|
87
|
+
onChangeView,
|
|
88
|
+
isLoading,
|
|
89
|
+
paginationInfo,
|
|
90
|
+
containerRef,
|
|
91
|
+
setVisibleEntries,
|
|
92
|
+
}: UseInfiniteScrollProps ): UseInfiniteScrollResult {
|
|
93
|
+
// Track an anchor element for scroll position preservation
|
|
94
|
+
// This approach is robust even when items are added/removed from both ends simultaneously
|
|
95
|
+
const anchorElementRef = useRef< {
|
|
96
|
+
posinset: number;
|
|
97
|
+
viewportOffset: number;
|
|
98
|
+
direction: 'up' | 'down' | null;
|
|
99
|
+
} | null >( null );
|
|
100
|
+
const viewRef = useRef( view );
|
|
101
|
+
const isLoadingRef = useRef( isLoading );
|
|
102
|
+
const onChangeViewRef = useRef( onChangeView );
|
|
103
|
+
const totalItemsRef = useRef( paginationInfo.totalItems );
|
|
104
|
+
|
|
105
|
+
useLayoutEffect( () => {
|
|
106
|
+
viewRef.current = view;
|
|
107
|
+
isLoadingRef.current = isLoading;
|
|
108
|
+
onChangeViewRef.current = onChangeView;
|
|
109
|
+
totalItemsRef.current = paginationInfo.totalItems;
|
|
110
|
+
}, [ view, isLoading, onChangeView, paginationInfo.totalItems ] );
|
|
111
|
+
|
|
112
|
+
const intersectionObserverCallback: IntersectionObserverCallback =
|
|
113
|
+
useCallback(
|
|
114
|
+
( entries: IntersectionObserverEntry[] ) => {
|
|
115
|
+
// Calculate new visible entries outside of setState
|
|
116
|
+
if ( ! setVisibleEntries ) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
setVisibleEntries( ( prev: number[] ) => {
|
|
120
|
+
const newVisibleEntries = new Set( prev );
|
|
121
|
+
let hasChanged = false;
|
|
122
|
+
|
|
123
|
+
entries.forEach( ( entry ) => {
|
|
124
|
+
const posInSet = Number(
|
|
125
|
+
entry.target?.attributes?.getNamedItem(
|
|
126
|
+
'aria-posinset'
|
|
127
|
+
)?.value
|
|
128
|
+
);
|
|
129
|
+
if ( isNaN( posInSet ) ) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if ( entry.isIntersecting ) {
|
|
133
|
+
if ( ! newVisibleEntries.has( posInSet ) ) {
|
|
134
|
+
newVisibleEntries.add( posInSet );
|
|
135
|
+
hasChanged = true;
|
|
136
|
+
}
|
|
137
|
+
} else if ( newVisibleEntries.has( posInSet ) ) {
|
|
138
|
+
newVisibleEntries.delete( posInSet );
|
|
139
|
+
hasChanged = true;
|
|
140
|
+
}
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
// Only return new array if something actually changed
|
|
144
|
+
return hasChanged
|
|
145
|
+
? Array.from( newVisibleEntries ).sort()
|
|
146
|
+
: prev;
|
|
147
|
+
} );
|
|
148
|
+
},
|
|
149
|
+
[ setVisibleEntries ]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Preserve scroll position when items are added or removed during infinite scroll
|
|
153
|
+
// Uses anchor element approach: find the same element after render and restore its viewport position
|
|
154
|
+
useLayoutEffect( () => {
|
|
155
|
+
const container = containerRef.current;
|
|
156
|
+
const anchor = anchorElementRef.current;
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
! container ||
|
|
160
|
+
! view.infiniteScrollEnabled ||
|
|
161
|
+
! anchor ||
|
|
162
|
+
isLoading
|
|
163
|
+
) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Find the anchor element by its posinset
|
|
168
|
+
const anchorElement = container.querySelector(
|
|
169
|
+
`[aria-posinset="${ anchor.posinset }"]`
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if ( anchorElement ) {
|
|
173
|
+
const containerRect = container.getBoundingClientRect();
|
|
174
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
175
|
+
const currentOffset = anchorRect.top - containerRect.top;
|
|
176
|
+
|
|
177
|
+
// Calculate how much the anchor has moved and adjust scroll to compensate
|
|
178
|
+
const scrollAdjustment = currentOffset - anchor.viewportOffset;
|
|
179
|
+
|
|
180
|
+
if ( Math.abs( scrollAdjustment ) > 1 ) {
|
|
181
|
+
container.scrollTop += scrollAdjustment;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Reset the anchor state now that we've adjusted
|
|
186
|
+
anchorElementRef.current = null;
|
|
187
|
+
}, [ containerRef, isLoading, view.infiniteScrollEnabled ] );
|
|
188
|
+
|
|
189
|
+
// Create and expose a shared IntersectionObserver for provider-level reuse.
|
|
190
|
+
const intersectionObserverRef = useRef< IntersectionObserver | null >(
|
|
191
|
+
null
|
|
192
|
+
);
|
|
193
|
+
useEffect( () => {
|
|
194
|
+
if ( ! view.infiniteScrollEnabled || ! intersectionObserverCallback ) {
|
|
195
|
+
if ( intersectionObserverRef.current ) {
|
|
196
|
+
intersectionObserverRef.current.disconnect();
|
|
197
|
+
intersectionObserverRef.current = null;
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
intersectionObserverRef.current = new IntersectionObserver(
|
|
203
|
+
intersectionObserverCallback,
|
|
204
|
+
{ root: null, rootMargin: '0px', threshold: 0.1 }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return () => {
|
|
208
|
+
if ( intersectionObserverRef.current ) {
|
|
209
|
+
intersectionObserverRef.current.disconnect();
|
|
210
|
+
intersectionObserverRef.current = null;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}, [ view.infiniteScrollEnabled, intersectionObserverCallback ] );
|
|
214
|
+
|
|
215
|
+
// Attach scroll event listener for infinite scroll
|
|
216
|
+
useEffect( () => {
|
|
217
|
+
if ( ! view.infiniteScrollEnabled || ! containerRef.current ) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let lastScrollTop = 0;
|
|
222
|
+
// Use larger thresholds to trigger loading earlier during fast scrolling
|
|
223
|
+
const BOTTOM_THRESHOLD = 600; // px from bottom to trigger load
|
|
224
|
+
const TOP_THRESHOLD = 800; // px from top to trigger load
|
|
225
|
+
|
|
226
|
+
const handleScroll = throttle( ( event: unknown ) => {
|
|
227
|
+
const currentView = viewRef.current;
|
|
228
|
+
const totalItems = totalItemsRef.current;
|
|
229
|
+
const target = ( event as Event ).target as HTMLElement;
|
|
230
|
+
const scrollTop = target.scrollTop;
|
|
231
|
+
const scrollHeight = target.scrollHeight;
|
|
232
|
+
const clientHeight = target.clientHeight;
|
|
233
|
+
|
|
234
|
+
// Determine scroll direction
|
|
235
|
+
const scrollDirection = scrollTop > lastScrollTop ? 'down' : 'up';
|
|
236
|
+
lastScrollTop = scrollTop;
|
|
237
|
+
|
|
238
|
+
// Don't trigger if already loading
|
|
239
|
+
if ( isLoadingRef.current ) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const currentStartPosition = currentView.startPosition || 1;
|
|
244
|
+
const batchSize = currentView.perPage || 10;
|
|
245
|
+
const currentEndPosition = Math.min(
|
|
246
|
+
currentStartPosition + batchSize,
|
|
247
|
+
totalItems
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Check if user has scrolled near the bottom
|
|
251
|
+
if (
|
|
252
|
+
scrollDirection === 'down' &&
|
|
253
|
+
scrollTop + clientHeight >= scrollHeight - BOTTOM_THRESHOLD
|
|
254
|
+
) {
|
|
255
|
+
// Check if there's more data to load
|
|
256
|
+
if ( currentEndPosition < totalItems ) {
|
|
257
|
+
const newStartPosition = currentEndPosition;
|
|
258
|
+
|
|
259
|
+
// Capture anchor element for scroll position preservation
|
|
260
|
+
captureAnchorElement( target, anchorElementRef, 'down' );
|
|
261
|
+
|
|
262
|
+
onChangeViewRef.current( {
|
|
263
|
+
...currentView,
|
|
264
|
+
startPosition: newStartPosition,
|
|
265
|
+
} );
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check if user has scrolled near the top
|
|
270
|
+
if ( scrollDirection === 'up' && scrollTop <= TOP_THRESHOLD ) {
|
|
271
|
+
// Check if there's more data to load
|
|
272
|
+
if ( currentStartPosition > 1 ) {
|
|
273
|
+
// Round to 1 if we're close to the beginning to avoid tiny batches
|
|
274
|
+
const calculatedStartPosition =
|
|
275
|
+
currentStartPosition - batchSize;
|
|
276
|
+
const newStartPosition =
|
|
277
|
+
calculatedStartPosition < 6
|
|
278
|
+
? 1
|
|
279
|
+
: calculatedStartPosition;
|
|
280
|
+
|
|
281
|
+
// Capture anchor element for scroll position preservation
|
|
282
|
+
captureAnchorElement( target, anchorElementRef, 'up' );
|
|
283
|
+
|
|
284
|
+
onChangeViewRef.current( {
|
|
285
|
+
...currentView,
|
|
286
|
+
startPosition: newStartPosition,
|
|
287
|
+
} );
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}, 50 ); // Faster throttle (50ms) for better response to fast scrolling
|
|
291
|
+
|
|
292
|
+
const container = containerRef.current;
|
|
293
|
+
container.addEventListener( 'scroll', handleScroll );
|
|
294
|
+
|
|
295
|
+
return () => {
|
|
296
|
+
container.removeEventListener( 'scroll', handleScroll );
|
|
297
|
+
handleScroll.cancel(); // Cancel any pending throttled calls
|
|
298
|
+
};
|
|
299
|
+
}, [ containerRef, view.infiniteScrollEnabled ] );
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
intersectionObserver: intersectionObserverRef.current,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useMemo, useRef } from '@wordpress/element';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import type { View } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook to get selected items, with support for infinite scroll.
|
|
13
|
+
*
|
|
14
|
+
* When infinite scroll is enabled, items that scroll out of view are cached
|
|
15
|
+
* so they remain available for bulk actions even when not in the current data set.
|
|
16
|
+
*
|
|
17
|
+
* @param view The current view configuration.
|
|
18
|
+
* @param data The current page of data items.
|
|
19
|
+
* @param selection Array of selected item IDs.
|
|
20
|
+
* @param getItemId Function to get the ID of an item.
|
|
21
|
+
* @param filterFn Optional filter function to apply to selected items (e.g., for selectability).
|
|
22
|
+
* @return Array of selected items.
|
|
23
|
+
*/
|
|
24
|
+
export default function useSelectedItems< Item >(
|
|
25
|
+
view: View,
|
|
26
|
+
data: Item[],
|
|
27
|
+
selection: string[],
|
|
28
|
+
getItemId: ( item: Item ) => string,
|
|
29
|
+
filterFn?: ( item: Item ) => boolean
|
|
30
|
+
): Item[] {
|
|
31
|
+
// With infinite scroll enabled items scroll out of view, but we want to keep the selection unaltered,
|
|
32
|
+
// unlike page-based navigation where we might clear selection upon navigating to a different page.
|
|
33
|
+
const selectedItemsCacheRef = useRef< Map< string, Item > >( new Map() );
|
|
34
|
+
|
|
35
|
+
return useMemo( () => {
|
|
36
|
+
const selectionSet = new Set( selection );
|
|
37
|
+
|
|
38
|
+
if ( view.infiniteScrollEnabled ) {
|
|
39
|
+
// Selection array contains selected item IDs
|
|
40
|
+
// Cache selected items so they remain available when scrolled out of view
|
|
41
|
+
data.forEach( ( item ) => {
|
|
42
|
+
const id = getItemId( item );
|
|
43
|
+
if ( selectionSet.has( id ) ) {
|
|
44
|
+
const passesFilter = filterFn ? filterFn( item ) : true;
|
|
45
|
+
if ( passesFilter ) {
|
|
46
|
+
selectedItemsCacheRef.current.set( id, item );
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
// Remove items from cache that are no longer selected
|
|
52
|
+
selectedItemsCacheRef.current.forEach( ( _, id ) => {
|
|
53
|
+
if ( ! selectionSet.has( id ) ) {
|
|
54
|
+
selectedItemsCacheRef.current.delete( id );
|
|
55
|
+
}
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
// Return all cached selected items
|
|
59
|
+
return Array.from( selectedItemsCacheRef.current.values() );
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Non-infinite scroll mode
|
|
63
|
+
return data.filter( ( item ) => {
|
|
64
|
+
const id = getItemId( item );
|
|
65
|
+
if ( ! selectionSet.has( id ) ) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
// Apply optional filter (e.g., selectability check for bulk actions)
|
|
69
|
+
return filterFn ? filterFn( item ) : true;
|
|
70
|
+
} );
|
|
71
|
+
}, [ view.infiniteScrollEnabled, selection, getItemId, data, filterFn ] );
|
|
72
|
+
}
|