Skip to content

Commit

Permalink
feat: Support ResizeObserver.Collection (#70)
Browse files Browse the repository at this point in the history
* chore: init

* chore: single node

* chore: single listener

* feat: Collection of it

* test: Test case fix

* test: test for Collection

* docs: Update docs

* docs: Demo add render info console
  • Loading branch information
zombieJ authored Nov 29, 2021
1 parent e9d5d0e commit 9fe9d1e
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 172 deletions.
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
"printWidth": 100,
"arrowParens": "avoid"
}
2 changes: 1 addition & 1 deletion docs/demo/basic.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## basic
## Basic

<code src="../../examples/basic.tsx">
5 changes: 5 additions & 0 deletions docs/demo/collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Collection

Use `ResizeObserver.Collection` to collect multiple `ResizeObserver` resize event within frame.

<code src="../../examples/collection.tsx">
57 changes: 57 additions & 0 deletions examples/collection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import '../assets/index.less';
import React from 'react';
import ResizeObserver from '../src';

function randomSize() {
return {
width: Math.round(50 + Math.random() * 150),
height: Math.round(50 + Math.random() * 150),
};
}

const sharedStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
};

export default function App() {
const [size1, setSize1] = React.useState(randomSize());
const [size2, setSize2] = React.useState(randomSize());

console.log('Render:', size1, size2);

return (
<ResizeObserver.Collection
onBatchResize={infoList => {
console.log(
'Batch Resize:',
infoList,
infoList.map(({ data, size }) => `${data}(${size.width}/${size.height})`),
);
}}
>
<div style={{ display: 'flex', columnGap: 4, marginBottom: 8 }}>
<button onClick={() => setSize1(randomSize())}>Resize: 1</button>
<button onClick={() => setSize2(randomSize())}>Resize: 2</button>
<button
onClick={() => {
setSize1(randomSize());
setSize2(randomSize());
}}
>
Resize: all
</button>
</div>
<div style={{ display: 'flex', columnGap: 16 }}>
<ResizeObserver data="shape_1">
<div style={{ ...sharedStyle, ...size1, background: 'red' }}>1</div>
</ResizeObserver>
<ResizeObserver data="shape_2">
<div style={{ ...sharedStyle, ...size2, background: 'blue' }}>2</div>
</ResizeObserver>
</div>
</ResizeObserver.Collection>
);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.1",
"rc-util": "^5.0.0",
"rc-util": "^5.15.0",
"resize-observer-polyfill": "^1.5.1"
},
"devDependencies": {
Expand Down
54 changes: 54 additions & 0 deletions src/Collection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import type { SizeInfo } from '.';

type onCollectionResize = (size: SizeInfo, element: HTMLElement, data: any) => void;

export const CollectionContext = React.createContext<onCollectionResize>(null);

export interface ResizeInfo {
size: SizeInfo;
data: any;
element: HTMLElement;
}

export interface CollectionProps {
/** Trigger when some children ResizeObserver changed. Collect by frame render level */
onBatchResize?: (resizeInfo: ResizeInfo[]) => void;
children?: React.ReactNode;
}

/**
* Collect all the resize event from children ResizeObserver
*/
export function Collection({ children, onBatchResize }: CollectionProps) {
const resizeIdRef = React.useRef(0);
const resizeInfosRef = React.useRef<ResizeInfo[]>([]);

const onCollectionResize = React.useContext(CollectionContext);

const onResize = React.useCallback<onCollectionResize>(
(size, element, data) => {
resizeIdRef.current += 1;
const currentId = resizeIdRef.current;

resizeInfosRef.current.push({
size,
element,
data,
});

Promise.resolve().then(() => {
if (currentId === resizeIdRef.current) {
onBatchResize?.(resizeInfosRef.current);
resizeInfosRef.current = [];
}
});

// Continue bubbling if parent exist
onCollectionResize?.(size, element, data);
},
[onBatchResize, onCollectionResize],
);

return <CollectionContext.Provider value={onResize}>{children}</CollectionContext.Provider>;
}
14 changes: 14 additions & 0 deletions src/SingleObserver/DomWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';

export interface DomWrapperProps {
children: React.ReactElement;
}

/**
* Fallback to findDOMNode if origin ref do not provide any dom element
*/
export default class DomWrapper extends React.Component<DomWrapperProps> {
render() {
return this.props.children;
}
}
110 changes: 110 additions & 0 deletions src/SingleObserver/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { composeRef, supportRef } from 'rc-util/lib/ref';
import * as React from 'react';
import findDOMNode from 'rc-util/lib/Dom/findDOMNode';
import { observe, unobserve } from '../utils/observerUtil';
import type { ResizeObserverProps } from '..';
import DomWrapper from './DomWrapper';
import { CollectionContext } from '../Collection';

export interface SingleObserverProps extends ResizeObserverProps {
children: React.ReactElement;
}

export default function SingleObserver(props: SingleObserverProps) {
const { children, disabled } = props;
const elementRef = React.useRef<Element>(null);
const wrapperRef = React.useRef<DomWrapper>(null);

const onCollectionResize = React.useContext(CollectionContext);

// ============================= Size =============================
const sizeRef = React.useRef({
width: 0,
height: 0,
offsetWidth: 0,
offsetHeight: 0,
});

// ============================= Ref ==============================
const canRef = React.isValidElement(children) && supportRef(children);
const originRef: React.Ref<Element> = canRef ? (children as any).ref : null;

const mergedRef = React.useMemo(
() => composeRef<Element>(originRef, elementRef),
[originRef, elementRef],
);

// =========================== Observe ============================
const propsRef = React.useRef<SingleObserverProps>(props);
propsRef.current = props;

// Handler
const onInternalResize = React.useCallback((target: HTMLElement) => {
const { onResize, data } = propsRef.current;

const { width, height } = target.getBoundingClientRect();
const { offsetWidth, 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 (
sizeRef.current.width !== fixedWidth ||
sizeRef.current.height !== fixedHeight ||
sizeRef.current.offsetWidth !== offsetWidth ||
sizeRef.current.offsetHeight !== offsetHeight
) {
const size = { width: fixedWidth, height: fixedHeight, offsetWidth, offsetHeight };
sizeRef.current = size;

// IE is strange, right?
const mergedOffsetWidth = offsetWidth === Math.round(width) ? width : offsetWidth;
const mergedOffsetHeight = offsetHeight === Math.round(height) ? height : offsetHeight;

const sizeInfo = {
...size,
offsetWidth: mergedOffsetWidth,
offsetHeight: mergedOffsetHeight,
};

// Let collection know what happened
onCollectionResize?.(sizeInfo, target, data);

if (onResize) {
// defer the callback but not defer to next frame
Promise.resolve().then(() => {
onResize(sizeInfo, target);
});
}
}
}, []);

// Dynamic observe
React.useEffect(() => {
const currentElement: HTMLElement =
findDOMNode(elementRef.current) || findDOMNode(wrapperRef.current);

if (currentElement && !disabled) {
observe(currentElement, onInternalResize);
}

return () => unobserve(currentElement, onInternalResize);
}, [elementRef.current, disabled]);

// ============================ Render ============================
if (canRef) {
return (
<DomWrapper ref={wrapperRef}>
{React.cloneElement(children as any, {
ref: mergedRef,
})}
</DomWrapper>
);
}
return children;
}
Loading

1 comment on commit 9fe9d1e

@vercel
Copy link

@vercel vercel bot commented on 9fe9d1e Nov 29, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.