Unverified Commit 405c6a4b authored by Tasso Evangelista's avatar Tasso Evangelista Committed by GitHub
Browse files

feat: useResizeObserver (#223)

parent 0863b265
......@@ -47,10 +47,12 @@ yarn test
- [Parameters](#parameters-8)
- [useMutableCallback](#usemutablecallback)
- [Parameters](#parameters-9)
- [useSafely](#usesafely)
- [useResizeObserver](#useresizeobserver)
- [Parameters](#parameters-10)
- [useToggle](#usetoggle)
- [useSafely](#usesafely)
- [Parameters](#parameters-11)
- [useToggle](#usetoggle)
- [Parameters](#parameters-12)
### useAutoFocus
......@@ -61,7 +63,7 @@ Hook to automatically request focus for an DOM element.
- `isFocused` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** if true, the focus will be requested (optional, default `true`)
- `options` **FocusOptions** options of the focus request
Returns **any** the ref which holds the element
Returns **{current: [HTMLElement](https://developer.mozilla.org/docs/Web/HTML/Element)?}** the ref which holds the element
### useDebouncedUpdates
......@@ -72,7 +74,7 @@ Hook to debounce the state updater function returned by hooks like `useState()`
- `pair` **\[any, function (): any]** the state and updater pair which will be debounced
- `pair.0` the state value
- `pair.1` the state updater function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay the updater
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay the updater
Returns **any** a state value and debounced updater pair
......@@ -85,7 +87,7 @@ Hook to create a reduced state with a debounced `dispatch()` function.
- `reducer` **function (any, any): any** the reducer function
- `initializerArg` **any** the initial state value or the argument passed to the initial state generator function
- `initializer` **function (any): any** the initial state generator function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay the updater
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay the updater
Returns **any** a state and debounced `dispatch()` function
......@@ -96,7 +98,7 @@ Hook to create a state with a debounced setter function.
#### Parameters
- `initialValue` **(any | function (): any)** the initial state value or the initial state generator function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay the updater
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay the updater
Returns **any** a state and debounced setter function
......@@ -107,7 +109,7 @@ Hook to memoize a debounced version of a callback.
#### Parameters
- `callback` **function (): any** the callback to debounce
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay
- `deps` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<any>?** the hook dependencies
Returns **function (): any** a memoized and debounced callback
......@@ -164,6 +166,17 @@ Hook to create a stable callback from a mutable one.
Returns **any** a stable callback
### useResizeObserver
Hook to track dimension changes in a DOM element using the ResizeObserver API.
#### Parameters
- `options` **UseResizeObserverOptions** (optional, default `{}`)
- `options.debounceDelay` the number of milliseconds to delay updates
Returns **UseResizeObserverReturn** a triple containing the ref and the size information
### useSafely
Hook that wraps pairs of state and updater to provide a new updater which
......
// @flow
export const debounce = (fn: (...Array<any>) => any, delay: number) => {
export const debounce = (fn: (...Array<any>) => any, delay: ?number) => {
if (delay === undefined || delay === null) {
return fn;
}
let timer;
let callback;
......
......@@ -8,6 +8,7 @@ export * from './useLazyRef';
export * from './useMediaQuery';
export * from './useMergedRefs';
export * from './useMutableCallback';
export * from './useResizeObserver';
export * from './useSafely';
export * from './useToggle';
export * from './useUniqueId';
......@@ -13,7 +13,7 @@ type FocusOptions = {
* @param options options of the focus request
* @return the ref which holds the element
*/
export const useAutoFocus = (isFocused: boolean = true, options: FocusOptions) => {
export const useAutoFocus = (isFocused: boolean = true, options: FocusOptions): { current: ?HTMLElement } => {
const elementRef = useRef<?HTMLElement>();
useEffect(() => {
......
......@@ -14,7 +14,7 @@ import { debounce } from './helpers';
*/
export const useDebouncedCallback = (
callback: (...Array<any>) => any,
delay: number,
deps?: Array<any>,
delay: ?number,
deps: ?Array<any>,
): ((...Array<any>) => any) =>
useMemo(() => debounce(callback, delay), Array.isArray(deps) ? [delay, ...deps] : undefined);
......@@ -15,7 +15,7 @@ import { useDebouncedCallback } from './useDebouncedCallback';
*/
export const useDebouncedUpdates = (
[value, update]: [any, () => any],
delay: number,
delay: ?number,
) => [value, useDebouncedCallback(update, delay, [])];
/**
......@@ -31,7 +31,7 @@ export const useDebouncedReducer = (
reducer: (any, any) => any,
initializerArg: any,
initializer: (any) => any,
delay: number,
delay: ?number,
) =>
useDebouncedUpdates(useReducer(reducer, initializerArg, initializer), delay);
......@@ -44,5 +44,5 @@ export const useDebouncedReducer = (
*/
export const useDebouncedState = (
initialValue: any | () => any,
delay: number,
delay: ?number,
) => useDebouncedUpdates(useState(initialValue), delay);
// @flow
import { useRef, useEffect } from 'react';
import { useDebouncedState } from './useDebouncedUpdates';
type UseResizeObserverOptions = {
debounceDelay: ?number,
};
type UseResizeObserverReturn = {
ref: { current: ?Element },
contentBoxSize: {
inlineSize: number,
blockSize: number,
},
borderBoxSize: {
inlineSize: number,
blockSize: number,
},
};
/**
* Hook to track dimension changes in a DOM element using the ResizeObserver API.
*
* @param options
* @param options.debounceDelay the number of milliseconds to delay updates
* @return a triple containing the ref and the size information
*/
export const useResizeObserver = ({
debounceDelay,
}: UseResizeObserverOptions = {}): UseResizeObserverReturn => {
const ref = useRef<?Element>();
const [{ contentBoxSize, borderBoxSize }, setSizes] = useDebouncedState({}, debounceDelay);
useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
const { contentBoxSize, borderBoxSize }: any = entry;
if (contentBoxSize && borderBoxSize) {
setSizes({
contentBoxSize,
borderBoxSize,
});
return;
}
const { target, contentRect } = entry;
const { width: contentBoxInlineSize, height: contentBoxBlockSize } = contentRect;
const { width: borderBoxInlineSize, height: borderBoxBlockSize } = target.getBoundingClientRect();
setSizes({
contentBoxSize: {
inlineSize: contentBoxInlineSize,
blockSize: contentBoxBlockSize,
},
borderBoxSize: {
inlineSize: borderBoxInlineSize,
blockSize: borderBoxBlockSize,
},
});
});
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.disconnect();
};
}, []);
return { ref, contentBoxSize, borderBoxSize };
};
/**
* @jest-environment node
*/
import { runHooksOnServer } from '../.jest/helpers';
import { useResizeObserver } from '../src';
describe('useResizeObserver hook on server', () => {
it('immediately returns undefined sizes', () => {
const { borderBoxSize, contentBoxSize } = runHooksOnServer(() => useResizeObserver());
expect(borderBoxSize).toBe(undefined);
expect(contentBoxSize).toBe(undefined);
});
});
import { runHooks } from '../.jest/helpers';
import { useResizeObserver } from '../src';
describe('useResizeObserver hook', () => {
let ro;
beforeEach(() => {
const contentBoxSize = {};
const borderBoxSize = {};
const trigger = () => {
ro.width = Math.round(1000 * Math.random());
ro.height = Math.round(1000 * Math.random());
ro.cb([{
borderBoxSize,
contentBoxSize,
contentRect: {
width: ro.width,
height: ro.height,
},
}]);
};
ro = {
width: Math.round(1000 * Math.random()),
height: Math.round(1000 * Math.random()),
cb: () => {},
observe: jest.fn(() => {
ro.cb([{
borderBoxSize,
contentBoxSize,
contentRect: {
width: ro.width,
height: ro.height,
},
}]);
}),
unobserve: jest.fn(),
resize: () => {
trigger();
return { contentBoxSize, borderBoxSize };
},
};
window.ResizeObserver = function(cb) {
ro.cb = cb;
this.observe = ro.observe;
this.unobserve = ro.unobserve;
};
});
it('immediately returns undefined size', () => {
ro.resize();
const [{ contentBoxSize, borderBoxSize }] = runHooks(() => useResizeObserver());
expect(contentBoxSize).toBe(undefined);
expect(borderBoxSize).toBe(undefined);
});
it('gets the observed element size', () => {
const {
contentBoxSize: expectedContentBoxSize,
borderBoxSize: expectedBorderBoxSize,
} = ro.resize();
const [, { contentBoxSize, borderBoxSize }] = runHooks(() => useResizeObserver(), [
({ ref }) => {
ref.current = document.createElement('div');
},
]);
expect(contentBoxSize.blockSize).toBe(expectedContentBoxSize.blockSize);
expect(contentBoxSize.inlineSize).toBe(expectedContentBoxSize.inlineSize);
expect(borderBoxSize.blockSize).toBe(expectedBorderBoxSize.blockSize);
expect(borderBoxSize.inlineSize).toBe(expectedBorderBoxSize.inlineSize);
});
it('gets the observed element size after resize', () => {
const {
contentBoxSize: expectedContentBoxSizeA,
borderBoxSize: expectedBorderBoxSizeA,
} = ro.resize();
let expectedContentBoxSizeB;
let expectedBorderBoxSizeB;
const [
,
{
contentBoxSize: contentBoxSizeA,
borderBoxSize: borderBoxSizeA,
},
{
contentBoxSize: contentBoxSizeB,
borderBoxSize: borderBoxSizeB,
},
] = runHooks(() => useResizeObserver(), [
({ ref }) => {
ref.current = document.createElement('div');
},
() => {
({
contentBoxSize: expectedContentBoxSizeB,
borderBoxSize: expectedBorderBoxSizeB,
} = ro.resize());
},
]);
expect(contentBoxSizeA.blockSize).toBe(expectedContentBoxSizeA.blockSize);
expect(contentBoxSizeA.inlineSize).toBe(expectedContentBoxSizeA.inlineSize);
expect(borderBoxSizeA.blockSize).toBe(expectedBorderBoxSizeA.blockSize);
expect(borderBoxSizeA.inlineSize).toBe(expectedBorderBoxSizeA.inlineSize);
expect(contentBoxSizeB.blockSize).toBe(expectedContentBoxSizeB.blockSize);
expect(contentBoxSizeB.inlineSize).toBe(expectedContentBoxSizeB.inlineSize);
expect(borderBoxSizeB.blockSize).toBe(expectedBorderBoxSizeB.blockSize);
expect(borderBoxSizeB.inlineSize).toBe(expectedBorderBoxSizeB.inlineSize);
});
});
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment