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

Remove resize observer polyfill #925

Merged
merged 10 commits into from
Dec 9, 2020
4 changes: 4 additions & 0 deletions packages/visx-responsive/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ let chartToRender = (
// ... Render the chartToRender somewhere
```

##### ⚠️ `ResizeObserver` dependency

If you don't need a polyfill for `ResizeObserver` or are already including it in your bundle, you should use `ParentSizeModern` and `withParentSizeModern` which doesn't include the polyfill.

## Installation

```
Expand Down
105 changes: 105 additions & 0 deletions packages/visx-responsive/src/components/ParentSizeModern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import debounce from 'lodash/debounce';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ResizeObserver } from '../types';

// This can be deleted once https://git.io/Jk9FD lands in TypeScript
declare global {
interface Window {
ResizeObserver: ResizeObserver;
}
}

export type ParentSizeProps = {
/** Optional `className` to add to the parent `div` wrapper used for size measurement. */
className?: string;
/** Child render updates upon resize are delayed until `debounceTime` milliseconds _after_ the last resize event is observed. */
debounceTime?: number;
/** Optional flag to toggle leading debounce calls. When set to true this will ensure that the component always renders immediately. (defaults to true) */
enableDebounceLeadingCall?: boolean;
/** Optional dimensions provided won't trigger a state change when changed. */
ignoreDimensions?: keyof ParentSizeState | (keyof ParentSizeState)[];
/** Optional `style` object to apply to the parent `div` wrapper used for size measurement. */
parentSizeStyles?: React.CSSProperties;
/** Child render function `({ width, height, top, left, ref, resize }) => ReactNode`. */
children: (
args: {
ref: HTMLDivElement | null;
resize: (state: ParentSizeState) => void;
} & ParentSizeState,
) => React.ReactNode;
};

type ParentSizeState = {
width: number;
height: number;
top: number;
left: number;
};

export type ParentSizeProvidedProps = ParentSizeState;

export default function ParentSize({
className,
children,
debounceTime = 300,
ignoreDimensions = [],
parentSizeStyles = { width: '100%', height: '100%' },
enableDebounceLeadingCall = true,
...restProps
}: ParentSizeProps & Omit<JSX.IntrinsicElements['div'], keyof ParentSizeProps>) {
const target = useRef<HTMLDivElement | null>(null);
const animationFrameID = useRef(0);

const [state, setState] = useState<ParentSizeState>({
width: 0,
height: 0,
top: 0,
left: 0,
});

const resize = useMemo(() => {
const normalized = Array.isArray(ignoreDimensions) ? ignoreDimensions : [ignoreDimensions];

return debounce(
(incoming: ParentSizeState) => {
setState(existing => {
const stateKeys = Object.keys(existing) as (keyof ParentSizeState)[];
const keysWithChanges = stateKeys.filter(key => existing[key] !== incoming[key]);
const shouldBail = keysWithChanges.every(key => normalized.includes(key));

return shouldBail ? existing : incoming;
});
},
debounceTime,
{ leading: enableDebounceLeadingCall },
);
}, [debounceTime, enableDebounceLeadingCall, ignoreDimensions]);

useEffect(() => {
const observer = new window.ResizeObserver(entries => {
entries.forEach(entry => {
const { left, top, width, height } = entry.contentRect;
animationFrameID.current = window.requestAnimationFrame(() => {
resize({ width, height, top, left });
});
});
});
if (target.current) observer.observe(target.current);

return () => {
window.cancelAnimationFrame(animationFrameID.current);
observer.disconnect();
resize.cancel();
};
}, [resize]);

return (
<div style={parentSizeStyles} ref={target} className={className} {...restProps}>
{children({
...state,
ref: target.current,
resize,
})}
</div>
);
}
95 changes: 95 additions & 0 deletions packages/visx-responsive/src/enhancers/withParentSizeModern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import debounce from 'lodash/debounce';
import { ResizeObserver } from '../types';

const CONTAINER_STYLES = { width: '100%', height: '100%' };

// This can be deleted once https://git.io/Jk9FD lands in TypeScript
declare global {
interface Window {
ResizeObserver: ResizeObserver;
}
}

export type WithParentSizeProps = {
debounceTime?: number;
enableDebounceLeadingCall?: boolean;
};

type WithParentSizeState = {
parentWidth?: number;
parentHeight?: number;
initialWidth?: number;
initialHeight?: number;
};

export type WithParentSizeProvidedProps = WithParentSizeState;

export default function withParentSize<BaseComponentProps extends WithParentSizeProps = {}>(
BaseComponent: React.ComponentType<BaseComponentProps & WithParentSizeProvidedProps>,
) {
return class WrappedComponent extends React.Component<
BaseComponentProps & WithParentSizeProvidedProps,
WithParentSizeState
> {
static defaultProps = {
debounceTime: 300,
enableDebounceLeadingCall: true,
};
state = {
parentWidth: undefined,
parentHeight: undefined,
};
animationFrameID: number = 0;
resizeObserver: ResizeObserver | undefined;
container: HTMLDivElement | null = null;

componentDidMount() {
this.resizeObserver = new window.ResizeObserver(entries => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
this.animationFrameID = window.requestAnimationFrame(() => {
this.resize({
width,
height,
});
});
});
});
if (this.container) this.resizeObserver.observe(this.container);
}

componentWillUnmount() {
window.cancelAnimationFrame(this.animationFrameID);
if (this.resizeObserver) this.resizeObserver.disconnect();
this.resize.cancel();
}

setRef = (ref: HTMLDivElement) => {
this.container = ref;
};

resize = debounce(
({ width, height }: { width: number; height: number }) => {
this.setState({
parentWidth: width,
parentHeight: height,
});
},
this.props.debounceTime,
{ leading: this.props.enableDebounceLeadingCall },
);

render() {
const { initialWidth, initialHeight } = this.props;
const { parentWidth = initialWidth, parentHeight = initialHeight } = this.state;
return (
<div style={CONTAINER_STYLES} ref={this.setRef}>
{parentWidth != null && parentHeight != null && (
<BaseComponent parentWidth={parentWidth} parentHeight={parentHeight} {...this.props} />
)}
</div>
);
}
};
}
2 changes: 2 additions & 0 deletions packages/visx-responsive/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default as ScaleSVG } from './components/ScaleSVG';
export { default as ParentSize } from './components/ParentSize';
export { default as ParentSizeModern } from './components/ParentSizeModern';
export { default as withParentSize } from './enhancers/withParentSize';
export { default as withParentSizeModern } from './enhancers/withParentSizeModern';
export { default as withScreenSize } from './enhancers/withScreenSize';
16 changes: 16 additions & 0 deletions packages/visx-responsive/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This file can be deleted once https://git.io/Jk9FD lands in TypeScript
interface ResizeObserverEntry {
contentRect: {
left: number;
top: number;
width: number;
height: number;
};
}
type ResizeObserverCallback = (entries: ResizeObserverEntry[]) => void;
export interface ResizeObserver {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new (callback: ResizeObserverCallback): ResizeObserver;
observe(el: Element): void;
disconnect(): void;
}