use-resize-observer
Advanced tools
Comparing version 7.0.1 to 7.1.0
@@ -0,8 +1,18 @@ | ||
# [7.1.0](https://github.com/ZeeCoder/use-resize-observer/compare/v7.0.1...v7.1.0) (2021-08-28) | ||
### Bug Fixes | ||
- The `onResize` callback is no longer incorrectly called with the same values. ([29938a1](https://github.com/ZeeCoder/use-resize-observer/commit/29938a12e6393bd8f5dc98d7cccea3d291db6cf1)) | ||
### Features | ||
- Added the `box` option ([f873597](https://github.com/ZeeCoder/use-resize-observer/commit/f873597744140698d941ac626a318ac82adcb736)), closes [#31](https://github.com/ZeeCoder/use-resize-observer/issues/31) [#57](https://github.com/ZeeCoder/use-resize-observer/issues/57) | ||
- Added the `round` option. ([1224bc8](https://github.com/ZeeCoder/use-resize-observer/commit/1224bc8f67bd72dd985b15ac359b7e9139cc7468)), closes [#55](https://github.com/ZeeCoder/use-resize-observer/issues/55) [#46](https://github.com/ZeeCoder/use-resize-observer/issues/46) [#61](https://github.com/ZeeCoder/use-resize-observer/issues/61) | ||
## [7.0.1](https://github.com/ZeeCoder/use-resize-observer/compare/v7.0.0...v7.0.1) (2021-07-27) | ||
### Bug Fixes | ||
* Removed unnecessary entries.length check ([3211d33](https://github.com/ZeeCoder/use-resize-observer/commit/3211d338117b0d2a97ccb229683eb8458de81d01)) | ||
* Undefined HTMLElement is no longer an issue in certain SSR edge cases. ([599cace](https://github.com/ZeeCoder/use-resize-observer/commit/599cace5c33ecd4276a0fe2848e0ed920f81e2fe)), closes [#74](https://github.com/ZeeCoder/use-resize-observer/issues/74) [#62](https://github.com/ZeeCoder/use-resize-observer/issues/62) | ||
- Removed unnecessary entries.length check ([3211d33](https://github.com/ZeeCoder/use-resize-observer/commit/3211d338117b0d2a97ccb229683eb8458de81d01)) | ||
- Undefined HTMLElement is no longer an issue in certain SSR edge cases. ([599cace](https://github.com/ZeeCoder/use-resize-observer/commit/599cace5c33ecd4276a0fe2848e0ed920f81e2fe)), closes [#74](https://github.com/ZeeCoder/use-resize-observer/issues/74) [#62](https://github.com/ZeeCoder/use-resize-observer/issues/62) | ||
@@ -9,0 +19,0 @@ # [7.0.0](https://github.com/ZeeCoder/use-resize-observer/compare/v6.1.0...v7.0.0) (2020-11-11) |
# CONTRIBUTING | ||
When contributing to this project, please keep in mind the following goals: | ||
When contributing to this project, please keep in mind the following: | ||
- The hook must remain as simple as possible. It's only a "proxy" to a | ||
ResizeObserver instance, and shouldn't add features that the resize observer | ||
doesn't do. | ||
- The hook must remain as simple as possible. It's only a low-level "proxy" to a | ||
ResizeObserver instance aiming for correctness, which should not add or polyfill | ||
features on top. All that can be done by composing hooks. | ||
- All features must be covered with test(s). | ||
@@ -31,8 +31,11 @@ | ||
To do that: | ||
To do so: | ||
- Run `yarn src:watch` in a terminal tab | ||
- Run `KARMA_BROWSERS=Chrome yarn karma:watch` in another. | ||
- Run `yarn karma:watch` in another. | ||
Don't forget to run `yarn test` at the end once you're done with everything, to | ||
make sure the new code is tested for regressions. | ||
If you have a Browserstack account, then you can also run the tests in real browsers using the `test:bs:*` commands. | ||
Just make sure you have the following env variables set: `BS_USERNAME`, `BS_ACCESS_KEY`. |
@@ -11,10 +11,5 @@ 'use strict'; | ||
var callbackRefElement = react.useRef(null); | ||
var refCallback = react.useCallback(function (element) { | ||
callbackRefElement.current = element; | ||
callSubscriber(); | ||
}, []); | ||
var lastReportedElementRef = react.useRef(null); | ||
var lastReportRef = react.useRef(null); | ||
var cleanupRef = react.useRef(); | ||
var callSubscriber = function callSubscriber() { | ||
var callSubscriber = react.useCallback(function () { | ||
var element = null; | ||
@@ -32,3 +27,3 @@ | ||
if (lastReportedElementRef.current === element) { | ||
if (lastReportRef.current && lastReportRef.current.element === element && lastReportRef.current.reporter === callSubscriber) { | ||
return; | ||
@@ -43,3 +38,6 @@ } | ||
lastReportedElementRef.current = element; // Only calling the subscriber, if there's an actual element to report. | ||
lastReportRef.current = { | ||
reporter: callSubscriber, | ||
element: element | ||
}; // Only calling the subscriber, if there's an actual element to report. | ||
@@ -49,17 +47,64 @@ if (element) { | ||
} | ||
}; // On each render, we check whether a ref changed, or if we got a new raw | ||
}, [refOrElement, subscriber]); // On each render, we check whether a ref changed, or if we got a new raw | ||
// element. | ||
react.useEffect(function () { | ||
// Note that this does not mean that "element" will necessarily be whatever | ||
// the ref currently holds. It'll simply "update" `element` each render to | ||
// the current ref value, but there's no guarantee that the ref value will | ||
// not change later without a render. | ||
// This may or may not be a problem depending on the specific use case. | ||
// With this we're *technically* supporting cases where ref objects' current value changes, but only if there's a | ||
// render accompanying that change as well. | ||
// To guarantee we always have the right element, one must use the ref callback provided instead, but we support | ||
// RefObjects to make the hook API more convenient in certain cases. | ||
callSubscriber(); | ||
}, [refOrElement]); | ||
return refCallback; | ||
}, [callSubscriber]); | ||
return react.useCallback(function (element) { | ||
callbackRefElement.current = element; | ||
callSubscriber(); | ||
}, [callSubscriber]); | ||
} | ||
// We're only using the first element of the size sequences, until future versions of the spec solidify on how | ||
// exactly it'll be used for fragments in multi-column scenarios: | ||
// From the spec: | ||
// > The box size properties are exposed as FrozenArray in order to support elements that have multiple fragments, | ||
// > which occur in multi-column scenarios. However the current definitions of content rect and border box do not | ||
// > mention how those boxes are affected by multi-column layout. In this spec, there will only be a single | ||
// > ResizeObserverSize returned in the FrozenArray, which will correspond to the dimensions of the first column. | ||
// > A future version of this spec will extend the returned FrozenArray to contain the per-fragment size information. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface) | ||
// | ||
// Also, testing these new box options revealed that in both Chrome and FF everything is returned in the callback, | ||
// regardless of the "box" option. | ||
// The spec states the following on this: | ||
// > This does not have any impact on which box dimensions are returned to the defined callback when the event | ||
// > is fired, it solely defines which box the author wishes to observe layout changes on. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface) | ||
// I'm not exactly clear on what this means, especially when you consider a later section stating the following: | ||
// > This section is non-normative. An author may desire to observe more than one CSS box. | ||
// > In this case, author will need to use multiple ResizeObservers. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface) | ||
// Which is clearly not how current browser implementations behave, and seems to contradict the previous quote. | ||
// For this reason I decided to only return the requested size, | ||
// even though it seems we have access to results for all box types. | ||
// This also means that we get to keep the current api, being able to return a simple { width, height } pair, | ||
// regardless of box option. | ||
var extractSize = function extractSize(entry, boxProp, sizeType) { | ||
if (!entry[boxProp]) { | ||
if (boxProp === "contentBoxSize") { | ||
// The dimensions in `contentBoxSize` and `contentRect` are equivalent according to the spec. | ||
// See the 6th step in the description for the RO algorithm: | ||
// https://drafts.csswg.org/resize-observer/#create-and-populate-resizeobserverentry-h | ||
// > Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box". | ||
// In real browser implementations of course these objects differ, but the width/height values should be equivalent. | ||
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"]; | ||
} | ||
return undefined; | ||
} // A couple bytes smaller than calling Array.isArray() and just as effective here. | ||
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 | ||
// behaviour of returning objects instead of arrays for `borderBoxSize` and `contentBoxSize`. | ||
// @ts-ignore | ||
entry[boxProp][sizeType]; | ||
}; | ||
function useResizeObserver(opts) { | ||
@@ -72,6 +117,7 @@ if (opts === void 0) { | ||
// effect dep array, and just passing in an anonymous function without memoising | ||
// will not reinstantiate the hook's ResizeObserver | ||
// will not reinstantiate the hook's ResizeObserver. | ||
var onResize = opts.onResize; | ||
var onResizeRef = react.useRef(undefined); | ||
onResizeRef.current = onResize; // Using a single instance throughout the hook's lifetime | ||
onResizeRef.current = onResize; | ||
var round = opts.round || Math.round; // Using a single instance throughout the hook's lifetime | ||
@@ -94,3 +140,3 @@ var resizeObserverRef = react.useRef(); | ||
}; | ||
}, []); // Using a ref to track the previous width / height to avoid unnecessary renders | ||
}, []); // Using a ref to track the previous width / height to avoid unnecessary renders. | ||
@@ -104,43 +150,46 @@ var previous = react.useRef({ | ||
var refCallback = useResolvedElement(function (element) { | ||
// Initialising the RO instance | ||
if (!resizeObserverRef.current) { | ||
// Saving a single instance, used by the hook from this point on. | ||
resizeObserverRef.current = new ResizeObserver(function (entries) { | ||
if (!Array.isArray(entries)) { | ||
return; | ||
} | ||
var refCallback = useResolvedElement(react.useCallback(function (element) { | ||
// We only use a single Resize Observer instance, and we're instantiating it on demand, only once there's something to observe. | ||
// 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. | ||
if (!resizeObserverRef.current || resizeObserverRef.current.box !== opts.box || resizeObserverRef.current.round !== round) { | ||
resizeObserverRef.current = { | ||
box: opts.box, | ||
round: round, | ||
instance: new ResizeObserver(function (entries) { | ||
var entry = entries[0]; | ||
var boxProp = opts.box === "border-box" ? "borderBoxSize" : opts.box === "device-pixel-content-box" ? "devicePixelContentBoxSize" : "contentBoxSize"; | ||
var reportedWidth = extractSize(entry, boxProp, "inlineSize"); | ||
var reportedHeight = extractSize(entry, boxProp, "blockSize"); | ||
var newWidth = reportedWidth ? round(reportedWidth) : undefined; | ||
var newHeight = reportedHeight ? round(reportedHeight) : undefined; | ||
var entry = entries[0]; // `Math.round` is in line with how CSS resolves sub-pixel values | ||
var newWidth = Math.round(entry.contentRect.width); | ||
var newHeight = Math.round(entry.contentRect.height); | ||
if (previous.current.width !== newWidth || previous.current.height !== newHeight) { | ||
var newSize = { | ||
width: newWidth, | ||
height: newHeight | ||
}; | ||
if (onResizeRef.current) { | ||
onResizeRef.current(newSize); | ||
} else { | ||
if (previous.current.width !== newWidth || previous.current.height !== newHeight) { | ||
var newSize = { | ||
width: newWidth, | ||
height: newHeight | ||
}; | ||
previous.current.width = newWidth; | ||
previous.current.height = newHeight; | ||
if (!didUnmount.current) { | ||
setSize(newSize); | ||
if (onResizeRef.current) { | ||
onResizeRef.current(newSize); | ||
} else { | ||
if (!didUnmount.current) { | ||
setSize(newSize); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}) | ||
}; | ||
} | ||
resizeObserverRef.current.observe(element); | ||
resizeObserverRef.current.instance.observe(element, { | ||
box: opts.box | ||
}); | ||
return function () { | ||
if (resizeObserverRef.current) { | ||
resizeObserverRef.current.unobserve(element); | ||
resizeObserverRef.current.instance.unobserve(element); | ||
} | ||
}; | ||
}, opts.ref); | ||
}, [opts.box, round]), opts.ref); | ||
return react.useMemo(function () { | ||
@@ -147,0 +196,0 @@ return { |
@@ -1,2 +0,2 @@ | ||
import { useRef, useState, useEffect, useMemo, useCallback } from 'react'; | ||
import { useRef, useState, useEffect, useCallback, useMemo } from 'react'; | ||
@@ -9,10 +9,5 @@ // This of course could've been more streamlined with internal state instead of | ||
var callbackRefElement = useRef(null); | ||
var refCallback = useCallback(function (element) { | ||
callbackRefElement.current = element; | ||
callSubscriber(); | ||
}, []); | ||
var lastReportedElementRef = useRef(null); | ||
var lastReportRef = useRef(null); | ||
var cleanupRef = useRef(); | ||
var callSubscriber = function callSubscriber() { | ||
var callSubscriber = useCallback(function () { | ||
var element = null; | ||
@@ -30,3 +25,3 @@ | ||
if (lastReportedElementRef.current === element) { | ||
if (lastReportRef.current && lastReportRef.current.element === element && lastReportRef.current.reporter === callSubscriber) { | ||
return; | ||
@@ -41,3 +36,6 @@ } | ||
lastReportedElementRef.current = element; // Only calling the subscriber, if there's an actual element to report. | ||
lastReportRef.current = { | ||
reporter: callSubscriber, | ||
element: element | ||
}; // Only calling the subscriber, if there's an actual element to report. | ||
@@ -47,17 +45,64 @@ if (element) { | ||
} | ||
}; // On each render, we check whether a ref changed, or if we got a new raw | ||
}, [refOrElement, subscriber]); // On each render, we check whether a ref changed, or if we got a new raw | ||
// element. | ||
useEffect(function () { | ||
// Note that this does not mean that "element" will necessarily be whatever | ||
// the ref currently holds. It'll simply "update" `element` each render to | ||
// the current ref value, but there's no guarantee that the ref value will | ||
// not change later without a render. | ||
// This may or may not be a problem depending on the specific use case. | ||
// With this we're *technically* supporting cases where ref objects' current value changes, but only if there's a | ||
// render accompanying that change as well. | ||
// To guarantee we always have the right element, one must use the ref callback provided instead, but we support | ||
// RefObjects to make the hook API more convenient in certain cases. | ||
callSubscriber(); | ||
}, [refOrElement]); | ||
return refCallback; | ||
}, [callSubscriber]); | ||
return useCallback(function (element) { | ||
callbackRefElement.current = element; | ||
callSubscriber(); | ||
}, [callSubscriber]); | ||
} | ||
// We're only using the first element of the size sequences, until future versions of the spec solidify on how | ||
// exactly it'll be used for fragments in multi-column scenarios: | ||
// From the spec: | ||
// > The box size properties are exposed as FrozenArray in order to support elements that have multiple fragments, | ||
// > which occur in multi-column scenarios. However the current definitions of content rect and border box do not | ||
// > mention how those boxes are affected by multi-column layout. In this spec, there will only be a single | ||
// > ResizeObserverSize returned in the FrozenArray, which will correspond to the dimensions of the first column. | ||
// > A future version of this spec will extend the returned FrozenArray to contain the per-fragment size information. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface) | ||
// | ||
// Also, testing these new box options revealed that in both Chrome and FF everything is returned in the callback, | ||
// regardless of the "box" option. | ||
// The spec states the following on this: | ||
// > This does not have any impact on which box dimensions are returned to the defined callback when the event | ||
// > is fired, it solely defines which box the author wishes to observe layout changes on. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface) | ||
// I'm not exactly clear on what this means, especially when you consider a later section stating the following: | ||
// > This section is non-normative. An author may desire to observe more than one CSS box. | ||
// > In this case, author will need to use multiple ResizeObservers. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface) | ||
// Which is clearly not how current browser implementations behave, and seems to contradict the previous quote. | ||
// For this reason I decided to only return the requested size, | ||
// even though it seems we have access to results for all box types. | ||
// This also means that we get to keep the current api, being able to return a simple { width, height } pair, | ||
// regardless of box option. | ||
var extractSize = function extractSize(entry, boxProp, sizeType) { | ||
if (!entry[boxProp]) { | ||
if (boxProp === "contentBoxSize") { | ||
// The dimensions in `contentBoxSize` and `contentRect` are equivalent according to the spec. | ||
// See the 6th step in the description for the RO algorithm: | ||
// https://drafts.csswg.org/resize-observer/#create-and-populate-resizeobserverentry-h | ||
// > Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box". | ||
// In real browser implementations of course these objects differ, but the width/height values should be equivalent. | ||
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"]; | ||
} | ||
return undefined; | ||
} // A couple bytes smaller than calling Array.isArray() and just as effective here. | ||
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 | ||
// behaviour of returning objects instead of arrays for `borderBoxSize` and `contentBoxSize`. | ||
// @ts-ignore | ||
entry[boxProp][sizeType]; | ||
}; | ||
function useResizeObserver(opts) { | ||
@@ -70,6 +115,7 @@ if (opts === void 0) { | ||
// effect dep array, and just passing in an anonymous function without memoising | ||
// will not reinstantiate the hook's ResizeObserver | ||
// will not reinstantiate the hook's ResizeObserver. | ||
var onResize = opts.onResize; | ||
var onResizeRef = useRef(undefined); | ||
onResizeRef.current = onResize; // Using a single instance throughout the hook's lifetime | ||
onResizeRef.current = onResize; | ||
var round = opts.round || Math.round; // Using a single instance throughout the hook's lifetime | ||
@@ -92,3 +138,3 @@ var resizeObserverRef = useRef(); | ||
}; | ||
}, []); // Using a ref to track the previous width / height to avoid unnecessary renders | ||
}, []); // Using a ref to track the previous width / height to avoid unnecessary renders. | ||
@@ -102,43 +148,46 @@ var previous = useRef({ | ||
var refCallback = useResolvedElement(function (element) { | ||
// Initialising the RO instance | ||
if (!resizeObserverRef.current) { | ||
// Saving a single instance, used by the hook from this point on. | ||
resizeObserverRef.current = new ResizeObserver(function (entries) { | ||
if (!Array.isArray(entries)) { | ||
return; | ||
} | ||
var refCallback = useResolvedElement(useCallback(function (element) { | ||
// We only use a single Resize Observer instance, and we're instantiating it on demand, only once there's something to observe. | ||
// 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. | ||
if (!resizeObserverRef.current || resizeObserverRef.current.box !== opts.box || resizeObserverRef.current.round !== round) { | ||
resizeObserverRef.current = { | ||
box: opts.box, | ||
round: round, | ||
instance: new ResizeObserver(function (entries) { | ||
var entry = entries[0]; | ||
var boxProp = opts.box === "border-box" ? "borderBoxSize" : opts.box === "device-pixel-content-box" ? "devicePixelContentBoxSize" : "contentBoxSize"; | ||
var reportedWidth = extractSize(entry, boxProp, "inlineSize"); | ||
var reportedHeight = extractSize(entry, boxProp, "blockSize"); | ||
var newWidth = reportedWidth ? round(reportedWidth) : undefined; | ||
var newHeight = reportedHeight ? round(reportedHeight) : undefined; | ||
var entry = entries[0]; // `Math.round` is in line with how CSS resolves sub-pixel values | ||
var newWidth = Math.round(entry.contentRect.width); | ||
var newHeight = Math.round(entry.contentRect.height); | ||
if (previous.current.width !== newWidth || previous.current.height !== newHeight) { | ||
var newSize = { | ||
width: newWidth, | ||
height: newHeight | ||
}; | ||
if (onResizeRef.current) { | ||
onResizeRef.current(newSize); | ||
} else { | ||
if (previous.current.width !== newWidth || previous.current.height !== newHeight) { | ||
var newSize = { | ||
width: newWidth, | ||
height: newHeight | ||
}; | ||
previous.current.width = newWidth; | ||
previous.current.height = newHeight; | ||
if (!didUnmount.current) { | ||
setSize(newSize); | ||
if (onResizeRef.current) { | ||
onResizeRef.current(newSize); | ||
} else { | ||
if (!didUnmount.current) { | ||
setSize(newSize); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}) | ||
}; | ||
} | ||
resizeObserverRef.current.observe(element); | ||
resizeObserverRef.current.instance.observe(element, { | ||
box: opts.box | ||
}); | ||
return function () { | ||
if (resizeObserverRef.current) { | ||
resizeObserverRef.current.unobserve(element); | ||
resizeObserverRef.current.instance.unobserve(element); | ||
} | ||
}; | ||
}, opts.ref); | ||
}, [opts.box, round]), opts.ref); | ||
return useMemo(function () { | ||
@@ -153,2 +202,2 @@ return { | ||
export default useResizeObserver; | ||
export { useResizeObserver as default }; |
@@ -10,6 +10,15 @@ import { RefObject, RefCallback } from "react"; | ||
} & ObservedSize; | ||
declare type ResizeObserverBoxOptions = "border-box" | "content-box" | "device-pixel-content-box"; | ||
declare global { | ||
interface ResizeObserverEntry { | ||
readonly devicePixelContentBoxSize: ReadonlyArray<ResizeObserverSize>; | ||
} | ||
} | ||
declare type RoundingFunction = (n: number) => number; | ||
declare function useResizeObserver<T extends HTMLElement>(opts?: { | ||
ref?: RefObject<T> | T | null | undefined; | ||
onResize?: ResizeHandler; | ||
box?: ResizeObserverBoxOptions; | ||
round?: RoundingFunction; | ||
}): HookResponse<T>; | ||
export default useResizeObserver; |
{ | ||
"name": "use-resize-observer", | ||
"version": "7.0.1", | ||
"version": "7.1.0", | ||
"main": "dist/bundle.cjs.js", | ||
@@ -35,3 +35,3 @@ "module": "dist/bundle.esm.js", | ||
"check:types": "tsc -p tests", | ||
"test": "run-s 'build' 'check:size' 'check:types' 'test:create:ssr' 'test:bs:*'", | ||
"test": "run-s 'build' 'check:size' 'check:types' 'test:create:ssr' 'test:headless:chrome'", | ||
"test:create:ssr": "node ./tests/ssr/create-ssr-test.js", | ||
@@ -43,12 +43,18 @@ "test:chrome": "KARMA_BROWSERS=Chrome yarn karma:run", | ||
"karma:run": "karma start --singleRun", | ||
"karma:watch": "karma start", | ||
"karma:watch": "KARMA_BROWSERS=Chrome karma start", | ||
"prepublish": "yarn build", | ||
"test:bs:modern": "yarn karma:run --useBrowserStack", | ||
"test:bs:ie": "yarn karma:run --useBrowserStack --runIeTests" | ||
"test:bs:all": "run-s 'test:bs:modern' 'test:bs:legacy'", | ||
"test:bs:modern": "KARMA_BROWSERS=modern yarn karma:run", | ||
"test:bs:legacy": "KARMA_BROWSERS=legacy yarn karma:run", | ||
"test:bs:chrome": "KARMA_BROWSERS=bs_chrome_latest yarn karma:run", | ||
"test:bs:firefox": "KARMA_BROWSERS=bs_firefox_latest yarn karma:run", | ||
"test:bs:safari": "KARMA_BROWSERS=bs_safari_13 yarn karma:run", | ||
"test:bs:edge": "KARMA_BROWSERS=bs_edge_latest yarn karma:run", | ||
"test:bs:opera": "KARMA_BROWSERS=bs_opera_latest yarn karma:run", | ||
"test:bs:ie": "KARMA_BROWSERS=bs_ie_11 yarn karma:run", | ||
"test:bs:ios_11": "KARMA_BROWSERS=bs_ios_11 yarn karma:run", | ||
"test:bs:ios_14": "KARMA_BROWSERS=bs_ios_14 yarn karma:run", | ||
"test:bs:samsung": "KARMA_BROWSERS=bs_samsung yarn karma:run", | ||
"prepare": "husky install" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "lint-staged" | ||
} | ||
}, | ||
"lint-staged": { | ||
@@ -86,33 +92,34 @@ "*.{js,ts,md}": [ | ||
"@semantic-release/release-notes-generator": "^9.0.1", | ||
"@size-limit/preset-small-lib": "^4.4.5", | ||
"@testing-library/react": "^11.0.4", | ||
"@types/karma": "^5.0.0", | ||
"@types/karma-jasmine": "^3.1.0", | ||
"@types/react": "^16.9.34", | ||
"@types/react-dom": "^16.9.6", | ||
"@size-limit/preset-small-lib": "^5.0.1", | ||
"@testing-library/react": "^12.0.0", | ||
"@types/karma": "^6.3.1", | ||
"@types/karma-jasmine": "^4.0.1", | ||
"@types/react": "^17.0.15", | ||
"@types/react-dom": "^17.0.9", | ||
"babel-loader": "^8.1.0", | ||
"delay": "^4.1.0", | ||
"husky": "^4.2.5", | ||
"karma": "^5.0.1", | ||
"delay": "^5.0.0", | ||
"husky": "^7.0.0", | ||
"karma": "^6.3.4", | ||
"karma-browserstack-launcher": "^1.6.0", | ||
"karma-chrome-launcher": "^3.0.0", | ||
"karma-firefox-launcher": "^1.3.0", | ||
"karma-firefox-launcher": "^2.1.1", | ||
"karma-jasmine": "^4.0.1", | ||
"karma-sourcemap-loader": "^0.3.7", | ||
"karma-spec-reporter": "^0.0.32", | ||
"karma-webpack": "^4.0.2", | ||
"lint-staged": "^10.1.3", | ||
"karma-webpack": "^5.0.0", | ||
"lint-staged": "^11.1.1", | ||
"npm-run-all": "^4.1.5", | ||
"prettier": "^2.0.4", | ||
"react": "^16.9.0", | ||
"react": "^17.0.2", | ||
"react-app-polyfill": "^2.0.0", | ||
"react-dom": "^16.9.0", | ||
"react-dom": "^17.0.2", | ||
"rollup": "^2.6.1", | ||
"semantic-release": "^17.2.2", | ||
"size-limit": "^4.4.5", | ||
"typescript": "^4.0.3" | ||
"size-limit": "^5.0.1", | ||
"typescript": "^4.3.5", | ||
"webpack": "~4" | ||
}, | ||
"dependencies": { | ||
"resize-observer-polyfill": "^1.5.1" | ||
"@juggle/resize-observer": "^3.3.1" | ||
} | ||
} |
@@ -10,6 +10,15 @@ import { RefObject, RefCallback } from "react"; | ||
} & ObservedSize; | ||
declare type ResizeObserverBoxOptions = "border-box" | "content-box" | "device-pixel-content-box"; | ||
declare global { | ||
interface ResizeObserverEntry { | ||
readonly devicePixelContentBoxSize: ReadonlyArray<ResizeObserverSize>; | ||
} | ||
} | ||
declare type RoundingFunction = (n: number) => number; | ||
declare function useResizeObserver<T extends HTMLElement>(opts?: { | ||
ref?: RefObject<T> | T | null | undefined; | ||
onResize?: ResizeHandler; | ||
box?: ResizeObserverBoxOptions; | ||
round?: RoundingFunction; | ||
}): HookResponse<T>; | ||
export default useResizeObserver; |
'use strict'; | ||
var ResizeObserver = require('resize-observer-polyfill'); | ||
var resizeObserver = require('@juggle/resize-observer'); | ||
var react = require('react'); | ||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
var ResizeObserver__default = /*#__PURE__*/_interopDefaultLegacy(ResizeObserver); | ||
// This of course could've been more streamlined with internal state instead of | ||
@@ -16,10 +12,5 @@ // refs, but then host hooks / components could not opt out of renders. | ||
var callbackRefElement = react.useRef(null); | ||
var refCallback = react.useCallback(function (element) { | ||
callbackRefElement.current = element; | ||
callSubscriber(); | ||
}, []); | ||
var lastReportedElementRef = react.useRef(null); | ||
var lastReportRef = react.useRef(null); | ||
var cleanupRef = react.useRef(); | ||
var callSubscriber = function callSubscriber() { | ||
var callSubscriber = react.useCallback(function () { | ||
var element = null; | ||
@@ -37,3 +28,3 @@ | ||
if (lastReportedElementRef.current === element) { | ||
if (lastReportRef.current && lastReportRef.current.element === element && lastReportRef.current.reporter === callSubscriber) { | ||
return; | ||
@@ -48,3 +39,6 @@ } | ||
lastReportedElementRef.current = element; // Only calling the subscriber, if there's an actual element to report. | ||
lastReportRef.current = { | ||
reporter: callSubscriber, | ||
element: element | ||
}; // Only calling the subscriber, if there's an actual element to report. | ||
@@ -54,17 +48,64 @@ if (element) { | ||
} | ||
}; // On each render, we check whether a ref changed, or if we got a new raw | ||
}, [refOrElement, subscriber]); // On each render, we check whether a ref changed, or if we got a new raw | ||
// element. | ||
react.useEffect(function () { | ||
// Note that this does not mean that "element" will necessarily be whatever | ||
// the ref currently holds. It'll simply "update" `element` each render to | ||
// the current ref value, but there's no guarantee that the ref value will | ||
// not change later without a render. | ||
// This may or may not be a problem depending on the specific use case. | ||
// With this we're *technically* supporting cases where ref objects' current value changes, but only if there's a | ||
// render accompanying that change as well. | ||
// To guarantee we always have the right element, one must use the ref callback provided instead, but we support | ||
// RefObjects to make the hook API more convenient in certain cases. | ||
callSubscriber(); | ||
}, [refOrElement]); | ||
return refCallback; | ||
}, [callSubscriber]); | ||
return react.useCallback(function (element) { | ||
callbackRefElement.current = element; | ||
callSubscriber(); | ||
}, [callSubscriber]); | ||
} | ||
// We're only using the first element of the size sequences, until future versions of the spec solidify on how | ||
// exactly it'll be used for fragments in multi-column scenarios: | ||
// From the spec: | ||
// > The box size properties are exposed as FrozenArray in order to support elements that have multiple fragments, | ||
// > which occur in multi-column scenarios. However the current definitions of content rect and border box do not | ||
// > mention how those boxes are affected by multi-column layout. In this spec, there will only be a single | ||
// > ResizeObserverSize returned in the FrozenArray, which will correspond to the dimensions of the first column. | ||
// > A future version of this spec will extend the returned FrozenArray to contain the per-fragment size information. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface) | ||
// | ||
// Also, testing these new box options revealed that in both Chrome and FF everything is returned in the callback, | ||
// regardless of the "box" option. | ||
// The spec states the following on this: | ||
// > This does not have any impact on which box dimensions are returned to the defined callback when the event | ||
// > is fired, it solely defines which box the author wishes to observe layout changes on. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface) | ||
// I'm not exactly clear on what this means, especially when you consider a later section stating the following: | ||
// > This section is non-normative. An author may desire to observe more than one CSS box. | ||
// > In this case, author will need to use multiple ResizeObservers. | ||
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface) | ||
// Which is clearly not how current browser implementations behave, and seems to contradict the previous quote. | ||
// For this reason I decided to only return the requested size, | ||
// even though it seems we have access to results for all box types. | ||
// This also means that we get to keep the current api, being able to return a simple { width, height } pair, | ||
// regardless of box option. | ||
var extractSize = function extractSize(entry, boxProp, sizeType) { | ||
if (!entry[boxProp]) { | ||
if (boxProp === "contentBoxSize") { | ||
// The dimensions in `contentBoxSize` and `contentRect` are equivalent according to the spec. | ||
// See the 6th step in the description for the RO algorithm: | ||
// https://drafts.csswg.org/resize-observer/#create-and-populate-resizeobserverentry-h | ||
// > Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box". | ||
// In real browser implementations of course these objects differ, but the width/height values should be equivalent. | ||
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"]; | ||
} | ||
return undefined; | ||
} // A couple bytes smaller than calling Array.isArray() and just as effective here. | ||
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 | ||
// behaviour of returning objects instead of arrays for `borderBoxSize` and `contentBoxSize`. | ||
// @ts-ignore | ||
entry[boxProp][sizeType]; | ||
}; | ||
function useResizeObserver(opts) { | ||
@@ -77,6 +118,7 @@ if (opts === void 0) { | ||
// effect dep array, and just passing in an anonymous function without memoising | ||
// will not reinstantiate the hook's ResizeObserver | ||
// will not reinstantiate the hook's ResizeObserver. | ||
var onResize = opts.onResize; | ||
var onResizeRef = react.useRef(undefined); | ||
onResizeRef.current = onResize; // Using a single instance throughout the hook's lifetime | ||
onResizeRef.current = onResize; | ||
var round = opts.round || Math.round; // Using a single instance throughout the hook's lifetime | ||
@@ -99,3 +141,3 @@ var resizeObserverRef = react.useRef(); | ||
}; | ||
}, []); // Using a ref to track the previous width / height to avoid unnecessary renders | ||
}, []); // Using a ref to track the previous width / height to avoid unnecessary renders. | ||
@@ -109,43 +151,46 @@ var previous = react.useRef({ | ||
var refCallback = useResolvedElement(function (element) { | ||
// Initialising the RO instance | ||
if (!resizeObserverRef.current) { | ||
// Saving a single instance, used by the hook from this point on. | ||
resizeObserverRef.current = new ResizeObserver__default['default'](function (entries) { | ||
if (!Array.isArray(entries)) { | ||
return; | ||
} | ||
var refCallback = useResolvedElement(react.useCallback(function (element) { | ||
// We only use a single Resize Observer instance, and we're instantiating it on demand, only once there's something to observe. | ||
// 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. | ||
if (!resizeObserverRef.current || resizeObserverRef.current.box !== opts.box || resizeObserverRef.current.round !== round) { | ||
resizeObserverRef.current = { | ||
box: opts.box, | ||
round: round, | ||
instance: new resizeObserver.ResizeObserver(function (entries) { | ||
var entry = entries[0]; | ||
var boxProp = opts.box === "border-box" ? "borderBoxSize" : opts.box === "device-pixel-content-box" ? "devicePixelContentBoxSize" : "contentBoxSize"; | ||
var reportedWidth = extractSize(entry, boxProp, "inlineSize"); | ||
var reportedHeight = extractSize(entry, boxProp, "blockSize"); | ||
var newWidth = reportedWidth ? round(reportedWidth) : undefined; | ||
var newHeight = reportedHeight ? round(reportedHeight) : undefined; | ||
var entry = entries[0]; // `Math.round` is in line with how CSS resolves sub-pixel values | ||
var newWidth = Math.round(entry.contentRect.width); | ||
var newHeight = Math.round(entry.contentRect.height); | ||
if (previous.current.width !== newWidth || previous.current.height !== newHeight) { | ||
var newSize = { | ||
width: newWidth, | ||
height: newHeight | ||
}; | ||
if (onResizeRef.current) { | ||
onResizeRef.current(newSize); | ||
} else { | ||
if (previous.current.width !== newWidth || previous.current.height !== newHeight) { | ||
var newSize = { | ||
width: newWidth, | ||
height: newHeight | ||
}; | ||
previous.current.width = newWidth; | ||
previous.current.height = newHeight; | ||
if (!didUnmount.current) { | ||
setSize(newSize); | ||
if (onResizeRef.current) { | ||
onResizeRef.current(newSize); | ||
} else { | ||
if (!didUnmount.current) { | ||
setSize(newSize); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}) | ||
}; | ||
} | ||
resizeObserverRef.current.observe(element); | ||
resizeObserverRef.current.instance.observe(element, { | ||
box: opts.box | ||
}); | ||
return function () { | ||
if (resizeObserverRef.current) { | ||
resizeObserverRef.current.unobserve(element); | ||
resizeObserverRef.current.instance.unobserve(element); | ||
} | ||
}; | ||
}, opts.ref); | ||
}, [opts.box, round]), opts.ref); | ||
return react.useMemo(function () { | ||
@@ -152,0 +197,0 @@ return { |
172
README.md
@@ -13,4 +13,4 @@ # use-resize-observer | ||
[![npm version](https://badge.fury.io/js/use-resize-observer.svg)](https://npmjs.com/package/use-resize-observer) | ||
![build](https://github.com/ZeeCoder/use-resize-observer/workflows/Testing/badge.svg) | ||
[![BrowserStack Status](https://automate.browserstack.com/badge.svg?badge_key=bTAyOUVpa3hENUgwMkJBTVhXcytCQjREangwcTJqT0czUGhRSEZta3ZwYz0tLVRSZ1NhVkdPZ01FMithOEh5ZGxoWHc9PQ==--49d9d8ad43d557894fb270c80fd1c24107a82f51)](https://automate.browserstack.com/public-build/bTAyOUVpa3hENUgwMkJBTVhXcytCQjREangwcTJqT0czUGhRSEZta3ZwYz0tLVRSZ1NhVkdPZ01FMithOEh5ZGxoWHc9PQ==--49d9d8ad43d557894fb270c80fd1c24107a82f51) | ||
[![build](https://github.com/ZeeCoder/use-resize-observer/workflows/Testing/badge.svg)](https://github.com/ZeeCoder/use-resize-observer/actions/workflows/testing.yml) | ||
[![BrowserStack Status](https://automate.browserstack.com/badge.svg?badge_key=ZDJvbEJRdDJ0Vy96M0g4citOQkNCWGxuby81T3VhaWVBODEwZ1pYWjVTVT0tLWE2RmVFN0lRenBJUVRLcW14dUpWN3c9PQ==--da71247daf12790835c0447759700ab462b915ee%)](https://automate.browserstack.com/public-build/ZDJvbEJRdDJ0Vy96M0g4citOQkNCWGxuby81T3VhaWVBODEwZ1pYWjVTVT0tLWE2RmVFN0lRenBJUVRLcW14dUpWN3c9PQ==--da71247daf12790835c0447759700ab462b915ee%) | ||
@@ -20,4 +20,5 @@ ## Highlights | ||
- Written in **TypeScript**. | ||
- **Tiny**: [500B](.size-limit.json) (minified, gzipped) Monitored by [size-limit](https://github.com/ai/size-limit). | ||
- **Tiny**: [645B](.size-limit.json) (minified, gzipped) Monitored by [size-limit](https://github.com/ai/size-limit). | ||
- Exposes an **onResize callback** if you need more control. | ||
- `box` [option](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#syntax). | ||
- Works with **SSR**. | ||
@@ -30,4 +31,4 @@ - Works with **CSS-in-JS**. | ||
(See this documentation and the test cases.) | ||
- [Throttle / Debounce](#throttle--debounce) | ||
- **Tested in real browsers** (Currently latest Chrome, Safari, Firefox and IE 11, sponsored by BrowserStack) | ||
- Easy to compose ([Throttle / Debounce](#throttle--debounce), [Breakpoints](#breakpoints)) | ||
- **Tested in real browsers** (Currently latest Chrome, Firefox, Edge, Safari, Opera, IE 11, iOS and Android, sponsored by BrowserStack) | ||
@@ -46,2 +47,19 @@ ## In Action | ||
## Options | ||
| Option | Type | Description | Default | | ||
| -------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -------------- | | ||
| ref | undefined | RefObject | HTMLElement | A ref or element to observe. | undefined | | ||
| box | undefined | "border-box" | "content-box" | "device-pixel-content-box" | The [box model](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#syntax) to use for observation. | "content-box" | | ||
| onResize | undefined | ({ width?: number, height?: number }) => void | A callback receiving the element size. If given, then the hook will not return the size, and instead will call this callback. | undefined | | ||
| round | undefined | (n: number) => number | A function to use for rounding values instead of the default. | `Math.round()` | | ||
## Response | ||
| Name | Type | Description | | ||
| ------ | ----------------------- | ---------------------------------------------- | | ||
| ref | RefCallback | A callback to be passed to React's "ref" prop. | | ||
| width | undefined | number | The width (or "blockSize") of the element. | | ||
| height | undefined | number | The height (or "inlineSize") of the element. | | ||
## Basic Usage | ||
@@ -67,36 +85,96 @@ | ||
Note that "ref" here is a `RefCallback`, not a `RefObject`, meaning you won't be | ||
able to access "ref.current" if you need the element itself. | ||
To get the raw element, either you use your own RefObject (see later in this doc) | ||
or you hook in the returned ref callback, like so: | ||
To observe a different box size other than content box, pass in the `box` option, like so: | ||
### Getting the raw element from the default `RefCallback` | ||
```tsx | ||
const { ref, width, height } = useResizeObserver<HTMLDivElement>({ | ||
box: "border-box", | ||
}); | ||
``` | ||
Note that if the browser does not support the given box type, then the hook won't report any sizes either. | ||
### Box Options | ||
Note that box options are experimental, and as such are not supported by all browsers that implemented ResizeObservers. (See [here](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry).) | ||
`content-box` (default) | ||
Safe to use by all browsers that implemented ResizeObservers. The hook internally will fall back to `contentRect` from | ||
the old spec in case `contentBoxSize` is not available. | ||
`border-box` | ||
Supported well for the most part by evergreen browsers. If you need to support older versions of these browsers however, | ||
then you may want to feature-detect for support, and optionally include a polyfill instead of the native implementation. | ||
`device-pixel-content-box` | ||
Surma has a [very good article](https://web.dev/device-pixel-content-box/) on how this allows us to do pixel perfect | ||
rendering. At the time of writing, however this has very limited support. | ||
The advices on feature detection for `border-box` apply here too. | ||
### Custom Rounding | ||
By default this hook passes the measured values through `Math.round()`, to avoid re-rendering on every subpixel changes. | ||
If this is not what you want, then you can provide your own function: | ||
**Rounding Down Reported Values** | ||
```tsx | ||
import React, { useCallback, useEffect, useRef } from "react"; | ||
const { ref, width, height } = useResizeObserver<HTMLDivElement>({ | ||
round: Math.floor, | ||
}); | ||
``` | ||
**Skipping Rounding** | ||
```tsx | ||
import React from "react"; | ||
import useResizeObserver from "use-resize-observer"; | ||
const useMergedCallbackRef = (...callbacks: Function[]) => { | ||
// Storing callbacks in a ref, so that we don't need to memoise them in | ||
// renders when using this hook. | ||
const callbacksRegistry = useRef<Function[]>(callbacks); | ||
// Outside the hook to ensure this instance does not change unnecessarily. | ||
const noop = (n) => n; | ||
useEffect(() => { | ||
callbacksRegistry.current = callbacks; | ||
}, [...callbacks]); | ||
const App = () => { | ||
const { | ||
ref, | ||
width = 1, | ||
height = 1, | ||
} = useResizeObserver<HTMLDivElement>({ round: noop }); | ||
return useCallback((element) => { | ||
callbacksRegistry.current.forEach((callback) => callback(element)); | ||
}, []); | ||
return ( | ||
<div ref={ref}> | ||
Size: {width}x{height} | ||
</div> | ||
); | ||
}; | ||
``` | ||
Note that the round option is sensitive to the function reference, so make sure you either use `useCallback` | ||
or declare your rounding function outside of the hook's function scope, if it does not rely on any hook state. | ||
(As shown above.) | ||
### Getting the Raw Element from the Default `RefCallback` | ||
Note that "ref" in the above examples is a `RefCallback`, not a `RefObject`, meaning you won't be | ||
able to access "ref.current" if you need the element itself. | ||
To get the raw element, either you use your own RefObject (see later in this doc), | ||
or you can merge the returned ref with one of your own: | ||
```tsx | ||
import React, { useCallback, useEffect, useRef } from "react"; | ||
import useResizeObserver from "use-resize-observer"; | ||
import mergeRefs from "react-merge-refs"; | ||
const App = () => { | ||
const { ref, width = 1, height = 1 } = useResizeObserver<HTMLDivElement>(); | ||
const mergedCallbackRef = useMergedCallbackRef( | ||
const mergedCallbackRef = mergeRefs([ | ||
ref, | ||
(element: HTMLDivElement) => { | ||
// Do whatever you want with the `element`. | ||
} | ||
); | ||
}, | ||
]); | ||
@@ -136,3 +214,3 @@ return ( | ||
## Using a single hook to measure multiple refs | ||
## Using a Single Hook to Measure Multiple Refs | ||
@@ -143,3 +221,3 @@ The hook reacts to ref changes, as it resolves it to an element to observe. | ||
## Opting Out of (or Delaying) ResizeObserver instantiation | ||
## Opting Out of (or Delaying) ResizeObserver Instantiation | ||
@@ -166,3 +244,3 @@ In certain cases you might want to delay creating a ResizeObserver instance. | ||
## The "onResize" callback | ||
## The "onResize" Callback | ||
@@ -199,11 +277,18 @@ By the default the hook will trigger a re-render on all changes to the target | ||
## Throttle / Debounce | ||
## Hook Composition | ||
As this hook intends to remain low-level, it is encouraged to build on top of it via hook composition, if additional features are required. | ||
### Throttle / Debounce | ||
You might want to receive values less frequently than changes actually occur. | ||
While this hook does not come with its own implementation of throttling / debouncing, | ||
you can use the `onResize` callback to implement your own version: | ||
[CodeSandbox Demo](https://codesandbox.io/s/use-resize-observer-throttle-and-debounce-8uvsg) | ||
### Breakpoints | ||
Another popular concept are breakpoints. Here is an example for a simple hook accomplishing that. | ||
[CodeSandbox Demo](https://codesandbox.io/s/use-resize-observer-breakpoints-3hiv8) | ||
## Defaults (SSR) | ||
@@ -253,9 +338,28 @@ | ||
That said, there's a [polyfilled](https://github.com/que-etc/resize-observer-polyfill) | ||
CJS module that can be used for convenience (Not affecting globals): | ||
That said, there's a [polyfilled](https://github.com/juggle/resize-observer) | ||
CJS module that can be used for convenience: | ||
```js | ||
```ts | ||
import useResizeObserver from "use-resize-observer/polyfilled"; | ||
``` | ||
Note that using the above will use the polyfill, [even if the native ResizeObserver is available](https://github.com/juggle/resize-observer#basic-usage). | ||
To use the polyfill as a fallback only when the native RO is unavailable, you can polyfill yourself instead, | ||
either in your app's entry file, or you could create a local `useResizeObserver` module, like so: | ||
```ts | ||
// useResizeObserver.ts | ||
import { ResizeObserver } from "@juggle/resize-observer"; | ||
import useResizeObserver from "use-resize-observer"; | ||
if (!window.ResizeObserver) { | ||
window.ResizeObserver = ResizeObserver; | ||
} | ||
export default useResizeObserver; | ||
``` | ||
The same technique can also be used to provide any of your preferred ResizeObserver polyfills out there. | ||
## Related | ||
@@ -262,0 +366,0 @@ |
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
57813
562
363
41
1
+ Added@juggle/resize-observer@3.4.0(transitive)
- Removedresize-observer-polyfill@^1.5.1
- Removedresize-observer-polyfill@1.5.1(transitive)