useResizeObserver.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import * as React from 'react'
  2. import { TLViewport, TLBounds, debounce } from '@tldraw/core'
  3. const getNearestScrollableContainer = (element: HTMLElement): HTMLElement | Document => {
  4. let parent = element.parentElement
  5. while (parent) {
  6. if (parent === document.body) {
  7. return document
  8. }
  9. const { overflowY } = window.getComputedStyle(parent)
  10. const hasScrollableContent = parent.scrollHeight > parent.clientHeight
  11. if (
  12. hasScrollableContent &&
  13. (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay')
  14. ) {
  15. return parent
  16. }
  17. parent = parent.parentElement
  18. }
  19. return document
  20. }
  21. export function useResizeObserver<T extends HTMLElement>(
  22. ref: React.RefObject<T>,
  23. viewport: TLViewport,
  24. onBoundsChange?: (bounds: TLBounds) => void
  25. ) {
  26. const rIsMounted = React.useRef(false)
  27. // When the element resizes, update the bounds (stored in inputs)
  28. // and broadcast via the onBoundsChange callback prop.
  29. const updateBounds = React.useCallback(() => {
  30. if (rIsMounted.current) {
  31. const rect = ref.current?.getBoundingClientRect()
  32. if (rect) {
  33. const bounds: TLBounds = {
  34. minX: rect.left,
  35. maxX: rect.left + rect.width,
  36. minY: rect.top,
  37. maxY: rect.top + rect.height,
  38. width: rect.width,
  39. height: rect.height,
  40. }
  41. viewport.updateBounds(bounds)
  42. onBoundsChange?.(bounds)
  43. }
  44. } else {
  45. // Skip the first mount
  46. rIsMounted.current = true
  47. }
  48. }, [ref, onBoundsChange])
  49. React.useEffect(() => {
  50. const scrollingAnchor = ref.current ? getNearestScrollableContainer(ref.current) : document
  51. const debouncedupdateBounds = debounce(updateBounds, 100)
  52. scrollingAnchor.addEventListener('scroll', debouncedupdateBounds)
  53. window.addEventListener('resize', debouncedupdateBounds)
  54. return () => {
  55. scrollingAnchor.removeEventListener('scroll', debouncedupdateBounds)
  56. window.removeEventListener('resize', debouncedupdateBounds)
  57. }
  58. }, [])
  59. React.useLayoutEffect(() => {
  60. const resizeObserver = new ResizeObserver(entries => {
  61. if (entries[0].contentRect) {
  62. updateBounds()
  63. }
  64. })
  65. if (ref.current) {
  66. resizeObserver.observe(ref.current)
  67. }
  68. return () => {
  69. resizeObserver.disconnect()
  70. }
  71. }, [ref])
  72. React.useEffect(() => {
  73. updateBounds()
  74. // make sure the document get focus when the component is mounted
  75. // so that the document can receive keyboard events
  76. setTimeout(() => {
  77. ref.current?.querySelector<HTMLElement>('.tl-canvas')?.focus()
  78. })
  79. }, [ref])
  80. }