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 30 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
52 changes: 41 additions & 11 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,29 @@ _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 );
useEffect( () => {
onClick();
// Won't trigger the effect again when props.onClick is updated.
}, [ onClick ] );
// Won't re-render Button when props.onClick is updated (if `Button` is
// wrapped in `React.memo`).
return <Button onClick={ 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 @@ -500,23 +523,30 @@ _Returns_

### useResizeObserver

Hook which allows to listen to the resize event of any target element when it changes size. \_Note: `useResizeObserver` will report `null` sizes until after first render.
Sets up a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API) for an HTML or SVG element.

Pass the returned setter as a callback ref to the React element you want to observe, or use it in layout effects for advanced use cases.

_Usage_

```js
const App = () => {
const [ resizeListener, sizes ] = useResizeObserver();
```tsx
const setElement = useResizeObserver(
( resizeObserverEntries ) => console.log( resizeObserverEntries ),
{ box: 'border-box' }
);
<div ref={ setElement } />;

return (
<div>
{ resizeListener }
Your content here
</div>
);
};
// The setter can be used in other ways, for example:
useLayoutEffect( () => {
setElement( document.querySelector( `data-element-id="${ elementId }"` ) );
}, [ elementId ] );
```

_Parameters_

- _callback_ `ResizeObserverCallback`: The `ResizeObserver` callback - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback).
- _options_ `ResizeObserverOptions`: Options passed to `ResizeObserver.observe` when called - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#options). Changes will be ignored.

### useStateWithHistory

useState with undo/redo history.
Expand Down
51 changes: 51 additions & 0 deletions packages/compose/src/hooks/use-event/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* 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 );
* useEffect( () => {
* onClick();
* // Won't trigger the effect again when props.onClick is updated.
* }, [ onClick ] );
* // Won't re-render Button when props.onClick is updated (if `Button` is
* // wrapped in `React.memo`).
* return <Button onClick={ onClick } />;
* }
* ```
*/
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(
'Callbacks created with `useEvent` cannot be called during rendering.'
);
} );
useInsertionEffect( () => {
ref.current = callback;
} );
return useCallback< AnyFunction >(
( ...args ) => ref.current?.( ...args ),
[]
) as T;
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ 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 useResizeObserver from '../index';
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved

type ObservedSize = {
export type ObservedSize = {
width: number | null;
height: number | null;
};
Expand Down Expand Up @@ -84,28 +83,10 @@ 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 resizeElementRef = useResizeObserver( ( entries ) => {
const newSize = extractSize( entries.at( -1 )! ); // Entries are never empty.
onResize( newSize );
} );

return (
<div
Expand Down Expand Up @@ -141,7 +122,10 @@ const NULL_SIZE: ObservedSize = { width: null, height: null };
* };
* ```
*/
export default function useResizeObserver(): [ ReactElement, ObservedSize ] {
export default function useLegacyResizeObserver(): [
ReactElement,
ObservedSize,
] {
const [ size, setSize ] = useState( NULL_SIZE );

// Using a ref to track the previous width / height to avoid unnecessary renders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { View } from 'react-native';
/**
* Internal dependencies
*/
import useResizeObserver from '../';
import useResizeObserver from '..';

const TestComponent = ( { onLayout } ) => {
const [ resizeObserver, sizes ] = useResizeObserver();
Expand Down
119 changes: 119 additions & 0 deletions packages/compose/src/hooks/use-resize-observer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* WordPress dependencies
*/
import { useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import useEvent from '../use-event';
import type { ObservedSize } from './_legacy';
import _useLegacyResizeObserver from './_legacy';
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
/**
* External dependencies
*/
import type { ReactElement } from 'react';

// This is the current implementation of `useResizeObserver`.
//
// The legacy implementation is still supported for backwards compatibility.
// This is achieved by overloading the exported function with both signatures,
// and detecting which API is being used at runtime.
function _useResizeObserver< T extends HTMLElement >(
callback: ResizeObserverCallback,
resizeObserverOptions: ResizeObserverOptions = {}
): ( element?: T | null ) => void {
const callbackEvent = useEvent( callback );

const observedElementRef = useRef< T | null >();
const resizeObserverRef = useRef< ResizeObserver >();
return useEvent( ( element?: T | null ) => {
if ( element === observedElementRef.current ) {
return;
}
observedElementRef.current = element;

// Set up `ResizeObserver`.
resizeObserverRef.current ??= new ResizeObserver( callbackEvent );
const { current: resizeObserver } = resizeObserverRef;

// Unobserve previous element.
if ( observedElementRef.current ) {
resizeObserver.unobserve( observedElementRef.current );
}
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved

// Observe new element.
if ( element ) {
resizeObserver.observe( element, resizeObserverOptions );
}
} );
}

/**
* Sets up a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API)
* for an HTML or SVG element.
*
* Pass the returned setter as a callback ref to the React element you want
* to observe, or use it in layout effects for advanced use cases.
*
* @example
*
* ```tsx
* const setElement = useResizeObserver(
* ( resizeObserverEntries ) => console.log( resizeObserverEntries ),
* { box: 'border-box' }
* );
* <div ref={ setElement } />;
*
* // The setter can be used in other ways, for example:
* useLayoutEffect( () => {
* setElement( document.querySelector( `data-element-id="${ elementId }"` ) );
* }, [ elementId ] );
* ```
*
* @param callback The `ResizeObserver` callback - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback).
* @param options Options passed to `ResizeObserver.observe` when called - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#options). Changes will be ignored.
*/
export default function useResizeObserver< T extends Element >(
/**
* The `ResizeObserver` callback - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback).
*/
callback: ResizeObserverCallback,
/**
* Options passed to `ResizeObserver.observe` when called - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#options). Changes will be ignored.
*/
options?: ResizeObserverOptions
): ( element?: T | null ) => void;

/**
* **This is a legacy API and should not be used.**
*
* @deprecated Use the other `useResizeObserver` API instead: `const ref = useResizeObserver( ( entries ) => { ... } )`.
*
* Hook which allows to listen to the resize event of any target element when it changes size.
* _Note: `useResizeObserver` will report `null` sizes until after first render.
*
* @example
*
* ```js
* const App = () => {
* const [ resizeListener, sizes ] = useResizeObserver();
*
* return (
* <div>
* { resizeListener }
* Your content here
* </div>
* );
* };
* ```
*/
export default function useResizeObserver(): [ ReactElement, ObservedSize ];

export default function useResizeObserver< T extends HTMLElement >(
callback?: ResizeObserverCallback,
options: ResizeObserverOptions = {}
): ( ( element?: T | null ) => void ) | [ ReactElement, ObservedSize ] {
return callback
? _useResizeObserver( callback, options )
: _useLegacyResizeObserver();
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 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 Down
4 changes: 4 additions & 0 deletions packages/docgen/lib/get-type-annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ function getTypeAnnotation( typeAnnotation ) {
* TODO: Remove the special-casing here once we're able to infer the types from TypeScript itself.
*/
function unwrapWrappedSelectors( token ) {
if ( babelTypes.isTSDeclareFunction( token ) ) {
return token;
}

if ( babelTypes.isFunctionDeclaration( token ) ) {
return token;
}
Expand Down
Loading