From 7881ca43fcd0db5750f8a26a8cc9bf23842641ec Mon Sep 17 00:00:00 2001 From: Leroy Korterink Date: Fri, 24 Nov 2023 19:37:28 +0100 Subject: [PATCH] Create useContentRect hook --- src/hooks/useContentRect/useContentRect.mdx | 45 +++++++++++++ .../useContentRect/useContentRect.stories.tsx | 67 +++++++++++++++++++ src/hooks/useContentRect/useContentRect.ts | 24 +++++++ .../useContentRectState.mdx | 43 ++++++++++++ .../useContentRectState.stories.tsx | 60 +++++++++++++++++ .../useContentRectState.ts | 26 +++++++ src/index.ts | 2 + 7 files changed, 267 insertions(+) create mode 100644 src/hooks/useContentRect/useContentRect.mdx create mode 100644 src/hooks/useContentRect/useContentRect.stories.tsx create mode 100644 src/hooks/useContentRect/useContentRect.ts create mode 100644 src/hooks/useContentRectState/useContentRectState.mdx create mode 100644 src/hooks/useContentRectState/useContentRectState.stories.tsx create mode 100644 src/hooks/useContentRectState/useContentRectState.ts diff --git a/src/hooks/useContentRect/useContentRect.mdx b/src/hooks/useContentRect/useContentRect.mdx new file mode 100644 index 0000000..db4ed6a --- /dev/null +++ b/src/hooks/useContentRect/useContentRect.mdx @@ -0,0 +1,45 @@ +import { Meta } from '@storybook/blocks'; + + + +# useContentRect + +The `useContentRect` hook is used to get the size of an HTML element, the `DOMRect` is stored in a +`RefObject`. It uses the `ResizeObserver` API to observe the element `DOMRect`, the target is +disconnected when the component unmounts. + +## Reference + +```ts +function useContentRect(ref: Unreffable): RefObject; +``` + +## Using `useContentRect` with a RefObject + +In this example, the `useRef` hook is used to create a `RefObject` for the `div` element which is +initially set to null. The `useContentRect` hook is then called with the ref variable. + +```tsx +function MyComponent() { + const ref = useRef(null); + + const contentRectRef = useContentRect(ref); + + return
; +} +``` + +## Using `useContentRect` with an element + +In this example, the `useState` hook is used to create a state variable element which is initially +set to null. The `useContentRect` hook is then called with the element variable. + +```tsx +function MyComponent() { + const [element, setElement] = useState(null); + + const contentRectRef = useContentRect(element); + + return
; +} +``` diff --git a/src/hooks/useContentRect/useContentRect.stories.tsx b/src/hooks/useContentRect/useContentRect.stories.tsx new file mode 100644 index 0000000..d08c802 --- /dev/null +++ b/src/hooks/useContentRect/useContentRect.stories.tsx @@ -0,0 +1,67 @@ +/* eslint-disable react/jsx-no-literals, react-hooks/rules-of-hooks */ +import type { Meta, StoryObj } from '@storybook/react'; +import { useEffect, useRef } from 'react'; +import { useForceRerender } from '../../index.js'; +import { useContentRect } from './useContentRect.js'; + +const meta = { + title: 'Hooks / useContentRect', +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const UseContentRect: Story = { + render() { + const onClick = useForceRerender(); + const ref = useRef(null); + const contentRectRef = useContentRect(ref); + + useEffect(() => { + const animation = ref.current?.animate( + [ + // keyframes + { inlineSize: '300px', blockSize: '300px' }, + { inlineSize: '500px', blockSize: '500px' }, + { inlineSize: '300px', blockSize: '300px' }, + ], + { + // timing options + duration: 2000, + iterations: Number.POSITIVE_INFINITY, + }, + ); + + return () => animation?.cancel(); + }, []); + + return ( + <> +

+ Element size:{' '} + +

+
+ + {JSON.stringify(contentRectRef.current, null, 2)} + +
+ + ); + }, +}; diff --git a/src/hooks/useContentRect/useContentRect.ts b/src/hooks/useContentRect/useContentRect.ts new file mode 100644 index 0000000..a6718bf --- /dev/null +++ b/src/hooks/useContentRect/useContentRect.ts @@ -0,0 +1,24 @@ +import { useRef, type RefObject } from 'react'; +import { useMount } from '../../lifecycle/hooks/useMount/useMount.js'; +import { unref, type Unreffable } from '../../utils/unref/unref.js'; +import { useResizeObserver } from '../useResizeObserver/useResizeObserver.js'; + +/** + * A hook that returns a ref object containing the content rectangle of the target + * element. The content rectangle is updated whenever the target element is resized. + */ +export function useContentRect( + target: Unreffable, +): RefObject { + const contentRectRef = useRef(null); + + useResizeObserver(target, (entries): void => { + contentRectRef.current = entries.at(0)?.contentRect ?? null; + }); + + useMount(() => { + contentRectRef.current = unref(target)?.getBoundingClientRect() ?? null; + }); + + return contentRectRef; +} diff --git a/src/hooks/useContentRectState/useContentRectState.mdx b/src/hooks/useContentRectState/useContentRectState.mdx new file mode 100644 index 0000000..c6566c3 --- /dev/null +++ b/src/hooks/useContentRectState/useContentRectState.mdx @@ -0,0 +1,43 @@ +import { Meta } from '@storybook/blocks'; + + + +# useContentRectState + +The `useContentRectState` hook is used to get the size of an HTML element, the `DOMRect` is stored +in a state variable. It uses the `ResizeObserver` API to observe the element `DOMRect`, the target +is disconnected when the component unmounts. + +## Reference + +```ts +function useContentRectState(ref: Unreffable): RefObject; +``` + +## Using `useContentRectState` with a RefObject + +In this example, the `useRef` hook is used to create a `RefObject` for the `div` element which is +initially set to null. The `useContentRectState` hook is then called with the ref variable. + +```tsx +function MyComponent() { + const ref = useRef(null); + const contentRect = useContentRectState(ref); + + return
; +} +``` + +## Using `useContentRectState` with an element + +In this example, the `useState` hook is used to create a state variable element which is initially +set to null. The `useContentRectState` hook is then called with the element variable. + +```tsx +function MyComponent() { + const [element, setElement] = useState(null); + const contentRect = useContentRectState(element); + + return
; +} +``` diff --git a/src/hooks/useContentRectState/useContentRectState.stories.tsx b/src/hooks/useContentRectState/useContentRectState.stories.tsx new file mode 100644 index 0000000..9f98e41 --- /dev/null +++ b/src/hooks/useContentRectState/useContentRectState.stories.tsx @@ -0,0 +1,60 @@ +/* eslint-disable react/jsx-no-literals, react-hooks/rules-of-hooks */ +import type { Meta, StoryObj } from '@storybook/react'; +import { useEffect, useRef } from 'react'; +import { useContentRectState } from './useContentRectState.js'; + +const meta = { + title: 'Hooks / useContentRectState', +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const UseContentRectState: Story = { + render() { + const ref = useRef(null); + const contentRect = useContentRectState(ref); + + useEffect(() => { + const animation = ref.current?.animate( + [ + // keyframes + { inlineSize: '300px', blockSize: '300px' }, + { inlineSize: '500px', blockSize: '500px' }, + { inlineSize: '300px', blockSize: '300px' }, + ], + { + // timing options + duration: 2000, + iterations: Number.POSITIVE_INFINITY, + }, + ); + + return () => animation?.cancel(); + }, []); + + return ( + <> +

Element size:

+
+ + {JSON.stringify(contentRect, null, 2)} + +
+ + ); + }, +}; diff --git a/src/hooks/useContentRectState/useContentRectState.ts b/src/hooks/useContentRectState/useContentRectState.ts new file mode 100644 index 0000000..aba3ff5 --- /dev/null +++ b/src/hooks/useContentRectState/useContentRectState.ts @@ -0,0 +1,26 @@ +import { useRef, useState } from 'react'; +import { useMount } from '../../index.js'; +import { unref, type Unreffable } from '../../utils/unref/unref.js'; +import { useResizeObserver } from '../useResizeObserver/useResizeObserver.js'; + +/** + * A hook that returns the content rectangle of the target element. + * The content rectangle is updated whenever the target element is resized. + */ +export function useContentRectState(target: Unreffable): DOMRectReadOnly | null { + const [contentRect, setContentRect] = useState(null); + const rafRef = useRef(0); + + useResizeObserver(target, (entries) => { + cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(() => { + setContentRect(entries.at(0)?.contentRect ?? null); + }); + }); + + useMount(() => { + setContentRect(unref(target)?.getBoundingClientRect() ?? null); + }); + + return contentRect; +} diff --git a/src/index.ts b/src/index.ts index 63b73c4..c2c4918 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,8 @@ export * from './gsap/hooks/useScrollAnimation/useScrollAnimation.js'; export * from './gsap/utils/getAnimation/getAnimation.js'; export * from './hocs/ensuredForwardRef/ensuredForwardRef.js'; export * from './hooks/useClientSideValue/useClientSideValue.js'; +export * from './hooks/useContentRect/useContentRect.js'; +export * from './hooks/useContentRectState/useContentRectState.js'; export * from './hooks/useEventListener/useEventListener.js'; export * from './hooks/useForceRerender/useForceRerender.js'; export * from './hooks/useHasFocus/useHasFocus.js';