Skip to content

Commit

Permalink
Merge pull request #273 from mediamonks/feature/create-use-content-re…
Browse files Browse the repository at this point in the history
…ct-hook

Create useContentRect hook
  • Loading branch information
leroykorterink authored Dec 1, 2023
2 parents c7fe9d0 + 7881ca4 commit 605b659
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 0 deletions.
45 changes: 45 additions & 0 deletions src/hooks/useContentRect/useContentRect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Meta } from '@storybook/blocks';

<Meta title="Hooks / useContentRect" />

# 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<Element | null>): RefObject<DOMRectReadOnly>;
```

## 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<HTMLDivElement>(null);

const contentRectRef = useContentRect(ref);

return <div ref={ref}></div>;
}
```

## 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<HTMLDivElement | null>(null);

const contentRectRef = useContentRect(element);

return <div ref={setElement}></div>;
}
```
67 changes: 67 additions & 0 deletions src/hooks/useContentRect/useContentRect.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof meta>;

export const UseContentRect: Story = {
render() {
const onClick = useForceRerender();
const ref = useRef<HTMLDivElement>(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 (
<>
<h4>
Element size:{' '}
<button type="button" onClick={onClick}>
Rerender
</button>
</h4>
<div
ref={ref}
style={{
outline: '1px solid red',
padding: 18,
marginBlock: 20,
}}
>
<code
style={{
display: 'block',
whiteSpace: 'pre',
}}
>
{JSON.stringify(contentRectRef.current, null, 2)}
</code>
</div>
</>
);
},
};
24 changes: 24 additions & 0 deletions src/hooks/useContentRect/useContentRect.ts
Original file line number Diff line number Diff line change
@@ -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<Element | null>,
): RefObject<DOMRectReadOnly | null> {
const contentRectRef = useRef<DOMRectReadOnly | null>(null);

useResizeObserver(target, (entries): void => {
contentRectRef.current = entries.at(0)?.contentRect ?? null;
});

useMount(() => {
contentRectRef.current = unref(target)?.getBoundingClientRect() ?? null;
});

return contentRectRef;
}
43 changes: 43 additions & 0 deletions src/hooks/useContentRectState/useContentRectState.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Meta } from '@storybook/blocks';

<Meta title="Hooks / useContentRectState" />

# 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<Element | null>): RefObject<DOMRectReadOnly>;
```

## 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<HTMLDivElement>(null);
const contentRect = useContentRectState(ref);

return <div ref={ref}></div>;
}
```

## 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<HTMLDivElement | null>(null);
const contentRect = useContentRectState(element);

return <div ref={setElement}></div>;
}
```
60 changes: 60 additions & 0 deletions src/hooks/useContentRectState/useContentRectState.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof meta>;

export const UseContentRectState: Story = {
render() {
const ref = useRef<HTMLDivElement>(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 (
<>
<h4>Element size:</h4>
<div
ref={ref}
style={{
outline: '1px solid red',
padding: 18,
marginBlock: 20,
}}
>
<code
style={{
display: 'block',
whiteSpace: 'pre',
}}
>
{JSON.stringify(contentRect, null, 2)}
</code>
</div>
</>
);
},
};
26 changes: 26 additions & 0 deletions src/hooks/useContentRectState/useContentRectState.ts
Original file line number Diff line number Diff line change
@@ -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<Element | null>): DOMRectReadOnly | null {
const [contentRect, setContentRect] = useState<DOMRectReadOnly | null>(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;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 605b659

Please sign in to comment.