bundle.esm.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import { useRef, useEffect, useCallback, useState, useMemo } from 'react';
  2. // This could've been more streamlined with internal state instead of abusing
  3. // refs to such extent, but then composing hooks and components could not opt out of unnecessary renders.
  4. function useResolvedElement(subscriber, refOrElement) {
  5. var lastReportRef = useRef(null);
  6. var refOrElementRef = useRef(null);
  7. refOrElementRef.current = refOrElement;
  8. var cbElementRef = useRef(null); // Calling re-evaluation after each render without using a dep array,
  9. // as the ref object's current value could've changed since the last render.
  10. useEffect(function () {
  11. evaluateSubscription();
  12. });
  13. var evaluateSubscription = useCallback(function () {
  14. var cbElement = cbElementRef.current;
  15. var refOrElement = refOrElementRef.current; // Ugly ternary. But smaller than an if-else block.
  16. var element = cbElement ? cbElement : refOrElement ? refOrElement instanceof Element ? refOrElement : refOrElement.current : null;
  17. if (lastReportRef.current && lastReportRef.current.element === element && lastReportRef.current.subscriber === subscriber) {
  18. return;
  19. }
  20. if (lastReportRef.current && lastReportRef.current.cleanup) {
  21. lastReportRef.current.cleanup();
  22. }
  23. lastReportRef.current = {
  24. element: element,
  25. subscriber: subscriber,
  26. // Only calling the subscriber, if there's an actual element to report.
  27. // Setting cleanup to undefined unless a subscriber returns one, as an existing cleanup function would've been just called.
  28. cleanup: element ? subscriber(element) : undefined
  29. };
  30. }, [subscriber]); // making sure we call the cleanup function on unmount
  31. useEffect(function () {
  32. return function () {
  33. if (lastReportRef.current && lastReportRef.current.cleanup) {
  34. lastReportRef.current.cleanup();
  35. lastReportRef.current = null;
  36. }
  37. };
  38. }, []);
  39. return useCallback(function (element) {
  40. cbElementRef.current = element;
  41. evaluateSubscription();
  42. }, [evaluateSubscription]);
  43. }
  44. // We're only using the first element of the size sequences, until future versions of the spec solidify on how
  45. // exactly it'll be used for fragments in multi-column scenarios:
  46. // From the spec:
  47. // > The box size properties are exposed as FrozenArray in order to support elements that have multiple fragments,
  48. // > which occur in multi-column scenarios. However the current definitions of content rect and border box do not
  49. // > mention how those boxes are affected by multi-column layout. In this spec, there will only be a single
  50. // > ResizeObserverSize returned in the FrozenArray, which will correspond to the dimensions of the first column.
  51. // > A future version of this spec will extend the returned FrozenArray to contain the per-fragment size information.
  52. // (https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface)
  53. //
  54. // Also, testing these new box options revealed that in both Chrome and FF everything is returned in the callback,
  55. // regardless of the "box" option.
  56. // The spec states the following on this:
  57. // > This does not have any impact on which box dimensions are returned to the defined callback when the event
  58. // > is fired, it solely defines which box the author wishes to observe layout changes on.
  59. // (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
  60. // I'm not exactly clear on what this means, especially when you consider a later section stating the following:
  61. // > This section is non-normative. An author may desire to observe more than one CSS box.
  62. // > In this case, author will need to use multiple ResizeObservers.
  63. // (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
  64. // Which is clearly not how current browser implementations behave, and seems to contradict the previous quote.
  65. // For this reason I decided to only return the requested size,
  66. // even though it seems we have access to results for all box types.
  67. // This also means that we get to keep the current api, being able to return a simple { width, height } pair,
  68. // regardless of box option.
  69. function extractSize(entry, boxProp, sizeType) {
  70. if (!entry[boxProp]) {
  71. if (boxProp === "contentBoxSize") {
  72. // The dimensions in `contentBoxSize` and `contentRect` are equivalent according to the spec.
  73. // See the 6th step in the description for the RO algorithm:
  74. // https://drafts.csswg.org/resize-observer/#create-and-populate-resizeobserverentry-h
  75. // > Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box".
  76. // In real browser implementations of course these objects differ, but the width/height values should be equivalent.
  77. return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
  78. }
  79. return undefined;
  80. } // A couple bytes smaller than calling Array.isArray() and just as effective here.
  81. return entry[boxProp][0] ? entry[boxProp][0][sizeType] : // TS complains about this, because the RO entry type follows the spec and does not reflect Firefox's current
  82. // behaviour of returning objects instead of arrays for `borderBoxSize` and `contentBoxSize`.
  83. // @ts-ignore
  84. entry[boxProp][sizeType];
  85. }
  86. function useResizeObserver(opts) {
  87. if (opts === void 0) {
  88. opts = {};
  89. }
  90. // Saving the callback as a ref. With this, I don't need to put onResize in the
  91. // effect dep array, and just passing in an anonymous function without memoising
  92. // will not reinstantiate the hook's ResizeObserver.
  93. var onResize = opts.onResize;
  94. var onResizeRef = useRef(undefined);
  95. onResizeRef.current = onResize;
  96. var round = opts.round || Math.round; // Using a single instance throughout the hook's lifetime
  97. var resizeObserverRef = useRef();
  98. var _useState = useState({
  99. width: undefined,
  100. height: undefined
  101. }),
  102. size = _useState[0],
  103. setSize = _useState[1]; // In certain edge cases the RO might want to report a size change just after
  104. // the component unmounted.
  105. var didUnmount = useRef(false);
  106. useEffect(function () {
  107. didUnmount.current = false;
  108. return function () {
  109. didUnmount.current = true;
  110. };
  111. }, []); // Using a ref to track the previous width / height to avoid unnecessary renders.
  112. var previous = useRef({
  113. width: undefined,
  114. height: undefined
  115. }); // This block is kinda like a useEffect, only it's called whenever a new
  116. // element could be resolved based on the ref option. It also has a cleanup
  117. // function.
  118. var refCallback = useResolvedElement(useCallback(function (element) {
  119. // We only use a single Resize Observer instance, and we're instantiating it on demand, only once there's something to observe.
  120. // This instance is also recreated when the `box` option changes, so that a new observation is fired if there was a previously observed element with a different box option.
  121. if (!resizeObserverRef.current || resizeObserverRef.current.box !== opts.box || resizeObserverRef.current.round !== round) {
  122. resizeObserverRef.current = {
  123. box: opts.box,
  124. round: round,
  125. instance: new ResizeObserver(function (entries) {
  126. var entry = entries[0];
  127. var boxProp = opts.box === "border-box" ? "borderBoxSize" : opts.box === "device-pixel-content-box" ? "devicePixelContentBoxSize" : "contentBoxSize";
  128. var reportedWidth = extractSize(entry, boxProp, "inlineSize");
  129. var reportedHeight = extractSize(entry, boxProp, "blockSize");
  130. var newWidth = reportedWidth ? round(reportedWidth) : undefined;
  131. var newHeight = reportedHeight ? round(reportedHeight) : undefined;
  132. if (previous.current.width !== newWidth || previous.current.height !== newHeight) {
  133. var newSize = {
  134. width: newWidth,
  135. height: newHeight
  136. };
  137. previous.current.width = newWidth;
  138. previous.current.height = newHeight;
  139. if (onResizeRef.current) {
  140. onResizeRef.current(newSize);
  141. } else {
  142. if (!didUnmount.current) {
  143. setSize(newSize);
  144. }
  145. }
  146. }
  147. })
  148. };
  149. }
  150. resizeObserverRef.current.instance.observe(element, {
  151. box: opts.box
  152. });
  153. return function () {
  154. if (resizeObserverRef.current) {
  155. resizeObserverRef.current.instance.unobserve(element);
  156. }
  157. };
  158. }, [opts.box, round]), opts.ref);
  159. return useMemo(function () {
  160. return {
  161. ref: refCallback,
  162. width: size.width,
  163. height: size.height
  164. };
  165. }, [refCallback, size.width, size.height]);
  166. }
  167. export { useResizeObserver as default };