Skip to content

Commit

Permalink
feat: support scroll direction
Browse files Browse the repository at this point in the history
  • Loading branch information
wellyshen committed Apr 26, 2020
1 parent 1be9908 commit 2167b1b
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 27 deletions.
84 changes: 61 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# React Cool Inview

A React [hook](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook) that monitors an element enters or leaves the viewport (or another element) with performant and efficient way, using [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). It's lightweight and super flexible, which can cover all the cases that you need, like [lazy-loading images](#lazy-loading-images) and videos, [infinite scrolling](#infinite-scrolling) web app, [triggering animations](#trigger-animations), [tracking impressions](#track-impressions) etc. Try it you will 👍🏻 it!
A React [hook](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook) that monitors an element enters or leaves the viewport (or another element) with performant and efficient way, using [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). It's lightweight and super flexible, which can cover all the cases that you need, like [lazy-loading images](#lazy-loading-images) and videos, [infinite scrolling](#infinite-scrolling) web app, [triggering animations](#trigger-animations), [tracking impressions](#track-impressions) and more. Try it you will 👍🏻 it!

[![build status](https://img.shields.io/travis/wellyshen/react-cool-inview/master?style=flat-square)](https://travis-ci.org/wellyshen/react-cool-inview)
[![coverage status](https://img.shields.io/coveralls/github/wellyshen/react-cool-inview?style=flat-square)](https://coveralls.io/github/wellyshen/react-cool-inview?branch=master)
Expand All @@ -18,6 +18,7 @@ A React [hook](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook) t
## Milestone

- [x] Detect an element is in-view or not
- [x] Detect the scrolling direction of the target element
- [x] `onEnter`, `onLeave`, `onChange` events
- [x] Trigger once feature
- [x] Server-side rendering compatibility
Expand All @@ -33,10 +34,11 @@ A React [hook](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook) t
- 🚀 Monitors elements with performant and non-main-thread blocking way, using [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
- 🎣 Easy to use, based on React [hook](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook).
- 🎛 Super flexible [API](#api) design which can cover [all the cases](#usage) that you need.
- 🖱️ Supports [scroll direction](#scroll-direction), cool right?
- ✌🏻 Supports [Intersection Observer v2](#intersection-observer-v2).
- 📜 Supports [TypeScript](https://www.typescriptlang.org) type definition.
- 🗄️ Server-side rendering compatibility.
- 🦠 Tiny size ([~ 1.7KB gzipped](https://bundlephobia.com/result?p=react-cool-inview)). No external dependencies, aside for the `react`.
- 🦠 Tiny size ([~ 1.8KB gzipped](https://bundlephobia.com/result?p=react-cool-inview)). No external dependencies, aside for the `react`.

## Requirement

Expand Down Expand Up @@ -68,19 +70,22 @@ import useInView from 'react-cool-inview';

const App = () => {
const ref = useRef();
const { inView, entry } = useInView(ref, {
threshold: 0.25, // Default is 0
onChange: ({ inView, entry, observe, unobserve }) => {
// Triggered whenever the target meets a threshold, e.g. [0.25, 0.5, ...]
},
onEnter: ({ entry, observe, unobserve }) => {
// Triggered when the target enters the viewport
},
onLeave: ({ entry, observe, unobserve }) => {
// Triggered when the target leaves the viewport
},
// More useful options...
});
const { inView, scrollDirection, entry, observe, unobserve } = useInView(
ref,
{
threshold: 0.25, // Default is 0
onChange: ({ inView, scrollDirection, entry, observe, unobserve }) => {
// Triggered whenever the target meets a threshold, e.g. [0.25, 0.5, ...]
},
onEnter: ({ scrollDirection, entry, observe, unobserve }) => {
// Triggered when the target enters the viewport
},
onLeave: ({ scrollDirection, entry, observe, unobserve }) => {
// Triggered when the target leaves the viewport
},
// More useful options...
}
);

return <div ref={ref}>{inView ? 'Hello, I am 🤗' : 'Bye, I am 😴'}</div>;
};
Expand Down Expand Up @@ -256,6 +261,38 @@ const App = () => {
};
```
## Scroll Direction
`react-cool-inview` not only monitors an element enters or leaves the viewport but also tells you its scroll direction by the `scrollDirection` object. The object contains vertical (y-axios) and horizontal (x-axios) properties, they're calculated whenever the target element meets a `threshold`. If there's no enough condition for calculating, the value of the properties will be `undefined`.
```js
import React, { useRef } from 'react';
import useInView from 'react-cool-inview';
const App = () => {
const ref = useRef();
const {
inView,
scrollDirection: { vertical, horizontal },
} = useInView(ref, {
// Scroll direction is calculated whenever the target meets a threshold
// more trigger points the calculation will be more instant and accurate
threshold: [0.2, 0.4, 0.6, 0.8, 1],
onChange: ({ scrollDirection }) => {
// We can also access the scroll direction from the event object
console.log('Scroll direction: ', scrollDirection.vertical);
},
});
return (
<div ref={ref}>
<div>{inView ? 'Hello, I am 🤗' : 'Bye, I am 😴'}</div>
<div>{`You're scrolling ${vertical === 'up' ? '⬆️' : '⬇️'}`}</div>
</div>
);
};
```

## Intersection Observer v2

The Intersection Observer v1 can perfectly tell you when an element is scrolled into the viewport, but it doesn't tell you whether the element is covered by something else on the page or whether the element has any visual effects applied on it (like `transform`, `opacity`, `filter` etc.) that can make it invisible. The main concern that has surfaced is how this kind of knowledge could be helpful in preventing [clickjacking](https://en.wikipedia.org/wiki/Clickjacking) and UI redress attacks (read this [article](https://developers.google.com/web/updates/2019/02/intersectionobserver-v2) to learn more).
Expand Down Expand Up @@ -284,10 +321,10 @@ const App = () => {
// Set a minimum delay between notifications, it must be set to 100 (ms) or greater
// For performance perspective, use the largest tolerable value as much as possible
delay: 100,
onEnter: ({ entry, observe, unobserve }) => {
onEnter: () => {
// Triggered when the target is visible and enters the viewport
},
onLeave: ({ entry, observe, unobserve }) => {
onLeave: () => {
// Triggered when the target is visible and leaves the viewport
},
});
Expand All @@ -306,12 +343,13 @@ const returnObj = useInView(ref: RefObject<HTMLElement>, options?: object);

It's returned with the following properties.
| Key | Type | Default | Description |
| ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inView` | boolean | | The visible state of the target element. If it's `true`, the target element has become at least as visible as the threshold that was passed. If it's `false`, the target element is no longer as visible as the given threshold. Supports [Intersection Observer v2](#intersection-observer-v2). |
| `entry` | object | | The [IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) of the target element. Which may contain the [isVisible](https://w3c.github.io/IntersectionObserver/v2/#dom-intersectionobserverentry-isvisible) property of the Intersection Observer v2, depends on the [browser compatibility](https://caniuse.com/#feat=intersectionobserver-v2). |
| `unobserve` | function | | To stop observing the target element. |
| `observe` | function | | To re-start observing the target element once it's stopped observing. |
| Key | Type | Default | Description |
| ----------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inView` | boolean | | The visible state of the target element. If it's `true`, the target element has become at least as visible as the threshold that was passed. If it's `false`, the target element is no longer as visible as the given threshold. Supports [Intersection Observer v2](#intersection-observer-v2). |
| `scrollDirection` | object | | The scroll direction of the target element. Which contains `vertical` and `horizontal` properties. See [scroll direction](#scroll-direction) for more information. |
| `entry` | object | | The [IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) of the target element. Which may contain the [isVisible](https://w3c.github.io/IntersectionObserver/v2/#dom-intersectionobserverentry-isvisible) property of the Intersection Observer v2, depends on the [browser compatibility](https://caniuse.com/#feat=intersectionobserver-v2). |
| `unobserve` | function | | To stop observing the target element. |
| `observe` | function | | To re-start observing the target element once it's stopped observing. |

### Parameters

Expand Down
51 changes: 47 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ interface IntersectionObserverInitV2 extends IntersectionObserverInit {
interface IntersectionObserverEntryV2 extends IntersectionObserverEntry {
readonly isVisible?: boolean;
}
interface ScrollDirection {
vertical?: 'up' | 'down';
horizontal?: 'left' | 'right';
}
interface BaseEvent {
entry?: IntersectionObserverEntryV2;
scrollDirection?: ScrollDirection;
observe?: () => void;
unobserve?: () => void;
}
Expand All @@ -39,12 +44,14 @@ interface Options {
}
interface Return {
readonly inView: boolean;
readonly scrollDirection: ScrollDirection;
readonly entry?: IntersectionObserverEntryV2;
readonly observe: () => void;
readonly unobserve: () => void;
}
interface State {
inView: boolean;
scrollDirection: ScrollDirection;
entry?: IntersectionObserverEntryV2;
}

Expand All @@ -62,8 +69,13 @@ const useInView = (
onLeave,
}: Options = {}
): Return => {
const [state, setState] = useState<State>({ inView: false });
const [state, setState] = useState<State>({
inView: false,
scrollDirection: {},
});
const prevInViewRef = useRef<boolean>(false);
const prevXRef = useRef<number>();
const prevYRef = useRef<number>();
const isObserveRef = useRef<boolean>(false);
const observerRef = useRef<IntersectionObserver>(null);
const warnedRef = useRef<boolean>(false);
Expand Down Expand Up @@ -113,13 +125,44 @@ const useInView = (

observerRef.current = new IntersectionObserver(
([entry]: IntersectionObserverEntryV2[]) => {
const e = { entry, observe, unobserve };
const { intersectionRatio, isIntersecting, isVisible } = entry;
const {
intersectionRatio,
isIntersecting,
boundingClientRect: { x, y },
isVisible,
} = entry;
let inView = getIsIntersecting(
threshold,
intersectionRatio,
isIntersecting
);
const scrollDirection: ScrollDirection = {};

if (!prevXRef.current) {
prevXRef.current = x;
} else if (x < prevXRef.current) {
scrollDirection.horizontal = 'left';
prevXRef.current = x;
} else if (x > prevXRef.current) {
scrollDirection.horizontal = 'right';
prevXRef.current = x;
} else {
delete scrollDirection.horizontal;
}

if (!prevYRef.current) {
prevYRef.current = y;
} else if (y < prevYRef.current) {
scrollDirection.vertical = 'up';
prevYRef.current = y;
} else if (y > prevYRef.current) {
scrollDirection.vertical = 'down';
prevYRef.current = y;
} else {
delete scrollDirection.vertical;
}

const e = { entry, scrollDirection, observe, unobserve };

if (trackVisibility) {
if (isVisible === undefined && !warnedRef.current) {
Expand All @@ -139,7 +182,7 @@ const useInView = (

if (onChangeRef.current) onChangeRef.current({ ...e, inView });

setState({ inView, entry });
setState({ inView, scrollDirection, entry });
prevInViewRef.current = inView;
},
{
Expand Down
7 changes: 7 additions & 0 deletions src/react-cool-inview.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ declare module 'react-cool-inview' {
readonly isVisible?: boolean;
}

interface ScrollDirection {
vertical?: 'up' | 'down';
horizontal?: 'left' | 'right';
}

export interface BaseEvent {
entry?: IntersectionObserverEntryV2;
scrollDirection?: ScrollDirection;
observe?: () => void;
unobserve?: () => void;
}
Expand All @@ -33,6 +39,7 @@ declare module 'react-cool-inview' {

interface Return {
readonly inView: boolean;
readonly scrollDirection: ScrollDirection;
readonly entry?: IntersectionObserverEntryV2;
readonly observe: () => void;
readonly unobserve: () => void;
Expand Down

0 comments on commit 2167b1b

Please sign in to comment.