usePassiveResizeableDrawer.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import {useCallback, useLayoutEffect, useRef} from 'react';
  2. export interface UsePassiveResizableDrawerOptions {
  3. direction: 'right' | 'left' | 'down' | 'up';
  4. initialSize: number;
  5. min: number;
  6. onResize: (size: number, min: number, user: boolean) => void;
  7. ref?: React.RefObject<HTMLElement>;
  8. }
  9. /**
  10. * Hook to support draggable container resizing
  11. *
  12. * This only resizes one dimension at a time.
  13. */
  14. export function usePassiveResizableDrawer(options: UsePassiveResizableDrawerOptions): {
  15. onMouseDown: React.MouseEventHandler<HTMLElement>;
  16. size: React.MutableRefObject<number>;
  17. } {
  18. const rafIdRef = useRef<number | null>(null);
  19. const sizeRef = useRef(options.initialSize);
  20. const currentMouseVectorRaf = useRef<[number, number] | null>(null);
  21. // We intentionally fire this once at mount to ensure the dimensions are set and
  22. // any potentional values set by CSS will be overriden. If no initialDimensions are provided,
  23. // invoke the onResize callback with the previously stored dimensions.
  24. const {direction, initialSize, onResize} = options;
  25. useLayoutEffect(() => {
  26. sizeRef.current = initialSize;
  27. onResize(initialSize, options.min, false);
  28. // eslint-disable-next-line react-hooks/exhaustive-deps
  29. }, [initialSize, direction, onResize]);
  30. const onMouseMove = useCallback(
  31. (event: MouseEvent) => {
  32. event.preventDefault();
  33. event.stopPropagation();
  34. const isXAxis = options.direction === 'left' || options.direction === 'right';
  35. const isInverted = options.direction === 'down' || options.direction === 'left';
  36. document.body.style.pointerEvents = 'none';
  37. document.body.style.userSelect = 'none';
  38. // We've disabled pointerEvents on the body, the cursor needs to be
  39. // applied to the root most element to work
  40. document.documentElement.style.cursor = isXAxis ? 'ew-resize' : 'ns-resize';
  41. if (rafIdRef.current !== null) {
  42. window.cancelAnimationFrame(rafIdRef.current);
  43. }
  44. rafIdRef.current = window.requestAnimationFrame(() => {
  45. if (!currentMouseVectorRaf.current) {
  46. return;
  47. }
  48. const newPositionVector: [number, number] = [event.clientX, event.clientY];
  49. const newAxisPosition = isXAxis ? newPositionVector[0] : newPositionVector[1];
  50. const currentAxisPosition = isXAxis
  51. ? currentMouseVectorRaf.current[0]
  52. : currentMouseVectorRaf.current[1];
  53. const positionDelta = currentAxisPosition - newAxisPosition;
  54. currentMouseVectorRaf.current = newPositionVector;
  55. sizeRef.current = Math.round(
  56. Math.max(options.min, sizeRef.current + positionDelta * (isInverted ? -1 : 1))
  57. );
  58. onResize(sizeRef.current, options.min, true);
  59. });
  60. },
  61. [options.direction, onResize, options.min]
  62. );
  63. const onMouseUp = useCallback(() => {
  64. document.body.style.pointerEvents = '';
  65. document.body.style.userSelect = '';
  66. document.documentElement.style.cursor = '';
  67. document.removeEventListener('mousemove', onMouseMove);
  68. document.removeEventListener('mouseup', onMouseUp);
  69. }, [onMouseMove]);
  70. const onMouseDown = useCallback(
  71. (evt: React.MouseEvent<HTMLElement>) => {
  72. currentMouseVectorRaf.current = [evt.clientX, evt.clientY];
  73. document.addEventListener('mousemove', onMouseMove, {passive: false});
  74. document.addEventListener('mouseup', onMouseUp);
  75. },
  76. [onMouseMove, onMouseUp]
  77. );
  78. useLayoutEffect(() => {
  79. return () => {
  80. if (rafIdRef.current !== null) {
  81. window.cancelAnimationFrame(rafIdRef.current);
  82. }
  83. };
  84. });
  85. return {onMouseDown, size: sizeRef};
  86. }