InfiniteScroll.tsx 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import type { Ref } from 'react';
  2. import React, { useEffect, useState } from 'react';
  3. import { LoadingSpinner } from '@growi/ui/dist/components';
  4. import type { SWRInfiniteResponse } from 'swr/infinite';
  5. type Props<T> = {
  6. swrInifiniteResponse: SWRInfiniteResponse<T>
  7. children: React.ReactNode,
  8. loadingIndicator?: React.ReactNode
  9. endingIndicator?: React.ReactNode
  10. isReachingEnd?: boolean
  11. offset?: number
  12. }
  13. const useIntersection = <E extends HTMLElement>(): [boolean, Ref<E>] => {
  14. const [intersecting, setIntersecting] = useState<boolean>(false);
  15. const [element, setElement] = useState<HTMLElement>();
  16. useEffect(() => {
  17. if (element != null) {
  18. const observer = new IntersectionObserver((entries) => {
  19. setIntersecting(entries[0]?.isIntersecting);
  20. });
  21. observer.observe(element);
  22. return () => observer.unobserve(element);
  23. }
  24. return;
  25. }, [element]);
  26. return [intersecting, el => el && setElement(el)];
  27. };
  28. const LoadingIndicator = (): React.ReactElement => {
  29. return (
  30. <div className="text-muted text-center">
  31. <LoadingSpinner className="me-1 fs-3" />
  32. </div>
  33. );
  34. };
  35. const InfiniteScroll = <E, >(props: Props<E>): React.ReactElement<Props<E>> => {
  36. const {
  37. swrInifiniteResponse: {
  38. setSize, isValidating,
  39. },
  40. children,
  41. loadingIndicator,
  42. endingIndicator,
  43. isReachingEnd,
  44. offset = 0,
  45. } = props;
  46. const [intersecting, ref] = useIntersection<HTMLDivElement>();
  47. useEffect(() => {
  48. if (intersecting && !isValidating && !isReachingEnd) {
  49. setSize(size => size + 1);
  50. }
  51. }, [setSize, intersecting, isValidating, isReachingEnd]);
  52. return (
  53. <>
  54. {children}
  55. <div style={{ position: 'relative' }}>
  56. <div ref={ref} style={{ position: 'absolute', top: offset }}></div>
  57. {isReachingEnd
  58. ? endingIndicator
  59. : loadingIndicator || <LoadingIndicator />
  60. }
  61. </div>
  62. </>
  63. );
  64. };
  65. export default InfiniteScroll;