diff --git a/examples/index.tsx b/examples/index.tsx index 25d21ce..6231e7d 100644 --- a/examples/index.tsx +++ b/examples/index.tsx @@ -9,7 +9,6 @@ import './styles.css'; const Placeholder = () =>
; const Test = () => { const div = React.useRef(null); - const rect = useRect(div); return
; }; @@ -23,6 +22,7 @@ class Example extends React.PureComponent< private container4: React.RefObject; private container5: React.RefObject; private container6: React.RefObject; + private container7: React.RefObject; constructor(props) { super(props); @@ -32,6 +32,7 @@ class Example extends React.PureComponent< this.container4 = React.createRef(); this.container5 = React.createRef(); this.container6 = React.createRef(); + this.container7 = React.createRef(); this.state = { disableHeader: false, disableAll: false, @@ -178,6 +179,14 @@ class Example extends React.PureComponent<
+
+ +
+ +
+
+
+
disableHardwareAcceleration: true
@@ -190,6 +199,21 @@ class Example extends React.PureComponent< } } +const DynamicContent = () => { + const [count, setCount] = React.useState(1); + + return ( +
+ + {Array(count) + .fill(null) + .map((_, index) => ( +
{index}
+ ))} +
+ ); +}; + render( diff --git a/lib/ElementResizeObserver.tsx b/lib/ElementResizeObserver.tsx new file mode 100644 index 0000000..7424eee --- /dev/null +++ b/lib/ElementResizeObserver.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { IRect } from 'react-viewport-utils'; + +interface IElementResizeObserverProps { + stickyRef: React.RefObject; + onUpdate: (rect: IRect) => void; +} + +class ElementResizeObserver extends React.PureComponent< + IElementResizeObserverProps +> { + private resizeObserver: ResizeObserver | null; + constructor(props: IElementResizeObserverProps) { + super(props); + this.resizeObserver = null; + } + + componentDidMount() { + this.resetObserver(); + this.installObserver(); + } + + componentDidUpdate() { + this.resetObserver(); + this.installObserver(); + } + + componentWillUnmount() { + this.resetObserver(); + } + + installObserver() { + if (!this.props.stickyRef.current) { + return; + } + if (typeof window.ResizeObserver !== 'undefined') { + this.resizeObserver = new window.ResizeObserver(entries => { + if (entries && entries[0] && entries[0].contentRect) { + this.props.onUpdate(entries[0].contentRect); + } + }); + this.resizeObserver!.observe(this.props.stickyRef.current); + } + } + + resetObserver() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + } + + render(): null { + return null; + } +} + +export default ElementResizeObserver; diff --git a/lib/StickyPlaceholder.tsx b/lib/StickyPlaceholder.tsx index 73b49d1..6a8bc87 100644 --- a/lib/StickyPlaceholder.tsx +++ b/lib/StickyPlaceholder.tsx @@ -6,6 +6,7 @@ import { requestAnimationFrame, cancelAnimationFrame, } from 'react-viewport-utils'; +import ElementResizeObserver from './ElementResizeObserver'; interface IProps { disableResizing: boolean; @@ -22,11 +23,12 @@ interface IState { isWaitingForRecalculation: boolean; stickyHeight: number | null; stickyWidth: number | null; - clientSize: string | null; + clientHash: string | null; } class StickyPlaceholder extends React.Component { private recalculationTick?: number; + private lastDimensions?: IDimensions; static defaultProps = { style: {}, }; @@ -36,7 +38,7 @@ class StickyPlaceholder extends React.Component { isWaitingForRecalculation: false, stickyHeight: null, stickyWidth: null, - clientSize: null, + clientHash: null, }; componentWillUnmount() { @@ -58,16 +60,17 @@ class StickyPlaceholder extends React.Component { { dimensions }: { dimensions: IDimensions }, stickyRect: IRect | null, ) => { + this.lastDimensions = dimensions; const { width, clientWidth } = dimensions; - const nextClientSize = [width, clientWidth].join(','); + const nextClientHash = [width, clientWidth].join(','); if ( !this.state.isWaitingForRecalculation && - this.state.clientSize !== nextClientSize + this.state.clientHash !== nextClientHash ) { this.setState( { - clientSize: nextClientSize, + clientHash: nextClientHash, isRecalculating: true, isWaitingForRecalculation: true, }, @@ -83,14 +86,29 @@ class StickyPlaceholder extends React.Component { return; } - if (stickyRect && this.state.isWaitingForRecalculation) { - this.setState({ - clientSize: nextClientSize, - stickyHeight: stickyRect.height, - stickyWidth: stickyRect.width, - isWaitingForRecalculation: false, - }); - return; + if (stickyRect) { + if ( + this.state.isWaitingForRecalculation || + stickyRect.height !== this.state.stickyHeight || + stickyRect.width !== this.state.stickyWidth + ) { + this.setState({ + clientHash: nextClientHash, + stickyHeight: stickyRect.height, + stickyWidth: stickyRect.width, + isWaitingForRecalculation: false, + }); + return; + } + } + }; + + handleElementResize = (stickyRect: IRect) => { + if (this.lastDimensions) { + this.handleDimensionsUpdate( + { dimensions: this.lastDimensions }, + stickyRect, + ); } }; @@ -119,13 +137,19 @@ class StickyPlaceholder extends React.Component { })} {!this.props.disableResizing && ( - + <> + + + )} ); diff --git a/lib/globals.d.ts b/lib/globals.d.ts new file mode 100644 index 0000000..3458738 --- /dev/null +++ b/lib/globals.d.ts @@ -0,0 +1,67 @@ +// @see https://gist.github.com/strothj/708afcf4f01dd04de8f49c92e88093c3 +interface Window { + ResizeObserver: ResizeObserver; +} + +/** + * The ResizeObserver interface is used to observe changes to Element's content + * rect. + * + * It is modeled after MutationObserver and IntersectionObserver. + */ +interface ResizeObserver { + new (callback: ResizeObserverCallback): ResizeObserver; + + /** + * Adds target to the list of observed elements. + */ + observe: (target: Element) => void; + + /** + * Removes target from the list of observed elements. + */ + unobserve: (target: Element) => void; + + /** + * Clears both the observationTargets and activeTargets lists. + */ + disconnect: () => void; +} + +/** + * This callback delivers ResizeObserver's notifications. It is invoked by a + * broadcast active observations algorithm. + */ +interface ResizeObserverCallback { + (entries: ResizeObserverEntry[], observer: ResizeObserver): void; +} + +interface ResizeObserverEntry { + /** + * @param target The Element whose size has changed. + */ + new (target: Element): ResizeObserverEntry; + + /** + * The Element whose size has changed. + */ + readonly target: Element; + + /** + * Element's content rect when ResizeObserverCallback is invoked. + */ + readonly contentRect: DOMRectReadOnly; +} + +interface DOMRectReadOnly { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; + readonly top: number; + readonly right: number; + readonly bottom: number; + readonly left: number; + + toJSON: () => any; +}