Skip to content

Commit

Permalink
Enforce that input components accept injected props (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrissantamaria authored Apr 3, 2023
1 parent 1a3aca1 commit ba32ebe
Show file tree
Hide file tree
Showing 14 changed files with 85 additions and 64 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>) => {
const { inViewport, forwardedRef } = props;
const color = inViewport ? '#217ac0' : '#ff9800';
const text = inViewport ? 'In viewport' : 'Not in viewport';
Expand Down
5 changes: 2 additions & 3 deletions src/__tests__/index.tsx
Original file line number Diff line number Diff line change
@@ -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<InjectedViewportProps> {
render() {
const { inViewport } = this.props;
return (
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 2 additions & 2 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -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,
};
58 changes: 22 additions & 36 deletions src/lib/handleViewport.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<any>;
}>;

type OmittedProps = 'onEnterViewport' | 'onLeaveViewport';
type RestPropsRef = Omit<Props, OmittedProps>;

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

function InViewport({
onEnterViewport = noop,
onLeaveViewport = noop,
...restProps
}) {
const node = useRef<any>();
}: Omit<TProps, keyof InjectedViewportProps<TElement>> & CallbackProps) {
const node = useRef<TElement>();
const { inViewport, enterCount, leaveCount } = useInViewport(
node,
options,
Expand All @@ -73,14 +58,15 @@ function handleViewport<P extends Props>(
},
);

const injectedProps: InjectedProps = {
const props = {
...restProps,
inViewport,
enterCount,
leaveCount,
};
} as React.PropsWithoutRef<TProps>;

return (
<ForwardedRefComponent {...restProps} {...injectedProps} ref={node} />
<ForwardedRefComponent {...props} ref={node} />
);
}

Expand Down
10 changes: 8 additions & 2 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ export type Config = {
disconnectOnLeave?: boolean;
};

export type Props = {
export type InjectedViewportProps<TElement extends HTMLElement = HTMLElement> = {
inViewport: boolean;
enterCount: number;
leaveCount: number;
forwardedRef: React.RefObject<TElement>;
};

export type CallbackProps = {
onEnterViewport?: () => void;
onLeaveViewport?: () => void;
[key: string]: any;
};

export type Options = IntersectionObserverInit;
4 changes: 2 additions & 2 deletions src/lib/useInViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>,
options: Options = defaultOptions,
config : Config = defaultConfig,
props: Props = defaultProps,
props: CallbackProps = defaultProps,
) => {
const { onEnterViewport, onLeaveViewport } = props;
const [, forceUpdate] = useState();
Expand Down
1 change: 0 additions & 1 deletion src/stories/chapters/enterCallback.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export default {
export function ClassBaseComponent() {
return (
<ViewportBlock
className="card"
onEnterViewport={() => action('callback')('onEnterViewport')}
onLeaveViewport={() => action('callback')('onLeaveViewport')}
/>
Expand Down
8 changes: 7 additions & 1 deletion src/stories/common/Iframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion src/stories/common/IframeFunctional.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> & {
src: string;
ratio: string;
};

function IframeFunctional(props: IframeFunctionalProps) {
const {
inViewport, src, ratio, forwardedRef,
} = props;
Expand All @@ -27,6 +33,8 @@ function IframeFunctional(props) {
<AspectRatio
ratio={ratio}
style={{ marginBottom: '200px', backgroundColor: 'rgba(0,0,0,.12)' }}
// @ts-expect-error
// TODO: fix upstream types in react-aspect-ratio to support ref
ref={forwardedRef}
>
<Component {...componentProps} />
Expand Down
8 changes: 7 additions & 1 deletion src/stories/common/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion src/stories/common/ImageFunctional.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> & {
src: string;
ratio: string;
};

function ImageObject(props: ImageObjectProps) {
const {
src: originalSrc, ratio, forwardedRef, inViewport,
} = props;
Expand Down Expand Up @@ -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}
>
<img src={src} alt="demo" />
Expand Down
7 changes: 2 additions & 5 deletions src/stories/common/Section.tsx
Original file line number Diff line number Diff line change
@@ -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<InjectedViewportProps> {
getStyle() {
const { inViewport, enterCount } = this.props;
const basicStyle = {
Expand Down
16 changes: 10 additions & 6 deletions src/stories/common/themeComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { memo } from 'react';
import { InjectedViewportProps } from '../../lib/types';

export const PageTitle = memo(
({
Expand Down Expand Up @@ -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<any> | undefined;
}> {
};

export class Card extends React.PureComponent<CardProps> {
static displayName = 'Card';

render() {
const { titleText, contentNode, forwardedRef } = this.props;
const { titleText, contentNode } = this.props;
return (
<div className="card" ref={forwardedRef}>
<div className="card">
<div className="card__head">
<h3 className="card__title">{titleText}</h3>
</div>
Expand All @@ -46,7 +48,9 @@ export class Card extends React.PureComponent<{
}
}

export function Block(props) {
type BlockProps = InjectedViewportProps<HTMLDivElement>;

export function Block(props: BlockProps) {
const {
inViewport, enterCount, leaveCount, forwardedRef,
} = props;
Expand Down

0 comments on commit ba32ebe

Please sign in to comment.