From ba32ebebba2f9b124db820e53d3428bcd05801d7 Mon Sep 17 00:00:00 2001 From: Chris Santamaria Date: Sun, 2 Apr 2023 20:47:13 -0400 Subject: [PATCH] Enforce that input components accept injected props (#119) --- README.md | 6 +- src/__tests__/index.tsx | 5 +- src/index.ts | 2 + src/lib/constants.ts | 4 +- src/lib/handleViewport.tsx | 58 +++++++------------ src/lib/types.ts | 10 +++- src/lib/useInViewport.tsx | 4 +- .../chapters/enterCallback.stories.tsx | 1 - src/stories/common/Iframe.tsx | 8 ++- src/stories/common/IframeFunctional.tsx | 10 +++- src/stories/common/Image.tsx | 8 ++- src/stories/common/ImageFunctional.tsx | 10 +++- src/stories/common/Section.tsx | 7 +-- src/stories/common/themeComponent.tsx | 16 +++-- 14 files changed, 85 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 00c9f57..eae6c34 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ _NOTE_: Stateless: Need to add `ref={this.props.forwardedRef}` to your component #### Example of a functional component -```javascript -import handleViewport from 'react-in-viewport'; +```tsx +import handleViewport, { type InjectedProps } from 'react-in-viewport'; -const Block = (props: { inViewport: boolean }) => { +const Block = (props: InjectedProps) => { const { inViewport, forwardedRef } = props; const color = inViewport ? '#217ac0' : '#ff9800'; const text = inViewport ? 'In viewport' : 'Not in viewport'; diff --git a/src/__tests__/index.tsx b/src/__tests__/index.tsx index 212a59d..225db86 100644 --- a/src/__tests__/index.tsx +++ b/src/__tests__/index.tsx @@ -1,10 +1,9 @@ import { PureComponent } from 'react'; import { render } from '@testing-library/react'; import { handleViewport } from '../index'; +import type { InjectedViewportProps } from '../lib/types'; -class DemoClass extends PureComponent<{ - inViewport: boolean; -}> { +class DemoClass extends PureComponent { render() { const { inViewport } = this.props; return ( diff --git a/src/index.ts b/src/index.ts index 11ef24e..932360a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,5 @@ export const customProps = ['inViewport', 'enterCount', 'leaveCount']; export default handleViewport; export { default as handleViewport } from './lib/handleViewport'; export { default as useInViewport } from './lib/useInViewport'; + +export type { InjectedViewportProps } from './lib/types'; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c19a5a7..387d405 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,9 +1,9 @@ -import type { Config, Props, Options } from './types'; +import type { Config, CallbackProps, Options } from './types'; export const defaultOptions: Options = {}; export const defaultConfig: Config = { disconnectOnLeave: false }; export const noop = () => {}; -export const defaultProps: Props = { +export const defaultProps: CallbackProps = { onEnterViewport: noop, onLeaveViewport: noop, }; diff --git a/src/lib/handleViewport.tsx b/src/lib/handleViewport.tsx index 20e39d4..6d5f853 100644 --- a/src/lib/handleViewport.tsx +++ b/src/lib/handleViewport.tsx @@ -1,7 +1,12 @@ import { useRef, forwardRef } from 'react'; import hoistNonReactStatic from 'hoist-non-react-statics'; -import type { Config, Props, Options } from './types'; +import type { + CallbackProps, + Config, + InjectedViewportProps, + Options, +} from './types'; import useInViewport from './useInViewport'; import { noop, defaultOptions, defaultConfig } from './constants'; @@ -17,52 +22,32 @@ const isReactComponent = (Component: React.ComponentClass) => { return Component.prototype && Component.prototype.isReactComponent; }; -type InjectedProps = { - enterCount: number; - inViewport: boolean; - leaveCount: number; -}; - -type RefProps = React.PropsWithRef<{ - forwardedRef?: React.ForwardedRef; -}>; - -type OmittedProps = 'onEnterViewport' | 'onLeaveViewport'; -type RestPropsRef = Omit; - -function handleViewport

( - TargetComponent: React.ElementType | React.ComponentClass

, +function handleViewport< + TElement extends HTMLElement, + TProps extends InjectedViewportProps, +>( + TargetComponent: React.ComponentType, options: Options = defaultOptions, config: Config = defaultConfig, ) { - const ForwardedRefComponent = forwardRef< - any, - InjectedProps & RefProps & RestPropsRef - >((props, ref) => { - const refProps: RefProps = { + const ForwardedRefComponent = forwardRef((props, ref) => { + const refProps = { forwardedRef: ref, // pass both ref/forwardedRef for class component for backward compatibility - ...(isReactComponent(TargetComponent as React.ComponentClass

) + ...(isReactComponent(TargetComponent as React.ComponentClass) && !isFunctionalComponent(TargetComponent) - ? { - ref, - } + ? { ref } : {}), }; - return ( - - ); + return ; }); function InViewport({ onEnterViewport = noop, onLeaveViewport = noop, ...restProps - }) { - const node = useRef(); + }: Omit> & CallbackProps) { + const node = useRef(); const { inViewport, enterCount, leaveCount } = useInViewport( node, options, @@ -73,14 +58,15 @@ function handleViewport

( }, ); - const injectedProps: InjectedProps = { + const props = { + ...restProps, inViewport, enterCount, leaveCount, - }; + } as React.PropsWithoutRef; return ( - + ); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 47d42c8..fbfe502 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2,10 +2,16 @@ export type Config = { disconnectOnLeave?: boolean; }; -export type Props = { +export type InjectedViewportProps = { + inViewport: boolean; + enterCount: number; + leaveCount: number; + forwardedRef: React.RefObject; +}; + +export type CallbackProps = { onEnterViewport?: () => void; onLeaveViewport?: () => void; - [key: string]: any; }; export type Options = IntersectionObserverInit; diff --git a/src/lib/useInViewport.tsx b/src/lib/useInViewport.tsx index 191fdd9..614c470 100644 --- a/src/lib/useInViewport.tsx +++ b/src/lib/useInViewport.tsx @@ -5,13 +5,13 @@ import { findDOMNode } from 'react-dom'; import { defaultOptions, defaultConfig, defaultProps } from './constants'; -import type { Config, Props, Options } from './types'; +import type { Config, CallbackProps, Options } from './types'; const useInViewport = ( target: React.RefObject, options: Options = defaultOptions, config : Config = defaultConfig, - props: Props = defaultProps, + props: CallbackProps = defaultProps, ) => { const { onEnterViewport, onLeaveViewport } = props; const [, forceUpdate] = useState(); diff --git a/src/stories/chapters/enterCallback.stories.tsx b/src/stories/chapters/enterCallback.stories.tsx index 0d90118..269795f 100644 --- a/src/stories/chapters/enterCallback.stories.tsx +++ b/src/stories/chapters/enterCallback.stories.tsx @@ -43,7 +43,6 @@ export default { export function ClassBaseComponent() { return ( action('callback')('onEnterViewport')} onLeaveViewport={() => action('callback')('onLeaveViewport')} /> diff --git a/src/stories/common/Iframe.tsx b/src/stories/common/Iframe.tsx index 3a01f25..b0598b1 100644 --- a/src/stories/common/Iframe.tsx +++ b/src/stories/common/Iframe.tsx @@ -2,9 +2,15 @@ import React, { PureComponent } from 'react'; import AspectRatio from 'react-aspect-ratio'; import { handleViewport } from '../../index'; +import type { InjectedViewportProps } from '../../lib/types'; + +type IframeProps = InjectedViewportProps & { + src: string; + ratio: string; +}; class Iframe extends PureComponent< -{ src: string; ratio: string }, +IframeProps, { loaded: boolean } > { constructor(props) { diff --git a/src/stories/common/IframeFunctional.tsx b/src/stories/common/IframeFunctional.tsx index 4c2c343..635d4d5 100644 --- a/src/stories/common/IframeFunctional.tsx +++ b/src/stories/common/IframeFunctional.tsx @@ -2,8 +2,14 @@ import React, { memo, useEffect, useState } from 'react'; import AspectRatio from 'react-aspect-ratio'; import { handleViewport } from '../../index'; +import type { InjectedViewportProps } from '../../lib/types'; -function IframeFunctional(props) { +type IframeFunctionalProps = InjectedViewportProps & { + src: string; + ratio: string; +}; + +function IframeFunctional(props: IframeFunctionalProps) { const { inViewport, src, ratio, forwardedRef, } = props; @@ -27,6 +33,8 @@ function IframeFunctional(props) { diff --git a/src/stories/common/Image.tsx b/src/stories/common/Image.tsx index 8ae0b35..4e98f2d 100644 --- a/src/stories/common/Image.tsx +++ b/src/stories/common/Image.tsx @@ -3,11 +3,17 @@ import AspectRatio from 'react-aspect-ratio'; import { INIT, LOADING, LOADED } from './constants'; import { handleViewport } from '../../index'; +import type { InjectedViewportProps } from '../../lib/types'; const DUMMY_IMAGE_SRC = 'https://www.gstatic.com/psa/static/1.gif'; +type ImageObjectProps = InjectedViewportProps & { + src: string; + ratio: string; +}; + class ImageObject extends PureComponent< -{ inViewport: boolean; src: string; ratio: string }, +ImageObjectProps, { status: number; src: string } > { // eslint-disable-line diff --git a/src/stories/common/ImageFunctional.tsx b/src/stories/common/ImageFunctional.tsx index 67d8663..c159657 100644 --- a/src/stories/common/ImageFunctional.tsx +++ b/src/stories/common/ImageFunctional.tsx @@ -3,10 +3,16 @@ import AspectRatio from 'react-aspect-ratio'; import { handleViewport } from '../../index'; import { INIT, LOADING, LOADED } from './constants'; +import type { InjectedViewportProps } from '../../lib/types'; const DUMMY_IMAGE_SRC = 'https://www.gstatic.com/psa/static/1.gif'; -function ImageObject(props) { +type ImageObjectProps = InjectedViewportProps & { + src: string; + ratio: string; +}; + +function ImageObject(props: ImageObjectProps) { const { src: originalSrc, ratio, forwardedRef, inViewport, } = props; @@ -51,6 +57,8 @@ function ImageObject(props) { marginBottom: '200px', backgroundColor: getBackgroundColor(), }} + // @ts-expect-error + // TODO: fix upstream types in react-aspect-ratio to support ref ref={forwardedRef} > demo diff --git a/src/stories/common/Section.tsx b/src/stories/common/Section.tsx index 4f78e2f..a04d646 100644 --- a/src/stories/common/Section.tsx +++ b/src/stories/common/Section.tsx @@ -1,12 +1,9 @@ import React, { PureComponent } from 'react'; import { handleViewport } from '../../index'; +import type { InjectedViewportProps } from '../../lib/types'; -class MySectionBlock extends PureComponent<{ - inViewport: boolean; - enterCount: number; - leaveCount: number; -}> { +class MySectionBlock extends PureComponent { getStyle() { const { inViewport, enterCount } = this.props; const basicStyle = { diff --git a/src/stories/common/themeComponent.tsx b/src/stories/common/themeComponent.tsx index 9612c2f..2e898e3 100644 --- a/src/stories/common/themeComponent.tsx +++ b/src/stories/common/themeComponent.tsx @@ -1,4 +1,5 @@ import React, { memo } from 'react'; +import { InjectedViewportProps } from '../../lib/types'; export const PageTitle = memo( ({ @@ -26,17 +27,18 @@ export const PageTitle = memo( ); PageTitle.displayName = 'PageTitle'; -export class Card extends React.PureComponent<{ +type CardProps = { titleText: string; contentNode: React.ReactNode; - forwardedRef?: React.Ref | undefined; -}> { +}; + +export class Card extends React.PureComponent { static displayName = 'Card'; render() { - const { titleText, contentNode, forwardedRef } = this.props; + const { titleText, contentNode } = this.props; return ( -

+

{titleText}

@@ -46,7 +48,9 @@ export class Card extends React.PureComponent<{ } } -export function Block(props) { +type BlockProps = InjectedViewportProps; + +export function Block(props: BlockProps) { const { inViewport, enterCount, leaveCount, forwardedRef, } = props;