diff --git a/README.md b/README.md index 426254aa..99a2cb4c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# react-intersection-observer - -[![Version Badge][npm-version-svg]][package-url] -[![Test][test-image]][test-url] -[![License][license-image]][license-url] -[![Downloads][downloads-image]][downloads-url] +

+ React Intersection Observer +

+ +[![Version Badge](https://img.shields.io/npm/v/react-intersection-observer.svg)](https://npmjs.org/package/react-intersection-observer) +[![Test](https://github.com/thebuilder/react-intersection-observer/workflows/Test/badge.svg)](https://github.com/thebuilder/react-intersection-observer/actions?query=workflow%3ATest) +[![License](http://img.shields.io/npm/l/react-intersection-observer.svg)](LICENSE) +[![Downloads](http://img.shields.io/npm/dm/react-intersection-observer.svg)](http://npm-stat.com/charts.html?package=react-intersection-observer) ![npm package minimized gzipped size](https://img.shields.io/bundlejs/size/react-intersection-observer?exports=InView%2C%20useOnInView%2C%20useInView&externals=react&format=both) A React implementation of the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) @@ -259,9 +261,9 @@ extend the `IntersectionObserverEntry` with the `isVisible` boolean. The `IntersectionObserver` itself is just a simple but powerful tool. Here's a few ideas for how you can use it. -- [Lazy image load](docs/Recipes.md#lazy-image-load) -- [Trigger animations](docs/Recipes.md#trigger-animations) -- [Track impressions](docs/Recipes.md#track-impressions) _(Google Analytics, Tag +- [Lazy image load](apps/docs/docs/guides/recipes.mdx#lazy-image-loading) +- [Trigger animations](apps/docs/docs/guides/recipes.mdx#scroll-triggered-animation) +- [Track impressions](apps/docs/docs/guides/recipes.mdx#track-an-impression) _(Google Analytics, Tag Manager, etc.)_ ## FAQ @@ -375,7 +377,7 @@ globally, alongside either `jest.fn` or `vi.fn`. See the instructions for [Vitest](#vitest). You should be able to use a similar setup/reset code, adapted to the testing library you are using. Failing that, -copy the code from [test-utils.ts][test-utils-url], and make your own version. +copy the code from [test-utils.ts](packages/react-intersection-observer/src/test-utils.ts), and make your own version. ### Fallback Behavior @@ -574,13 +576,3 @@ order to destroy the observer again. > You most likely won't need this, but it can be useful if you > need to handle IntersectionObservers outside React, or need full control over > how instances are created. - -[package-url]: https://npmjs.org/package/react-intersection-observer -[npm-version-svg]: https://img.shields.io/npm/v/react-intersection-observer.svg -[license-image]: http://img.shields.io/npm/l/react-intersection-observer.svg -[license-url]: LICENSE -[downloads-image]: http://img.shields.io/npm/dm/react-intersection-observer.svg -[downloads-url]: http://npm-stat.com/charts.html?package=react-intersection-observer -[test-image]: https://github.com/thebuilder/react-intersection-observer/workflows/Test/badge.svg -[test-url]: https://github.com/thebuilder/react-intersection-observer/actions?query=workflow%3ATest -[test-utils-url]: https://github.com/thebuilder/react-intersection-observer/blob/master/src/test-utils.ts diff --git a/apps/docs/public/logo-horizontal.svg b/apps/docs/public/logo-horizontal.svg new file mode 100644 index 00000000..a25fb66d --- /dev/null +++ b/apps/docs/public/logo-horizontal.svg @@ -0,0 +1,20 @@ + + + React Intersection Observer + + + + + + + + + + + + diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index 57c841a0..2135815c 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -1,12 +1,29 @@ import type { StorybookConfig } from "@storybook/react-vite"; +const favicon = ``; + const config: StorybookConfig = { framework: "@storybook/react-vite", + staticDirs: [{ from: "../../docs/public", to: "/" }], stories: [ "../stories/**/*.mdx", "../stories/**/*.@(story|stories).@(ts|tsx)", ], addons: ["@storybook/addon-docs", "@storybook/addon-vitest"], + // Storybook's generated brandImage rule uses !important, so this needs to be + // appended after it and match that priority. + managerHead: (head) => ` + ${head} + ${favicon} + + `, + previewHead: (head) => `${head}${favicon}`, core: { builder: "@storybook/builder-vite", }, diff --git a/apps/storybook/.storybook/manager.ts b/apps/storybook/.storybook/manager.ts index 51a50669..191a850c 100644 --- a/apps/storybook/.storybook/manager.ts +++ b/apps/storybook/.storybook/manager.ts @@ -1,10 +1,12 @@ import { addons } from "storybook/manager-api"; -import { themes } from "storybook/theming"; +import docsLogo from "../../docs/public/logo-horizontal.svg"; +import { storybookTheme } from "./theme"; addons.setConfig({ theme: { - ...themes.dark, - brandTitle: "React IntersectionObserver", - brandUrl: "https://github.com/thebuilder/react-intersection-observer", + ...storybookTheme, + brandImage: docsLogo, + brandTitle: "React Intersection Observer", + brandUrl: "https://react-intersection-observer.vercel.app/", }, }); diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts index 18645f25..ac85b998 100644 --- a/apps/storybook/.storybook/preview.ts +++ b/apps/storybook/.storybook/preview.ts @@ -1,14 +1,12 @@ -import { themes } from "storybook/theming"; +import { storybookTheme } from "./theme"; import "../styles.css"; export const parameters = { controls: { expanded: true, }, - theme: { - ...themes.dark, - }, + theme: storybookTheme, docs: { - theme: themes.dark, + theme: storybookTheme, }, }; diff --git a/apps/storybook/.storybook/theme.ts b/apps/storybook/.storybook/theme.ts new file mode 100644 index 00000000..78ebd21e --- /dev/null +++ b/apps/storybook/.storybook/theme.ts @@ -0,0 +1,12 @@ +import { themes } from "storybook/theming"; + +// sRGB equivalent of the docs light-mode accent: oklch(0.6 0.15 290). +// Storybook's theme engine does not support oklch() values. +const docsAccent = "#816dd2"; + +export const storybookTheme = { + ...themes.light, + barSelectedColor: docsAccent, + colorPrimary: docsAccent, + colorSecondary: docsAccent, +}; diff --git a/apps/storybook/stories/Intro.mdx b/apps/storybook/stories/Intro.mdx index 28cf3d66..29ac5db1 100644 --- a/apps/storybook/stories/Intro.mdx +++ b/apps/storybook/stories/Intro.mdx @@ -1,4 +1,7 @@ -import { Markdown } from '@storybook/addon-docs/blocks'; -import Readme from '../../../README.md?raw'; +# Explore the API -{Readme} +A React implementation of the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). + +Use the stories in this library to explore the `InView` component, `useInView`, and `useOnInView` in isolation. + +For installation, API reference, guides, and recipes, visit the [documentation site](https://react-intersection-observer.vercel.app/). diff --git a/apps/storybook/stories/Recipes.mdx b/apps/storybook/stories/Recipes.mdx deleted file mode 100644 index 0851bbd8..00000000 --- a/apps/storybook/stories/Recipes.mdx +++ /dev/null @@ -1,4 +0,0 @@ -import { Markdown } from '@storybook/addon-docs/blocks'; -import Recipes from '../../../docs/Recipes.md?raw'; - -{Recipes} diff --git a/docs/Recipes.md b/docs/Recipes.md deleted file mode 100644 index 9f1bffd5..00000000 --- a/docs/Recipes.md +++ /dev/null @@ -1,143 +0,0 @@ -# Recipes - -The `IntersectionObserver` itself is just a simple but powerful tool. Here's a -few ideas for how you can use it. - -## Lazy image load - -It's actually easy to create your own lazy image loader, and this allows you to -build it according to your needs. - -**Couple of tips** - -- Don't set the `src` (or `srcset`) on the image until it's visible. Images will - always load their `src`, even if you set `display: none;`. -- Make sure to set the - [root margin](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) - for top and bottom, so the Intersection Observer gets triggered before the - image enters the viewport. This gives the image a chance to be loaded before - the user even sees it. Try to start with something like `200px 0px`, but - experiment with it until you find the right value. -- Set `triggerOnce`, so you don't keep monitoring for changes. -- You should always create a wrapping element, that sets the correct aspect - ratio for the image. You can set the padding bottom to be - `${height / width * 100}%` to maintain aspect ratio. -- Either hide the `` with CSS, or skip rendering it until it's inside the - viewport. - -> [!TIP] -> All modern browsers support the native `loading` attribute on `` tags, so unless you need -> fine-grained control, you can skip the `IntersectionObserver` and use `loading="lazy"` instead. -> -> https://web.dev/articles/browser-level-image-lazy-loading - -```jsx -import React from "react"; -import { useInView } from "react-intersection-observer"; - -const LazyImage = ({ width, height, src, ...rest }) => { - const { ref, inView } = useInView({ - triggerOnce: true, - rootMargin: "200px 0px", - }); - - return ( -
- {inView ? ( - - ) : null} -
- ); -}; - -export default LazyImage; -``` - -**See [Codesandbox](https://codesandbox.io/embed/lazy-image-load-mjsgc)** - -## Trigger animations - -Triggering animations once they enter the viewport is also a perfect use case -for an IntersectionObserver. - -- Set `triggerOnce`, to only trigger the animation the first time. -- Set `threshold`, to control how much of the element should be visible before - firing the event. -- Instead of `threshold`, you can use `rootMargin` to have a fixed amount be - visible before triggering. Use a negative margin value, like `-100px 0px`, to - have it go inwards. You can also use a percentage value, instead of pixels. - -```jsx -import React from "react"; -import { useInView } from "react-intersection-observer"; - -const LazyAnimation = () => { - const { ref, inView } = useInView({ - triggerOnce: true, - rootMargin: "-100px 0px", - }); - - return ( -
- 👋 -
- ); -}; - -export default LazyAnimation; -``` - -## Track impressions - -You can use `IntersectionObserver` to track when a user views your element, and -fire an event on your tracking service. Consider using the `useOnInView` to -trigger changes via a callback. - -- Set `triggerOnce`, to only trigger an event the first time the element enters - the viewport. -- Set `threshold`, to control how much of the element should visible before - firing the event. -- Instead of `threshold`, you can use `rootMargin` to have a fixed amount be - visible before triggering. Use a negative margin value, like `-100px 0px`, to - have it go inwards. You can also use a percentage value, instead of pixels. - -```jsx -import * as React from "react"; -import { useOnInView } from "react-intersection-observer"; - -const TrackImpression = () => { - const ref = useOnInView((inView) => { - if (inView) { - // Fire a tracking event to your tracking service of choice. - dataLayer.push("Section shown"); // Here's a GTM dataLayer push - } - }, { - triggerOnce: true, - rootMargin: "-100px 0", - }); - - return ( -
- Exemplars sunt zeluss de bassus fuga. Credere velox ducunt ad audax amor. -
- ); -}; - -export default TrackImpression; -```