Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useEvent and revamped useResizeObserver to @wordpress/compose #64943

Merged
merged 32 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
95881cc
Simplify useResizeObserver
jsnajdr Aug 27, 2024
1e305a9
Loop through all resize entries
jsnajdr Aug 30, 2024
55172e3
Add `useEvent` util.
DaniGuardiola Aug 30, 2024
0b91189
Add `useObserveElementSize` util.
DaniGuardiola Aug 30, 2024
c4ee26c
Simplify `useResizeObserver` by using `useEvent` and `useObserveEleme…
DaniGuardiola Aug 30, 2024
575423c
Merge branch 'trunk' of https://github.com/WordPress/gutenberg into f…
DaniGuardiola Aug 30, 2024
93a36e1
Switch to layout effect and accept refs too.
DaniGuardiola Sep 2, 2024
fc33849
Prevent initial re-render in ResizeElement.
DaniGuardiola Sep 2, 2024
b422ccc
Better error message.
DaniGuardiola Sep 2, 2024
cff6661
Improved example of useEvent.
DaniGuardiola Sep 2, 2024
9531c10
Update packages/compose/src/hooks/use-event/index.ts
DaniGuardiola Sep 2, 2024
c58e10b
Sync docs.
DaniGuardiola Sep 2, 2024
0da2846
Avoid redundant resize listener calls.
DaniGuardiola Sep 2, 2024
bc77100
Switch to structural check.
DaniGuardiola Sep 2, 2024
638f551
Improve example.
DaniGuardiola Sep 2, 2024
654bac5
Fix docs.
DaniGuardiola Sep 2, 2024
00f53f7
Make `useObserveElementSize` generic.
DaniGuardiola Sep 3, 2024
b6dc4f4
New API that returns a ref.
DaniGuardiola Sep 5, 2024
74c4a00
Make utility private for now.
DaniGuardiola Sep 5, 2024
4fb82b9
Mark legacy `useResizeObserver` as such.
DaniGuardiola Sep 5, 2024
a381140
Rename `useObserveElementSize` to `useResizeObserver`.
DaniGuardiola Sep 5, 2024
7b4ecce
Add return type.
DaniGuardiola Sep 5, 2024
f42603c
Add signature as overload.
DaniGuardiola Sep 5, 2024
63deb16
Add support for legacy API.
DaniGuardiola Sep 5, 2024
30a07f0
Move into subdirectory.
DaniGuardiola Sep 5, 2024
e57cc0e
Minor import fix.
DaniGuardiola Sep 5, 2024
15eaebc
Fix docgen to support overloads (will pick up the first function sign…
DaniGuardiola Sep 5, 2024
3387a0a
Replace legacy utility with the new one.
DaniGuardiola Sep 6, 2024
8c2094f
Apply feedback.
DaniGuardiola Sep 8, 2024
6f3abb7
Clean up and document.
DaniGuardiola Sep 8, 2024
4aa01b3
Added changelog entries.
DaniGuardiola Sep 9, 2024
55946ef
Merge branch 'trunk' of https://github.com/WordPress/gutenberg into f…
DaniGuardiola Sep 9, 2024
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
42 changes: 42 additions & 0 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,23 @@ _Returns_

- `import('react').RefCallback<HTMLElement>`: Element Ref.

### useEvent

Creates a stable callback function that has access to the latest state and can be used within event handlers and effect callbacks. Throws when used in the render phase.

_Usage_

```tsx
function Component( props ) {
const onClick = useEvent( props.onClick );
React.useEffect( () => {}, [ onClick ] );
}
```

_Parameters_

- _callback_ `T`: The callback function to wrap.

### useFocusableIframe

Dispatches a bubbling focus event when the iframe receives focus. Use `onFocus` as usual on the iframe or a parent element.
Expand Down Expand Up @@ -463,6 +480,31 @@ _Returns_

- `V | undefined`: The value corresponding to the map key requested.

### useObserveElementSize

Tracks a given element's size and calls `onUpdate` for all of its discrete values using a `ResizeObserver`. The element can change dynamically and **it must not be stored in a ref**. Instead, it should be stored in a React state or equivalent.

_Usage_

```tsx
const [ targetElement, setTargetElement ] = useState< HTMLElement | null >();
useObserveElementSize(
targetElement,
( resizeObserverEntries, element ) => {
console.log( 'Resize observer entries:', resizeObserverEntries );
console.log( 'Element that was resized:', element );
},
{ box: 'border-box' }
);
<div ref={ setTargetElement } />;
```

_Parameters_

- _targetElement_ `HTMLElement | undefined | null`: The target element to observe. It can be changed dynamically.
- _onUpdate_ `( resizeObserverEntries: ResizeObserverEntry[], element: HTMLElement ) => void`: Callback that will be called when the element is resized. It is passed the list of [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects passed to the `ResizeObserver.observe` callback internally, and the element being tracked at the time of this update.
- _resizeObserverOptions_ `ResizeObserverOptions`: Options to pass to `ResizeObserver.observe` when called internally. Updating this option will not cause the observer to be re-created, and it will only take effect if a new element is observed.

### usePrevious

Use something's value from the previous render. Based on <https://usehooks.com/usePrevious/>.
Expand Down
43 changes: 43 additions & 0 deletions packages/compose/src/hooks/use-event/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* WordPress dependencies
*/
import { useRef, useInsertionEffect, useCallback } from '@wordpress/element';

/**
* Any function.
*/
export type AnyFunction = ( ...args: any ) => any;

/**
* Creates a stable callback function that has access to the latest state and
* can be used within event handlers and effect callbacks. Throws when used in
* the render phase.
*
* @param callback The callback function to wrap.
*
* @example
*
* ```tsx
* function Component(props) {
* const onClick = useEvent(props.onClick);
* React.useEffect(() => {}, [onClick]);
* }
* ```
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
*/
export default function useEvent< T extends AnyFunction >(
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
/**
* The callback function to wrap.
*/
callback?: T
) {
const ref = useRef< AnyFunction | undefined >( () => {
throw new Error( 'Cannot call an event handler while rendering.' );
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
} );
useInsertionEffect( () => {
ref.current = callback;
} );
return useCallback< AnyFunction >(
( ...args ) => ref.current?.( ...args ),
[]
) as T;
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
}
109 changes: 109 additions & 0 deletions packages/compose/src/hooks/use-observe-element-size/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* WordPress dependencies
*/
import { useRef, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import useEvent from '../use-event';

/**
* Tracks a given element's size and calls `onUpdate` for all of its discrete
* values using a `ResizeObserver`. The element can change dynamically and **it
* must not be stored in a ref**. Instead, it should be stored in a React
* state or equivalent.
*
* @param targetElement The target element to observe. It can be changed dynamically.
* @param onUpdate Callback that will be called when the element is resized. It is passed the list of [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects passed to the `ResizeObserver.observe` callback internally, and the element being tracked at the time of this update.
* @param resizeObserverOptions Options to pass to `ResizeObserver.observe` when called internally. Updating this option will not cause the observer to be re-created, and it will only take effect if a new element is observed.
*
* @example
*
* ```tsx
* const [ targetElement, setTargetElement ] = useState< HTMLElement | null >();
* useObserveElementSize(
* targetElement,
* ( resizeObserverEntries, element ) => {
* console.log( 'Resize observer entries:', resizeObserverEntries );
* console.log( 'Element that was resized:', element );
* },
* { box: 'border-box' }
* );
* <div ref={ setTargetElement } />;
* ```
*/
export default function useObserveElementSize(
/**
* The target element to observe. It can be changed dynamically.
*/
targetElement: HTMLElement | undefined | null,
/**
* Callback that will be called when the element is resized.
*/
onUpdate: (
/**
* The list of
* [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* objects passed to the `ResizeObserver.observe` callback internally.
*/
resizeObserverEntries: ResizeObserverEntry[],
/**
* The element being tracked at the time of this update.
*/
element: HTMLElement
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
) => void,
/**
* Options to pass to `ResizeObserver.observe` when called internally.
*
* Updating this option will not cause the observer to be re-created, and it
* will only take effect if a new element is observed.
*/
resizeObserverOptions?: ResizeObserverOptions
) {
const onUpdateEvent = useEvent( onUpdate );

const observedElementRef = useRef< HTMLElement | null >();
const resizeObserverRef = useRef< ResizeObserver >();

// Options are passed on `.observe` once and never updated, so we store them
// in an up-to-date ref to avoid unnecessary cycles of the effect due to
// unstable option objects such as inlined literals.
const resizeObserverOptionsRef = useRef( resizeObserverOptions );
useEffect( () => {
resizeObserverOptionsRef.current = resizeObserverOptions;
}, [ resizeObserverOptions ] );

// TODO: could/should this be a layout effect?
useEffect( () => {
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
if ( targetElement === observedElementRef.current ) {
return;
}

observedElementRef.current = targetElement;

// Set up a ResizeObserver.
if ( ! resizeObserverRef.current ) {
resizeObserverRef.current = new ResizeObserver( ( entries ) => {
if ( observedElementRef.current ) {
onUpdateEvent( entries, observedElementRef.current );
}
} );
}
const { current: resizeObserver } = resizeObserverRef;

// Observe new element.
if ( targetElement ) {
resizeObserver.observe(
targetElement,
resizeObserverOptionsRef.current
);
}

return () => {
// Unobserve previous element.
if ( observedElementRef.current ) {
resizeObserver.unobserve( observedElementRef.current );
}
};
}, [ onUpdateEvent, targetElement ] );
}
47 changes: 18 additions & 29 deletions packages/compose/src/hooks/use-resize-observer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import type { ReactElement } from 'react';
/**
* WordPress dependencies
*/
import {
useCallback,
useLayoutEffect,
useRef,
useState,
} from '@wordpress/element';
import { useCallback, useRef, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import useEvent from '../use-event';
import useObserveElementSize from '../use-observe-element-size';

type ObservedSize = {
width: number | null;
Expand Down Expand Up @@ -84,32 +84,21 @@ type ResizeElementProps = {
};

function ResizeElement( { onResize }: ResizeElementProps ) {
const resizeElementRef = useRef< HTMLDivElement >( null );
const resizeCallbackRef = useRef( onResize );

useLayoutEffect( () => {
resizeCallbackRef.current = onResize;
}, [ onResize ] );

useLayoutEffect( () => {
const resizeElement = resizeElementRef.current as HTMLDivElement;
const resizeObserver = new ResizeObserver( ( entries ) => {
for ( const entry of entries ) {
const newSize = extractSize( entry );
resizeCallbackRef.current( newSize );
}
} );

resizeObserver.observe( resizeElement );

return () => {
resizeObserver.unobserve( resizeElement );
};
}, [] );
const resizeCallbackEvent = useEvent( onResize );

const [ resizeElement, setResizeElement ] =
useState< HTMLDivElement | null >();
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved

useObserveElementSize( resizeElement, ( entries ) => {
for ( const entry of entries ) {
const newSize = extractSize( entry );
resizeCallbackEvent( newSize );
}
} );

return (
<div
ref={ resizeElementRef }
ref={ setResizeElement }
style={ RESIZE_ELEMENT_STYLES }
aria-hidden="true"
/>
Expand Down
2 changes: 2 additions & 0 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { default as useCopyOnClick } from './hooks/use-copy-on-click';
export { default as useCopyToClipboard } from './hooks/use-copy-to-clipboard';
export { default as __experimentalUseDialog } from './hooks/use-dialog';
export { default as useDisabled } from './hooks/use-disabled';
export { default as useEvent } from './hooks/use-event';
export { default as __experimentalUseDragging } from './hooks/use-dragging';
export { default as useFocusOnMount } from './hooks/use-focus-on-mount';
export { default as __experimentalUseFocusOutside } from './hooks/use-focus-outside';
Expand All @@ -33,6 +34,7 @@ export { default as useInstanceId } from './hooks/use-instance-id';
export { default as useIsomorphicLayoutEffect } from './hooks/use-isomorphic-layout-effect';
export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut';
export { default as useMediaQuery } from './hooks/use-media-query';
export { default as useObserveElementSize } from './hooks/use-observe-element-size';
export { default as usePrevious } from './hooks/use-previous';
export { default as useReducedMotion } from './hooks/use-reduced-motion';
export { default as useStateWithHistory } from './hooks/use-state-with-history';
Expand Down
Loading