├── .all-contributorsrc ├── .codecov.yml ├── .eslintrc.js ├── .github └── workflows │ ├── publish.yml │ └── push.yml ├── .gitignore ├── .npmignore ├── .prettierrc.toml ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── README.md ├── docs └── README-3.0.2.md ├── jest.config.js ├── license ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ └── index.test.tsx ├── index.tsx ├── stories │ ├── InfiniteScrollWithHeight.tsx │ ├── PullDownToRefreshInfScroll.tsx │ ├── ScrollableTargetInfScroll.tsx │ ├── ScrolleableTop.tsx │ ├── WindowInfiniteScrollComponent.tsx │ └── stories.tsx └── utils │ └── threshold.ts ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "contributorsPerLine": 5, 7 | "skipCi": true, 8 | "contributors": [ 9 | { 10 | "login": "ankeetmaini", 11 | "name": "Ankeet Maini", 12 | "avatar_url": "https://avatars.githubusercontent.com/u/6652823?v=4", 13 | "profile": "https://ankeetmaini.dev/", 14 | "contributions": [ 15 | "question", 16 | "doc", 17 | "code", 18 | "review", 19 | "maintenance" 20 | ] 21 | }, 22 | { 23 | "login": "iamdarshshah", 24 | "name": "Darsh Shah", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/25670841?v=4", 26 | "profile": "https://github.com/iamdarshshah", 27 | "contributions": [ 28 | "infra" 29 | ] 30 | } 31 | ], 32 | "projectName": "react-infinite-scroll-component", 33 | "projectOwner": "ankeetmaini", 34 | "repoType": "github", 35 | "repoHost": "https://github.com" 36 | } 37 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: '10...100' 9 | 10 | status: 11 | project: 12 | default: 13 | threshold: 1 14 | patch: 15 | default: 16 | threshold: 1 17 | changes: no 18 | 19 | parsers: 20 | gcov: 21 | branch_detection: 22 | conditional: yes 23 | loop: yes 24 | method: no 25 | macro: no 26 | 27 | comment: 28 | layout: 'header, diff' 29 | behavior: default 30 | require_changes: no 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | project: path.resolve(__dirname, './tsconfig.json'), 6 | tsconfigRootDir: __dirname, 7 | }, 8 | env: { 9 | browser: true, 10 | node: true, 11 | }, 12 | plugins: ['@typescript-eslint'], 13 | extends: [ 14 | 'eslint:recommended', 15 | 'plugin:@typescript-eslint/eslint-recommended', 16 | 'plugin:react/recommended', 17 | 'plugin:@typescript-eslint/recommended', 18 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 19 | 'prettier', 20 | 'prettier/@typescript-eslint', 21 | 'prettier/react', 22 | ], 23 | rules: { 24 | '@typescript-eslint/prefer-regexp-exec': 1, 25 | '@typescript-eslint/ban-ts-ignore': 0, 26 | '@typescript-eslint/unbound-method': 1, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish on release 2 | 3 | on: [release] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | 12 | - name: install dependencies 13 | run: yarn 14 | 15 | - name: npm publish 16 | run: | 17 | npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN 18 | npm publish || true 19 | env: 20 | CI: true 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: react-infinite-scroll-component 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | 12 | - name: install dependencies 13 | run: yarn 14 | 15 | - name: lint 16 | run: yarn lint 17 | 18 | - name: prettier 19 | run: yarn prettier:check 20 | 21 | - name: unit tests 22 | run: yarn test 23 | 24 | - name: ts type checks 25 | run: yarn ts-check 26 | 27 | - uses: codecov/codecov-action@v1.0.3 28 | with: 29 | token: ${{secrets.CODECOV_TOKEN}} 30 | file: ./coverage/lcov.info 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .DS_Store 3 | .idea 4 | node_modules 5 | /lib 6 | .rts2_* 7 | dist 8 | .vscode 9 | storybook-static/ 10 | coverage/ 11 | yarn-error.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.gitignore 2 | /.npmignore 3 | /demos/ 4 | /server.js 5 | /webpack.* 6 | npm-debug.log 7 | __tests__ 8 | stories/ 9 | .rts2* 10 | .storybook 11 | coverage/ 12 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | trailingComma = "es5" 2 | tabWidth = 2 3 | semi = true 4 | singleQuote = true -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | 3 | configure(require.context("../src", true, /\.?stories\.tsx$/), module); 4 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | use: [ 5 | { 6 | loader: require.resolve("awesome-typescript-loader") 7 | }, 8 | // Optional 9 | { 10 | loader: require.resolve("react-docgen-typescript-loader") 11 | } 12 | ] 13 | }); 14 | config.resolve.extensions.push(".ts", ".tsx"); 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-infinite-scroll-component [![npm](https://img.shields.io/npm/dt/react-infinite-scroll-component.svg?style=flat-square)](https://www.npmjs.com/package/react-infinite-scroll-component) [![npm](https://img.shields.io/npm/v/react-infinite-scroll-component.svg?style=flat-square)](https://www.npmjs.com/package/react-infinite-scroll-component) 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | A component to make all your infinite scrolling woes go away with just 4.15 kB! `Pull Down to Refresh` feature 7 | added. An infinite-scroll that actually works and super-simple to integrate! 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm install --save react-infinite-scroll-component 13 | 14 | or 15 | 16 | yarn add react-infinite-scroll-component 17 | 18 | // in code ES6 19 | import InfiniteScroll from 'react-infinite-scroll-component'; 20 | // or commonjs 21 | var InfiniteScroll = require('react-infinite-scroll-component'); 22 | ``` 23 | 24 | ## Using 25 | 26 | ```jsx 27 | Loading...} 32 | endMessage={ 33 |

34 | Yay! You have seen it all 35 |

36 | } 37 | // below props only if you need pull down functionality 38 | refreshFunction={this.refresh} 39 | pullDownToRefresh 40 | pullDownToRefreshThreshold={50} 41 | pullDownToRefreshContent={ 42 |

↓ Pull down to refresh

43 | } 44 | releaseToRefreshContent={ 45 |

↑ Release to refresh

46 | } 47 | > 48 | {items} 49 |
50 | ``` 51 | 52 | ## Using scroll on top 53 | 54 | ```jsx 55 |
64 | {/*Put the scroll bar always on the bottom*/} 65 | Loading...} 72 | scrollableTarget="scrollableDiv" 73 | > 74 | {this.state.items.map((_, index) => ( 75 |
76 | div - #{index} 77 |
78 | ))} 79 |
80 |
81 | ``` 82 | 83 | The `InfiniteScroll` component can be used in three ways. 84 | 85 | - Specify a value for the `height` prop if you want your **scrollable** content to have a specific height, providing scrollbars for scrolling your content and fetching more data. 86 | - If your **scrollable** content is being rendered within a parent element that is already providing overflow scrollbars, you can set the `scrollableTarget` prop to reference the DOM element and use it's scrollbars for fetching more data. 87 | - Without setting either the `height` or `scrollableTarget` props, the scroll will happen at `document.body` like _Facebook's_ timeline scroll. 88 | 89 | ## docs version wise 90 | 91 | [3.0.2](docs/README-3.0.2.md) 92 | 93 | ## live examples 94 | 95 | - infinite scroll (never ending) example using react (body/window scroll) 96 | - [![Edit yk7637p62z](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/yk7637p62z) 97 | - infinte scroll till 500 elements (body/window scroll) 98 | - [![Edit 439v8rmqm0](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/439v8rmqm0) 99 | - infinite scroll in an element (div of height 400px) 100 | - [![Edit w3w89k7x8](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/w3w89k7x8) 101 | - infinite scroll with `scrollableTarget` (a parent element which is scrollable) 102 | - [![Edit r7rp40n0zm](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/r7rp40n0zm) 103 | 104 | ## props 105 | 106 | | name | type | description | 107 | | ------------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 108 | | **next** | function | a function which must be called after reaching the bottom. It must trigger some sort of action which fetches the next data. **The data is passed as `children` to the `InfiniteScroll` component and the data should contain previous items too.** e.g. _Initial data = [1, 2, 3]_ and then next load of data should be _[1, 2, 3, 4, 5, 6]_. | 109 | | **hasMore** | boolean | it tells the `InfiniteScroll` component on whether to call `next` function on reaching the bottom and shows an `endMessage` to the user | 110 | | **children** | node (list) | the data items which you need to scroll. | 111 | | **dataLength** | number | set the length of the data.This will unlock the subsequent calls to next. | 112 | | **loader** | node | you can send a loader component to show while the component waits for the next load of data. e.g. `

Loading...

` or any fancy loader element | 113 | | **scrollThreshold** | number | string | A threshold value defining when `InfiniteScroll` will call `next`. Default value is `0.8`. It means the `next` will be called when user comes below 80% of the total height. If you pass threshold in pixels (`scrollThreshold="200px"`), `next` will be called once you scroll at least (100% - scrollThreshold) pixels down. | 114 | | **onScroll** | function | a function that will listen to the scroll event on the scrolling container. Note that the scroll event is throttled, so you may not receive as many events as you would expect. | 115 | | **endMessage** | node | this message is shown to the user when he has seen all the records which means he's at the bottom and `hasMore` is `false` | 116 | | **className** | string | add any custom class you want | 117 | | **style** | object | any style which you want to override | 118 | | **height** | number | optional, give only if you want to have a fixed height scrolling content | 119 | | **scrollableTarget** | node or string | optional, reference to a (parent) DOM element that is already providing overflow scrollbars to the `InfiniteScroll` component. _You should provide the `id` of the DOM node preferably._ | 120 | | **hasChildren** | bool | `children` is by default assumed to be of type array and it's length is used to determine if loader needs to be shown or not, if your `children` is not an array, specify this prop to tell if your items are 0 or more. | 121 | | **pullDownToRefresh** | bool | to enable **Pull Down to Refresh** feature | 122 | | **pullDownToRefreshContent** | node | any JSX that you want to show the user, `default={

Pull down to refresh

}` | 123 | | **releaseToRefreshContent** | node | any JSX that you want to show the user, `default={

Release to refresh

}` | 124 | | **pullDownToRefreshThreshold** | number | minimum distance the user needs to pull down to trigger the refresh, `default=100px` , a lower value may be needed to trigger the refresh depending your users browser. | 125 | | **refreshFunction** | function | this function will be called, it should return the fresh data that you want to show the user | 126 | | **initialScrollY** | number | set a scroll y position for the component to render with. | 127 | | **inverse** | bool | set infinite scroll on top | 128 | 129 | ## Contributors ✨ 130 | 131 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |

Ankeet Maini

💬 📖 💻 👀 🚧

Darsh Shah

🚇
142 | 143 | 144 | 145 | 146 | 147 | 148 | This project follows the [all-contributors](https://allcontributors.org) specification. Contributions of any kind are welcome! 149 | 150 | ## LICENSE 151 | 152 | [MIT](LICENSE) 153 | -------------------------------------------------------------------------------- /docs/README-3.0.2.md: -------------------------------------------------------------------------------- 1 | # react-infinite-scroll-component [![npm](https://img.shields.io/npm/dt/react-infinite-scroll-component.svg?style=flat-square)](https://www.npmjs.com/package/react-infinite-scroll-component) [![npm](https://img.shields.io/npm/v/react-infinite-scroll-component.svg?style=flat-square)](https://www.npmjs.com/package/react-infinite-scroll-component) 2 | 3 | A component to make all your infinite scrolling woes go away with just 4.15 kB! `Pull Down to Refresh` feature 4 | added. An infinite-scroll that actually works and super-simple to integrate! 5 | 6 | # install 7 | ```bash 8 | npm install --save react-infinite-scroll-component 9 | 10 | // in code ES6 11 | import InfiniteScroll from 'react-infinite-scroll-component'; 12 | // or commonjs 13 | var InfiniteScroll = require('react-infinite-scroll-component'); 14 | ``` 15 | 16 | # demos 17 | - [See the demo in action at http://ankeetmaini.github.io/react-infinite-scroll-component/](http://ankeetmaini.github.io/react-infinite-scroll-component/). Thanks [@kdenz](https://github.com/kdenz)! 18 | - The code for demos is in the `demos/` directory. You can also clone and open `lib/index.html` in your browser to see the demos in action. 19 | 20 | # using 21 | 22 | ```jsx 23 | ↓ Pull down to refresh 27 | } 28 | releaseToRefreshContent={ 29 |

↑ Release to refresh

30 | } 31 | refreshFunction={this.refresh} 32 | next={fetchData} 33 | hasMore={true} 34 | loader={

Loading...

} 35 | endMessage={ 36 |

37 | Yay! You have seen it all 38 |

39 | }> 40 | {items} 41 |
42 | ``` 43 | 44 | The `InfiniteScroll` component can be used in three ways. 45 | 46 | - Specify a value for the `height` prop if you want your **scrollable** content to have a specific height, providing scrollbars for scrolling your content and fetching more data. 47 | - If your **scrollable** content is being rendered within a parent element that is already providing overflow scrollbars, you can set the `scrollableTarget` prop to reference the DOM element and use it's scrollbars for fetching more data. 48 | - Without setting either the `height` or `scrollableTarget` props, the scroll will happen at `document.body` like *Facebook's* timeline scroll. 49 | 50 | 51 | # props 52 | name | type | description 53 | -----|------|------------ 54 | **next** | function | a function which must be called after reaching the bottom. It must trigger some sort of action which fetches the next data. **The data is passed as `children` to the `InfiniteScroll` component and the data should contain previous items too.** e.g. *Initial data = [1, 2, 3]* and then next load of data should be *[1, 2, 3, 4, 5, 6]*. 55 | **hasMore** | boolean | it tells the `InfiniteScroll` component on whether to call `next` function on reaching the bottom and shows an `endMessage` to the user 56 | **children** | node (list) | the data items which you need to scroll. 57 | **loader** | node | you can send a loader component to show while the component waits for the next load of data. e.g. `

Loading...

` or any fancy loader element 58 | **scrollThreshold** | number | a threshold value after that the `InfiniteScroll` will call `next`. By default it's `0.8`. It means the `next` will be called when the user comes below 80% of the total height. 59 | **onScroll** | function | a function that will listen to the scroll event on the scrolling container. Note that the scroll event is throttled, so you may not receive as many events as you would expect. 60 | **endMessage** | node | this message is shown to the user when he has seen all the records which means he's at the bottom and `hasMore` is `false` 61 | **style** | object | any style which you want to override 62 | **height** | number | optional, give only if you want to have a fixed height scrolling content 63 | **scrollableTarget** | node | optional, reference to a (parent) DOM element that is already providing overflow scrollbars to the `InfiniteScroll` component. 64 | **hasChildren** | bool | `children` is by default assumed to be of type array and it's length is used to determine if loader needs to be shown or not, if your `children` is not an array, specify this prop to tell if your items are 0 or more. 65 | **pullDownToRefresh** | bool | to enable **Pull Down to Refresh** feature 66 | **pullDownToRefreshContent** | node | any JSX that you want to show the user, `default={

Pull down to refresh

}` 67 | **releaseToRefreshContent** | node | any JSX that you want to show the user, `default={

Release to refresh

}` 68 | **pullDownToRefreshThreshold** | number | minimum distance the user needs to pull down to trigger the refresh, `default=100px` 69 | **refreshFunction** | function | this function will be called, it should return the fresh data that you want to show the user 70 | **initialScrollY** | number | set a scroll y position for the component to render with. -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // All imported modules in your tests should be mocked automatically 3 | // automock: false, 4 | 5 | // Stop running tests after the first failure 6 | // bail: false, 7 | 8 | // Respect "browser" field in package.json when resolving modules 9 | // browser: false, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/var/folders/xp/bp966jvd70j25zc1cz6sd905psfs4r/T/jest_ueoec8", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | clearMocks: false, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | collectCoverage: true, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | collectCoverageFrom: [ 22 | '/src/**/*{js,ts,jsx,tsx}', 23 | '!/src/**/*stories', 24 | '!**/node_modules/**', 25 | ], 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: 'coverage', 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | coveragePathIgnorePatterns: ['/node_modules/', 'stories', 'dist'], 32 | 33 | // A list of reporter names that Jest uses when writing coverage reports 34 | coverageReporters: ['lcov', 'text-summary'], 35 | 36 | // An object that configures minimum threshold enforcement for coverage results 37 | // coverageThreshold: null, 38 | 39 | // Make calling deprecated APIs throw helpful error messages 40 | // errorOnDeprecated: false, 41 | 42 | // Force coverage collection from ignored files usin a array of glob patterns 43 | // forceCoverageMatch: [], 44 | 45 | // A path to a module which exports an async function that is triggered once before all test suites 46 | // globalSetup: null, 47 | 48 | // A path to a module which exports an async function that is triggered once after all test suites 49 | // globalTeardown: null, 50 | 51 | // A set of global variables that need to be available in all test environments 52 | // globals: {}, 53 | 54 | // An array of directory names to be searched recursively up from the requiring module's location 55 | // moduleDirectories: [ 56 | // "node_modules" 57 | // ], 58 | 59 | // An array of file extensions your modules use 60 | // moduleFileExtensions: [ 61 | // "js", 62 | // "json", 63 | // "jsx", 64 | // "node" 65 | // ], 66 | 67 | // A map from regular expressions to module names that allow to stub out resources with a single module 68 | // moduleNameMapper: {}, 69 | 70 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 71 | // modulePathIgnorePatterns: [], 72 | 73 | // Activates notifications for test results 74 | // notify: false, 75 | 76 | // An enum that specifies notification mode. Requires { notify: true } 77 | // notifyMode: "always", 78 | 79 | // A preset that is used as a base for Jest's configuration 80 | preset: 'ts-jest', 81 | 82 | // Run tests from one or more projects 83 | // projects: null, 84 | 85 | // Use this configuration option to add custom reporters to Jest 86 | // reporters: undefined, 87 | 88 | // Automatically reset mock state between every test 89 | // resetMocks: false, 90 | 91 | // Reset the module registry before running each individual test 92 | // resetModules: false, 93 | 94 | // A path to a custom resolver 95 | // resolver: null, 96 | 97 | // Automatically restore mock state between every test 98 | // restoreMocks: false, 99 | 100 | // The root directory that Jest should scan for tests and modules within 101 | // rootDir: null, 102 | 103 | // A list of paths to directories that Jest should use to search for files in 104 | roots: ['src'], 105 | 106 | // Allows you to use a custom runner instead of Jest's default test runner 107 | // runner: "jest-runner", 108 | 109 | // The paths to modules that run some code to configure or set up the testing environment before each test 110 | // setupFiles: [], 111 | 112 | // The path to a module that runs some code to configure or set up the testing framework before each test 113 | // setupTestFrameworkScriptFile: null, 114 | 115 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 116 | // snapshotSerializers: [], 117 | 118 | // The test environment that will be used for testing 119 | testEnvironment: 'jsdom', 120 | 121 | // Options that will be passed to the testEnvironment 122 | // testEnvironmentOptions: {}, 123 | 124 | // Adds a location field to test results 125 | // testLocationInResults: false, 126 | 127 | // The glob patterns Jest uses to detect test files 128 | // testMatch: [ 129 | // "**/__tests__/**/*.js?(x)", 130 | // "**/?(*.)+(spec|test).js?(x)" 131 | // ], 132 | 133 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 134 | // testPathIgnorePatterns: [ 135 | // "/node_modules/" 136 | // ], 137 | 138 | // The regexp pattern Jest uses to detect test files 139 | // testRegex: "", 140 | 141 | // This option allows the use of a custom results processor 142 | // testResultsProcessor: null, 143 | 144 | // This option allows use of a custom test runner 145 | // testRunner: "jasmine2", 146 | 147 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 148 | // testURL: "http://localhost", 149 | 150 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 151 | // timers: "real", 152 | 153 | // A map from regular expressions to paths to transformers 154 | // transform: null, 155 | 156 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 157 | // transformIgnorePatterns: [ 158 | // "/node_modules/" 159 | // ], 160 | 161 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 162 | // unmockedModulePathPatterns: undefined, 163 | 164 | // Indicates whether each individual test should be reported during the run 165 | // verbose: null, 166 | 167 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 168 | // watchPathIgnorePatterns: [], 169 | 170 | // Whether to use watchman for file crawling 171 | // watchman: true, 172 | }; 173 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 react-infinite-scroll-component 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-infinite-scroll-component", 3 | "version": "6.1.0", 4 | "description": "An Infinite Scroll component in react.", 5 | "source": "src/index.tsx", 6 | "main": "dist/index.js", 7 | "unpkg": "dist/index.umd.js", 8 | "module": "dist/index.es.js", 9 | "types": "dist/index.d.ts", 10 | "scripts": { 11 | "build": "rimraf dist && rollup -c", 12 | "prepublish": "yarn build", 13 | "storybook": "start-storybook -p 6006", 14 | "build-storybook": "build-storybook", 15 | "codecov": "codecov", 16 | "lint": "eslint 'src/**/*.{ts,tsx,js,jsx}'", 17 | "lint:fix": "yarn lint --fix", 18 | "prettier:check": "prettier --check 'src/**/*'", 19 | "prettify": "prettier --write 'src/**/*'", 20 | "ts-check": "tsc -p tsconfig.json --noEmit", 21 | "test": "jest" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/ankeetmaini/react-infinite-scroll-component.git" 26 | }, 27 | "keywords": [ 28 | "react", 29 | "infinite-scroll", 30 | "infinite", 31 | "scroll", 32 | "component", 33 | "react-component" 34 | ], 35 | "author": "Ankeet Maini ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/ankeetmaini/react-infinite-scroll-component/issues" 39 | }, 40 | "homepage": "https://github.com/ankeetmaini/react-infinite-scroll-component#readme", 41 | "peerDependencies": { 42 | "react": ">=16.0.0" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.6.2", 46 | "@storybook/addon-actions": "^5.2.1", 47 | "@storybook/addon-info": "^5.2.1", 48 | "@storybook/addon-links": "^5.2.1", 49 | "@storybook/addons": "^5.2.1", 50 | "@storybook/react": "^5.2.1", 51 | "@testing-library/react": "^9.2.0", 52 | "@types/jest": "^24.0.18", 53 | "@types/react": "^16.9.2", 54 | "@types/react-dom": "^16.9.1", 55 | "@types/storybook__react": "^4.0.2", 56 | "@types/throttle-debounce": "^2.1.0", 57 | "@typescript-eslint/eslint-plugin": "^2.3.2", 58 | "@typescript-eslint/parser": "^2.3.2", 59 | "awesome-typescript-loader": "^5.2.1", 60 | "babel-loader": "^8.0.6", 61 | "eslint": "^6.5.1", 62 | "eslint-config-prettier": "^6.3.0", 63 | "eslint-plugin-react": "^7.15.0", 64 | "husky": ">=1", 65 | "jest": "^24.9.0", 66 | "lint-staged": ">=8", 67 | "prettier": "1.18.2", 68 | "react": "^16.10.1", 69 | "react-docgen-typescript-loader": "^3.2.1", 70 | "react-dom": "^16.10.1", 71 | "rimraf": "^3.0.0", 72 | "rollup": "^1.26.3", 73 | "rollup-plugin-node-resolve": "^5.2.0", 74 | "rollup-plugin-typescript2": "^0.25.2", 75 | "ts-jest": "^24.1.0", 76 | "typescript": "^3.7.2" 77 | }, 78 | "dependencies": { 79 | "throttle-debounce": "^2.1.0" 80 | }, 81 | "husky": { 82 | "hooks": { 83 | "pre-commit": "yarn run ts-check && lint-staged" 84 | } 85 | }, 86 | "lint-staged": { 87 | "*.{js,css,json,md}": [ 88 | "prettier --write", 89 | "git add" 90 | ], 91 | "*.js": [ 92 | "prettier --write", 93 | "eslint --fix", 94 | "git add" 95 | ], 96 | "*.{ts,tsx}": [ 97 | "prettier --write", 98 | "eslint --fix", 99 | "git add" 100 | ] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import pkg from './package.json'; 4 | export default { 5 | input: './src/index.tsx', 6 | output: [ 7 | { 8 | file: pkg.main, 9 | format: 'cjs', 10 | sourcemap: true, 11 | }, 12 | { 13 | file: pkg.module, 14 | format: 'es', 15 | sourcemap: true, 16 | }, 17 | { 18 | file: pkg.unpkg, 19 | format: 'iife', 20 | sourcemap: true, 21 | name: 'InfiniteScroll', 22 | }, 23 | ], 24 | external: [...Object.keys(pkg.peerDependencies || {})], 25 | plugins: [resolve(), typescript({ useTsconfigDeclarationDir: true })], 26 | }; 27 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import InfiniteScroll from '../index'; 4 | 5 | describe('React Infinite Scroll Component', () => { 6 | const originalConsoleError = console.error; 7 | 8 | afterEach(() => { 9 | cleanup(); 10 | console.error = originalConsoleError; 11 | }); 12 | 13 | it('renders .infinite-scroll-component', () => { 14 | const { container } = render( 15 | {}} 20 | > 21 |
22 | 23 | ); 24 | expect( 25 | container.querySelectorAll('.infinite-scroll-component').length 26 | ).toBe(1); 27 | }); 28 | 29 | it('renders custom class', () => { 30 | const { container } = render( 31 | {}} 37 | > 38 |
39 | 40 | ); 41 | expect(container.querySelectorAll('.custom-class').length).toBe(1); 42 | }); 43 | 44 | it('renders children when passed in', () => { 45 | const { container } = render( 46 | {}} 51 | > 52 |
53 | 54 | ); 55 | expect(container.querySelectorAll('.child').length).toBe(1); 56 | }); 57 | 58 | it('calls scroll handler if provided, when user scrolls', () => { 59 | jest.useFakeTimers(); 60 | const onScrollMock = jest.fn(); 61 | 62 | const { container } = render( 63 | {}} 70 | > 71 |
72 | 73 | ); 74 | 75 | const scrollEvent = new Event('scroll'); 76 | const node = container.querySelector( 77 | '.infinite-scroll-component' 78 | ) as HTMLElement; 79 | 80 | node.dispatchEvent(scrollEvent); 81 | jest.runOnlyPendingTimers(); 82 | expect(setTimeout).toHaveBeenCalledTimes(1); 83 | expect(onScrollMock).toHaveBeenCalled(); 84 | }); 85 | 86 | describe('When missing the dataLength prop', () => { 87 | it('throws an error', () => { 88 | console.error = jest.fn(); 89 | const props = { loader: 'Loading...', hasMore: false, next: () => {} }; 90 | 91 | // @ts-ignore 92 | expect(() => render()).toThrow(Error); 93 | // @ts-ignore 94 | expect(console.error.mock.calls[0][0]).toContain( 95 | '"dataLength" is missing' 96 | ); 97 | }); 98 | }); 99 | 100 | describe('When user scrolls to the bottom', () => { 101 | it('does not show loader if hasMore is false', () => { 102 | const { container, queryByText } = render( 103 | {}} 109 | > 110 |
111 | 112 | ); 113 | 114 | const scrollEvent = new Event('scroll'); 115 | const node = container.querySelector( 116 | '.infinite-scroll-component' 117 | ) as HTMLElement; 118 | node.dispatchEvent(scrollEvent); 119 | expect(queryByText('Loading...')).toBeFalsy(); 120 | }); 121 | 122 | it('shows loader if hasMore is true', () => { 123 | const { container, getByText } = render( 124 | {}} 130 | height={100} 131 | > 132 |
133 | 134 | ); 135 | 136 | const scrollEvent = new Event('scroll'); 137 | const node = container.querySelector( 138 | '.infinite-scroll-component' 139 | ) as HTMLElement; 140 | node.dispatchEvent(scrollEvent); 141 | expect(getByText('Loading...')).toBeTruthy(); 142 | }); 143 | }); 144 | 145 | it('shows end message', () => { 146 | const { queryByText } = render( 147 | {}} 153 | > 154 |
155 | 156 | ); 157 | expect(queryByText('Reached end.')).toBeTruthy(); 158 | }); 159 | 160 | it('adds a classname to the outer div', () => { 161 | const { container } = render( 162 | {}} 166 | loader={
Loading...
} 167 | > 168 |
169 | 170 | ); 171 | const outerDiv = container.getElementsByClassName( 172 | 'infinite-scroll-component__outerdiv' 173 | ); 174 | expect(outerDiv.length).toBeTruthy(); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode, CSSProperties } from 'react'; 2 | import { throttle } from 'throttle-debounce'; 3 | import { ThresholdUnits, parseThreshold } from './utils/threshold'; 4 | 5 | type Fn = () => any; 6 | export interface Props { 7 | next: Fn; 8 | hasMore: boolean; 9 | children: ReactNode; 10 | loader: ReactNode; 11 | scrollThreshold?: number | string; 12 | endMessage?: ReactNode; 13 | style?: CSSProperties; 14 | height?: number | string; 15 | scrollableTarget?: ReactNode; 16 | hasChildren?: boolean; 17 | inverse?: boolean; 18 | pullDownToRefresh?: boolean; 19 | pullDownToRefreshContent?: ReactNode; 20 | releaseToRefreshContent?: ReactNode; 21 | pullDownToRefreshThreshold?: number; 22 | refreshFunction?: Fn; 23 | onScroll?: (e: MouseEvent) => any; 24 | dataLength: number; 25 | initialScrollY?: number; 26 | className?: string; 27 | } 28 | 29 | interface State { 30 | showLoader: boolean; 31 | pullToRefreshThresholdBreached: boolean; 32 | prevDataLength: number | undefined; 33 | } 34 | 35 | export default class InfiniteScroll extends Component { 36 | constructor(props: Props) { 37 | super(props); 38 | 39 | this.state = { 40 | showLoader: false, 41 | pullToRefreshThresholdBreached: false, 42 | prevDataLength: props.dataLength, 43 | }; 44 | 45 | this.throttledOnScrollListener = throttle(150, this.onScrollListener).bind( 46 | this 47 | ); 48 | this.onStart = this.onStart.bind(this); 49 | this.onMove = this.onMove.bind(this); 50 | this.onEnd = this.onEnd.bind(this); 51 | } 52 | 53 | private throttledOnScrollListener: (e: MouseEvent) => void; 54 | private _scrollableNode: HTMLElement | undefined | null; 55 | private el: HTMLElement | undefined | Window & typeof globalThis; 56 | private _infScroll: HTMLDivElement | undefined; 57 | private lastScrollTop = 0; 58 | private actionTriggered = false; 59 | private _pullDown: HTMLDivElement | undefined; 60 | 61 | // variables to keep track of pull down behaviour 62 | private startY = 0; 63 | private currentY = 0; 64 | private dragging = false; 65 | 66 | // will be populated in componentDidMount 67 | // based on the height of the pull down element 68 | private maxPullDownDistance = 0; 69 | 70 | componentDidMount() { 71 | if (typeof this.props.dataLength === 'undefined') { 72 | throw new Error( 73 | `mandatory prop "dataLength" is missing. The prop is needed` + 74 | ` when loading more content. Check README.md for usage` 75 | ); 76 | } 77 | 78 | this._scrollableNode = this.getScrollableTarget(); 79 | this.el = this.props.height 80 | ? this._infScroll 81 | : this._scrollableNode || window; 82 | 83 | if (this.el) { 84 | this.el.addEventListener('scroll', this 85 | .throttledOnScrollListener as EventListenerOrEventListenerObject); 86 | } 87 | 88 | if ( 89 | typeof this.props.initialScrollY === 'number' && 90 | this.el && 91 | this.el instanceof HTMLElement && 92 | this.el.scrollHeight > this.props.initialScrollY 93 | ) { 94 | this.el.scrollTo(0, this.props.initialScrollY); 95 | } 96 | 97 | if (this.props.pullDownToRefresh && this.el) { 98 | this.el.addEventListener('touchstart', this.onStart); 99 | this.el.addEventListener('touchmove', this.onMove); 100 | this.el.addEventListener('touchend', this.onEnd); 101 | 102 | this.el.addEventListener('mousedown', this.onStart); 103 | this.el.addEventListener('mousemove', this.onMove); 104 | this.el.addEventListener('mouseup', this.onEnd); 105 | 106 | // get BCR of pullDown element to position it above 107 | this.maxPullDownDistance = 108 | (this._pullDown && 109 | this._pullDown.firstChild && 110 | (this._pullDown.firstChild as HTMLDivElement).getBoundingClientRect() 111 | .height) || 112 | 0; 113 | this.forceUpdate(); 114 | 115 | if (typeof this.props.refreshFunction !== 'function') { 116 | throw new Error( 117 | `Mandatory prop "refreshFunction" missing. 118 | Pull Down To Refresh functionality will not work 119 | as expected. Check README.md for usage'` 120 | ); 121 | } 122 | } 123 | } 124 | 125 | componentWillUnmount() { 126 | if (this.el) { 127 | this.el.removeEventListener('scroll', this 128 | .throttledOnScrollListener as EventListenerOrEventListenerObject); 129 | 130 | if (this.props.pullDownToRefresh) { 131 | this.el.removeEventListener('touchstart', this.onStart); 132 | this.el.removeEventListener('touchmove', this.onMove); 133 | this.el.removeEventListener('touchend', this.onEnd); 134 | 135 | this.el.removeEventListener('mousedown', this.onStart); 136 | this.el.removeEventListener('mousemove', this.onMove); 137 | this.el.removeEventListener('mouseup', this.onEnd); 138 | } 139 | } 140 | } 141 | 142 | componentDidUpdate(prevProps: Props) { 143 | // do nothing when dataLength is unchanged 144 | if (this.props.dataLength === prevProps.dataLength) return; 145 | 146 | this.actionTriggered = false; 147 | 148 | // update state when new data was sent in 149 | this.setState({ 150 | showLoader: false, 151 | }); 152 | } 153 | 154 | static getDerivedStateFromProps(nextProps: Props, prevState: State) { 155 | const dataLengthChanged = nextProps.dataLength !== prevState.prevDataLength; 156 | 157 | // reset when data changes 158 | if (dataLengthChanged) { 159 | return { 160 | ...prevState, 161 | prevDataLength: nextProps.dataLength, 162 | }; 163 | } 164 | return null; 165 | } 166 | 167 | getScrollableTarget = () => { 168 | if (this.props.scrollableTarget instanceof HTMLElement) 169 | return this.props.scrollableTarget; 170 | if (typeof this.props.scrollableTarget === 'string') { 171 | return document.getElementById(this.props.scrollableTarget); 172 | } 173 | if (this.props.scrollableTarget === null) { 174 | console.warn(`You are trying to pass scrollableTarget but it is null. This might 175 | happen because the element may not have been added to DOM yet. 176 | See https://github.com/ankeetmaini/react-infinite-scroll-component/issues/59 for more info. 177 | `); 178 | } 179 | return null; 180 | }; 181 | 182 | onStart: EventListener = (evt: Event) => { 183 | if (this.lastScrollTop) return; 184 | 185 | this.dragging = true; 186 | 187 | if (evt instanceof MouseEvent) { 188 | this.startY = evt.pageY; 189 | } else if (evt instanceof TouchEvent) { 190 | this.startY = evt.touches[0].pageY; 191 | } 192 | this.currentY = this.startY; 193 | 194 | if (this._infScroll) { 195 | this._infScroll.style.willChange = 'transform'; 196 | this._infScroll.style.transition = `transform 0.2s cubic-bezier(0,0,0.31,1)`; 197 | } 198 | }; 199 | 200 | onMove: EventListener = (evt: Event) => { 201 | if (!this.dragging) return; 202 | 203 | if (evt instanceof MouseEvent) { 204 | this.currentY = evt.pageY; 205 | } else if (evt instanceof TouchEvent) { 206 | this.currentY = evt.touches[0].pageY; 207 | } 208 | 209 | // user is scrolling down to up 210 | if (this.currentY < this.startY) return; 211 | 212 | if ( 213 | this.currentY - this.startY >= 214 | Number(this.props.pullDownToRefreshThreshold) 215 | ) { 216 | this.setState({ 217 | pullToRefreshThresholdBreached: true, 218 | }); 219 | } 220 | 221 | // so you can drag upto 1.5 times of the maxPullDownDistance 222 | if (this.currentY - this.startY > this.maxPullDownDistance * 1.5) return; 223 | 224 | if (this._infScroll) { 225 | this._infScroll.style.overflow = 'visible'; 226 | this._infScroll.style.transform = `translate3d(0px, ${this.currentY - 227 | this.startY}px, 0px)`; 228 | } 229 | }; 230 | 231 | onEnd: EventListener = () => { 232 | this.startY = 0; 233 | this.currentY = 0; 234 | 235 | this.dragging = false; 236 | 237 | if (this.state.pullToRefreshThresholdBreached) { 238 | this.props.refreshFunction && this.props.refreshFunction(); 239 | this.setState({ 240 | pullToRefreshThresholdBreached: false, 241 | }); 242 | } 243 | 244 | requestAnimationFrame(() => { 245 | // this._infScroll 246 | if (this._infScroll) { 247 | this._infScroll.style.overflow = 'auto'; 248 | this._infScroll.style.transform = 'none'; 249 | this._infScroll.style.willChange = 'unset'; 250 | } 251 | }); 252 | }; 253 | 254 | isElementAtTop(target: HTMLElement, scrollThreshold: string | number = 0.8) { 255 | const clientHeight = 256 | target === document.body || target === document.documentElement 257 | ? window.screen.availHeight 258 | : target.clientHeight; 259 | 260 | const threshold = parseThreshold(scrollThreshold); 261 | 262 | if (threshold.unit === ThresholdUnits.Pixel) { 263 | return ( 264 | target.scrollTop <= 265 | threshold.value + clientHeight - target.scrollHeight + 1 266 | ); 267 | } 268 | 269 | return ( 270 | target.scrollTop <= 271 | threshold.value / 100 + clientHeight - target.scrollHeight + 1 272 | ); 273 | } 274 | 275 | isElementAtBottom( 276 | target: HTMLElement, 277 | scrollThreshold: string | number = 0.8 278 | ) { 279 | const clientHeight = 280 | target === document.body || target === document.documentElement 281 | ? window.screen.availHeight 282 | : target.clientHeight; 283 | 284 | const threshold = parseThreshold(scrollThreshold); 285 | 286 | if (threshold.unit === ThresholdUnits.Pixel) { 287 | return ( 288 | target.scrollTop + clientHeight >= target.scrollHeight - threshold.value 289 | ); 290 | } 291 | 292 | return ( 293 | target.scrollTop + clientHeight >= 294 | (threshold.value / 100) * target.scrollHeight 295 | ); 296 | } 297 | 298 | onScrollListener = (event: MouseEvent) => { 299 | if (typeof this.props.onScroll === 'function') { 300 | // Execute this callback in next tick so that it does not affect the 301 | // functionality of the library. 302 | setTimeout(() => this.props.onScroll && this.props.onScroll(event), 0); 303 | } 304 | 305 | const target = 306 | this.props.height || this._scrollableNode 307 | ? (event.target as HTMLElement) 308 | : document.documentElement.scrollTop 309 | ? document.documentElement 310 | : document.body; 311 | 312 | // return immediately if the action has already been triggered, 313 | // prevents multiple triggers. 314 | if (this.actionTriggered) return; 315 | 316 | const atBottom = this.props.inverse 317 | ? this.isElementAtTop(target, this.props.scrollThreshold) 318 | : this.isElementAtBottom(target, this.props.scrollThreshold); 319 | 320 | // call the `next` function in the props to trigger the next data fetch 321 | if (atBottom && this.props.hasMore) { 322 | this.actionTriggered = true; 323 | this.setState({ showLoader: true }); 324 | this.props.next && this.props.next(); 325 | } 326 | 327 | this.lastScrollTop = target.scrollTop; 328 | }; 329 | 330 | render() { 331 | const style = { 332 | height: this.props.height || 'auto', 333 | overflow: 'auto', 334 | WebkitOverflowScrolling: 'touch', 335 | ...this.props.style, 336 | } as CSSProperties; 337 | const hasChildren = 338 | this.props.hasChildren || 339 | !!( 340 | this.props.children && 341 | this.props.children instanceof Array && 342 | this.props.children.length 343 | ); 344 | 345 | // because heighted infiniteScroll visualy breaks 346 | // on drag down as overflow becomes visible 347 | const outerDivStyle = 348 | this.props.pullDownToRefresh && this.props.height 349 | ? { overflow: 'auto' } 350 | : {}; 351 | return ( 352 |
356 |
(this._infScroll = infScroll)} 359 | style={style} 360 | > 361 | {this.props.pullDownToRefresh && ( 362 |
(this._pullDown = pullDown)} 365 | > 366 |
374 | {this.state.pullToRefreshThresholdBreached 375 | ? this.props.releaseToRefreshContent 376 | : this.props.pullDownToRefreshContent} 377 |
378 |
379 | )} 380 | {this.props.children} 381 | {!this.state.showLoader && 382 | !hasChildren && 383 | this.props.hasMore && 384 | this.props.loader} 385 | {this.state.showLoader && this.props.hasMore && this.props.loader} 386 | {!this.props.hasMore && this.props.endMessage} 387 |
388 |
389 | ); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/stories/InfiniteScrollWithHeight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import InfiniteScroll from '../index'; 4 | 5 | const style = { 6 | height: 30, 7 | border: '1px solid green', 8 | margin: 6, 9 | padding: 8, 10 | }; 11 | 12 | export default class App extends React.Component { 13 | state = { 14 | items: Array.from({ length: 20 }), 15 | hasMore: true, 16 | }; 17 | 18 | fetchMoreData = () => { 19 | if (this.state.items.length >= 500) { 20 | this.setState({ hasMore: false }); 21 | return; 22 | } 23 | // a fake async api call like which sends 24 | // 20 more records in .5 secs 25 | setTimeout(() => { 26 | this.setState({ 27 | items: this.state.items.concat(Array.from({ length: 20 })), 28 | }); 29 | }, 500); 30 | }; 31 | 32 | render() { 33 | return ( 34 |
35 |

demo: Infinite Scroll with fixed height

36 |
37 | Loading...} 42 | height={400} 43 | endMessage={ 44 |

45 | Yay! You have seen it all 46 |

47 | } 48 | > 49 | {this.state.items.map((_, index) => ( 50 |
51 | div - #{index} 52 |
53 | ))} 54 |
55 |
56 | ); 57 | } 58 | } 59 | 60 | render(, document.getElementById('root')); 61 | -------------------------------------------------------------------------------- /src/stories/PullDownToRefreshInfScroll.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import InfiniteScroll from '../index'; 4 | 5 | const style = { 6 | height: 30, 7 | border: '1px solid green', 8 | margin: 6, 9 | padding: 8, 10 | }; 11 | 12 | export default class App extends React.Component { 13 | state = { 14 | items: Array.from({ length: 20 }), 15 | }; 16 | 17 | fetchMoreData = () => { 18 | // a fake async api call like which sends 19 | // 20 more records in 1.5 secs 20 | setTimeout(() => { 21 | this.setState({ 22 | items: this.state.items.concat(Array.from({ length: 20 })), 23 | }); 24 | }, 1500); 25 | }; 26 | 27 | render() { 28 | return ( 29 |
30 |

demo: Pull down to refresh

31 |
32 | Loading...} 37 | pullDownToRefresh 38 | pullDownToRefreshContent={ 39 |

40 | ↓ Pull down to refresh 41 |

42 | } 43 | releaseToRefreshContent={ 44 |

↑ Release to refresh

45 | } 46 | refreshFunction={this.fetchMoreData} 47 | > 48 | {this.state.items.map((_, index) => ( 49 |
50 | div - #{index} 51 |
52 | ))} 53 |
54 |
55 | ); 56 | } 57 | } 58 | 59 | render(, document.getElementById('root')); 60 | -------------------------------------------------------------------------------- /src/stories/ScrollableTargetInfScroll.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import InfiniteScroll from '../index'; 4 | 5 | const style = { 6 | height: 30, 7 | border: '1px solid green', 8 | margin: 6, 9 | padding: 8, 10 | }; 11 | 12 | export default class App extends React.Component { 13 | state = { 14 | items: Array.from({ length: 20 }), 15 | }; 16 | 17 | fetchMoreData = () => { 18 | // a fake async api call like which sends 19 | // 20 more records in 1.5 secs 20 | setTimeout(() => { 21 | this.setState({ 22 | items: this.state.items.concat(Array.from({ length: 20 })), 23 | }); 24 | }, 1500); 25 | }; 26 | 27 | render() { 28 | return ( 29 |
30 |

demo: Infinite Scroll with scrollable target

31 |
32 |
33 | Loading...} 38 | scrollableTarget="scrollableDiv" 39 | > 40 | {this.state.items.map((_, index) => ( 41 |
42 | div - #{index} 43 |
44 | ))} 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | render(, document.getElementById('root')); 53 | -------------------------------------------------------------------------------- /src/stories/ScrolleableTop.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import InfiniteScroll from '../index'; 4 | 5 | const style = { 6 | height: 30, 7 | border: '1px solid green', 8 | margin: 6, 9 | padding: 8, 10 | }; 11 | 12 | export default class App extends React.Component { 13 | state = { 14 | items: Array.from({ length: 20 }), 15 | }; 16 | 17 | fetchMoreData = () => { 18 | // a fake async api call like which sends 19 | // 20 more records in 1.5 secs 20 | setTimeout(() => { 21 | this.setState({ 22 | items: this.state.items.concat(Array.from({ length: 20 })), 23 | }); 24 | }, 1500); 25 | }; 26 | 27 | render() { 28 | return ( 29 |
30 |

demo: Infinite Scroll on top

31 |
32 |
41 | Loading...} 48 | scrollableTarget="scrollableDiv" 49 | > 50 | {this.state.items.map((_, index) => ( 51 |
52 | div - #{index} 53 |
54 | ))} 55 |
56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | render(, document.getElementById('root')); 63 | -------------------------------------------------------------------------------- /src/stories/WindowInfiniteScrollComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InfiniteScroll from '../index'; 3 | type State = { 4 | data: number[]; 5 | }; 6 | export default class WindowInfiniteScrollComponent extends React.Component< 7 | {}, 8 | State 9 | > { 10 | state = { 11 | data: new Array(100).fill(1), 12 | }; 13 | 14 | next = () => { 15 | setTimeout(() => { 16 | const newData = [...this.state.data, new Array(100).fill(1)]; 17 | this.setState({ data: newData }); 18 | }, 2000); 19 | }; 20 | render() { 21 | return ( 22 | <> 23 | Loading...} 27 | dataLength={this.state.data.length} 28 | > 29 | {this.state.data.map((_, i) => ( 30 |
34 | #{i + 1} row 35 |
36 | ))} 37 |
38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/stories/stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import WindowInf from './WindowInfiniteScrollComponent'; 5 | import PullDownToRefreshInfScroll from './PullDownToRefreshInfScroll'; 6 | import InfiniteScrollWithHeight from './InfiniteScrollWithHeight'; 7 | import ScrollableTargetInfiniteScroll from './ScrollableTargetInfScroll'; 8 | import ScrolleableTop from './ScrolleableTop'; 9 | 10 | const stories = storiesOf('Components', module); 11 | 12 | stories.add('InfiniteScroll', () => , { 13 | info: { inline: true }, 14 | }); 15 | 16 | stories.add('PullDownToRefresh', () => , { 17 | info: { inline: true }, 18 | }); 19 | 20 | stories.add('InfiniteScrollWithHeight', () => , { 21 | info: { inline: true }, 22 | }); 23 | 24 | stories.add( 25 | 'ScrollableTargetInfiniteScroll', 26 | () => , 27 | { 28 | info: { inline: true }, 29 | } 30 | ); 31 | 32 | stories.add('InfiniteScrollTop', () => , { 33 | info: { inline: true }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/utils/threshold.ts: -------------------------------------------------------------------------------- 1 | export const ThresholdUnits = { 2 | Pixel: 'Pixel', 3 | Percent: 'Percent', 4 | }; 5 | 6 | const defaultThreshold = { 7 | unit: ThresholdUnits.Percent, 8 | value: 0.8, 9 | }; 10 | 11 | export function parseThreshold(scrollThreshold: string | number) { 12 | if (typeof scrollThreshold === 'number') { 13 | return { 14 | unit: ThresholdUnits.Percent, 15 | value: scrollThreshold * 100, 16 | }; 17 | } 18 | 19 | if (typeof scrollThreshold === 'string') { 20 | if (scrollThreshold.match(/^(\d*(\.\d+)?)px$/)) { 21 | return { 22 | unit: ThresholdUnits.Pixel, 23 | value: parseFloat(scrollThreshold), 24 | }; 25 | } 26 | 27 | if (scrollThreshold.match(/^(\d*(\.\d+)?)%$/)) { 28 | return { 29 | unit: ThresholdUnits.Percent, 30 | value: parseFloat(scrollThreshold), 31 | }; 32 | } 33 | 34 | console.warn( 35 | 'scrollThreshold format is invalid. Valid formats: "120px", "50%"...' 36 | ); 37 | 38 | return defaultThreshold; 39 | } 40 | 41 | console.warn('scrollThreshold should be string or number'); 42 | 43 | return defaultThreshold; 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "ES2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 10 | "declaration": true /* Generates corresponding '.d.ts' file. */, 11 | "declarationDir": "./dist", 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true /* Enable all strict type-checking options. */, 26 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 27 | "strictNullChecks": true /* Enable strict null checks. */, 28 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 29 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 30 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 31 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 32 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 33 | 34 | /* Additional Checks */ 35 | "noUnusedLocals": true /* Report errors on unused locals. */, 36 | "noUnusedParameters": true /* Report errors on unused parameters. */, 37 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 38 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 39 | 40 | /* Module Resolution Options */ 41 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | // "types": [], /* Type declaration files to be included in compilation. */ 47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 50 | 51 | /* Source Map Options */ 52 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 60 | }, 61 | "include": [ 62 | "src/**/*", 63 | "lint-staged.config.js", 64 | "jest.config.js", 65 | "rollup.config.js" 66 | ] 67 | } 68 | --------------------------------------------------------------------------------