StickyStretchableScroller.tsx 2.9 KB

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