InfiniteScroll.tsx 2.0 KB

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