All files / platform/ui-next/src/hooks useDynamicMaxHeight.ts

100% Statements 24/24
100% Branches 7/7
100% Functions 7/7
100% Lines 24/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106    69x                 3724x   1706x 1706x 1595x                                                           2045x 2045x   2045x 1862x 1595x 1595x 1595x 1595x                     1862x       1862x         1862x                 1862x           1862x 1850x 1850x       1862x 1769x 1769x         2045x        
import { useRef, useState, useEffect, RefObject } from 'react';
 
const _getMovementIntersectionObserver = ({
  callback,
  rootMargin,
  threshold,
}: {
  callback: () => void;
  rootMargin: string;
  threshold: number[];
}): IntersectionObserver => {
  return new IntersectionObserver(
    entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          callback();
        }
      });
    },
    {
      threshold,
      rootMargin,
    }
  );
};
 
/**
 * Calculates the maximum height for an element based on its position
 * relative to the bottom of the viewport.
 *
 * @param data The data that, when changed, should trigger a recalculation.
 * @param buffer Optional buffer space (in pixels) to leave below the element. Defaults to 20.
 * @param minHeight Optional minimum height (in pixels) for the element. Defaults to 100.
 * @returns An object containing:
 *  - `ref`: A RefObject to attach to the target DOM element.
 *  - `maxHeight`: The calculated maximum height string (e.g., "500px").
 */
export function useDynamicMaxHeight(
  data: any,
  buffer = 20,
  minHeight = 100
): {
  ref: RefObject<HTMLDivElement>;
  maxHeight: string;
} {
  const ref = useRef<HTMLDivElement>(null);
  const [maxHeight, setMaxHeight] = useState<string>('100vh'); // Start with full viewport height initially
 
  useEffect(() => {
    const calculateMaxHeight = () => {
      if (ref.current) {
        const rect = ref.current.getBoundingClientRect();
        const availableHeight = window.innerHeight - rect.top - buffer;
        setMaxHeight(`${Math.max(minHeight, availableHeight)}px`);
      }
    };
 
    // Two intersection observers to trigger a recalculation when the target element
    // moves up or down. One for moving up and one for moving down.
    // Note that with this approach we don't need to use a resize observer nor
    // a window resize listener.
 
    // The trick is to use a margin for the IntersectionObserver to detect movement.
    // See more below.
    const rootMarginHeight = maxHeight === '100vh' ? `${window.innerHeight}px` : `${maxHeight}`;
 
    // Note that we use a fine grained threshold because we don't know how
    // much it will move and we want any movement to trigger the intersection observer.
    const threshold = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
 
    // The trick here is to use the calculated maxHeight as the root margin height
    // so that any movement of the target element down (i.e. "out of the" viewport)
    // will trigger the intersection observer.
    const moveDownIntersectionObserver = _getMovementIntersectionObserver({
      callback: calculateMaxHeight,
      rootMargin: `0px 0px ${rootMarginHeight} 0px`,
      threshold,
    });
 
    // The trick here is to use the calculated maxHeight as the negative
    // root margin height so that any movement of the target element up
    // (i.e. "into the" viewport) will trigger the intersection observer.
    const moveUpIntersectionObserver = _getMovementIntersectionObserver({
      callback: calculateMaxHeight,
      rootMargin: `0px 0px -${rootMarginHeight} 0px`,
      threshold,
    });
 
    if (ref.current) {
      moveUpIntersectionObserver.observe(ref.current);
      moveDownIntersectionObserver.observe(ref.current);
    }
 
    // Cleanup listener and requestAnimationFrame on component unmount
    return () => {
      moveUpIntersectionObserver.disconnect();
      moveDownIntersectionObserver.disconnect();
    };
    // Dependencies: buffer, minHeight, and data.
  }, [data, buffer, minHeight, maxHeight]);
 
  return { ref, maxHeight };
}
 
export default useDynamicMaxHeight;