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

fix: Improve typing in @visx/responsive enhancers #1783

Merged
merged 4 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
200 changes: 137 additions & 63 deletions packages/visx-responsive/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,158 @@
<img src="https://img.shields.io/npm/dm/@visx/responsive.svg?style=flat-square" />
</a>

The `@visx/responsive` package is here to help you make responsive graphs.
The `@visx/responsive` package is here to help you make responsive graphs by providing a collection
of hooks, enhancers and components.

**Enhancers**
## Installation

```
npm install --save @visx/responsive
```

## Hooks

### `useScreenSize`

If you would like your graph to adapt to the screen size, you can use the `useScreenSize()` hook. It
returns current screen width and height and updates the value automatically on browser window
resize. You can optionally pass a config object as an argument to the hook. Config object attributes
are:

- `debounceTime` - determines how often the size is updated in milliseconds, defaults to `300`.
- `enableDebounceLeadingCall` - determines whether the size is updated immediately on first render,
defaults to `true`. This is essentially the value of
[`options.leading` in Lodash's `debounce`](https://lodash.com/docs/4.17.15#debounce).

#### Example

```tsx
import { useScreenSize } from '@visx/responsive';

`withScreenSize`
const ChartToRender = () => {
const { width, height } = useScreenSize({ debounceTime: 150 });

return (
<svg width={width} height={height}>
{/* content */}
</svg>
);
};

const chartToRender = <ChartToRender myProp="string" />;
```

### `useParentSize`

If you want your graph to adapt to its parent size, you can use `useParentSize()` hook.
`<ParentSize>` uses this hook internally. The hook returns `width`, `height`, `left`, `top`
properties which describe dimensions of the container which received `parentRef` ref. You can
optionally pass a config object as an argument to the hook. Config object attributes are:

- `debounceTime` - determines how often the size is updated in miliseconds, defaults to `300`.
- `enableDebounceLeadingCall` - determines whether the size is updated immediately on first render,
defaults to `true`. This is essentially the value of
[`options.leading` in Lodash's `debounce`](https://lodash.com/docs/4.17.15#debounce).
- `ignoreDimensions` - array of dimensions for which an update should be skipped. For example, if
you pass `['width']`, width changes of the component that received `parentRef` won't be
propagated. Defaults to `[]` (all dimensions changes trigger updates).

#### Example

```tsx
import { useParentSize } from '@visx/responsive';

`withParentSize`
const ChartToRender = () => {
const { parentRef, width, height } = useParentSize({ debounceTime: 150 });

**Components**
return (
<div ref={parentRef}>
<svg width={width} height={height}>
{/* content */}
</svg>
</div>
);
};

`ParentSize`
const chartToRender = <ChartToRender myProp="string" />;
```

`ScaleSVG`
## Enhancers / (HOCs)

### `withScreenSize`

If you would like your graph to adapt to the screen size, you can use `withScreenSize()`. The
resulting component will pass `screenWidth` and `screenHeight` props to the wrapped component
containing the respective screen dimensions.
If you prefer to use an enhancer, you can use the `withScreenSize()`. The resulting component will
pass `screenWidth` and `screenHeight` props to the wrapped component containing the respective
screen dimensions. You can also optionally pass config props to the wrapped component:

- `windowResizeDebounceTime` - determines how often the size is updated in milliseconds, defaults to
`300`.
- `enableDebounceLeadingCall` - determines whether the size is updated immediately on first render,
defaults to `true`. This is essentially the value of
[`options.leading` in Lodash's `debounce`](https://lodash.com/docs/4.17.15#debounce).

#### Example

```tsx
import { withScreenSize, WithScreenSizeProvidedProps } from '@visx/responsive';

### Example:
interface Props extends WithScreenSizeProvidedProps {
myProp: string;
}

```js
import { withScreenSize } from '@visx/responsive';
// or
// import * as Responsive from '@visx/responsive';
// Responsive.withScreenSize(...);
const MySuperCoolVisxChart = ({ myProp, screenWidth, screenHeight }: Props) => {
// ...
};

let chartToRender = withScreenSize(MySuperCoolVisxChart);
const ChartToRender = withScreenSize(MySuperCoolVisxChart);

// ... Render the chartToRender somewhere
const chartToRender = <ChartToRender myProp="string" />;
```

## `withParentSize`
### `withParentSize`

If you would like your graph to adapt to it's parent component's size, you can use
If you prefer to use an enhancer to adapt your graph to its parent component's size, you can use
`withParentSize()`. The resulting component will pass `parentWidth` and `parentHeight` props to the
wrapped component containing the respective parent's dimensions.
wrapped component containing the respective parent's dimensions. You can also optionally pass config
props to the wrapped component:

- `initialWidth` - initial chart width used before the parent size is determined.
- `initialHeight` - initial chart height used before the parent size is determined.
- `debounceTime` - determines how often the size is updated in miliseconds, defaults to `300`.
- `enableDebounceLeadingCall` - determines whether the size is updated immediately on first render,
defaults to `true`. This is essentially the value of
[`options.leading` in Lodash's `debounce`](https://lodash.com/docs/4.17.15#debounce).

#### Example

### Example:
```tsx
import { withParentSize, WithParentSizeProvidedProps } from '@visx/responsive';

interface Props extends WithParentSizeProvidedProps {
myProp: string;
}

```js
import { withParentSize } from '@visx/responsive';
// or
// import * as Responsive from '@visx/responsive';
// Responsive.withParentSize(...);
const MySuperCoolVisxChart = ({ myProp, parentWidth, parentHeight }: Props) => {
// ...
};

let chartToRender = withParentSize(MySuperCoolVisxChart);
const ChartWithParentSize = withParentSize(MySuperCoolVisxChart);

// ... Render the chartToRender somewhere
const chartToRender = <ChartWithParentSize myProp="string" initialWidth={400} />;
```

## `ParentSize`
## Components

### `ParentSize`

You might do the same thing using the `ParentSize` component.
You might do the same thing as `useParentSize` or `withParentSize` using the `ParentSize` component.

### Example:
#### Example

```js
```tsx
import { ParentSize } from '@visx/responsive';
// or
// import * as Responsive from '@visx/responsive';
// <Responsive.ParentSize />;

let chartToRender = (
const chartToRender = (
paolostyle marked this conversation as resolved.
Show resolved Hide resolved
<ParentSize>
{(parent) => (
<MySuperCoolVisxChart
Expand All @@ -84,50 +171,37 @@ let chartToRender = (
)}
</ParentSize>
);

// ... Render the chartToRender somewhere
```

## `ScaleSVG`
### `ScaleSVG`

You can also create a responsive chart with a specific viewBox with the `ScaleSVG` component.

### Example:
#### Example

```js
```tsx
import { ScaleSVG } from '@visx/responsive';
// or
// import * as Responsive from '@visx/responsive';
// <Responsive.ScaleSVG />

let chartToRender = (
const chartToRender = (
<ScaleSVG width={400} height={400}>
<MySuperCoolVXChart />
</ScaleSVG>
);

// ... Render the chartToRender somewhere
```

### ⚠️ `ResizeObserver` dependency
## ⚠️ `ResizeObserver` dependency

The `ParentSize` component and `withParentSize` enhancer rely on `ResizeObserver`s for auto-sizing.
If you need a polyfill, you can either polute the `window` object or inject it cleanly through
props:
`useParentSize`, `ParentSize` and `withParentSize` rely on `ResizeObserver`s for auto-sizing. If you
need a polyfill (although [it is widely supported now](https://caniuse.com/resizeobserver)), it is
recommended to use `setResizeObserverPolyfill` function, which won't pollute `window` object.

```tsx
import { ResizeObserver } from 'your-favorite-polyfill';
import { setResizeObserverPolyfill } from '@visx/responsive';

function App() {
return (
<ParentSize resizeObserverPolyfill={ResizeObserver} {...}>
{() => {...}}
</ParentSize>
);
// You only have to do this once
setResizeObserverPolyfill(ResizeObserver);
```

## Installation

```
npm install --save @visx/responsive
```
Now `useParentSize`, `ParentSize` and `withParentSize` will use that polyfill instead of global
`ResizeObserver`.
108 changes: 23 additions & 85 deletions packages/visx-responsive/src/components/ParentSize.tsx
Original file line number Diff line number Diff line change
@@ -1,110 +1,48 @@
import debounce from 'lodash/debounce';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ResizeObserverPolyfill } from '../types';
import React from 'react';
import useParentSize, { ParentSizeState, UseParentSizeConfig } from '../hooks/useParentSize';

// @TODO remove when upgraded to TS 4 which has its own declaration
interface PrivateWindow {
ResizeObserver: ResizeObserverPolyfill;
}
export type ParentSizeProvidedProps = ParentSizeState & {
ref: HTMLDivElement | null;
resize: (state: ParentSizeState) => void;
};

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. */
/**
* @deprecated - use `style` prop as all other props are passed directly to the parent `div`.
* @TODO remove in the next major version.
* Optional `style` object to apply to the parent `div` wrapper used for size measurement.
* */
parentSizeStyles?: React.CSSProperties;
/** Optionally inject a ResizeObserver polyfill, else this *must* be globally available. */
resizeObserverPolyfill?: ResizeObserverPolyfill;
/** Child render function `({ width, height, top, left, ref, resize }) => ReactNode`. */
children: (
args: {
ref: HTMLDivElement | null;
resize: (state: ParentSizeState) => void;
} & ParentSizeState,
) => React.ReactNode;
};
children: (args: ParentSizeProvidedProps) => React.ReactNode;
} & UseParentSizeConfig;

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

export type ParentSizeProvidedProps = ParentSizeState;

const defaultIgnoreDimensions: ParentSizeProps['ignoreDimensions'] = [];
const defaultParentSizeStyles = { width: '100%', height: '100%' };

export default function ParentSize({
className,
children,
debounceTime = 300,
ignoreDimensions = defaultIgnoreDimensions,
debounceTime,
ignoreDimensions,
parentSizeStyles = defaultParentSizeStyles,
enableDebounceLeadingCall = true,
resizeObserverPolyfill,
...restProps
}: ParentSizeProps & Omit<React.HTMLAttributes<HTMLDivElement>, 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 { parentRef, resize, ...dimensions } = useParentSize({
debounceTime,
ignoreDimensions,
enableDebounceLeadingCall,
resizeObserverPolyfill,
});

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 LocalResizeObserver =
resizeObserverPolyfill || (window as unknown as PrivateWindow).ResizeObserver;

const observer = new LocalResizeObserver((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, resizeObserverPolyfill]);

return (
<div style={parentSizeStyles} ref={target} className={className} {...restProps}>
<div style={parentSizeStyles} ref={parentRef} className={className} {...restProps}>
{children({
...state,
ref: target.current,
...dimensions,
ref: parentRef.current,
resize,
})}
</div>
Expand Down
Loading
Loading