Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/useMeasure/index.dom.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {act, renderHook} from '@ver0/react-hooks-testing';
import {useEffect} from 'react';
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
import {useMeasure} from '../index.js';
import {borderBoxMeasurer, useMeasure} from '../index.js';
import {expectResultValue} from '../util/testing/test-helpers.js';

describe('useMeasure', () => {
Expand Down Expand Up @@ -116,4 +116,34 @@ describe('useMeasure', () => {
expect(value[1]).toStrictEqual({current: div});
expect(value[0]).toStrictEqual(measures);
});

it('should set state by the given measurer', async () => {
const div = document.createElement('div');
const {result} = await renderHook(() => {
const measure = useMeasure<HTMLDivElement>(true, borderBoxMeasurer);

useEffect(() => {
measure[1].current = div;
});

return measure;
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const entry = {
target: div,
contentRect: {width: 5, height: 3},
borderBoxSize: [{inlineSize: 9, blockSize: 7}],
contentBoxSize: {},
} as unknown as ResizeObserverEntry;

ResizeObserverSpy.mock.calls[0][0]([entry]);

await act(async () => {
vi.advanceTimersByTime(1);
});

const value = expectResultValue(result);
expect(value[0]).toStrictEqual({width: 9, height: 7});
});
});
31 changes: 29 additions & 2 deletions src/useMeasure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,39 @@ export type Measures = {
height: number;
};

export type Measurer = (entry: ResizeObserverEntry) => Measures;

/**
* Measures an observed element by the `content-box` sizing model, excluding paddings and borders.
*
* Default measurer of `useMeasure`.
*/
export const contentBoxMeasurer: Measurer = (entry) => ({
width: entry.contentRect.width,
height: entry.contentRect.height,
});

/**
* Measures an observed element by the `border-box` sizing model, including paddings and borders.
*
* Border box sizes are writing-mode relative, so `inlineSize` maps to width and `blockSize` to
* height only in horizontal writing modes.
*/
export const borderBoxMeasurer: Measurer = (entry) => ({
width: entry.borderBoxSize[0].inlineSize,
height: entry.borderBoxSize[0].blockSize,
});

/**
* Uses ResizeObserver to track element dimensions and re-render component when they change.
*
* @param enabled Whether resize observer is enabled or not.
* @param measurer Derives measures from the observer entry, `contentBoxMeasurer` by default.
*/
export function useMeasure<T extends Element>(enabled = true): [Measures | undefined, RefObject<T | null>] {
export function useMeasure<T extends Element>(
enabled = true,
measurer: Measurer = contentBoxMeasurer,
): [Measures | undefined, RefObject<T | null>] {
const [element, setElement] = useState<T | null>(null);
const elementRef = useHookableRef<T | null>(null, (v) => {
setElement(v);
Expand All @@ -25,7 +52,7 @@ export function useMeasure<T extends Element>(enabled = true): [Measures | undef

const [measures, setMeasures] = useState<Measures>();
const [observerHandler] = useRafCallback<UseResizeObserverCallback>((entry) => {
setMeasures({width: entry.contentRect.width, height: entry.contentRect.height});
setMeasures(measurer(entry));
});

useResizeObserver(element, observerHandler, enabled);
Expand Down