useResizableDrawer.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import {useCallback, useEffect, useRef, useState} from 'react';
  2. export interface UseResizableDrawerOptions {
  3. direction: 'horizontal-ltr' | 'horizontal-rtl' | 'vertical';
  4. initialDimensions: [number, number];
  5. min: [number, number];
  6. onResize: (newDimensions: [number, number]) => void;
  7. }
  8. export function useResizableDrawer(options: UseResizableDrawerOptions): {
  9. dimensions: [number, number];
  10. onMouseDown: React.MouseEventHandler<HTMLElement>;
  11. } {
  12. const rafIdRef = useRef<number | null>(null);
  13. const startResizeVectorRef = useRef<[number, number] | null>(null);
  14. const [dimensions, setDimensions] = useState<[number, number]>([
  15. options.initialDimensions[0],
  16. options.initialDimensions[1],
  17. ]);
  18. // We intentionally fire this once at mount to ensure the dimensions are set and
  19. // any potentional values set by CSS will be overriden.
  20. useEffect(() => {
  21. options.onResize(options.initialDimensions);
  22. // eslint-disable-next-line react-hooks/exhaustive-deps
  23. }, [options.direction]);
  24. const dimensionsRef = useRef<[number, number]>(dimensions);
  25. dimensionsRef.current = dimensions;
  26. const onMouseMove = useCallback(
  27. (event: MouseEvent) => {
  28. document.body.style.pointerEvents = 'none';
  29. document.body.style.userSelect = 'none';
  30. if (rafIdRef.current !== null) {
  31. window.cancelAnimationFrame(rafIdRef.current);
  32. }
  33. rafIdRef.current = window.requestAnimationFrame(() => {
  34. if (!startResizeVectorRef.current) {
  35. return;
  36. }
  37. const currentPositionVector: [number, number] = [event.clientX, event.clientY];
  38. const distance = [
  39. startResizeVectorRef.current[0] - currentPositionVector[0],
  40. startResizeVectorRef.current[1] - currentPositionVector[1],
  41. ];
  42. startResizeVectorRef.current = currentPositionVector;
  43. const newDimensions: [number, number] = [
  44. // Round to 1px precision
  45. Math.round(
  46. Math.max(
  47. options.min[0],
  48. dimensionsRef.current[0] +
  49. distance[0] * (options.direction === 'horizontal-ltr' ? -1 : 1)
  50. )
  51. ),
  52. // Round to 1px precision
  53. Math.round(
  54. Math.max(options.min[1] ?? 0, dimensionsRef.current[1] + distance[1])
  55. ),
  56. ];
  57. options.onResize(newDimensions);
  58. setDimensions(newDimensions);
  59. });
  60. },
  61. [options]
  62. );
  63. const onMouseUp = useCallback(() => {
  64. document.body.style.pointerEvents = '';
  65. document.body.style.userSelect = '';
  66. document.removeEventListener('mousemove', onMouseMove);
  67. document.removeEventListener('mouseup', onMouseUp);
  68. }, [onMouseMove]);
  69. const onMouseDown = useCallback(
  70. (evt: React.MouseEvent<HTMLElement>) => {
  71. startResizeVectorRef.current = [evt.clientX, evt.clientY];
  72. document.addEventListener('mousemove', onMouseMove, {passive: true});
  73. document.addEventListener('mouseup', onMouseUp);
  74. },
  75. [onMouseMove, onMouseUp]
  76. );
  77. useEffect(() => {
  78. return () => {
  79. if (rafIdRef.current !== null) {
  80. window.cancelAnimationFrame(rafIdRef.current);
  81. }
  82. };
  83. });
  84. return {dimensions, onMouseDown};
  85. }