├── .editorconfig
├── .github
└── workflows
│ ├── build-test.yml
│ └── gh-pages.yml
├── .gitignore
├── .yarnrc.yml
├── README.md
├── biome.json
├── example
├── index.html
├── package.json
├── postcss.config.js
├── src
│ ├── App.jsx
│ ├── ComponentExample.jsx
│ ├── HookExample.jsx
│ ├── global.css
│ └── main.jsx
├── tailwind.config.js
└── vite.config.ts
├── gifs
├── example.gif
└── example_inner.gif
├── package.json
├── src
├── .eslintrc
├── component
│ └── index.tsx
├── hook
│ ├── index.test.tsx
│ └── index.tsx
└── index.tsx
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/workflows/build-test.yml:
--------------------------------------------------------------------------------
1 | name: Build & Test
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - run: corepack enable
11 | - uses: actions/setup-node@v4
12 | with:
13 | node-version: '20'
14 | cache: 'yarn'
15 | - run: yarn --immutable
16 | - run: yarn lint
17 | - run: yarn test
18 | - name: 'Report Coverage'
19 | if: always()
20 | uses: davelosert/vitest-coverage-report-action@v2
21 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy example app to Github Pages
2 |
3 | on:
4 | push:
5 | branches: ['main']
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: read
10 | pages: write
11 | id-token: write
12 |
13 | concurrency:
14 | group: 'pages'
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | deploy:
19 | environment:
20 | name: github-pages
21 | url: ${{ steps.deployment.outputs.page_url }}
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4
25 | - run: corepack enable
26 | - uses: actions/setup-node@v4
27 | with:
28 | node-version: 20
29 | cache: 'yarn'
30 | - name: Install dependencies
31 | run: yarn --immutable
32 | - name: Build lib
33 | run: yarn build
34 | - name: Build
35 | run: yarn build:example
36 | - name: Setup Pages
37 | uses: actions/configure-pages@v4
38 | - name: Upload artifact
39 | uses: actions/upload-pages-artifact@v3
40 | with:
41 | path: './example/dist'
42 | - name: Deploy to GitHub Pages
43 | id: deployment
44 | uses: actions/deploy-pages@v4
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 | .eslintcache
12 |
13 | # misc
14 | .DS_Store
15 | .env
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .idea
26 | .vscode
27 | coverage/
28 |
29 | # yarn 4
30 | .pnp.*
31 | .yarn/*
32 | !.yarn/patches
33 | !.yarn/plugins
34 | !.yarn/releases
35 | !.yarn/sdks
36 | !.yarn/versions
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-bottom-scroll-listener [](https://www.npmjs.com/package/react-bottom-scroll-listener) [](https://www.npmjs.com/package/react-bottom-scroll-listener) [](https://github.com/karl-run/react-bottom-scroll-listener)
2 |
3 | A simple **React hook** and **React component** that lets you listen for when you have scrolled to the bottom.
4 |
5 | ### Window
6 |
7 | 
8 |
9 | ### Container
10 |
11 | 
12 |
13 | ## Installation
14 |
15 | npm:
16 | `npm install react-bottom-scroll-listener`
17 |
18 | yarn:
19 | `yarn add react-bottom-scroll-listener`
20 |
21 | ## Migrating to V5
22 |
23 | Version 5 is only a refactor for the hook to use an options parameter, instead
24 | of relying of function parameters which were starting to get out of hand.
25 |
26 | #### If you are using the component, there are no breaking changes
27 |
28 | If your hook looks like this:
29 |
30 | ```tsx
31 | useBottomScrollListener(callback, 0, 200, undefined, true);
32 | ```
33 |
34 | You will have to change it to use the options parameter:
35 |
36 | ```
37 | useBottomScrollListener(callback, {
38 | offset: 100,
39 | debounce: 0,
40 | triggerOnNoScroll: true
41 | })
42 | ```
43 |
44 | Remember that you can omit any values that are using the defaults! The defaults are ase following:
45 |
46 | ```
47 | offset: 0,
48 | debounce: 200,
49 | debounceOptions: { leading: true },
50 | triggerOnNoScroll: false,
51 | ```
52 |
53 | So for the average use case, you are probably only setting one of these values, so your hook
54 | might look like this:
55 |
56 | ```
57 | useBottomScrollListener(callback, { triggerOnNoScroll: true })
58 | ```
59 |
60 | You can refer to the Usage-section for more details.
61 |
62 | ## Usage
63 |
64 | ### Hook
65 |
66 | [Full example](/example/src/HookExample.js)
67 |
68 | #### On bottom of entire screen
69 |
70 | Use the hook in any functional component, the callback will be invoked
71 | when the user scrolls to the bottom of the document
72 |
73 | ```jsx
74 | import { useBottomScrollListener } from 'react-bottom-scroll-listener';
75 |
76 | useBottomScrollListener(callback);
77 | ```
78 |
79 | #### On bottom of specific container
80 |
81 | Use the hook in any functional component, use the ref given from the hook
82 | and pass it to the element you want to use as a scroll container
83 |
84 | The callback will be invoked when the user scrolls to the bottom of the container
85 |
86 | ```jsx
87 | import { useBottomScrollListener } from 'react-bottom-scroll-listener';
88 |
89 | const scrollRef = useBottomScrollListener(callback);
90 |
91 |
Callback will be invoked when this container is scrolled to bottom.
;
92 | ```
93 |
94 | **Parameters**
95 |
96 | ```
97 | useBottomScrollListener(
98 | // Required callback that will be invoked when scrolled to bottom
99 | onBottom: () => void,
100 | // Options, entirely optional, you can provide one or several to overwrite the defaults
101 | options?: {
102 | // Offset from bottom of page in pixels. E.g. 300 will trigger onBottom 300px from the bottom of the page
103 | offset?: number
104 | // Optional debounce in milliseconds, defaults to 200ms
105 | debounce?: number
106 | // Overwrite the debounceOptions for lodash.debounce, default to { leading: true }
107 | debounceOptions?: DebounceOptions
108 | // If container is too short, enables a trigger of the callback if that happens, defaults to false
109 | triggerOnNoScroll?: boolean
110 | },
111 | ); // returns React.MutableRefObject Optionally you can use this to pass to a element to use that as the scroll container
112 | ```
113 |
114 | ### Component
115 |
116 | [Full example](/example/src/ComponentExample.js)
117 |
118 | #### On bottom of entire screen
119 |
120 | Simply have the BottomScrollListener anywhere in your application and pass it a function as `onBottom`-prop.
121 |
122 | ```jsx
123 | import BottomScrollListener from 'react-bottom-scroll-listener';
124 |
125 | ;
126 | ```
127 |
128 | #### On bottom of specific container
129 |
130 | Pass the BottomScrollListener a function inside the JSX_tag, receive the `scrollRef` in this function from the BottomScrollListener
131 | and pass it to the component you want to listen for a scroll event on.
132 |
133 | ```jsx
134 | import BottomScrollListener from 'react-bottom-scroll-listener';
135 |
136 |
137 | {(scrollRef) =>
Callback will be invoked when this container is scrolled to bottom.
}
138 | ;
139 | ```
140 |
141 | > Those are some weird children, what's going on?
142 |
143 | This pattern is called "function as a child". What this allows is that the BottomScrollListener can pass you a `React.RefObject`. This
144 | `React.RefObject` can then be passed to whatever component you want to be notified when you hit the bottom of. Without this it would be
145 | difficult to attach event listeners for scrolling to an arbitrary element.
146 |
147 | **Props**
148 |
149 | | Property | Type | Default | Description |
150 | | ----------------- | :----------------------: | :-------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
151 | | onBottom | Function | null | **(required):** callback invoked when bottom is reached |
152 | | debounce | number | 200 | milliseconds, how much debounce there should be on the callback |
153 | | offset | number | 0 | offset from bottom in pixels. E.g. 300 if it should invoke `onBottom` 300px before the bottom. |
154 | | debounceOptions | DebounceOptions | {leading: true} | see the lodash.debounce options: see https://lodash.com/docs/4.17.15#debounce |
155 | | triggerOnNoScroll | boolean | false | if container is too short, enables a trigger of the callback if that happens |
156 | | children | React.Node _or_ Function | null | Not required, but you can use this to wrap your components. Most useful when you have some conditional rendering. If this is a function, that function will receive a React.RefObject that _needs_ to be passed to a child element. This element will then be used as the scroll container. |
157 |
158 | # Migrating from 2.x.x to 3.x.x
159 |
160 | There are no breaking changes except that the required version of React is now 16.8.0. If you are on an
161 | older version of React you can either upgrade React, or stay on version 2.x.x. If you already
162 | are on a newer version of React you don't have to do anything, except maybe try out the new hook implementation. :)
163 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "ignore": []
11 | },
12 | "formatter": {
13 | "enabled": false
14 | },
15 | "organizeImports": {
16 | "enabled": true
17 | },
18 | "linter": {
19 | "enabled": true,
20 | "rules": {
21 | "recommended": true
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | react-bottom-scroll-listener
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-bottom-scroll-listener-example",
3 | "homepage": "https://karl-run.github.io/react-bottom-scroll-listener",
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "private": true,
7 | "type": "module",
8 | "dependencies": {
9 | "prop-types": "^15.8.1",
10 | "react": "18.3.1",
11 | "react-bottom-scroll-listener": "link:..",
12 | "react-dom": "18.3.1",
13 | "react-toggle": "^4.1.3"
14 | },
15 | "browserslist": [
16 | ">0.2%",
17 | "not dead",
18 | "not ie <= 11",
19 | "not op_mini all"
20 | ],
21 | "devDependencies": {
22 | "@types/prop-types": "^15.7.14",
23 | "autoprefixer": "^10.4.21",
24 | "postcss": "^8.5.3",
25 | "tailwindcss": "^3.4.17",
26 | "vite": "^6.3.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/example/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/example/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Toggle from 'react-toggle'
3 |
4 | import ComponentExample from './ComponentExample'
5 | import HookExample from './HookExample'
6 |
7 | const App = () => {
8 | const [hookExample, setHookExample] = useState(true)
9 | const [alertOnBottom, setAlertOnBottom] = useState(true)
10 |
11 | return (
12 |
13 |
14 |
react-bottom-scroll-listener
15 |
16 |
25 |
34 |
35 |
36 | {hookExample ? : }
37 |
38 | )
39 | }
40 |
41 | export default App
42 |
--------------------------------------------------------------------------------
/example/src/ComponentExample.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import 'react-toggle/style.css'
4 |
5 | import { BottomScrollListener } from 'react-bottom-scroll-listener'
6 |
7 | class ComponentExample extends Component {
8 | handleOnDocumentBottom = () => {
9 | console.log(`I am at bottom! ${Math.round(performance.now())}`)
10 |
11 | if (this.props.alertOnBottom) {
12 | alert('Bottom hit! Too slow? Reduce "debounce" value in props')
13 | }
14 | }
15 |
16 | handleContainerOnBottom = () => {
17 | console.log(`I am at bottom in optional container! ${Math.round(performance.now())}`)
18 |
19 | if (this.props.alertOnBottom) {
20 | alert('Bottom of this container hit! Too slow? Reduce "debounce" value in props')
21 | }
22 | }
23 |
24 | render() {
25 | return (
26 |
27 | {/* If you want to listen for the bottom of a specific container you need to forward
28 | a scrollRef as a ref to your container */}
29 |
30 | {(scrollRef) => (
31 |
35 |
Callback when this container hits bottom
36 |
Scroll down! ▼▼▼
37 |
A bit more... ▼▼
38 |
Almost there... ▼
39 |
You've reached the bottom!
40 |
41 | )}
42 |
43 |
44 |
45 |
Component example
46 |
Callback when document hits bottom
47 |
Scroll down! ▼▼▼
48 |
A bit more... ▼▼
49 |
Almost there... ▼
50 |
You've reached the bottom!
51 |
52 |
53 | {/* When you only want to listen to the bottom of "document", you can put it anywhere */}
54 |
55 |
56 | )
57 | }
58 | }
59 |
60 | export default ComponentExample
61 |
--------------------------------------------------------------------------------
/example/src/HookExample.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 |
3 | import { useBottomScrollListener } from 'react-bottom-scroll-listener'
4 |
5 | const HookExample = ({ alertOnBottom }) => {
6 | const handleOnDocumentBottom = useCallback(() => {
7 | console.log(`I am at bottom! ${Math.round(performance.now())}`)
8 |
9 | if (alertOnBottom) {
10 | alert('Bottom hit!')
11 | }
12 | }, [alertOnBottom])
13 |
14 | const handleContainerOnBottom = useCallback(() => {
15 | console.log(`I am at bottom in optional container! ${Math.round(performance.now())}`)
16 |
17 | if (alertOnBottom) {
18 | alert('Bottom of this container hit!')
19 | }
20 | }, [alertOnBottom])
21 |
22 | /* This will trigger handleOnDocumentBottom when the body of the page hits the bottom */
23 | useBottomScrollListener(handleOnDocumentBottom)
24 |
25 | /* This will trigger handleOnContainerBottom when the container that is passed the ref hits the bottom */
26 | const containerRef = useBottomScrollListener(handleContainerOnBottom)
27 |
28 | return (
29 |
30 |
34 |
Callback when this container hits bottom
35 |
Scroll down! ▼▼▼
36 |
A bit more... ▼▼
37 |
Almost there... ▼
38 |
You've reached the bottom!
39 |
40 |
41 |
Hook example
42 |
Callback when document hits bottom
43 |
Scroll down! ▼▼▼
44 |
A bit more... ▼▼
45 |
Almost there... ▼
46 |
You've reached the bottom!
47 |
48 |
49 | )
50 | }
51 |
52 | export default HookExample
53 |
--------------------------------------------------------------------------------
/example/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | @font-face {
7 | font-family: 'Roboto';
8 | font-style: normal;
9 | font-weight: 400;
10 | font-display: swap;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/src/main.jsx:
--------------------------------------------------------------------------------
1 | import './global.css';
2 |
3 | import React from 'react';
4 | import { createRoot } from 'react-dom/client';
5 |
6 | import App from './App';
7 |
8 | const container = document.getElementById('root');
9 | const root = createRoot(container); // createRoot(container!) if you use TypeScript
10 | root.render();
11 |
--------------------------------------------------------------------------------
/example/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/example/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | build: {
7 | sourcemap: true,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/gifs/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karl-run/react-bottom-scroll-listener/646a37a88030f7d0764fd58e27f32336f035d1ff/gifs/example.gif
--------------------------------------------------------------------------------
/gifs/example_inner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karl-run/react-bottom-scroll-listener/646a37a88030f7d0764fd58e27f32336f035d1ff/gifs/example_inner.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-bottom-scroll-listener",
3 | "type": "commonjs",
4 | "version": "5.2.0",
5 | "author": "Karl J. Overå",
6 | "license": "MIT",
7 | "description": "A simple React component that lets you listen for when you have scrolled to the bottom.",
8 | "repository": {
9 | "url": "git+https://github.com/karl-run/react-bottom-scroll-listener.git"
10 | },
11 | "homepage": "https://github.com/karl-run/react-bottom-scroll-listener#readme",
12 | "bugs": {
13 | "url": "https://github.com/karl-run/react-bottom-scroll-listener/issues"
14 | },
15 | "keywords": [
16 | "react",
17 | "scrolling",
18 | "bottom",
19 | "listener",
20 | "callback"
21 | ],
22 | "files": [
23 | "dist"
24 | ],
25 | "exports": {
26 | ".": {
27 | "types": "./dist/index.d.ts",
28 | "import": "./dist/react-bottom-scroll-listener.mjs",
29 | "require": "./dist/react-bottom-scroll-listener.js"
30 | }
31 | },
32 | "main": "dist/react-bottom-scroll-listener.js",
33 | "module": "dist/react-bottom-scroll-listener.mjs",
34 | "types": "dist/index.d.ts",
35 | "source": "src/index.tsx",
36 | "packageManager": "yarn@4.9.1",
37 | "engines": {
38 | "node": ">=10"
39 | },
40 | "workspaces": [
41 | "example"
42 | ],
43 | "scripts": {
44 | "build": "vite build && tsc",
45 | "dev": "vite build --watch",
46 | "build:example": "cd example && vite build --base=/react-bottom-scroll-listener/",
47 | "dev:example": "cd example && vite",
48 | "lint": "biome check src",
49 | "test": "vitest --run --coverage",
50 | "prepublish": "run-s build"
51 | },
52 | "dependencies": {
53 | "lodash.debounce": "^4.0.8"
54 | },
55 | "peerDependencies": {
56 | "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
57 | "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
58 | },
59 | "devDependencies": {
60 | "@biomejs/biome": "1.9.4",
61 | "@karl-run/prettier-config": "^1.0.1",
62 | "@testing-library/dom": "^10.4.0",
63 | "@testing-library/react": "^16.3.0",
64 | "@types/lodash.debounce": "^4.0.9",
65 | "@types/node": "^22.15.3",
66 | "@types/react": "^19.1.2",
67 | "@types/react-dom": "^19.1.2",
68 | "@vitejs/plugin-react": "^4.4.1",
69 | "@vitest/coverage-v8": "3.1.2",
70 | "jsdom": "^26.1.0",
71 | "npm-run-all": "^4.1.5",
72 | "prettier": "^3.5.3",
73 | "react": "^19.0.0",
74 | "react-dom": "^19.0.0",
75 | "typescript": "^5.8.3",
76 | "vite": "^6.3.3",
77 | "vitest": "^3.1.2"
78 | },
79 | "prettier": "@karl-run/prettier-config"
80 | }
81 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/component/index.tsx:
--------------------------------------------------------------------------------
1 | import type { JSX, MutableRefObject } from 'react'
2 | import useBottomScrollListener, { type DebounceOptions } from '../hook'
3 |
4 | export interface BottomScrollListenerProps {
5 | /**
6 | * Required callback that will be invoked when scrolled to bottom
7 | */
8 | onBottom: () => void
9 |
10 | /**
11 | * Offset from bottom of page in pixels. E.g. 300 will trigger onBottom 300px from the bottom of the page
12 | */
13 | offset?: number
14 |
15 | /**
16 | * Optional debounce in milliseconds, defaults to 200ms
17 | */
18 | debounce?: number
19 |
20 | /**
21 | * Options passed to lodash.debounce, see https://lodash.com/docs/4.17.15#debounce
22 | */
23 | debounceOptions?: DebounceOptions
24 |
25 | /**
26 | * Triggers the onBottom callback when the page has no scrollbar, defaults to false
27 | */
28 | triggerOnNoScroll?: boolean
29 |
30 | /**
31 | * Optional children to be rendered.
32 | *
33 | * If children passed is a function, that function will be passed a React.RefObject
34 | * that ref shall be passed to a child tag that will be used for the scrolling container.
35 | * */
36 | children?: JSX.Element | ((ref: ((instance: T | null) => void) | MutableRefObject | null) => JSX.Element)
37 | }
38 |
39 | /**
40 | * A simple React component that lets you listen for when you have scrolled to the bottom.
41 | *
42 | * @param {BottomScrollListenerProps} props
43 | */
44 | const BottomScrollListener = ({
45 | children,
46 | onBottom,
47 | offset,
48 | debounce,
49 | debounceOptions,
50 | triggerOnNoScroll,
51 | }: BottomScrollListenerProps): JSX.Element | null => {
52 | const optionalScrollContainerRef = useBottomScrollListener(onBottom, {
53 | offset,
54 | debounce,
55 | debounceOptions,
56 | triggerOnNoScroll,
57 | })
58 |
59 | if (!children) return null
60 | if (typeof children === 'function') return children(optionalScrollContainerRef)
61 | return children
62 | }
63 |
64 | export default BottomScrollListener
65 |
--------------------------------------------------------------------------------
/src/hook/index.test.tsx:
--------------------------------------------------------------------------------
1 | import { afterEach, describe, expect, it, vi } from 'vitest'
2 |
3 | import { cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react'
4 | import type React from 'react'
5 |
6 | import useBottomScrollListener from './'
7 |
8 | /* Mock out scrollHeight so we can change it before dispatching scroll event */
9 | Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
10 | configurable: true,
11 | get: function () {
12 | return this._scrollHeight || 0
13 | },
14 | set(val) {
15 | this._scrollHeight = val
16 | },
17 | })
18 |
19 | /* Mock out clientHeight so we can change it before dispatching scroll event in custom containers */
20 | Object.defineProperty(HTMLElement.prototype, 'clientHeight', {
21 | configurable: true,
22 | get: function () {
23 | return this._clientHeight || 0
24 | },
25 | set(val) {
26 | this._clientHeight = val
27 | },
28 | })
29 |
30 | describe('useBottomScrollListener', () => {
31 | describe('given no ref it should use the document and', () => {
32 | it('shall not invoke onBottom when body has not hit bottom', async () => {
33 | const onBottom = vi.fn()
34 |
35 | renderHook(() => useBottomScrollListener(onBottom, { offset: 0, debounce: 0 }))
36 |
37 | // window size is 768.
38 | // 768 + 400 = 1168, should not scroll
39 | // @ts-expect-error scrollHeight is a mock property
40 | document.documentElement.scrollHeight = 1200
41 | document.documentElement.scrollTop = 400
42 |
43 | window.dispatchEvent(new Event('scroll'))
44 |
45 | expect(onBottom).not.toHaveBeenCalled()
46 | })
47 |
48 | it('shall invoke onBottom when body is exactly at bottom', async () => {
49 | const onBottom = vi.fn()
50 |
51 | renderHook(() => useBottomScrollListener(onBottom, { offset: 0, debounce: 0 }))
52 |
53 | // window size is 768.
54 | // 768 + 432 = 1200, should scroll
55 | // @ts-expect-error scrollHeight is a mock property
56 | document.documentElement.scrollHeight = 1200
57 | document.documentElement.scrollTop = 432
58 |
59 | window.dispatchEvent(new Event('scroll'))
60 |
61 | expect(onBottom).toHaveBeenCalledTimes(1)
62 | })
63 |
64 | it('shall invoke onBottom when there is no place to scroll with triggerOnNoScroll true', () => {
65 | const onBottom = vi.fn()
66 | renderHook(() => useBottomScrollListener(onBottom, { triggerOnNoScroll: true }))
67 | expect(onBottom).toHaveBeenCalledTimes(1)
68 | })
69 | })
70 |
71 | describe('given a ref it should use the given ref and', () => {
72 | const TestComponent = ({ onBottom }: { onBottom: () => void }) => {
73 | const ref = useBottomScrollListener(onBottom, { offset: 0, debounce: 0 })
74 |
75 | return (
76 |