1 | /*
|
2 | * Copyright 2024 Adobe. All rights reserved.
|
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
4 | * you may not use this file except in compliance with the License. You may obtain a copy
|
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
|
6 | *
|
7 | * Unless required by applicable law or agreed to in writing, software distributed under
|
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
9 | * OF ANY KIND, either express or implied. See the License for the specific language
|
10 | * governing permissions and limitations under the License.
|
11 | */
|
12 |
|
13 | import {RefObject, useCallback, useRef} from 'react';
|
14 | import {useEvent} from './useEvent';
|
15 |
|
16 | import {useLayoutEffect} from './useLayoutEffect';
|
17 |
|
18 | export interface LoadMoreProps {
|
19 | /** Whether data is currently being loaded. */
|
20 | isLoading?: boolean,
|
21 | /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */
|
22 | onLoadMore?: () => void,
|
23 | /**
|
24 | * The amount of offset from the bottom of your scrollable region that should trigger load more.
|
25 | * Uses a percentage value relative to the scroll body's client height. Load more is then triggered
|
26 | * when your current scroll position's distance from the bottom of the currently loaded list of items is less than
|
27 | * or equal to the provided value. (e.g. 1 = 100% of the scroll region's height).
|
28 | * @default 1
|
29 | */
|
30 | scrollOffset?: number,
|
31 | /** The data currently loaded. */
|
32 | items?: any
|
33 | }
|
34 |
|
35 | export function useLoadMore(props: LoadMoreProps, ref: RefObject<HTMLElement | null>) {
|
36 | let {isLoading, onLoadMore, scrollOffset = 1, items} = props;
|
37 |
|
38 | // Handle scrolling, and call onLoadMore when nearing the bottom.
|
39 | let isLoadingRef = useRef(isLoading);
|
40 | let prevProps = useRef(props);
|
41 | let onScroll = useCallback(() => {
|
42 | if (ref.current && !isLoadingRef.current && onLoadMore) {
|
43 | let shouldLoadMore = ref.current.scrollHeight - ref.current.scrollTop - ref.current.clientHeight < ref.current.clientHeight * scrollOffset;
|
44 |
|
45 | if (shouldLoadMore) {
|
46 | isLoadingRef.current = true;
|
47 | onLoadMore();
|
48 | }
|
49 | }
|
50 | }, [onLoadMore, ref, scrollOffset]);
|
51 |
|
52 | let lastItems = useRef(items);
|
53 | useLayoutEffect(() => {
|
54 | // Only update isLoadingRef if props object actually changed,
|
55 | // not if a local state change occurred.
|
56 | if (props !== prevProps.current) {
|
57 | isLoadingRef.current = isLoading;
|
58 | prevProps.current = props;
|
59 | }
|
60 |
|
61 | // TODO: Eventually this hook will move back into RAC during which we will accept the collection as a option to this hook.
|
62 | // We will only load more if the collection has changed after the last load to prevent multiple onLoadMore from being called
|
63 | // while the data from the last onLoadMore is being processed by RAC collection.
|
64 | let shouldLoadMore = ref?.current
|
65 | && !isLoadingRef.current
|
66 | && onLoadMore
|
67 | && (!items || items !== lastItems.current)
|
68 | && ref.current.clientHeight === ref.current.scrollHeight;
|
69 |
|
70 | if (shouldLoadMore) {
|
71 | isLoadingRef.current = true;
|
72 | onLoadMore?.();
|
73 | }
|
74 |
|
75 | lastItems.current = items;
|
76 | }, [isLoading, onLoadMore, props, ref, items]);
|
77 |
|
78 | // TODO: maybe this should still just return scroll props?
|
79 | // Test against case where the ref isn't defined when this is called
|
80 | // Think this was a problem when trying to attach to the scrollable body of the table in OnLoadMoreTableBodyScroll
|
81 | useEvent(ref, 'scroll', onScroll);
|
82 | }
|