Skip to content

Commit

Permalink
Create new ParentSizeModern component.
Browse files Browse the repository at this point in the history
This component is a clone of `ParentSize` that doesn't use a
`ResizeObserver` polyfill.
  • Loading branch information
koddsson committed Nov 26, 2020
1 parent 293d5fe commit 77beb80
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 5 deletions.
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
3 changes: 2 additions & 1 deletion packages/visx-responsive/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"@types/lodash": "^4.14.146",
"@types/react": "*",
"lodash": "^4.17.10",
"prop-types": "^15.6.1"
"prop-types": "^15.6.1",
"resize-observer-polyfill": "1.5.1"
},
"peerDependencies": {
"react": "^15.0.0-0 || ^16.0.0-0"
Expand Down
1 change: 1 addition & 0 deletions packages/visx-responsive/src/components/ParentSize.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import debounce from 'lodash/debounce';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import ResizeObserver from 'resize-observer-polyfill';

export type ParentSizeProps = {
/** Optional `className` to add to the parent `div` wrapper used for size measurement. */
Expand Down
92 changes: 92 additions & 0 deletions packages/visx-responsive/src/components/ParentSizeModern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import debounce from 'lodash/debounce';
import React, { useEffect, useMemo, useRef, useState } from 'react';

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 ResizeObserver((entries = [] /** , observer */) => {
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>
);
}
1 change: 1 addition & 0 deletions packages/visx-responsive/src/enhancers/withParentSize.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import debounce from 'lodash/debounce';
import ResizeObserver from 'resize-observer-polyfill';

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

Expand Down
87 changes: 87 additions & 0 deletions packages/visx-responsive/src/enhancers/withParentSizeModern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import debounce from 'lodash/debounce';

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

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 ResizeObserver((entries /** , observer */) => {
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>
);
}
};
}
10 changes: 6 additions & 4 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 withParentSize } from './enhancers/withParentSize';
export { default as withScreenSize } from './enhancers/withScreenSize';
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";

0 comments on commit 77beb80

Please sign in to comment.