StickyStretchableScroller.tsx 3.4 KB

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