From 7ac5d9650b8acda069ffedf6d34b9b36d093fc0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C4=85browski?= Date: Mon, 8 Jan 2024 21:48:29 +0100 Subject: [PATCH 1/4] improve responsive enhancers typing --- packages/visx-responsive/Readme.md | 71 ++++++++++++------- .../src/enhancers/withParentSize.tsx | 39 +++++----- .../src/enhancers/withScreenSize.tsx | 33 +++++---- packages/visx-responsive/src/index.ts | 6 +- packages/visx-responsive/src/types/index.ts | 2 + .../test/withParentSize.test.tsx | 22 +++--- 6 files changed, 107 insertions(+), 66 deletions(-) diff --git a/packages/visx-responsive/Readme.md b/packages/visx-responsive/Readme.md index 5157a8df0..787be7c1f 100644 --- a/packages/visx-responsive/Readme.md +++ b/packages/visx-responsive/Readme.md @@ -22,17 +22,31 @@ The `@visx/responsive` package is here to help you make responsive graphs. 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. +containing the respective screen dimensions. You can also optionally pass two config props to the +wrapped component, although in 99% of the cases this is not necessary: + +- `windowResizeDebounceTime` - 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: -```js -import { withScreenSize } from '@visx/responsive'; -// or -// import * as Responsive from '@visx/responsive'; -// Responsive.withScreenSize(...); +```tsx +import { withScreenSize, WithScreenSizeProvidedProps } from '@visx/responsive'; + +interface Props extends WithScreenSizeProvidedProps { + myProp: string; +} -let chartToRender = withScreenSize(MySuperCoolVisxChart); +const MySuperCoolVisxChart = ({ myProp, screenWidth, screenHeight }: Props) => { + // ... +}; + +const ChartToRender = withScreenSize(MySuperCoolVisxChart); + +const chartToRender = ; // ... Render the chartToRender somewhere ``` @@ -41,17 +55,32 @@ let chartToRender = withScreenSize(MySuperCoolVisxChart); If you would like your graph to adapt to it's 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: -```js -import { withParentSize } from '@visx/responsive'; -// or -// import * as Responsive from '@visx/responsive'; -// Responsive.withParentSize(...); +```tsx +import { withParentSize, WithParentSizeProvidedProps } from '@visx/responsive'; + +interface Props extends WithParentSizeProvidedProps { + myProp: string; +} + +const MySuperCoolVisxChart = ({ myProp, parentWidth, parentHeight }: Props) => { + // ... +}; -let chartToRender = withParentSize(MySuperCoolVisxChart); +const ChartWithParentSize = withParentSize(MySuperCoolVisxChart); + +const chartToRender = ; // ... Render the chartToRender somewhere ``` @@ -62,13 +91,10 @@ You might do the same thing using the `ParentSize` component. ### Example: -```js +```tsx import { ParentSize } from '@visx/responsive'; -// or -// import * as Responsive from '@visx/responsive'; -// ; -let chartToRender = ( +const chartToRender = ( {(parent) => ( -let chartToRender = ( +const chartToRender = ( diff --git a/packages/visx-responsive/src/enhancers/withParentSize.tsx b/packages/visx-responsive/src/enhancers/withParentSize.tsx index 105e23b4d..5473c41b4 100644 --- a/packages/visx-responsive/src/enhancers/withParentSize.tsx +++ b/packages/visx-responsive/src/enhancers/withParentSize.tsx @@ -1,6 +1,6 @@ -import React from 'react'; import debounce from 'lodash/debounce'; -import { ResizeObserver, ResizeObserverPolyfill } from '../types'; +import React from 'react'; +import { ResizeObserver, ResizeObserverPolyfill, Simplify } from '../types'; const CONTAINER_STYLES = { width: '100%', height: '100%' }; @@ -9,33 +9,36 @@ interface PrivateWindow { ResizeObserver: ResizeObserverPolyfill; } -export type WithParentSizeProps = { +type WithParentSizeConfig = { debounceTime?: number; enableDebounceLeadingCall?: boolean; + initialWidth?: number; + initialHeight?: number; }; type WithParentSizeState = { parentWidth?: number; parentHeight?: number; - initialWidth?: number; - initialHeight?: number; }; export type WithParentSizeProvidedProps = WithParentSizeState; -export default function withParentSize( - BaseComponent: React.ComponentType, +type WithParentSizeComponentProps

= Simplify< + Omit & WithParentSizeConfig +>; + +export default function withParentSize

( + BaseComponent: React.ComponentType

, /** Optionally inject a ResizeObserver polyfill, else this *must* be globally available. */ resizeObserverPolyfill?: ResizeObserverPolyfill, -) { +): React.ComponentType> { return class WrappedComponent extends React.Component< - BaseComponentProps & WithParentSizeProvidedProps, + WithParentSizeComponentProps

, WithParentSizeState > { - static defaultProps = { - debounceTime: 300, - enableDebounceLeadingCall: true, - }; + displayName = `withParentSize(${ + BaseComponent.displayName ?? BaseComponent.name ?? 'Component' + })`; state = { parentWidth: undefined, parentHeight: undefined, @@ -80,8 +83,8 @@ export default function withParentSize {parentWidth != null && parentHeight != null && ( - + )} ); diff --git a/packages/visx-responsive/src/enhancers/withScreenSize.tsx b/packages/visx-responsive/src/enhancers/withScreenSize.tsx index 3f91a2e2c..8e715a76e 100644 --- a/packages/visx-responsive/src/enhancers/withScreenSize.tsx +++ b/packages/visx-responsive/src/enhancers/withScreenSize.tsx @@ -1,7 +1,8 @@ import debounce from 'lodash/debounce'; import React from 'react'; +import { Simplify } from '../types'; -export type WithScreenSizeProps = { +type WithScreenSizeConfig = { windowResizeDebounceTime?: number; enableDebounceLeadingCall?: boolean; }; @@ -13,18 +14,20 @@ type WithScreenSizeState = { export type WithScreenSizeProvidedProps = WithScreenSizeState; -export default function withScreenSize( - BaseComponent: React.ComponentType, -) { +type WithScreenSizeComponentProps

= Simplify< + Omit & WithScreenSizeConfig +>; + +export default function withScreenSize

( + BaseComponent: React.ComponentType

, +): React.ComponentType> { return class WrappedComponent extends React.Component< - BaseComponentProps & WithScreenSizeProvidedProps, + WithScreenSizeComponentProps

, WithScreenSizeState > { - static defaultProps = { - windowResizeDebounceTime: 300, - enableDebounceLeadingCall: true, - }; - + displayName = `withScreenSize(${ + BaseComponent.displayName ?? BaseComponent.name ?? 'Component' + })`; state = { screenWidth: undefined, screenHeight: undefined, @@ -48,14 +51,18 @@ export default function withScreenSize + ); } }; diff --git a/packages/visx-responsive/src/index.ts b/packages/visx-responsive/src/index.ts index 53cedbcee..311c89a3a 100644 --- a/packages/visx-responsive/src/index.ts +++ b/packages/visx-responsive/src/index.ts @@ -1,4 +1,4 @@ -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 withParentSize, WithParentSizeProvidedProps } from './enhancers/withParentSize'; +export { default as withScreenSize, WithScreenSizeProvidedProps } from './enhancers/withScreenSize'; diff --git a/packages/visx-responsive/src/types/index.ts b/packages/visx-responsive/src/types/index.ts index 105ed1644..02ae06585 100644 --- a/packages/visx-responsive/src/types/index.ts +++ b/packages/visx-responsive/src/types/index.ts @@ -23,3 +23,5 @@ interface ResizeObserverPolyfill { } export { ResizeObserver, ResizeObserverCallback, ResizeObserverPolyfill }; + +export type Simplify = { [Key in keyof T]: T[Key] } & {}; diff --git a/packages/visx-responsive/test/withParentSize.test.tsx b/packages/visx-responsive/test/withParentSize.test.tsx index 6cac60406..3f3fd4cc8 100644 --- a/packages/visx-responsive/test/withParentSize.test.tsx +++ b/packages/visx-responsive/test/withParentSize.test.tsx @@ -1,16 +1,18 @@ -import React from 'react'; -import { render } from '@testing-library/react'; import { ResizeObserver } from '@juggle/resize-observer'; import '@testing-library/jest-dom'; -import { withParentSize } from '../src'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { withParentSize, WithParentSizeProvidedProps } from '../src'; -type ComponentProps = { - parentWidth?: number; - parentHeight?: number; -}; +interface ComponentProps extends WithParentSizeProvidedProps { + // only there to ensure that TS allows enhanced component to have own props, different than the ones passed by the HOC + role: string; +} -function Component({ parentWidth, parentHeight }: ComponentProps) { - return

; +function Component({ parentWidth, parentHeight, role }: ComponentProps) { + return ( +
+ ); } describe('withParentSize', () => { @@ -20,7 +22,7 @@ describe('withParentSize', () => { test('it should pass parentWidth and parentHeight props to its child', () => { const HOC = withParentSize(Component, ResizeObserver); - const { getByTestId } = render(); + const { getByTestId } = render(); const RenderedComponent = getByTestId('Component'); expect(RenderedComponent).toHaveStyle('width: 200px; height: 200px'); From 81018937fd11c5746c079a71e46ab5597c19b061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C4=85browski?= Date: Fri, 12 Jan 2024 00:37:48 +0100 Subject: [PATCH 2/4] review changes --- packages/visx-responsive/Readme.md | 4 ++-- .../visx-responsive/src/enhancers/withParentSize.tsx | 9 +++++++++ .../visx-responsive/src/enhancers/withScreenSize.tsx | 6 ++++++ packages/visx-responsive/test/withParentSize.test.tsx | 10 ++++++++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/visx-responsive/Readme.md b/packages/visx-responsive/Readme.md index 787be7c1f..c3454aa3d 100644 --- a/packages/visx-responsive/Readme.md +++ b/packages/visx-responsive/Readme.md @@ -46,7 +46,7 @@ const MySuperCoolVisxChart = ({ myProp, screenWidth, screenHeight }: Props) => { const ChartToRender = withScreenSize(MySuperCoolVisxChart); -const chartToRender = ; +const chartToRender = ; // ... Render the chartToRender somewhere ``` @@ -80,7 +80,7 @@ const MySuperCoolVisxChart = ({ myProp, parentWidth, parentHeight }: Props) => { const ChartWithParentSize = withParentSize(MySuperCoolVisxChart); -const chartToRender = ; +const chartToRender = ; // ... Render the chartToRender somewhere ``` diff --git a/packages/visx-responsive/src/enhancers/withParentSize.tsx b/packages/visx-responsive/src/enhancers/withParentSize.tsx index 5473c41b4..47a9158ea 100644 --- a/packages/visx-responsive/src/enhancers/withParentSize.tsx +++ b/packages/visx-responsive/src/enhancers/withParentSize.tsx @@ -9,6 +9,15 @@ interface PrivateWindow { ResizeObserver: ResizeObserverPolyfill; } +/** + * @deprecated + * @TODO remove in the next major version - exported for backwards compatibility + */ +export type WithParentSizeProps = Pick< + WithParentSizeConfig, + 'debounceTime' | 'enableDebounceLeadingCall' +>; + type WithParentSizeConfig = { debounceTime?: number; enableDebounceLeadingCall?: boolean; diff --git a/packages/visx-responsive/src/enhancers/withScreenSize.tsx b/packages/visx-responsive/src/enhancers/withScreenSize.tsx index 8e715a76e..0c207341f 100644 --- a/packages/visx-responsive/src/enhancers/withScreenSize.tsx +++ b/packages/visx-responsive/src/enhancers/withScreenSize.tsx @@ -7,6 +7,12 @@ type WithScreenSizeConfig = { enableDebounceLeadingCall?: boolean; }; +/** + * @deprecated + * @TODO remove in the next major version - exported for backwards compatibility + */ +export type WithParentSizeProps = WithScreenSizeConfig; + type WithScreenSizeState = { screenWidth?: number; screenHeight?: number; diff --git a/packages/visx-responsive/test/withParentSize.test.tsx b/packages/visx-responsive/test/withParentSize.test.tsx index 3f3fd4cc8..ec3aded2e 100644 --- a/packages/visx-responsive/test/withParentSize.test.tsx +++ b/packages/visx-responsive/test/withParentSize.test.tsx @@ -21,8 +21,14 @@ describe('withParentSize', () => { }); test('it should pass parentWidth and parentHeight props to its child', () => { - const HOC = withParentSize(Component, ResizeObserver); - const { getByTestId } = render(); + const WrappedComponent = withParentSize(Component, ResizeObserver); + + // @ts-expect-error ensure unknown types still error + render(); + + const { getByTestId } = render( + , + ); const RenderedComponent = getByTestId('Component'); expect(RenderedComponent).toHaveStyle('width: 200px; height: 200px'); From 5a187eedff1c5086d0c5223627c2c8a1c23b42bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C4=85browski?= Date: Sun, 18 Feb 2024 15:34:36 +0100 Subject: [PATCH 3/4] add useParentSize and useScreenSize hooks --- packages/visx-responsive/Readme.md | 147 ++++++++++++------ .../src/components/ParentSize.tsx | 108 +++---------- .../src/enhancers/withParentSize.tsx | 17 +- .../src/hooks/useParentSize.ts | 87 +++++++++++ .../src/hooks/useScreenSize.ts | 47 ++++++ packages/visx-responsive/src/index.ts | 3 + .../visx-responsive/src/resizeObserver.ts | 36 +++++ packages/visx-responsive/src/types/index.ts | 31 +--- .../test/useScreenSize.test.ts | 43 +++++ .../test/withParentSize.test.tsx | 13 +- 10 files changed, 364 insertions(+), 168 deletions(-) create mode 100644 packages/visx-responsive/src/hooks/useParentSize.ts create mode 100644 packages/visx-responsive/src/hooks/useScreenSize.ts create mode 100644 packages/visx-responsive/src/resizeObserver.ts create mode 100644 packages/visx-responsive/test/useScreenSize.test.ts diff --git a/packages/visx-responsive/Readme.md b/packages/visx-responsive/Readme.md index c3454aa3d..fac61559f 100644 --- a/packages/visx-responsive/Readme.md +++ b/packages/visx-responsive/Readme.md @@ -4,34 +4,97 @@ -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'; + +const ChartToRender = () => { + const { width, height } = useScreenSize({ debounceTime: 150 }); + + return ( + + {/* content */} + + ); +}; + +const chartToRender = ; +``` + +### `useParentSize` -`withScreenSize` +If you want your graph to adapt to its parent size, you can use `useParentSize()` hook. +`` 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: -`withParentSize` +- `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'; -**Components** +const ChartToRender = () => { + const { parentRef, width, height } = useParentSize({ debounceTime: 150 }); -`ParentSize` + return ( +
+ + {/* content */} + +
+ ); +}; + +const chartToRender = ; +``` -`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. You can also optionally pass two config props to the -wrapped component, although in 99% of the cases this is not necessary: +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 miliseconds, defaults to - `300`, +- `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: +#### Example ```tsx import { withScreenSize, WithScreenSizeProvidedProps } from '@visx/responsive'; @@ -47,25 +110,23 @@ const MySuperCoolVisxChart = ({ myProp, screenWidth, screenHeight }: Props) => { const ChartToRender = withScreenSize(MySuperCoolVisxChart); const chartToRender = ; - -// ... Render the chartToRender somewhere ``` -## `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. 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`, +- `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'; @@ -81,15 +142,15 @@ const MySuperCoolVisxChart = ({ myProp, parentWidth, parentHeight }: Props) => { const ChartWithParentSize = withParentSize(MySuperCoolVisxChart); const chartToRender = ; - -// ... Render the chartToRender somewhere ``` -## `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 ```tsx import { ParentSize } from '@visx/responsive'; @@ -110,15 +171,13 @@ const chartToRender = ( )}
); - -// ... Render the chartToRender somewhere ``` -## `ScaleSVG` +### `ScaleSVG` You can also create a responsive chart with a specific viewBox with the `ScaleSVG` component. -### Example: +#### Example ```tsx import { ScaleSVG } from '@visx/responsive'; @@ -128,29 +187,21 @@ const chartToRender = ( ); - -// ... 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 ( - - {() => {...}} - - ); +// 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`. diff --git a/packages/visx-responsive/src/components/ParentSize.tsx b/packages/visx-responsive/src/components/ParentSize.tsx index b67e5a0e5..705f41eaa 100644 --- a/packages/visx-responsive/src/components/ParentSize.tsx +++ b/packages/visx-responsive/src/components/ParentSize.tsx @@ -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, keyof ParentSizeProps>) { - const target = useRef(null); - const animationFrameID = useRef(0); - - const [state, setState] = useState({ - 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 ( -
+
{children({ - ...state, - ref: target.current, + ...dimensions, + ref: parentRef.current, resize, })}
diff --git a/packages/visx-responsive/src/enhancers/withParentSize.tsx b/packages/visx-responsive/src/enhancers/withParentSize.tsx index 47a9158ea..de9636bba 100644 --- a/packages/visx-responsive/src/enhancers/withParentSize.tsx +++ b/packages/visx-responsive/src/enhancers/withParentSize.tsx @@ -1,14 +1,10 @@ import debounce from 'lodash/debounce'; import React from 'react'; -import { ResizeObserver, ResizeObserverPolyfill, Simplify } from '../types'; +import { getResizeObserver, ResizeObserver, ResizeObserverPolyfill } from '../resizeObserver'; +import { Simplify } from '../types'; const CONTAINER_STYLES = { width: '100%', height: '100%' }; -// @TODO remove when upgraded to TS 4 which has its own declaration -interface PrivateWindow { - ResizeObserver: ResizeObserverPolyfill; -} - /** * @deprecated * @TODO remove in the next major version - exported for backwards compatibility @@ -38,7 +34,11 @@ type WithParentSizeComponentProps

= Simpl export default function withParentSize

( BaseComponent: React.ComponentType

, - /** Optionally inject a ResizeObserver polyfill, else this *must* be globally available. */ + /** + * @deprecated - use `setResizeObserverPolyfill` + * @TODO remove in the next major version + * Optionally inject a ResizeObserver polyfill, else this *must* be globally available. + */ resizeObserverPolyfill?: ResizeObserverPolyfill, ): React.ComponentType> { return class WrappedComponent extends React.Component< @@ -57,8 +57,7 @@ export default function withParentSize

( container: HTMLDivElement | null = null; componentDidMount() { - const ResizeObserverLocal = - resizeObserverPolyfill || (window as unknown as PrivateWindow).ResizeObserver; + const ResizeObserverLocal = resizeObserverPolyfill || getResizeObserver(); this.resizeObserver = new ResizeObserverLocal((entries) => { entries.forEach((entry) => { diff --git a/packages/visx-responsive/src/hooks/useParentSize.ts b/packages/visx-responsive/src/hooks/useParentSize.ts new file mode 100644 index 000000000..e9e518b88 --- /dev/null +++ b/packages/visx-responsive/src/hooks/useParentSize.ts @@ -0,0 +1,87 @@ +import debounce from 'lodash/debounce'; +import { RefObject, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { getResizeObserver, ResizeObserverPolyfill } from '../resizeObserver'; +import { DebounceSettings } from '../types'; + +export type ParentSizeState = { + width: number; + height: number; + top: number; + left: number; +}; + +export type UseParentSizeConfig = { + /** + * @deprecated - use `setResizeObserverPolyfill` function instead. + * @TODO - remove in the next major version. + * Optionally inject a ResizeObserver polyfill, else this *must* be globally available. + */ + resizeObserverPolyfill?: ResizeObserverPolyfill; + /** Optional dimensions provided won't trigger a state change when changed. */ + ignoreDimensions?: keyof ParentSizeState | (keyof ParentSizeState)[]; +} & DebounceSettings; + +type UseParentSizeResult = ParentSizeState & { + parentRef: RefObject; + resize: (state: ParentSizeState) => void; +}; + +const defaultIgnoreDimensions: UseParentSizeConfig['ignoreDimensions'] = []; + +export default function useParentSize({ + debounceTime = 300, + ignoreDimensions = defaultIgnoreDimensions, + enableDebounceLeadingCall = true, + resizeObserverPolyfill, +}: UseParentSizeConfig = {}): UseParentSizeResult { + const parentRef = useRef(null); + const animationFrameID = useRef(0); + + const [state, setState] = useState({ + width: 0, + height: 0, + top: 0, + left: 0, + }); + + const resize = useMemo(() => { + const normalized = Array.isArray(ignoreDimensions) ? ignoreDimensions : [ignoreDimensions]; + + return debounce( + (incoming: ParentSizeState) => { + console.log('eee'); + 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]); + + useLayoutEffect(() => { + const LocalResizeObserver = resizeObserverPolyfill || getResizeObserver(); + + 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 (parentRef.current) observer.observe(parentRef.current); + + return () => { + window.cancelAnimationFrame(animationFrameID.current); + observer.disconnect(); + resize.cancel(); + }; + }, [resize, resizeObserverPolyfill]); + + return { parentRef, resize, ...state }; +} diff --git a/packages/visx-responsive/src/hooks/useScreenSize.ts b/packages/visx-responsive/src/hooks/useScreenSize.ts new file mode 100644 index 000000000..1a1873291 --- /dev/null +++ b/packages/visx-responsive/src/hooks/useScreenSize.ts @@ -0,0 +1,47 @@ +import debounce from 'lodash/debounce'; +import { useEffect, useMemo, useState } from 'react'; +import { DebounceSettings } from '../types/index'; + +interface ScreenSize { + width: number; + height: number; +} + +export type UseScreenSizeConfig = DebounceSettings; + +const useScreenSize = ({ + debounceTime = 300, + enableDebounceLeadingCall = true, +}: UseScreenSizeConfig = {}) => { + const [screenSize, setScreenSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + + const handleResize = useMemo( + () => + debounce( + () => { + setScreenSize(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + }, + debounceTime, + { leading: enableDebounceLeadingCall }, + ), + [debounceTime, enableDebounceLeadingCall], + ); + + useEffect(() => { + window.addEventListener('resize', handleResize, false); + return () => { + window.removeEventListener('resize', handleResize, false); + handleResize.cancel(); + }; + }, [handleResize]); + + return screenSize; +}; + +export default useScreenSize; diff --git a/packages/visx-responsive/src/index.ts b/packages/visx-responsive/src/index.ts index 311c89a3a..f8200cc0f 100644 --- a/packages/visx-responsive/src/index.ts +++ b/packages/visx-responsive/src/index.ts @@ -2,3 +2,6 @@ export { default as ParentSize } from './components/ParentSize'; export { default as ScaleSVG } from './components/ScaleSVG'; export { default as withParentSize, WithParentSizeProvidedProps } from './enhancers/withParentSize'; export { default as withScreenSize, WithScreenSizeProvidedProps } from './enhancers/withScreenSize'; +export { default as useParentSize, UseParentSizeConfig } from './hooks/useParentSize'; +export { default as useScreenSize, UseScreenSizeConfig } from './hooks/useScreenSize'; +export { setResizeObserverPolyfill } from './resizeObserver'; diff --git a/packages/visx-responsive/src/resizeObserver.ts b/packages/visx-responsive/src/resizeObserver.ts new file mode 100644 index 000000000..3f7b08fe4 --- /dev/null +++ b/packages/visx-responsive/src/resizeObserver.ts @@ -0,0 +1,36 @@ +// @TODO remove all these types when upgraded to TS 4 which has its own declaration +interface ResizeObserverEntry { + contentRect: { + left: number; + top: number; + width: number; + height: number; + }; +} + +type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void; + +export declare class ResizeObserver { + constructor(callback: ResizeObserverCallback); + observe(target: Element, options?: any): void; + unobserve(target: Element): void; + disconnect(): void; + static toString(): string; +} + +export interface ResizeObserverPolyfill { + new (callback: ResizeObserverCallback): ResizeObserver; +} + +interface PrivateWindow { + ResizeObserver: ResizeObserverPolyfill; +} + +let resizeObserverPolyfill: ResizeObserverPolyfill | undefined; + +export const getResizeObserver = () => + resizeObserverPolyfill || (window as unknown as PrivateWindow).ResizeObserver; + +export const setResizeObserverPolyfill = (polyfill: ResizeObserverPolyfill | undefined) => { + resizeObserverPolyfill = polyfill; +}; diff --git a/packages/visx-responsive/src/types/index.ts b/packages/visx-responsive/src/types/index.ts index 02ae06585..a32bc07dc 100644 --- a/packages/visx-responsive/src/types/index.ts +++ b/packages/visx-responsive/src/types/index.ts @@ -1,27 +1,8 @@ -// @TODO remove when upgraded to TS 4 which has its own declaration -interface ResizeObserverEntry { - contentRect: { - left: number; - top: number; - width: number; - height: number; - }; -} - -type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void; - -declare class ResizeObserver { - constructor(callback: ResizeObserverCallback); - observe(target: Element, options?: any): void; - unobserve(target: Element): void; - disconnect(): void; - static toString(): string; -} +export type Simplify = { [Key in keyof T]: T[Key] } & {}; -interface ResizeObserverPolyfill { - new (callback: ResizeObserverCallback): ResizeObserver; +export interface DebounceSettings { + /** Child render updates upon resize are delayed until `debounceTime` milliseconds _after_ the last resize event is observed. Defaults to `300`. */ + 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; } - -export { ResizeObserver, ResizeObserverCallback, ResizeObserverPolyfill }; - -export type Simplify = { [Key in keyof T]: T[Key] } & {}; diff --git a/packages/visx-responsive/test/useScreenSize.test.ts b/packages/visx-responsive/test/useScreenSize.test.ts new file mode 100644 index 000000000..32fdfc37d --- /dev/null +++ b/packages/visx-responsive/test/useScreenSize.test.ts @@ -0,0 +1,43 @@ +import { fireEvent } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import useScreenSize from '../src/hooks/useScreenSize'; + +const setWindowSize = (width: number, height: number) => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: width, + }); + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: height, + }); +}; + +describe('useScreenSize', () => { + beforeEach(() => { + setWindowSize(1280, 1024); + }); + + afterEach(() => { + // @ts-ignore is just a test why you heff to be mad + delete window.innerWidth; + // @ts-ignore + delete window.innerHeight; + }); + + test('it should return the initial screen size', () => { + const { result } = renderHook(() => useScreenSize()); + expect(result.current).toEqual({ width: 1280, height: 1024 }); + }); + + test('it should update the screen size on window resize', () => { + const { result } = renderHook(() => useScreenSize()); + + setWindowSize(800, 600); + fireEvent(window, new Event('resize')); + + expect(result.current).toEqual({ width: 800, height: 600 }); + }); +}); diff --git a/packages/visx-responsive/test/withParentSize.test.tsx b/packages/visx-responsive/test/withParentSize.test.tsx index ec3aded2e..3bb3710ce 100644 --- a/packages/visx-responsive/test/withParentSize.test.tsx +++ b/packages/visx-responsive/test/withParentSize.test.tsx @@ -2,7 +2,7 @@ import { ResizeObserver } from '@juggle/resize-observer'; import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; import React from 'react'; -import { withParentSize, WithParentSizeProvidedProps } from '../src'; +import { setResizeObserverPolyfill, withParentSize, WithParentSizeProvidedProps } from '../src'; interface ComponentProps extends WithParentSizeProvidedProps { // only there to ensure that TS allows enhanced component to have own props, different than the ones passed by the HOC @@ -33,4 +33,15 @@ describe('withParentSize', () => { const RenderedComponent = getByTestId('Component'); expect(RenderedComponent).toHaveStyle('width: 200px; height: 200px'); }); + + test('should not throw when resize observer is set through setResizeObserverPolyfill', () => { + setResizeObserverPolyfill(ResizeObserver); + + const WrappedComponent = withParentSize(Component); + expect(() => { + render(); + }).not.toThrow(); + + setResizeObserverPolyfill(undefined); + }); }); From 07946b956ef09b4bea1e851358a8b52fb86a391c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C4=85browski?= Date: Thu, 22 Feb 2024 14:28:30 +0100 Subject: [PATCH 4/4] revert resize observer changes, add initialSize props, review window properties usage --- packages/visx-responsive/Readme.md | 26 ++++++++------ .../src/components/ParentSize.tsx | 2 ++ .../src/enhancers/withParentSize.tsx | 27 +++++++------- .../src/enhancers/withScreenSize.tsx | 10 +++--- .../src/hooks/useParentSize.ts | 33 +++++++++-------- .../src/hooks/useScreenSize.ts | 17 ++++++--- packages/visx-responsive/src/index.ts | 1 - .../visx-responsive/src/resizeObserver.ts | 36 ------------------- packages/visx-responsive/src/types/index.ts | 28 +++++++++++++++ .../test/useScreenSize.test.ts | 14 ++++++-- .../test/withParentSize.test.tsx | 13 +------ 11 files changed, 103 insertions(+), 104 deletions(-) delete mode 100644 packages/visx-responsive/src/resizeObserver.ts diff --git a/packages/visx-responsive/Readme.md b/packages/visx-responsive/Readme.md index fac61559f..280bd4484 100644 --- a/packages/visx-responsive/Readme.md +++ b/packages/visx-responsive/Readme.md @@ -22,6 +22,7 @@ returns current screen width and height and updates the value automatically on b resize. You can optionally pass a config object as an argument to the hook. Config object attributes are: +- `initialSize` - initial size before measuring the screen, defaults to `{ width: 0, height: 0 }`. - `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 @@ -52,6 +53,8 @@ If you want your graph to adapt to its parent size, you can use `useParentSize() 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: +- `initialSize` - initial size before measuring the parent, defaults to + `{ width: 0, height: 0, left: 0, top: 0 }`. - `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 @@ -88,8 +91,8 @@ If you prefer to use an enhancer, you can use the `withScreenSize()`. The result 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`. +- `debounceTime` - determines how often the size is updated in milliseconds, defaults to `300`. +- `windowResizeDebounceTime` - deprecated, equivalent to the above, kept for backwards compatibility - `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). @@ -192,16 +195,19 @@ const chartToRender = ( ## ⚠️ `ResizeObserver` dependency `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. +need a polyfill, you can either pollute the `window` object or inject it cleanly like this: ```tsx import { ResizeObserver } from 'your-favorite-polyfill'; -import { setResizeObserverPolyfill } from '@visx/responsive'; -// You only have to do this once -setResizeObserverPolyfill(ResizeObserver); -``` +// hook +useParentSize({ resizeObserverPolyfill: ResizeObserver }); -Now `useParentSize`, `ParentSize` and `withParentSize` will use that polyfill instead of global -`ResizeObserver`. +// component + + {() => {...}} + + +// enhancer +withParentSize(MyComponent, ResizeObserver); +``` diff --git a/packages/visx-responsive/src/components/ParentSize.tsx b/packages/visx-responsive/src/components/ParentSize.tsx index 705f41eaa..25f507058 100644 --- a/packages/visx-responsive/src/components/ParentSize.tsx +++ b/packages/visx-responsive/src/components/ParentSize.tsx @@ -26,12 +26,14 @@ export default function ParentSize({ children, debounceTime, ignoreDimensions, + initialSize, parentSizeStyles = defaultParentSizeStyles, enableDebounceLeadingCall = true, resizeObserverPolyfill, ...restProps }: ParentSizeProps & Omit, keyof ParentSizeProps>) { const { parentRef, resize, ...dimensions } = useParentSize({ + initialSize, debounceTime, ignoreDimensions, enableDebounceLeadingCall, diff --git a/packages/visx-responsive/src/enhancers/withParentSize.tsx b/packages/visx-responsive/src/enhancers/withParentSize.tsx index de9636bba..507a32e00 100644 --- a/packages/visx-responsive/src/enhancers/withParentSize.tsx +++ b/packages/visx-responsive/src/enhancers/withParentSize.tsx @@ -1,7 +1,12 @@ import debounce from 'lodash/debounce'; import React from 'react'; -import { getResizeObserver, ResizeObserver, ResizeObserverPolyfill } from '../resizeObserver'; -import { Simplify } from '../types'; +import { + DebounceSettings, + Simplify, + PrivateWindow, + ResizeObserverPolyfill, + ResizeObserver, +} from '../types'; const CONTAINER_STYLES = { width: '100%', height: '100%' }; @@ -9,17 +14,12 @@ const CONTAINER_STYLES = { width: '100%', height: '100%' }; * @deprecated * @TODO remove in the next major version - exported for backwards compatibility */ -export type WithParentSizeProps = Pick< - WithParentSizeConfig, - 'debounceTime' | 'enableDebounceLeadingCall' ->; +export type WithParentSizeProps = DebounceSettings; type WithParentSizeConfig = { - debounceTime?: number; - enableDebounceLeadingCall?: boolean; initialWidth?: number; initialHeight?: number; -}; +} & DebounceSettings; type WithParentSizeState = { parentWidth?: number; @@ -34,11 +34,7 @@ type WithParentSizeComponentProps

= Simpl export default function withParentSize

( BaseComponent: React.ComponentType

, - /** - * @deprecated - use `setResizeObserverPolyfill` - * @TODO remove in the next major version - * Optionally inject a ResizeObserver polyfill, else this *must* be globally available. - */ + /** Optionally inject a ResizeObserver polyfill, else this *must* be globally available. */ resizeObserverPolyfill?: ResizeObserverPolyfill, ): React.ComponentType> { return class WrappedComponent extends React.Component< @@ -57,7 +53,8 @@ export default function withParentSize

( container: HTMLDivElement | null = null; componentDidMount() { - const ResizeObserverLocal = resizeObserverPolyfill || getResizeObserver(); + const ResizeObserverLocal = + resizeObserverPolyfill || (window as unknown as PrivateWindow).ResizeObserver; this.resizeObserver = new ResizeObserverLocal((entries) => { entries.forEach((entry) => { diff --git a/packages/visx-responsive/src/enhancers/withScreenSize.tsx b/packages/visx-responsive/src/enhancers/withScreenSize.tsx index 0c207341f..86d603262 100644 --- a/packages/visx-responsive/src/enhancers/withScreenSize.tsx +++ b/packages/visx-responsive/src/enhancers/withScreenSize.tsx @@ -1,17 +1,17 @@ import debounce from 'lodash/debounce'; import React from 'react'; -import { Simplify } from '../types'; +import { Simplify, DebounceSettings } from '../types'; type WithScreenSizeConfig = { + /** @deprecated use `debounceTime` instead */ windowResizeDebounceTime?: number; - enableDebounceLeadingCall?: boolean; -}; +} & DebounceSettings; /** * @deprecated * @TODO remove in the next major version - exported for backwards compatibility */ -export type WithParentSizeProps = WithScreenSizeConfig; +export type WithParentSizeProps = Omit; type WithScreenSizeState = { screenWidth?: number; @@ -57,7 +57,7 @@ export default function withScreenSize

( screenHeight: window.innerHeight, })); }, - this.props.windowResizeDebounceTime ?? 300, + this.props.debounceTime ?? this.props.windowResizeDebounceTime ?? 300, { leading: this.props.enableDebounceLeadingCall ?? true }, ); diff --git a/packages/visx-responsive/src/hooks/useParentSize.ts b/packages/visx-responsive/src/hooks/useParentSize.ts index e9e518b88..770488d09 100644 --- a/packages/visx-responsive/src/hooks/useParentSize.ts +++ b/packages/visx-responsive/src/hooks/useParentSize.ts @@ -1,7 +1,6 @@ import debounce from 'lodash/debounce'; -import { RefObject, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { getResizeObserver, ResizeObserverPolyfill } from '../resizeObserver'; -import { DebounceSettings } from '../types'; +import { RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import { DebounceSettings, PrivateWindow, ResizeObserverPolyfill } from '../types'; export type ParentSizeState = { width: number; @@ -11,11 +10,9 @@ export type ParentSizeState = { }; export type UseParentSizeConfig = { - /** - * @deprecated - use `setResizeObserverPolyfill` function instead. - * @TODO - remove in the next major version. - * Optionally inject a ResizeObserver polyfill, else this *must* be globally available. - */ + /** Initial size before measuring the parent. */ + initialSize?: Partial; + /** Optionally inject a ResizeObserver polyfill, else this *must* be globally available. */ resizeObserverPolyfill?: ResizeObserverPolyfill; /** Optional dimensions provided won't trigger a state change when changed. */ ignoreDimensions?: keyof ParentSizeState | (keyof ParentSizeState)[]; @@ -27,8 +24,15 @@ type UseParentSizeResult = ParentSizeSta }; const defaultIgnoreDimensions: UseParentSizeConfig['ignoreDimensions'] = []; +const defaultInitialSize: ParentSizeState = { + width: 0, + height: 0, + top: 0, + left: 0, +}; export default function useParentSize({ + initialSize = defaultInitialSize, debounceTime = 300, ignoreDimensions = defaultIgnoreDimensions, enableDebounceLeadingCall = true, @@ -37,19 +41,13 @@ export default function useParentSize({ const parentRef = useRef(null); const animationFrameID = useRef(0); - const [state, setState] = useState({ - width: 0, - height: 0, - top: 0, - left: 0, - }); + const [state, setState] = useState({ ...defaultInitialSize, ...initialSize }); const resize = useMemo(() => { const normalized = Array.isArray(ignoreDimensions) ? ignoreDimensions : [ignoreDimensions]; return debounce( (incoming: ParentSizeState) => { - console.log('eee'); setState((existing) => { const stateKeys = Object.keys(existing) as (keyof ParentSizeState)[]; const keysWithChanges = stateKeys.filter((key) => existing[key] !== incoming[key]); @@ -63,8 +61,9 @@ export default function useParentSize({ ); }, [debounceTime, enableDebounceLeadingCall, ignoreDimensions]); - useLayoutEffect(() => { - const LocalResizeObserver = resizeObserverPolyfill || getResizeObserver(); + useEffect(() => { + const LocalResizeObserver = + resizeObserverPolyfill || (window as unknown as PrivateWindow).ResizeObserver; const observer = new LocalResizeObserver((entries) => { entries.forEach((entry) => { diff --git a/packages/visx-responsive/src/hooks/useScreenSize.ts b/packages/visx-responsive/src/hooks/useScreenSize.ts index 1a1873291..7e73a44a3 100644 --- a/packages/visx-responsive/src/hooks/useScreenSize.ts +++ b/packages/visx-responsive/src/hooks/useScreenSize.ts @@ -7,16 +7,22 @@ interface ScreenSize { height: number; } -export type UseScreenSizeConfig = DebounceSettings; +const defaultInitialSize: ScreenSize = { + width: 0, + height: 0, +}; + +export type UseScreenSizeConfig = { + /** Initial size before measuring the screen. */ + initialSize?: ScreenSize; +} & DebounceSettings; const useScreenSize = ({ + initialSize = defaultInitialSize, debounceTime = 300, enableDebounceLeadingCall = true, }: UseScreenSizeConfig = {}) => { - const [screenSize, setScreenSize] = useState({ - width: window.innerWidth, - height: window.innerHeight, - }); + const [screenSize, setScreenSize] = useState(initialSize); const handleResize = useMemo( () => @@ -34,6 +40,7 @@ const useScreenSize = ({ ); useEffect(() => { + handleResize(); window.addEventListener('resize', handleResize, false); return () => { window.removeEventListener('resize', handleResize, false); diff --git a/packages/visx-responsive/src/index.ts b/packages/visx-responsive/src/index.ts index f8200cc0f..736efdf8b 100644 --- a/packages/visx-responsive/src/index.ts +++ b/packages/visx-responsive/src/index.ts @@ -4,4 +4,3 @@ export { default as withParentSize, WithParentSizeProvidedProps } from './enhanc export { default as withScreenSize, WithScreenSizeProvidedProps } from './enhancers/withScreenSize'; export { default as useParentSize, UseParentSizeConfig } from './hooks/useParentSize'; export { default as useScreenSize, UseScreenSizeConfig } from './hooks/useScreenSize'; -export { setResizeObserverPolyfill } from './resizeObserver'; diff --git a/packages/visx-responsive/src/resizeObserver.ts b/packages/visx-responsive/src/resizeObserver.ts deleted file mode 100644 index 3f7b08fe4..000000000 --- a/packages/visx-responsive/src/resizeObserver.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @TODO remove all these types when upgraded to TS 4 which has its own declaration -interface ResizeObserverEntry { - contentRect: { - left: number; - top: number; - width: number; - height: number; - }; -} - -type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void; - -export declare class ResizeObserver { - constructor(callback: ResizeObserverCallback); - observe(target: Element, options?: any): void; - unobserve(target: Element): void; - disconnect(): void; - static toString(): string; -} - -export interface ResizeObserverPolyfill { - new (callback: ResizeObserverCallback): ResizeObserver; -} - -interface PrivateWindow { - ResizeObserver: ResizeObserverPolyfill; -} - -let resizeObserverPolyfill: ResizeObserverPolyfill | undefined; - -export const getResizeObserver = () => - resizeObserverPolyfill || (window as unknown as PrivateWindow).ResizeObserver; - -export const setResizeObserverPolyfill = (polyfill: ResizeObserverPolyfill | undefined) => { - resizeObserverPolyfill = polyfill; -}; diff --git a/packages/visx-responsive/src/types/index.ts b/packages/visx-responsive/src/types/index.ts index a32bc07dc..729cf538c 100644 --- a/packages/visx-responsive/src/types/index.ts +++ b/packages/visx-responsive/src/types/index.ts @@ -1,3 +1,31 @@ +// @TODO remove all these types when upgraded to TS 4 which has its own declaration +interface ResizeObserverEntry { + contentRect: { + left: number; + top: number; + width: number; + height: number; + }; +} + +type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void; + +export declare class ResizeObserver { + constructor(callback: ResizeObserverCallback); + observe(target: Element, options?: any): void; + unobserve(target: Element): void; + disconnect(): void; + static toString(): string; +} + +export interface ResizeObserverPolyfill { + new (callback: ResizeObserverCallback): ResizeObserver; +} + +export interface PrivateWindow { + ResizeObserver: ResizeObserverPolyfill; +} + export type Simplify = { [Key in keyof T]: T[Key] } & {}; export interface DebounceSettings { diff --git a/packages/visx-responsive/test/useScreenSize.test.ts b/packages/visx-responsive/test/useScreenSize.test.ts index 32fdfc37d..862e91949 100644 --- a/packages/visx-responsive/test/useScreenSize.test.ts +++ b/packages/visx-responsive/test/useScreenSize.test.ts @@ -1,4 +1,4 @@ -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import useScreenSize from '../src/hooks/useScreenSize'; @@ -32,12 +32,20 @@ describe('useScreenSize', () => { expect(result.current).toEqual({ width: 1280, height: 1024 }); }); - test('it should update the screen size on window resize', () => { + test('it should update the screen size on window resize', async () => { + // fake timers in Jest 25 are completely unusable so I'm using real timers here + // when it's upgraded should be updated to use advanceTimersByTime + jest.useRealTimers(); + const { result } = renderHook(() => useScreenSize()); + expect(result.current).toEqual({ width: 1280, height: 1024 }); + setWindowSize(800, 600); fireEvent(window, new Event('resize')); - expect(result.current).toEqual({ width: 800, height: 600 }); + await waitFor(() => expect(result.current).toEqual({ width: 800, height: 600 })); + + jest.useFakeTimers(); }); }); diff --git a/packages/visx-responsive/test/withParentSize.test.tsx b/packages/visx-responsive/test/withParentSize.test.tsx index 3bb3710ce..ec3aded2e 100644 --- a/packages/visx-responsive/test/withParentSize.test.tsx +++ b/packages/visx-responsive/test/withParentSize.test.tsx @@ -2,7 +2,7 @@ import { ResizeObserver } from '@juggle/resize-observer'; import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; import React from 'react'; -import { setResizeObserverPolyfill, withParentSize, WithParentSizeProvidedProps } from '../src'; +import { withParentSize, WithParentSizeProvidedProps } from '../src'; interface ComponentProps extends WithParentSizeProvidedProps { // only there to ensure that TS allows enhanced component to have own props, different than the ones passed by the HOC @@ -33,15 +33,4 @@ describe('withParentSize', () => { const RenderedComponent = getByTestId('Component'); expect(RenderedComponent).toHaveStyle('width: 200px; height: 200px'); }); - - test('should not throw when resize observer is set through setResizeObserverPolyfill', () => { - setResizeObserverPolyfill(ResizeObserver); - - const WrappedComponent = withParentSize(Component); - expect(() => { - render(); - }).not.toThrow(); - - setResizeObserverPolyfill(undefined); - }); });