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

feat: add useResizeObserver hook #68

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions docs/demo/hook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## hook

<code src="../../examples/hook.tsx">
5 changes: 3 additions & 2 deletions examples/basic.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '../assets/index.less';
import React from 'react';
import ResizeObserver, { ResizeObserverProps } from '../src';
import type { ResizeObserverProps } from '../src/ResizeObserver';
import ResizeObserver from '../src/ResizeObserver';

export default function App() {
const [times, setTimes] = React.useState(0);
Expand All @@ -17,7 +18,7 @@ export default function App() {
offsetHeight,
offsetWidth,
}) => {
setTimes(prevTimes => prevTimes + 1);
setTimes((prevTimes) => prevTimes + 1);
console.log(
'Resize:',
'\n',
Expand Down
4 changes: 2 additions & 2 deletions examples/debug.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '../assets/index.less';
import React from 'react';
import ResizeObserver from '../src';
import ResizeObserver from '../src/ResizeObserver';

export default function App() {
const [times, setTimes] = React.useState(0);
Expand All @@ -9,7 +9,7 @@ export default function App() {
const [disabled, setDisabled] = React.useState(false);

const onResize = (size: { width: number; height: number }) => {
setTimes(prevTimes => prevTimes + 1);
setTimes((prevTimes) => prevTimes + 1);
setWidth(size.width);
setHeight(size.height);
};
Expand Down
53 changes: 53 additions & 0 deletions examples/hook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import '../assets/index.less';
import React from 'react';
import type { OnResizeHandler } from '../src/OnResizeHandler';
import { useResizeObserver } from '../src/useResizeObserver';

export default function App() {
const [times, setTimes] = React.useState(0);
const [disabled, setDisabled] = React.useState(false);
const textareaRef = React.useRef<HTMLTextAreaElement>(null);

const onResize: OnResizeHandler = ({ width, height, offsetWidth, offsetHeight }) => {
setTimes((prevTimes) => prevTimes + 1);
console.log(
'Resize:',
'\n',
'BoundingBox',
width,
height,
'\n',
'Offset',
offsetWidth,
offsetHeight,
);
};

useResizeObserver({ ref: textareaRef, onResize, disabled });

React.useEffect(() => {
console.log('Ref:', textareaRef.current);
}, []);

return (
<React.StrictMode>
<div style={{ transform: 'scale(1.1)', transformOrigin: '0% 0%' }}>
<div>
<label>
<input
type="checkbox"
onChange={() => {
setDisabled(!disabled);
}}
checked={disabled}
/>{' '}
Disabled Observe
</label>
{' >>> '}
<span>Resize times: {times}</span>
</div>
<textarea ref={textareaRef} placeholder="I'm a textarea!" />
</div>
</React.StrictMode>
);
}
3 changes: 3 additions & 0 deletions src/OnResizeHandler.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Size } from './Size';

export type OnResizeHandler = (size: Size, element: HTMLElement) => void;
21 changes: 5 additions & 16 deletions src/index.tsx → src/ResizeObserver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,24 @@ import toArray from 'rc-util/lib/Children/toArray';
import warning from 'rc-util/lib/warning';
import { composeRef, supportRef } from 'rc-util/lib/ref';
import ResizeObserver from 'resize-observer-polyfill';
import type { Size } from './Size';
import type { OnResizeHandler } from './OnResizeHandler';

const INTERNAL_PREFIX_KEY = 'rc-observer-key';

export interface ResizeObserverProps {
children: React.ReactNode;
disabled?: boolean;
/** Trigger if element resized. Will always trigger when first time render. */
onResize?: (
size: {
width: number;
height: number;
offsetWidth: number;
offsetHeight: number;
},
element: HTMLElement,
) => void;
onResize?: OnResizeHandler;
}

interface ResizeObserverState {
height: number;
width: number;
offsetHeight: number;
offsetWidth: number;
}
type ResizeObserverState = Size;

type RefNode = React.ReactInstance | HTMLElement | null;

// Still need to be compatible with React 15, we use class component here
class ReactResizeObserver extends React.Component<ResizeObserverProps, ResizeObserverState> {
export class ReactResizeObserver extends React.Component<ResizeObserverProps, ResizeObserverState> {
static displayName = 'ResizeObserver';

resizeObserver: ResizeObserver | null = null;
Expand Down
6 changes: 6 additions & 0 deletions src/Size.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Size = {
width: number;
height: number;
offsetWidth: number;
offsetHeight: number;
};
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ReactResizeObserver } from './ResizeObserver';

export { ResizeObserverProps } from './ResizeObserver';
export { useResizeObserver, UseResizeObserverArgs } from './useResizeObserver';
export { Size } from './Size.d';
export { OnResizeHandler } from './OnResizeHandler.d';
export default ReactResizeObserver;
111 changes: 111 additions & 0 deletions src/useResizeObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as React from 'react';
import type { OnResizeHandler } from './OnResizeHandler';
import type { Size } from './Size';

export interface UseResizeObserverArgs {
ref: React.RefObject<React.ReactNode>;
onResize?: OnResizeHandler;
disabled?: boolean;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`
/**
*

*/
`

export function useResizeObserver({
ref,
onResize: resizeHandler,
disabled,
}: UseResizeObserverArgs): Size {
const observerRef = React.useRef<ResizeObserver>();
const elementRef = React.useRef<HTMLElement>();

const [height, setHeight] = React.useState(0);
const [width, setWidth] = React.useState(0);
const [offsetHeight, setOffsetHeight] = React.useState(0);
const [offsetWidth, setOffsetWidth] = React.useState(0);

const destroyObserver = React.useCallback(() => {
if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;
}
}, [observerRef]);

const onResize = React.useCallback(
(entries: ResizeObserverEntry[]): void => {
const target = entries[0].target as HTMLElement;

const { width: _width, height: _height } = target.getBoundingClientRect();
const { offsetWidth: _offsetWidth, offsetHeight: _offsetHeight } = target;

/**
* Resize observer trigger when content size changed.
* In most case we just care about element size,
* let's use `boundary` instead of `contentRect` here to avoid shaking.
*/
const fixedWidth = Math.floor(_width);
const fixedHeight = Math.floor(_height);

if (
width !== fixedWidth ||
height !== fixedHeight ||
offsetWidth !== _offsetWidth ||
offsetHeight !== _offsetHeight
) {
setHeight(fixedHeight);
setWidth(fixedWidth);
setOffsetHeight(_offsetHeight);
setOffsetWidth(_offsetWidth);

if (resizeHandler) {
Promise.resolve().then(() => {
resizeHandler(
{
width: fixedWidth,
height: fixedHeight,
offsetWidth: _offsetWidth,
offsetHeight: _offsetHeight,
},
target,
);
});
}
}
},
[width, height, offsetWidth, offsetHeight, resizeHandler],
);

const repopulateObserver = React.useCallback(() => {
if (disabled) {
destroyObserver();
return;
}

const element = ref.current as HTMLElement;
const elementChanged = element !== elementRef.current;
if (elementChanged) {
destroyObserver();
elementRef.current = element;
}

if (!observerRef.current && element) {
observerRef.current = new ResizeObserver(onResize);
observerRef.current.observe(element);
}
}, [destroyObserver, disabled, onResize, ref]);

React.useEffect(() => {
repopulateObserver();

// eslint-disable-next-line consistent-return
return () => {
destroyObserver();
};
}, [repopulateObserver, destroyObserver]);

return {
width,
height,
offsetWidth,
offsetHeight,
};
}

export default useResizeObserver;