diff --git a/.size-limit.json b/.size-limit.json index 187ca43..3d07a17 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -1,32 +1,32 @@ [ { "path": "dist/bundle.esm.js", - "limit": "495 B", + "limit": "523 B", "gzip": true }, { "path": "dist/bundle.esm.js", - "limit": "396 B", + "limit": "423 B", "brotli": true }, { "path": "dist/bundle.cjs.js", - "limit": "480 B", + "limit": "508 B", "gzip": true }, { "path": "dist/bundle.cjs.js", - "limit": "385 B", + "limit": "408 B", "brotli": true }, { "path": "polyfilled.js", - "limit": "2808 B", + "limit": "2846 B", "gzip": true }, { "path": "polyfilled.js", - "limit": "2510 B", + "limit": "2542 B", "brotli": true } ] diff --git a/src/index.ts b/src/index.ts index e5a365a..d493b6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,9 +92,15 @@ type HookResponse = { ref: RefCallback; } & ObservedSize; +export type RoundingFunction = ( + width: number, + height: number +) => [number, number]; + function useResizeObserver( opts: { ref?: RefObject | T | null | undefined; + round?: RoundingFunction; onResize?: ResizeHandler; } = {} ): HookResponse { @@ -156,9 +162,15 @@ function useResizeObserver( const entry = entries[0]; - // `Math.round` is in line with how CSS resolves sub-pixel values - const newWidth = Math.round(entry.contentRect.width); - const newHeight = Math.round(entry.contentRect.height); + // Rounding defaults to Math.round but can be customized using opts + const round = + opts.round ?? + ((width, height) => [Math.round(width), Math.round(height)]); + const [newWidth, newHeight] = round( + entry.contentRect.width, + entry.contentRect.height + ); + if ( previous.current.width !== newWidth || previous.current.height !== newHeight diff --git a/tests/basic.tsx b/tests/basic.tsx index 494de90..0a822ea 100644 --- a/tests/basic.tsx +++ b/tests/basic.tsx @@ -229,6 +229,28 @@ describe("Vanilla tests", () => { handler.assertRenderCount(3); }); + it("should trigger renders on subpixel changes if a custom rounding function is provided", async () => { + const handler = await render(Observed, {}, { round: (meassurements: [number, number]) => meassurements }); + + handler.assertDefaultSize(); + + // Default render + first measurement + await awaitNextFrame(); + handler.assertRenderCount(2); + + handler.setSize({ width: 100, height: 102 }); + await awaitNextFrame(); + handler.assertSize({ width: 100, height: 102 }); + handler.assertRenderCount(3); + + // Shouldn't trigger on subpixel values that are rounded to be the same as the + // previous size + handler.setSize({ width: 100.4, height: 102.4 }); + await awaitNextFrame(); + handler.assertSize({ width: 100.4, height: 102.4 }); + handler.assertRenderCount(4); + }); + it("should keep the same response instance between renders if nothing changed", async () => { let assertSameInstance = (): void => { throw new Error( diff --git a/tests/utils/index.tsx b/tests/utils/index.tsx index ef7f19b..c457029 100644 --- a/tests/utils/index.tsx +++ b/tests/utils/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from "react-dom"; import useResizeObserver from "../.."; import useMergedCallbackRef from "./useMergedCallbackRef"; import awaitNextFrame from "./awaitNextFrame"; +import { RoundingFunction } from '../../src'; export type Size = { width: number; @@ -107,14 +108,15 @@ export const Observed: FunctionComponent< defaultWidth?: number; defaultHeight?: number; onResize?: (size: ObservedSize) => void; + round?: RoundingFunction; } -> = ({ resolveHandler, defaultWidth, defaultHeight, onResize, ...props }) => { +> = ({ resolveHandler, defaultWidth, defaultHeight, onResize, round, ...props }) => { const renderCountRef = useRef(0); const { ref, width = defaultWidth, height = defaultHeight, - } = useResizeObserver({ onResize }); + } = useResizeObserver({ onResize, round }); const currentSizeRef = useRef({ width: undefined, height: undefined,