StickyStretchableScroller.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import type { RefObject } from 'react';
  2. import React, {
  3. type JSX,
  4. useCallback,
  5. useEffect,
  6. useMemo,
  7. useRef,
  8. useState,
  9. } from 'react';
  10. import SimpleBar from 'simplebar-react';
  11. import { debounce } from 'throttle-debounce';
  12. import { useSticky } from '~/client/services/side-effects/use-sticky';
  13. import loggerFactory from '~/utils/logger';
  14. const logger = loggerFactory('growi:cli:StickyStretchableScroller');
  15. export type StickyStretchableScrollerProps = {
  16. stickyElemSelector: string;
  17. simplebarRef?: (ref: RefObject<SimpleBar | null>) => void;
  18. calcViewHeight?: (scrollElement: HTMLElement) => number;
  19. children?: JSX.Element;
  20. };
  21. /**
  22. * USAGE:
  23. *
  24. const calcViewHeight = useCallback(() => {
  25. const containerElem = document.querySelector('#sticky-elem');
  26. const containerTop = containerElem.getBoundingClientRect().top;
  27. // stretch to the bottom of window
  28. return window.innerHeight - containerTop;
  29. });
  30. return (
  31. <StickyStretchableScroller
  32. stickyElemSelector="#sticky-elem"
  33. calcViewHeight={calcViewHeight}
  34. >
  35. <div>
  36. ...
  37. </div>
  38. </StickyStretchableScroller>
  39. );
  40. */
  41. export const StickyStretchableScroller = (
  42. props: StickyStretchableScrollerProps,
  43. ): JSX.Element => {
  44. const {
  45. children,
  46. stickyElemSelector,
  47. calcViewHeight,
  48. simplebarRef: setSimplebarRef,
  49. } = props;
  50. const simplebarRef = useRef<SimpleBar>(null);
  51. const [simplebarMaxHeight, setSimplebarMaxHeight] = useState<
  52. number | undefined
  53. >();
  54. // Get sticky status
  55. const isSticky = useSticky(stickyElemSelector);
  56. /**
  57. * Reset scrollbar
  58. */
  59. const resetScrollbar = useCallback(() => {
  60. if (simplebarRef.current == null || calcViewHeight == null) {
  61. return;
  62. }
  63. const scrollElement = simplebarRef.current.getScrollElement();
  64. const newHeight = calcViewHeight(scrollElement);
  65. logger.debug('Set new height to simplebar', newHeight);
  66. // set new height
  67. setSimplebarMaxHeight(newHeight);
  68. // reculculate
  69. simplebarRef.current.recalculate();
  70. }, [calcViewHeight]);
  71. const resetScrollbarDebounced = useMemo(
  72. () => debounce(100, resetScrollbar),
  73. [resetScrollbar],
  74. );
  75. // biome-ignore lint/correctness/useExhaustiveDependencies: ignore
  76. useEffect(() => {
  77. resetScrollbarDebounced();
  78. }, [isSticky, resetScrollbarDebounced]);
  79. // setup effect by resizing event
  80. useEffect(() => {
  81. const resizeHandler = () => {
  82. resetScrollbarDebounced();
  83. };
  84. window.addEventListener('resize', resizeHandler);
  85. // return clean up handler
  86. return () => {
  87. window.removeEventListener('resize', resizeHandler);
  88. };
  89. }, [resetScrollbarDebounced]);
  90. // setup effect on init
  91. useEffect(() => {
  92. resetScrollbarDebounced();
  93. }, [resetScrollbarDebounced]);
  94. // update ref
  95. useEffect(() => {
  96. if (setSimplebarRef != null) {
  97. setSimplebarRef(simplebarRef);
  98. }
  99. }, [setSimplebarRef]);
  100. return (
  101. <SimpleBar style={{ maxHeight: simplebarMaxHeight }} ref={simplebarRef}>
  102. {children}
  103. </SimpleBar>
  104. );
  105. };