usePassiveResizeableDrawer.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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?: HTMLElement | null;
  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 stateRef = useRef<undefined | 'resizing'>();
  19. const rafIdRef = useRef<number | null>(null);
  20. const sizeRef = useRef(options.initialSize);
  21. const currentMouseVectorRaf = useRef<[number, number] | null>(null);
  22. // We intentionally fire this once at mount to ensure the dimensions are set and
  23. // any potentional values set by CSS will be overriden. If no initialDimensions are provided,
  24. // invoke the onResize callback with the previously stored dimensions.
  25. const {direction, initialSize, onResize} = options;
  26. useLayoutEffect(() => {
  27. sizeRef.current = initialSize;
  28. onResize(initialSize, options.min, false);
  29. // eslint-disable-next-line react-hooks/exhaustive-deps
  30. }, [initialSize, direction, onResize]);
  31. const onMouseMove = useCallback(
  32. (event: MouseEvent) => {
  33. event.preventDefault();
  34. event.stopPropagation();
  35. const isXAxis = options.direction === 'left' || options.direction === 'right';
  36. const isInverted = options.direction === 'down' || options.direction === 'left';
  37. document.body.style.pointerEvents = 'none';
  38. document.body.style.userSelect = 'none';
  39. // We've disabled pointerEvents on the body, the cursor needs to be
  40. // applied to the root most element to work
  41. document.documentElement.style.cursor = isXAxis ? 'ew-resize' : 'ns-resize';
  42. if (rafIdRef.current !== null) {
  43. window.cancelAnimationFrame(rafIdRef.current);
  44. }
  45. rafIdRef.current = window.requestAnimationFrame(() => {
  46. if (!currentMouseVectorRaf.current) {
  47. return;
  48. }
  49. const newPositionVector: [number, number] = [event.clientX, event.clientY];
  50. const newAxisPosition = isXAxis ? newPositionVector[0] : newPositionVector[1];
  51. const currentAxisPosition = isXAxis
  52. ? currentMouseVectorRaf.current[0]
  53. : currentMouseVectorRaf.current[1];
  54. const positionDelta = currentAxisPosition - newAxisPosition;
  55. currentMouseVectorRaf.current = newPositionVector;
  56. sizeRef.current = Math.round(
  57. Math.max(options.min, sizeRef.current + positionDelta * (isInverted ? -1 : 1))
  58. );
  59. onResize(sizeRef.current, options.min, true);
  60. });
  61. },
  62. [options.direction, onResize, options.min]
  63. );
  64. const onMouseUp = useCallback(() => {
  65. stateRef.current = undefined;
  66. document.body.style.pointerEvents = '';
  67. document.body.style.userSelect = '';
  68. document.body.style.cursor = '';
  69. document.documentElement.style.cursor = '';
  70. document.removeEventListener('mousemove', onMouseMove);
  71. document.removeEventListener('mouseup', onMouseUp);
  72. }, [onMouseMove]);
  73. const onMouseDown = useCallback(
  74. (evt: React.MouseEvent<HTMLElement>) => {
  75. stateRef.current = 'resizing';
  76. currentMouseVectorRaf.current = [evt.clientX, evt.clientY];
  77. document.body.style.cursor =
  78. (direction === 'left' || direction === 'right' ? 'ew-resize' : 'ns-resize') +
  79. ' !important';
  80. document.addEventListener('mousemove', onMouseMove, {passive: false});
  81. document.addEventListener('mouseup', onMouseUp);
  82. },
  83. [onMouseMove, onMouseUp, direction]
  84. );
  85. useLayoutEffect(() => {
  86. // Watches for external resize events and ensures the local size value is kept in sync
  87. const ref = options.ref;
  88. const observer = new ResizeObserver(elements => {
  89. const container = elements[0];
  90. if (!container) {
  91. return;
  92. }
  93. if (stateRef.current === 'resizing') {
  94. return;
  95. }
  96. if (container.contentRect) {
  97. const width = container.contentRect.width;
  98. const height = container.contentRect.height;
  99. if (typeof width !== 'number' || typeof height !== 'number') {
  100. return;
  101. }
  102. sizeRef.current =
  103. options.direction === 'left' || options.direction === 'right' ? width : height;
  104. }
  105. });
  106. if (ref) {
  107. observer.observe(ref);
  108. }
  109. return () => {
  110. observer.disconnect();
  111. if (rafIdRef.current !== null) {
  112. window.cancelAnimationFrame(rafIdRef.current);
  113. }
  114. };
  115. }, [options.direction, options.ref]);
  116. return {onMouseDown, size: sizeRef};
  117. }