polyfilled.js 8.6 KB

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