bundle.cjs.js 8.5 KB

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