From ed9bb2f181a497d970ee1ef0aefe88dbe086dbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C4=85browski?= Date: Fri, 8 Mar 2024 19:56:08 +0100 Subject: [PATCH] fix: Improve typing in @visx/responsive enhancers (#1783) * improve responsive enhancers typing * review changes * add useParentSize and useScreenSize hooks * revert resize observer changes, add initialSize props, review window properties usage --- packages/visx-responsive/Readme.md | 204 ++++++++++++------ .../src/components/ParentSize.tsx | 110 +++------- .../src/enhancers/withParentSize.tsx | 58 +++-- .../src/enhancers/withScreenSize.tsx | 43 ++-- .../src/hooks/useParentSize.ts | 86 ++++++++ .../src/hooks/useScreenSize.ts | 54 +++++ packages/visx-responsive/src/index.ts | 8 +- packages/visx-responsive/src/types/index.ts | 19 +- .../test/useScreenSize.test.ts | 51 +++++ .../test/withParentSize.test.tsx | 30 ++- 10 files changed, 460 insertions(+), 203 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/test/useScreenSize.test.ts diff --git a/packages/visx-responsive/Readme.md b/packages/visx-responsive/Readme.md index 5157a8df0..280bd4484 100644 --- a/packages/visx-responsive/Readme.md +++ b/packages/visx-responsive/Readme.md @@ -4,71 +4,161 @@ -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 -`withScreenSize` +### `useScreenSize` -`withParentSize` +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: -**Components** +- `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 + [`options.leading` in Lodash's `debounce`](https://lodash.com/docs/4.17.15#debounce). -`ParentSize` +#### Example + +```tsx +import { useScreenSize } from '@visx/responsive'; + +const ChartToRender = () => { + const { width, height } = useScreenSize({ debounceTime: 150 }); + + return ( + + {/* content */} + + ); +}; -`ScaleSVG` +const chartToRender = ; +``` + +### `useParentSize` + +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: + +- `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 + [`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'; + +const ChartToRender = () => { + const { parentRef, width, height } = useParentSize({ debounceTime: 150 }); + + return ( +
+ + {/* content */} + +
+ ); +}; + +const chartToRender = ; +``` + +## 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: -### Example: +- `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). + +#### Example + +```tsx +import { withScreenSize, WithScreenSizeProvidedProps } from '@visx/responsive'; -```js -import { withScreenSize } from '@visx/responsive'; -// or -// import * as Responsive from '@visx/responsive'; -// Responsive.withScreenSize(...); +interface Props extends WithScreenSizeProvidedProps { + myProp: string; +} -let chartToRender = withScreenSize(MySuperCoolVisxChart); +const MySuperCoolVisxChart = ({ myProp, screenWidth, screenHeight }: Props) => { + // ... +}; -// ... Render the chartToRender somewhere +const ChartToRender = withScreenSize(MySuperCoolVisxChart); + +const chartToRender = ; ``` -## `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 + +```tsx +import { withParentSize, WithParentSizeProvidedProps } from '@visx/responsive'; -### Example: +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 = ; ``` -## `ParentSize` +## Components -You might do the same thing using the `ParentSize` component. +### `ParentSize` -### Example: +You might do the same thing as `useParentSize` or `withParentSize` using the `ParentSize` component. -```js +#### Example + +```tsx import { ParentSize } from '@visx/responsive'; -// or -// import * as Responsive from '@visx/responsive'; -// ; -let chartToRender = ( +const chartToRender = ( {(parent) => ( ); - -// ... 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'; -// -let chartToRender = ( +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, you can either pollute the `window` object or inject it cleanly like this: ```tsx import { ResizeObserver } from 'your-favorite-polyfill'; -function App() { - return ( - - {() => {...}} - - ); -``` +// hook +useParentSize({ resizeObserverPolyfill: ResizeObserver }); -## Installation +// component + + {() => {...}} + -``` -npm install --save @visx/responsive +// enhancer +withParentSize(MyComponent, ResizeObserver); ``` diff --git a/packages/visx-responsive/src/components/ParentSize.tsx b/packages/visx-responsive/src/components/ParentSize.tsx index b67e5a0e5..25f507058 100644 --- a/packages/visx-responsive/src/components/ParentSize.tsx +++ b/packages/visx-responsive/src/components/ParentSize.tsx @@ -1,110 +1,50 @@ -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, + initialSize, 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({ + initialSize, + 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 105e23b4d..507a32e00 100644 --- a/packages/visx-responsive/src/enhancers/withParentSize.tsx +++ b/packages/visx-responsive/src/enhancers/withParentSize.tsx @@ -1,41 +1,49 @@ -import React from 'react'; import debounce from 'lodash/debounce'; -import { ResizeObserver, ResizeObserverPolyfill } from '../types'; +import React from 'react'; +import { + DebounceSettings, + Simplify, + PrivateWindow, + ResizeObserverPolyfill, + ResizeObserver, +} 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 + */ +export type WithParentSizeProps = DebounceSettings; -export type WithParentSizeProps = { - debounceTime?: number; - enableDebounceLeadingCall?: boolean; -}; +type WithParentSizeConfig = { + initialWidth?: number; + initialHeight?: number; +} & DebounceSettings; 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 +88,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..86d603262 100644 --- a/packages/visx-responsive/src/enhancers/withScreenSize.tsx +++ b/packages/visx-responsive/src/enhancers/withScreenSize.tsx @@ -1,10 +1,17 @@ import debounce from 'lodash/debounce'; import React from 'react'; +import { Simplify, DebounceSettings } from '../types'; -export type WithScreenSizeProps = { +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 = Omit; type WithScreenSizeState = { screenWidth?: number; @@ -13,18 +20,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 +57,18 @@ export default function withScreenSize + ); } }; diff --git a/packages/visx-responsive/src/hooks/useParentSize.ts b/packages/visx-responsive/src/hooks/useParentSize.ts new file mode 100644 index 000000000..770488d09 --- /dev/null +++ b/packages/visx-responsive/src/hooks/useParentSize.ts @@ -0,0 +1,86 @@ +import debounce from 'lodash/debounce'; +import { RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import { DebounceSettings, PrivateWindow, ResizeObserverPolyfill } from '../types'; + +export type ParentSizeState = { + width: number; + height: number; + top: number; + left: number; +}; + +export type UseParentSizeConfig = { + /** 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)[]; +} & DebounceSettings; + +type UseParentSizeResult = ParentSizeState & { + parentRef: RefObject; + resize: (state: ParentSizeState) => void; +}; + +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, + resizeObserverPolyfill, +}: UseParentSizeConfig = {}): UseParentSizeResult { + const parentRef = useRef(null); + const animationFrameID = useRef(0); + + const [state, setState] = useState({ ...defaultInitialSize, ...initialSize }); + + 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 (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..7e73a44a3 --- /dev/null +++ b/packages/visx-responsive/src/hooks/useScreenSize.ts @@ -0,0 +1,54 @@ +import debounce from 'lodash/debounce'; +import { useEffect, useMemo, useState } from 'react'; +import { DebounceSettings } from '../types/index'; + +interface ScreenSize { + width: number; + height: number; +} + +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(initialSize); + + const handleResize = useMemo( + () => + debounce( + () => { + setScreenSize(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + }, + debounceTime, + { leading: enableDebounceLeadingCall }, + ), + [debounceTime, enableDebounceLeadingCall], + ); + + useEffect(() => { + handleResize(); + 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 53cedbcee..736efdf8b 100644 --- a/packages/visx-responsive/src/index.ts +++ b/packages/visx-responsive/src/index.ts @@ -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 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'; diff --git a/packages/visx-responsive/src/types/index.ts b/packages/visx-responsive/src/types/index.ts index 105ed1644..729cf538c 100644 --- a/packages/visx-responsive/src/types/index.ts +++ b/packages/visx-responsive/src/types/index.ts @@ -1,4 +1,4 @@ -// @TODO remove when upgraded to TS 4 which has its own declaration +// @TODO remove all these types when upgraded to TS 4 which has its own declaration interface ResizeObserverEntry { contentRect: { left: number; @@ -10,7 +10,7 @@ interface ResizeObserverEntry { type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void; -declare class ResizeObserver { +export declare class ResizeObserver { constructor(callback: ResizeObserverCallback); observe(target: Element, options?: any): void; unobserve(target: Element): void; @@ -18,8 +18,19 @@ declare class ResizeObserver { static toString(): string; } -interface ResizeObserverPolyfill { +export interface ResizeObserverPolyfill { new (callback: ResizeObserverCallback): ResizeObserver; } -export { ResizeObserver, ResizeObserverCallback, ResizeObserverPolyfill }; +export interface PrivateWindow { + ResizeObserver: ResizeObserverPolyfill; +} + +export type Simplify = { [Key in keyof T]: T[Key] } & {}; + +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; +} diff --git a/packages/visx-responsive/test/useScreenSize.test.ts b/packages/visx-responsive/test/useScreenSize.test.ts new file mode 100644 index 000000000..862e91949 --- /dev/null +++ b/packages/visx-responsive/test/useScreenSize.test.ts @@ -0,0 +1,51 @@ +import { fireEvent, waitFor } 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', 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')); + + 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 6cac60406..ec3aded2e 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', () => { @@ -19,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');