├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── dist └── InfiniteScroll.js ├── docs ├── .babelrc ├── .gitignore ├── demo │ └── index.html ├── gulpfile.js ├── index.html ├── js │ └── script.js ├── package.json ├── src │ └── index.js ├── stylesheets │ ├── github-light.css │ ├── normalize.css │ └── stylesheet.css └── yarn.lock ├── index.js ├── package-lock.json ├── package.json ├── src └── InfiniteScroll.js └── test ├── infiniteScroll_test.js └── test_helper.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"], 3 | "plugins": [ 4 | ["transform-object-rest-spread", { "useBuiltIns": true }], 5 | ["add-module-exports"] 6 | ], 7 | "env": { 8 | "test": { 9 | "plugins": [ 10 | "istanbul" 11 | ] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig - http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [**.html] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [**.js] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const prettierOptions = JSON.parse(fs.readFileSync('./.prettierrc', 'utf8')); 4 | 5 | module.exports = { 6 | extends: ['last', 'prettier', 'prettier/react', 'plugin:react/recommended'], 7 | plugins: ['react', 'prettier'], 8 | env: { 9 | browser: true, 10 | }, 11 | globals: { 12 | describe: true, 13 | it: true, 14 | module: true, 15 | exports: true, 16 | require: true 17 | }, 18 | rules: { 19 | 'prettier/prettier': ['error', prettierOptions], 20 | 'no-unused-vars': [ 21 | 'off', 22 | { 23 | vars: 'all', 24 | args: 'after-used', 25 | ignoreRestSiblings: false 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Submit a bug you found using the package 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Please clone your layout and use of react-infinite-scroller by forking [this Code Sandbox](https://codesandbox.io/s/my6vo3yo78) and linking it here. Doing so will massively expedite getting the bug fixed! 👊 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Device (please complete the following information):** 20 | - OS: [e.g. Mac] 21 | - Browser [e.g. chrome, safari] 22 | - Version [e.g. 22] 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Cache node modules 13 | id: cache 14 | uses: actions/cache@v2 15 | with: 16 | path: node_modules 17 | key: cache-node-modules-${{ hashFiles('package-lock.json') }} 18 | - name: npm install 19 | if: steps.cache.outputs.cache-hit != 'true' 20 | run: npm ci 21 | - run: npm run lint 22 | - run: npm run test 23 | - run: npm run build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | .idea 5 | .nyc_output 6 | coverage 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | test 3 | .nyc_output 4 | docs 5 | .eslintrc.js 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dan Bovey 4 | Copyright (c) 2013 guillaumervls 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Infinite Scroller 2 | 3 | [![npm](https://img.shields.io/npm/dt/react-infinite-scroller.svg?style=flat-square)](https://www.npmjs.com/package/react-infinite-scroller) 4 | [![npm](https://img.shields.io/npm/v/react-infinite-scroller.svg?style=flat-square)](https://www.npmjs.com/package/react-infinite-scroller) 5 | [![npm](https://img.shields.io/npm/l/react-infinite-scroller.svg?style=flat-square)](https://github.com/danbovey/react-infinite-scroller/blob/master/LICENSE) 6 | 7 | Infinitely load a grid or list of items in React. This component allows you to create a simple, lightweight infinite scrolling page or element by supporting both window and scrollable elements. 8 | 9 | - ⏬ Ability to use window or a scrollable element 10 | - 📏 No need to specify item heights 11 | - 💬 Support for "chat history" (reverse) mode 12 | - ✅ Fully unit tested and used in hundreds of production sites around the 13 | world! 14 | - 📦 Lightweight alternative to other available React scroll libs ~ 2.2KB 15 | minified & gzipped 16 | 17 | --- 18 | 19 | - [Demo](https://danbovey.uk/react-infinite-scroller/demo/) 20 | - [Demo Source](https://github.com/danbovey/react-infinite-scroller/blob/master/docs/src/index.js) 21 | 22 | ## Installation 23 | 24 | ``` 25 | npm install react-infinite-scroller --save 26 | ``` 27 | ``` 28 | yarn add react-infinite-scroller 29 | ``` 30 | 31 | ## How to use 32 | 33 | ```js 34 | import InfiniteScroll from 'react-infinite-scroller'; 35 | ``` 36 | 37 | ### Window scroll events 38 | 39 | ```js 40 | Loading ...} 45 | > 46 | {items} // <-- This is the content you want to load 47 | 48 | ``` 49 | 50 | ### DOM scroll events 51 | 52 | ```js 53 |
54 | Loading ...
} 59 | useWindow={false} 60 | > 61 | {items} 62 | 63 | 64 | ``` 65 | 66 | ### Custom parent element 67 | 68 | You can define a custom `parentNode` element to base the scroll calulations on. 69 | 70 | ```js 71 |
this.scrollParentRef = ref}> 72 |
73 | Loading ...
} 78 | useWindow={false} 79 | getScrollParent={() => this.scrollParentRef} 80 | > 81 | {items} 82 | 83 |
84 | 85 | ``` 86 | 87 | ## Props 88 | 89 | | Name | Required | Type | Default | Description | 90 | | :---------------- | :------- | :----------- | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 91 | | `children`       | Yes | `Node`   |           | Anything that can be rendered (same as PropType's Node) | 92 | | `loadMore`       | Yes | `Function`   |           | A callback when more items are requested by the user. Receives a single parameter specifying the page to load e.g. `function handleLoadMore(page) { /* load more items here */ }` } | 93 | | `element` | | `Component` | `'div'` | Name of the element that the component should render as. | 94 | | `hasMore` | | `Boolean` | `false` | Whether there are more items to be loaded. Event listeners are removed if `false`. | 95 | | `initialLoad` | | `Boolean` | `true` | Whether the component should load the first set of items. | 96 | | `isReverse` | | `Boolean` | `false` | Whether new items should be loaded when user scrolls to the top of the scrollable area. | 97 | | `loader` | | `Component` | | A React component to render while more items are loading. The parent component must have a unique key prop. | 98 | | `pageStart` | | `Number` | `0` | The number of the first page to load, With the default of `0`, the first page is `1`. | 99 | | `getScrollParent` | | `Function` | | Override method to return a different scroll listener if it's not the immediate parent of InfiniteScroll. | 100 | | `threshold` | | `Number` | `250` | The distance in pixels before the end of the items that will trigger a call to `loadMore`. | 101 | | `useCapture` | | `Boolean` | `false` | Proxy to the `useCapture` option of the added event listeners. | 102 | | `useWindow` | | `Boolean` | `true` | Add scroll listeners to the window, or else, the component's `parentNode`. | 103 | 104 | ## Troubleshooting 105 | 106 | ### Double or non-stop calls to `loadMore` 107 | 108 | If you experience double or non-stop calls to your `loadMore` callback, make 109 | sure you have your CSS layout working properly before adding this component in. 110 | Calculations are made based on the height of the container (the element the 111 | component creates to wrap the items), so the height of the container must equal 112 | the entire height of the items. 113 | 114 | ```css 115 | .my-container { 116 | overflow: auto; 117 | } 118 | ``` 119 | 120 | Some people have found success using [react-infinite-scroll-component](https://github.com/ankeetmaini/react-infinite-scroll-component). 121 | 122 | ### But you should just add an `isLoading` prop! 123 | 124 | This component doesn't make any assumptions about what you do in terms of API 125 | calls. It's up to you to store whether you are currently loading items from an 126 | API in your state/reducers so that you don't make overlapping API calls. 127 | 128 | ```js 129 | loadMore() { 130 | if(!this.state.isLoading) { 131 | this.props.fetchItems(); 132 | } 133 | } 134 | ``` 135 | -------------------------------------------------------------------------------- /dist/InfiniteScroll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function() { 8 | function defineProperties(target, props) { 9 | for (var i = 0; i < props.length; i++) { 10 | var descriptor = props[i]; 11 | descriptor.enumerable = descriptor.enumerable || false; 12 | descriptor.configurable = true; 13 | if ('value' in descriptor) descriptor.writable = true; 14 | Object.defineProperty(target, descriptor.key, descriptor); 15 | } 16 | } 17 | return function(Constructor, protoProps, staticProps) { 18 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 19 | if (staticProps) defineProperties(Constructor, staticProps); 20 | return Constructor; 21 | }; 22 | })(); 23 | 24 | var _react = require('react'); 25 | 26 | var _react2 = _interopRequireDefault(_react); 27 | 28 | var _propTypes = require('prop-types'); 29 | 30 | var _propTypes2 = _interopRequireDefault(_propTypes); 31 | 32 | function _interopRequireDefault(obj) { 33 | return obj && obj.__esModule ? obj : { default: obj }; 34 | } 35 | 36 | function _objectWithoutProperties(obj, keys) { 37 | var target = {}; 38 | for (var i in obj) { 39 | if (keys.indexOf(i) >= 0) continue; 40 | if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; 41 | target[i] = obj[i]; 42 | } 43 | return target; 44 | } 45 | 46 | function _classCallCheck(instance, Constructor) { 47 | if (!(instance instanceof Constructor)) { 48 | throw new TypeError('Cannot call a class as a function'); 49 | } 50 | } 51 | 52 | function _possibleConstructorReturn(self, call) { 53 | if (!self) { 54 | throw new ReferenceError( 55 | "this hasn't been initialised - super() hasn't been called" 56 | ); 57 | } 58 | return call && (typeof call === 'object' || typeof call === 'function') 59 | ? call 60 | : self; 61 | } 62 | 63 | function _inherits(subClass, superClass) { 64 | if (typeof superClass !== 'function' && superClass !== null) { 65 | throw new TypeError( 66 | 'Super expression must either be null or a function, not ' + 67 | typeof superClass 68 | ); 69 | } 70 | subClass.prototype = Object.create(superClass && superClass.prototype, { 71 | constructor: { 72 | value: subClass, 73 | enumerable: false, 74 | writable: true, 75 | configurable: true 76 | } 77 | }); 78 | if (superClass) 79 | Object.setPrototypeOf 80 | ? Object.setPrototypeOf(subClass, superClass) 81 | : (subClass.__proto__ = superClass); 82 | } 83 | 84 | var InfiniteScroll = (function(_Component) { 85 | _inherits(InfiniteScroll, _Component); 86 | 87 | function InfiniteScroll(props) { 88 | _classCallCheck(this, InfiniteScroll); 89 | 90 | var _this = _possibleConstructorReturn( 91 | this, 92 | (InfiniteScroll.__proto__ || Object.getPrototypeOf(InfiniteScroll)).call( 93 | this, 94 | props 95 | ) 96 | ); 97 | 98 | _this.scrollListener = _this.scrollListener.bind(_this); 99 | _this.eventListenerOptions = _this.eventListenerOptions.bind(_this); 100 | _this.mousewheelListener = _this.mousewheelListener.bind(_this); 101 | return _this; 102 | } 103 | 104 | _createClass(InfiniteScroll, [ 105 | { 106 | key: 'componentDidMount', 107 | value: function componentDidMount() { 108 | this.pageLoaded = this.props.pageStart; 109 | this.options = this.eventListenerOptions(); 110 | this.attachScrollListener(); 111 | } 112 | }, 113 | { 114 | key: 'componentDidUpdate', 115 | value: function componentDidUpdate() { 116 | if (this.props.isReverse && this.loadMore) { 117 | var parentElement = this.getParentElement(this.scrollComponent); 118 | parentElement.scrollTop = 119 | parentElement.scrollHeight - 120 | this.beforeScrollHeight + 121 | this.beforeScrollTop; 122 | this.loadMore = false; 123 | } 124 | this.attachScrollListener(); 125 | } 126 | }, 127 | { 128 | key: 'componentWillUnmount', 129 | value: function componentWillUnmount() { 130 | this.detachScrollListener(); 131 | this.detachMousewheelListener(); 132 | } 133 | }, 134 | { 135 | key: 'isPassiveSupported', 136 | value: function isPassiveSupported() { 137 | var passive = false; 138 | 139 | var testOptions = { 140 | get passive() { 141 | passive = true; 142 | } 143 | }; 144 | 145 | try { 146 | document.addEventListener('test', null, testOptions); 147 | document.removeEventListener('test', null, testOptions); 148 | } catch (e) { 149 | // ignore 150 | } 151 | return passive; 152 | } 153 | }, 154 | { 155 | key: 'eventListenerOptions', 156 | value: function eventListenerOptions() { 157 | var options = this.props.useCapture; 158 | 159 | if (this.isPassiveSupported()) { 160 | options = { 161 | useCapture: this.props.useCapture, 162 | passive: true 163 | }; 164 | } else { 165 | options = { 166 | passive: false 167 | }; 168 | } 169 | return options; 170 | } 171 | 172 | // Set a defaut loader for all your `InfiniteScroll` components 173 | }, 174 | { 175 | key: 'setDefaultLoader', 176 | value: function setDefaultLoader(loader) { 177 | this.defaultLoader = loader; 178 | } 179 | }, 180 | { 181 | key: 'detachMousewheelListener', 182 | value: function detachMousewheelListener() { 183 | var scrollEl = window; 184 | if (this.props.useWindow === false) { 185 | scrollEl = this.scrollComponent.parentNode; 186 | } 187 | 188 | scrollEl.removeEventListener( 189 | 'mousewheel', 190 | this.mousewheelListener, 191 | this.options ? this.options : this.props.useCapture 192 | ); 193 | } 194 | }, 195 | { 196 | key: 'detachScrollListener', 197 | value: function detachScrollListener() { 198 | var scrollEl = window; 199 | if (this.props.useWindow === false) { 200 | scrollEl = this.getParentElement(this.scrollComponent); 201 | } 202 | 203 | scrollEl.removeEventListener( 204 | 'scroll', 205 | this.scrollListener, 206 | this.options ? this.options : this.props.useCapture 207 | ); 208 | scrollEl.removeEventListener( 209 | 'resize', 210 | this.scrollListener, 211 | this.options ? this.options : this.props.useCapture 212 | ); 213 | } 214 | }, 215 | { 216 | key: 'getParentElement', 217 | value: function getParentElement(el) { 218 | var scrollParent = 219 | this.props.getScrollParent && this.props.getScrollParent(); 220 | if (scrollParent != null) { 221 | return scrollParent; 222 | } 223 | return el && el.parentNode; 224 | } 225 | }, 226 | { 227 | key: 'filterProps', 228 | value: function filterProps(props) { 229 | return props; 230 | } 231 | }, 232 | { 233 | key: 'attachScrollListener', 234 | value: function attachScrollListener() { 235 | var parentElement = this.getParentElement(this.scrollComponent); 236 | 237 | if (!this.props.hasMore || !parentElement) { 238 | return; 239 | } 240 | 241 | var scrollEl = window; 242 | if (this.props.useWindow === false) { 243 | scrollEl = parentElement; 244 | } 245 | 246 | scrollEl.addEventListener( 247 | 'mousewheel', 248 | this.mousewheelListener, 249 | this.options ? this.options : this.props.useCapture 250 | ); 251 | scrollEl.addEventListener( 252 | 'scroll', 253 | this.scrollListener, 254 | this.options ? this.options : this.props.useCapture 255 | ); 256 | scrollEl.addEventListener( 257 | 'resize', 258 | this.scrollListener, 259 | this.options ? this.options : this.props.useCapture 260 | ); 261 | 262 | if (this.props.initialLoad) { 263 | this.scrollListener(); 264 | } 265 | } 266 | }, 267 | { 268 | key: 'mousewheelListener', 269 | value: function mousewheelListener(e) { 270 | // Prevents Chrome hangups 271 | // See: https://stackoverflow.com/questions/47524205/random-high-content-download-time-in-chrome/47684257#47684257 272 | if (e.deltaY === 1 && !this.isPassiveSupported()) { 273 | e.preventDefault(); 274 | } 275 | } 276 | }, 277 | { 278 | key: 'scrollListener', 279 | value: function scrollListener() { 280 | var el = this.scrollComponent; 281 | var scrollEl = window; 282 | var parentNode = this.getParentElement(el); 283 | 284 | var offset = void 0; 285 | if (this.props.useWindow) { 286 | var doc = 287 | document.documentElement || 288 | document.body.parentNode || 289 | document.body; 290 | var scrollTop = 291 | scrollEl.pageYOffset !== undefined 292 | ? scrollEl.pageYOffset 293 | : doc.scrollTop; 294 | if (this.props.isReverse) { 295 | offset = scrollTop; 296 | } else { 297 | offset = this.calculateOffset(el, scrollTop); 298 | } 299 | } else if (this.props.isReverse) { 300 | offset = parentNode.scrollTop; 301 | } else { 302 | offset = 303 | el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight; 304 | } 305 | 306 | // Here we make sure the element is visible as well as checking the offset 307 | if ( 308 | offset < Number(this.props.threshold) && 309 | el && 310 | el.offsetParent !== null 311 | ) { 312 | this.detachScrollListener(); 313 | this.beforeScrollHeight = parentNode.scrollHeight; 314 | this.beforeScrollTop = parentNode.scrollTop; 315 | // Call loadMore after detachScrollListener to allow for non-async loadMore functions 316 | if (typeof this.props.loadMore === 'function') { 317 | this.props.loadMore((this.pageLoaded += 1)); 318 | this.loadMore = true; 319 | } 320 | } 321 | } 322 | }, 323 | { 324 | key: 'calculateOffset', 325 | value: function calculateOffset(el, scrollTop) { 326 | if (!el) { 327 | return 0; 328 | } 329 | 330 | return ( 331 | this.calculateTopPosition(el) + 332 | (el.offsetHeight - scrollTop - window.innerHeight) 333 | ); 334 | } 335 | }, 336 | { 337 | key: 'calculateTopPosition', 338 | value: function calculateTopPosition(el) { 339 | if (!el) { 340 | return 0; 341 | } 342 | return el.offsetTop + this.calculateTopPosition(el.offsetParent); 343 | } 344 | }, 345 | { 346 | key: 'render', 347 | value: function render() { 348 | var _this2 = this; 349 | 350 | var renderProps = this.filterProps(this.props); 351 | 352 | var children = renderProps.children, 353 | element = renderProps.element, 354 | hasMore = renderProps.hasMore, 355 | initialLoad = renderProps.initialLoad, 356 | isReverse = renderProps.isReverse, 357 | loader = renderProps.loader, 358 | loadMore = renderProps.loadMore, 359 | pageStart = renderProps.pageStart, 360 | ref = renderProps.ref, 361 | threshold = renderProps.threshold, 362 | useCapture = renderProps.useCapture, 363 | useWindow = renderProps.useWindow, 364 | getScrollParent = renderProps.getScrollParent, 365 | props = _objectWithoutProperties(renderProps, [ 366 | 'children', 367 | 'element', 368 | 'hasMore', 369 | 'initialLoad', 370 | 'isReverse', 371 | 'loader', 372 | 'loadMore', 373 | 'pageStart', 374 | 'ref', 375 | 'threshold', 376 | 'useCapture', 377 | 'useWindow', 378 | 'getScrollParent' 379 | ]); 380 | 381 | props.ref = function(node) { 382 | _this2.scrollComponent = node; 383 | if (ref) { 384 | ref(node); 385 | } 386 | }; 387 | 388 | var childrenArray = [children]; 389 | if (hasMore) { 390 | if (loader) { 391 | isReverse 392 | ? childrenArray.unshift(loader) 393 | : childrenArray.push(loader); 394 | } else if (this.defaultLoader) { 395 | isReverse 396 | ? childrenArray.unshift(this.defaultLoader) 397 | : childrenArray.push(this.defaultLoader); 398 | } 399 | } 400 | return _react2.default.createElement(element, props, childrenArray); 401 | } 402 | } 403 | ]); 404 | 405 | return InfiniteScroll; 406 | })(_react.Component); 407 | 408 | InfiniteScroll.propTypes = { 409 | children: _propTypes2.default.node.isRequired, 410 | element: _propTypes2.default.node, 411 | hasMore: _propTypes2.default.bool, 412 | initialLoad: _propTypes2.default.bool, 413 | isReverse: _propTypes2.default.bool, 414 | loader: _propTypes2.default.node, 415 | loadMore: _propTypes2.default.func.isRequired, 416 | pageStart: _propTypes2.default.number, 417 | ref: _propTypes2.default.func, 418 | getScrollParent: _propTypes2.default.func, 419 | threshold: _propTypes2.default.number, 420 | useCapture: _propTypes2.default.bool, 421 | useWindow: _propTypes2.default.bool 422 | }; 423 | InfiniteScroll.defaultProps = { 424 | element: 'div', 425 | hasMore: false, 426 | initialLoad: true, 427 | pageStart: 0, 428 | ref: null, 429 | threshold: 250, 430 | useWindow: true, 431 | isReverse: false, 432 | useCapture: false, 433 | loader: null, 434 | getScrollParent: null 435 | }; 436 | exports.default = InfiniteScroll; 437 | module.exports = exports['default']; 438 | -------------------------------------------------------------------------------- /docs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Infinite Scroller 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 |
25 |

Demo

26 |

27 | The source code to this demo can be found in the repo's 28 | docs folder. 29 | 30 | Below, the component infinite scrolls React's Github issues. 31 |

32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var rename = require('gulp-rename'); 3 | var source = require('vinyl-source-stream') 4 | var buffer = require('vinyl-buffer') 5 | var browserify = require('browserify'); 6 | var babelify = require('babelify'); 7 | 8 | gulp.task('js', function() { 9 | return browserify('src/index.js').transform(babelify, {presets: ["es2015", "react"]}) 10 | .bundle() 11 | .pipe(source('index.js')) 12 | .pipe(buffer()) 13 | .pipe(rename('script.js')) 14 | .pipe(gulp.dest('./js')); 15 | }); 16 | 17 | gulp.task('default', gulp.series('js')); 18 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Infinite Scroller 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 |
25 |

26 | 27 | npm 28 | 29 | 30 | React Version 32 | 33 | 34 | npm 35 | 36 | 37 | npm 38 | 39 |

40 | 41 |

Infinitely load content using a React Component. This fork maintains a simple, lightweight infinite scroll 42 | package that supports both window and scrollable elements.

43 | 44 |

45 | 47 | Installation 48 |

49 | 50 |
npm i react-infinite-scroller
51 | 52 |

53 | 55 | How to use 56 |

57 | 58 |
import InfiniteScroll from 'react-infinite-scroller'
59 | 60 |

61 | 63 | Window scroll events 64 |

65 | 66 |
67 |
<InfiniteScroll
 68 |     pageStart={0}
 69 |     loadMore={loadFunc}
 70 |     hasMore={true || false}
 71 |     loader={<div className="loader">Loading ...</div>}>
 72 |   {items} // <-- This is the content you want to load
 73 | </InfiniteScroll>
74 |
75 | 76 |

77 | 79 | DOM scroll events 80 |

81 | 82 |
83 |
<div style="height:700px;overflow:auto;">
 84 |   <InfiniteScroll
 85 |       pageStart={0}
 86 |       loadMore={loadFunc}
 87 |       hasMore={true || false}
 88 |       loader={<div className="loader">Loading ...</div>}
 89 |       useWindow={false}>
 90 |     {items}
 91 |   </InfiniteScroll>
 92 | </div>
93 |
94 | 95 |

96 | 98 | Props 99 |

100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 143 | 144 | 145 | 146 | 147 | 148 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
NameTypeDefaultDescription
elementString'div'Name of the element that the component should render as.
hasMoreBooleanfalseWhether there are more items to be loaded. Event listeners are removed if false. 122 |
initialLoadBooleantrueWhether the component should load the first set of items.
loadMoreFunctionA callback when more items are requested by the user.
pageStartObject0The number of the first page to load, With the default of 0, the first page is 141 | 1. 142 |
thresholdNumber250The distance in pixels before the end of the items that will trigger a call to 149 | loadMore. 150 |
useWindowBooleantrueAdd scroll listeners to the window, or else, the component's parentNode.
160 | 161 | 171 |
172 | 173 | 176 | 177 | 180 | 183 | 184 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-infinite-scroll-gh-pages", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "dependencies": { 7 | "parse-link-header": "^2.0.0", 8 | "react": "^18.0.0", 9 | "react-dom": "^18.0.0", 10 | "react-infinite-scroller": "file:.." 11 | }, 12 | "devDependencies": { 13 | "babel-preset-es2015": "^6.9.0", 14 | "babel-preset-react": "^6.5.0", 15 | "babelify": "^7.3.0", 16 | "browserify": "^13.0.1", 17 | "gulp": "^4.0.2", 18 | "gulp-rename": "^1.2.2", 19 | "vinyl-buffer": "^1.0.1", 20 | "vinyl-source-stream": "^2.0.0", 21 | "watchify": "^3.7.0" 22 | }, 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/danbovey/react-infinite-scroller.git" 29 | }, 30 | "author": "Dan Bovey ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/danbovey/react-infinite-scroller/issues" 34 | }, 35 | "homepage": "https://github.com/danbovey/react-infinite-scroller#readme" 36 | } 37 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import InfiniteScroll from 'react-infinite-scroller'; 4 | import parseLinkHeader from 'parse-link-header'; 5 | 6 | async function fetchIssues(url) { 7 | const response = await fetch(url, { 8 | method: 'GET', 9 | headers: new Headers({ 10 | Accept: 'application/vnd.github.v3+json' 11 | }) 12 | }); 13 | 14 | const links = parseLinkHeader(response.headers.get('Link')); 15 | const issues = await response.json(); 16 | 17 | return { 18 | links, 19 | issues 20 | }; 21 | } 22 | 23 | const App = () => { 24 | const [items, setItems] = useState([]); 25 | const [nextPageUrl, setNextPageUrl] = useState( 26 | 'https://api.github.com/repos/facebook/react/issues' 27 | ); 28 | const [fetching, setFetching] = useState(false); 29 | 30 | const fetchItems = useCallback( 31 | async () => { 32 | if (fetching) { 33 | return; 34 | } 35 | 36 | setFetching(true); 37 | 38 | try { 39 | const { issues, links } = await fetchIssues(nextPageUrl); 40 | 41 | setItems([...items, ...issues]); 42 | 43 | if (links.next) { 44 | setNextPageUrl(links.next.url); 45 | } else { 46 | setNextPageUrl(null); 47 | } 48 | } finally { 49 | setFetching(false); 50 | } 51 | }, 52 | [items, fetching, nextPageUrl] 53 | ); 54 | 55 | const hasMoreItems = !!nextPageUrl; 56 | 57 | const loader = ( 58 |
59 | Loading ... 60 |
61 | ); 62 | 63 | return ( 64 | 69 | 78 | 79 | ); 80 | }; 81 | 82 | const root = createRoot(document.getElementById('root')); 83 | root.render(); 84 | -------------------------------------------------------------------------------- /docs/stylesheets/github-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 GitHub, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | .pl-c /* comment */ { 27 | color: #969896; 28 | } 29 | 30 | .pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, 31 | .pl-s .pl-v /* string variable */ { 32 | color: #0086b3; 33 | } 34 | 35 | .pl-e /* entity */, 36 | .pl-en /* entity.name */ { 37 | color: #795da3; 38 | } 39 | 40 | .pl-s .pl-s1 /* string source */, 41 | .pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { 42 | color: #333; 43 | } 44 | 45 | .pl-ent /* entity.name.tag */ { 46 | color: #63a35c; 47 | } 48 | 49 | .pl-k /* keyword, storage, storage.type */ { 50 | color: #a71d5d; 51 | } 52 | 53 | .pl-pds /* punctuation.definition.string, string.regexp.character-class */, 54 | .pl-s /* string */, 55 | .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, 56 | .pl-sr /* string.regexp */, 57 | .pl-sr .pl-cce /* string.regexp constant.character.escape */, 58 | .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, 59 | .pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { 60 | color: #183691; 61 | } 62 | 63 | .pl-v /* variable */ { 64 | color: #ed6a43; 65 | } 66 | 67 | .pl-id /* invalid.deprecated */ { 68 | color: #b52a1d; 69 | } 70 | 71 | .pl-ii /* invalid.illegal */ { 72 | background-color: #b52a1d; 73 | color: #f8f8f8; 74 | } 75 | 76 | .pl-sr .pl-cce /* string.regexp constant.character.escape */ { 77 | color: #63a35c; 78 | font-weight: bold; 79 | } 80 | 81 | .pl-ml /* markup.list */ { 82 | color: #693a17; 83 | } 84 | 85 | .pl-mh /* markup.heading */, 86 | .pl-mh .pl-en /* markup.heading entity.name */, 87 | .pl-ms /* meta.separator */ { 88 | color: #1d3e81; 89 | font-weight: bold; 90 | } 91 | 92 | .pl-mq /* markup.quote */ { 93 | color: #008080; 94 | } 95 | 96 | .pl-mi /* markup.italic */ { 97 | color: #333; 98 | font-style: italic; 99 | } 100 | 101 | .pl-mb /* markup.bold */ { 102 | color: #333; 103 | font-weight: bold; 104 | } 105 | 106 | .pl-md /* markup.deleted, meta.diff.header.from-file */ { 107 | background-color: #ffecec; 108 | color: #bd2c00; 109 | } 110 | 111 | .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { 112 | background-color: #eaffea; 113 | color: #55a532; 114 | } 115 | 116 | .pl-mdr /* meta.diff.range */ { 117 | color: #795da3; 118 | font-weight: bold; 119 | } 120 | 121 | .pl-mo /* meta.output */ { 122 | color: #1d3e81; 123 | } 124 | 125 | -------------------------------------------------------------------------------- /docs/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | box-sizing: content-box; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Contain overflow in all browsers. 218 | */ 219 | 220 | pre { 221 | overflow: auto; 222 | } 223 | 224 | /** 225 | * Address odd `em`-unit font size rendering in all browsers. 226 | */ 227 | 228 | code, 229 | kbd, 230 | pre, 231 | samp { 232 | font-family: monospace, monospace; 233 | font-size: 1em; 234 | } 235 | 236 | /* Forms 237 | ========================================================================== */ 238 | 239 | /** 240 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 241 | * styling of `select`, unless a `border` property is set. 242 | */ 243 | 244 | /** 245 | * 1. Correct color not being inherited. 246 | * Known issue: affects color of disabled elements. 247 | * 2. Correct font properties not being inherited. 248 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 249 | */ 250 | 251 | button, 252 | input, 253 | optgroup, 254 | select, 255 | textarea { 256 | color: inherit; /* 1 */ 257 | font: inherit; /* 2 */ 258 | margin: 0; /* 3 */ 259 | } 260 | 261 | /** 262 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 263 | */ 264 | 265 | button { 266 | overflow: visible; 267 | } 268 | 269 | /** 270 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 271 | * All other form control elements do not inherit `text-transform` values. 272 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 273 | * Correct `select` style inheritance in Firefox. 274 | */ 275 | 276 | button, 277 | select { 278 | text-transform: none; 279 | } 280 | 281 | /** 282 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 283 | * and `video` controls. 284 | * 2. Correct inability to style clickable `input` types in iOS. 285 | * 3. Improve usability and consistency of cursor style between image-type 286 | * `input` and others. 287 | */ 288 | 289 | button, 290 | html input[type="button"], /* 1 */ 291 | input[type="reset"], 292 | input[type="submit"] { 293 | -webkit-appearance: button; /* 2 */ 294 | cursor: pointer; /* 3 */ 295 | } 296 | 297 | /** 298 | * Re-set default cursor for disabled elements. 299 | */ 300 | 301 | button[disabled], 302 | html input[disabled] { 303 | cursor: default; 304 | } 305 | 306 | /** 307 | * Remove inner padding and border in Firefox 4+. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | input::-moz-focus-inner { 312 | border: 0; 313 | padding: 0; 314 | } 315 | 316 | /** 317 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 318 | * the UA stylesheet. 319 | */ 320 | 321 | input { 322 | line-height: normal; 323 | } 324 | 325 | /** 326 | * It's recommended that you don't attempt to style these elements. 327 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 328 | * 329 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 330 | * 2. Remove excess padding in IE 8/9/10. 331 | */ 332 | 333 | input[type="checkbox"], 334 | input[type="radio"] { 335 | box-sizing: border-box; /* 1 */ 336 | padding: 0; /* 2 */ 337 | } 338 | 339 | /** 340 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 341 | * `font-size` values of the `input`, it causes the cursor style of the 342 | * decrement button to change from `default` to `text`. 343 | */ 344 | 345 | input[type="number"]::-webkit-inner-spin-button, 346 | input[type="number"]::-webkit-outer-spin-button { 347 | height: auto; 348 | } 349 | 350 | /** 351 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 352 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 353 | * (include `-moz` to future-proof). 354 | */ 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; /* 1 */ /* 2 */ 358 | box-sizing: content-box; 359 | } 360 | 361 | /** 362 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | * Safari (but not Chrome) clips the cancel button when the search input has 364 | * padding (and `textfield` appearance). 365 | */ 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | /** 373 | * Define consistent border, margin, and padding. 374 | */ 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | /** 383 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | */ 386 | 387 | legend { 388 | border: 0; /* 1 */ 389 | padding: 0; /* 2 */ 390 | } 391 | 392 | /** 393 | * Remove default vertical scrollbar in IE 8/9/10/11. 394 | */ 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | /** 401 | * Don't inherit the `font-weight` (applied by a rule above). 402 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | */ 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | /* Tables 410 | ========================================================================== */ 411 | 412 | /** 413 | * Remove most spacing between table cells. 414 | */ 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | -------------------------------------------------------------------------------- /docs/stylesheets/stylesheet.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; } 3 | 4 | body { 5 | padding: 0; 6 | margin: 0; 7 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 16px; 9 | line-height: 1.5; 10 | color: #606c71; } 11 | 12 | a { 13 | color: #1e6bb8; 14 | text-decoration: none; } 15 | a:hover { 16 | text-decoration: underline; } 17 | 18 | .btn { 19 | display: inline-block; 20 | margin-bottom: 1rem; 21 | color: rgba(255, 255, 255, 0.7); 22 | background-color: rgba(255, 255, 255, 0.08); 23 | border-color: rgba(255, 255, 255, 0.2); 24 | border-style: solid; 25 | border-width: 1px; 26 | border-radius: 0.3rem; 27 | transition: color 0.2s, background-color 0.2s, border-color 0.2s; } 28 | .btn + .btn { 29 | margin-left: 1rem; } 30 | 31 | .btn:hover { 32 | color: rgba(255, 255, 255, 0.8); 33 | text-decoration: none; 34 | background-color: rgba(255, 255, 255, 0.2); 35 | border-color: rgba(255, 255, 255, 0.3); } 36 | 37 | @media screen and (min-width: 64em) { 38 | .btn { 39 | padding: 0.75rem 1rem; } } 40 | 41 | @media screen and (min-width: 42em) and (max-width: 64em) { 42 | .btn { 43 | padding: 0.6rem 0.9rem; 44 | font-size: 0.9rem; } } 45 | 46 | @media screen and (max-width: 42em) { 47 | .btn { 48 | display: block; 49 | width: 100%; 50 | padding: 0.75rem; 51 | font-size: 0.9rem; } 52 | .btn + .btn { 53 | margin-top: 1rem; 54 | margin-left: 0; } } 55 | 56 | .page-header { 57 | color: #fff; 58 | text-align: center; 59 | background-color: #159957; 60 | background-image: linear-gradient(120deg, #155799, #159957); } 61 | 62 | @media screen and (min-width: 64em) { 63 | .page-header { 64 | padding: 5rem 6rem; } } 65 | 66 | @media screen and (min-width: 42em) and (max-width: 64em) { 67 | .page-header { 68 | padding: 3rem 4rem; } } 69 | 70 | @media screen and (max-width: 42em) { 71 | .page-header { 72 | padding: 2rem 1rem; } } 73 | 74 | .project-name { 75 | margin-top: 0; 76 | margin-bottom: 0.1rem; } 77 | 78 | @media screen and (min-width: 64em) { 79 | .project-name { 80 | font-size: 3.25rem; } } 81 | 82 | @media screen and (min-width: 42em) and (max-width: 64em) { 83 | .project-name { 84 | font-size: 2.25rem; } } 85 | 86 | @media screen and (max-width: 42em) { 87 | .project-name { 88 | font-size: 1.75rem; } } 89 | 90 | .project-tagline { 91 | margin-bottom: 2rem; 92 | font-weight: normal; 93 | opacity: 0.7; } 94 | 95 | @media screen and (min-width: 64em) { 96 | .project-tagline { 97 | font-size: 1.25rem; } } 98 | 99 | @media screen and (min-width: 42em) and (max-width: 64em) { 100 | .project-tagline { 101 | font-size: 1.15rem; } } 102 | 103 | @media screen and (max-width: 42em) { 104 | .project-tagline { 105 | font-size: 1rem; } } 106 | 107 | .main-content :first-child { 108 | margin-top: 0; } 109 | .main-content img { 110 | max-width: 100%; } 111 | .main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 { 112 | margin-top: 2rem; 113 | margin-bottom: 1rem; 114 | font-weight: normal; 115 | color: #159957; } 116 | .main-content p { 117 | margin-bottom: 1em; } 118 | .main-content code { 119 | padding: 2px 4px; 120 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 121 | font-size: 0.9rem; 122 | color: #383e41; 123 | background-color: #f3f6fa; 124 | border-radius: 0.3rem; } 125 | .main-content pre { 126 | padding: 0.8rem; 127 | margin-top: 0; 128 | margin-bottom: 1rem; 129 | font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace; 130 | color: #567482; 131 | word-wrap: normal; 132 | background-color: #f3f6fa; 133 | border: solid 1px #dce6f0; 134 | border-radius: 0.3rem; } 135 | .main-content pre > code { 136 | padding: 0; 137 | margin: 0; 138 | font-size: 0.9rem; 139 | color: #567482; 140 | word-break: normal; 141 | white-space: pre; 142 | background: transparent; 143 | border: 0; } 144 | .main-content .highlight { 145 | margin-bottom: 1rem; } 146 | .main-content .highlight pre { 147 | margin-bottom: 0; 148 | word-break: normal; } 149 | .main-content .highlight pre, .main-content pre { 150 | padding: 0.8rem; 151 | overflow: auto; 152 | font-size: 0.9rem; 153 | line-height: 1.45; 154 | border-radius: 0.3rem; } 155 | .main-content pre code, .main-content pre tt { 156 | display: inline; 157 | max-width: initial; 158 | padding: 0; 159 | margin: 0; 160 | overflow: initial; 161 | line-height: inherit; 162 | word-wrap: normal; 163 | background-color: transparent; 164 | border: 0; } 165 | .main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after { 166 | content: normal; } 167 | .main-content ul, .main-content ol { 168 | margin-top: 0; } 169 | .main-content blockquote { 170 | padding: 0 1rem; 171 | margin-left: 0; 172 | color: #819198; 173 | border-left: 0.3rem solid #dce6f0; } 174 | .main-content blockquote > :first-child { 175 | margin-top: 0; } 176 | .main-content blockquote > :last-child { 177 | margin-bottom: 0; } 178 | .main-content table { 179 | display: block; 180 | width: 100%; 181 | overflow: auto; 182 | word-break: normal; 183 | word-break: keep-all; } 184 | .main-content table th { 185 | font-weight: bold; } 186 | .main-content table th, .main-content table td { 187 | padding: 0.5rem 1rem; 188 | border: 1px solid #e9ebec; } 189 | .main-content dl { 190 | padding: 0; } 191 | .main-content dl dt { 192 | padding: 0; 193 | margin-top: 1rem; 194 | font-size: 1rem; 195 | font-weight: bold; } 196 | .main-content dl dd { 197 | padding: 0; 198 | margin-bottom: 1rem; } 199 | .main-content hr { 200 | height: 2px; 201 | padding: 0; 202 | margin: 1rem 0; 203 | background-color: #eff0f1; 204 | border: 0; } 205 | 206 | @media screen and (min-width: 64em) { 207 | .main-content { 208 | max-width: 64rem; 209 | padding: 2rem 6rem; 210 | margin: 0 auto; 211 | font-size: 1.1rem; } } 212 | 213 | @media screen and (min-width: 42em) and (max-width: 64em) { 214 | .main-content { 215 | padding: 2rem 4rem; 216 | font-size: 1.1rem; } } 217 | 218 | @media screen and (max-width: 42em) { 219 | .main-content { 220 | padding: 2rem 1rem; 221 | font-size: 1rem; } } 222 | 223 | .site-footer { 224 | padding-top: 2rem; 225 | margin-top: 2rem; 226 | border-top: solid 1px #eff0f1; } 227 | 228 | .site-footer-owner { 229 | display: block; 230 | font-weight: bold; } 231 | 232 | .site-footer-credits { 233 | color: #819198; } 234 | 235 | @media screen and (min-width: 64em) { 236 | .site-footer { 237 | font-size: 1rem; } } 238 | 239 | @media screen and (min-width: 42em) and (max-width: 64em) { 240 | .site-footer { 241 | font-size: 1rem; } } 242 | 243 | @media screen and (max-width: 42em) { 244 | .site-footer { 245 | font-size: 0.9rem; } } 246 | 247 | .loader { 248 | clear: both; 249 | } 250 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/InfiniteScroll') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-infinite-scroller", 3 | "version": "1.2.6", 4 | "description": "Infinite scroll component for React in ES6", 5 | "main": "index.js", 6 | "jsnext:main": "src/InfiniteScroll.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/danbovey/react-infinite-scroller.git" 10 | }, 11 | "scripts": { 12 | "build": "mkdirp dist && babel src/InfiniteScroll.js --out-file dist/InfiniteScroll.js", 13 | "prepublish": "npm run build", 14 | "test": "nyc npm run spec", 15 | "spec": "_mocha -R spec ./test/test_helper.js --recursive test/*_test.js", 16 | "lint": "node_modules/.bin/eslint ./src ./test", 17 | "precommit": "lint-staged" 18 | }, 19 | "lint-staged": { 20 | "*.{js,jsx}": [ 21 | "node_modules/.bin/eslint --fix --max-warnings 0", 22 | "git add" 23 | ] 24 | }, 25 | "keywords": [ 26 | "infinite", 27 | "scroll", 28 | "react" 29 | ], 30 | "author": "Dan Bovey ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/danbovey/react-infinite-scroller/issues" 34 | }, 35 | "dependencies": { 36 | "prop-types": "^15.5.8" 37 | }, 38 | "peerDependencies": { 39 | "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" 40 | }, 41 | "devDependencies": { 42 | "@testing-library/react": "^13.0.1", 43 | "babel-cli": "^6.6.5", 44 | "babel-core": "^6.26.3", 45 | "babel-eslint": "^7.2.3", 46 | "babel-istanbul": "^0.12.2", 47 | "babel-plugin-add-module-exports": "^0.1.2", 48 | "babel-plugin-istanbul": "^4.1.6", 49 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 50 | "babel-preset-es2015": "^6.6.0", 51 | "babel-preset-react": "^6.5.0", 52 | "babel-preset-stage-2": "^6.13.0", 53 | "chai": "^3.5.0", 54 | "eslint": "^4.10.0", 55 | "eslint-config-last": "^0.0.3", 56 | "eslint-config-prettier": "^2.6.0", 57 | "eslint-plugin-prettier": "^2.3.1", 58 | "eslint-plugin-react": "^7.4.0", 59 | "husky": "^0.14.3", 60 | "istanbul": "^0.4.5", 61 | "jsdom": "^10.0.0", 62 | "lint-staged": "^4.3.0", 63 | "mkdirp": "^0.5.1", 64 | "mocha": "^3.3.0", 65 | "nyc": "^15.1.0", 66 | "prettier": "^1.7.4", 67 | "react": "^18.0.0", 68 | "react-dom": "^18.0.0", 69 | "sinon": "^2.1.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/InfiniteScroll.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class InfiniteScroll extends Component { 5 | static propTypes = { 6 | children: PropTypes.node.isRequired, 7 | element: PropTypes.node, 8 | hasMore: PropTypes.bool, 9 | initialLoad: PropTypes.bool, 10 | isReverse: PropTypes.bool, 11 | loader: PropTypes.node, 12 | loadMore: PropTypes.func.isRequired, 13 | pageStart: PropTypes.number, 14 | ref: PropTypes.func, 15 | getScrollParent: PropTypes.func, 16 | threshold: PropTypes.number, 17 | useCapture: PropTypes.bool, 18 | useWindow: PropTypes.bool 19 | }; 20 | 21 | static defaultProps = { 22 | element: 'div', 23 | hasMore: false, 24 | initialLoad: true, 25 | pageStart: 0, 26 | ref: null, 27 | threshold: 250, 28 | useWindow: true, 29 | isReverse: false, 30 | useCapture: false, 31 | loader: null, 32 | getScrollParent: null 33 | }; 34 | 35 | constructor(props) { 36 | super(props); 37 | 38 | this.scrollListener = this.scrollListener.bind(this); 39 | this.eventListenerOptions = this.eventListenerOptions.bind(this); 40 | this.mousewheelListener = this.mousewheelListener.bind(this); 41 | } 42 | 43 | componentDidMount() { 44 | this.pageLoaded = this.props.pageStart; 45 | this.options = this.eventListenerOptions(); 46 | this.attachScrollListener(); 47 | } 48 | 49 | componentDidUpdate() { 50 | if (this.props.isReverse && this.loadMore) { 51 | const parentElement = this.getParentElement(this.scrollComponent); 52 | parentElement.scrollTop = 53 | parentElement.scrollHeight - 54 | this.beforeScrollHeight + 55 | this.beforeScrollTop; 56 | this.loadMore = false; 57 | } 58 | this.attachScrollListener(); 59 | } 60 | 61 | componentWillUnmount() { 62 | this.detachScrollListener(); 63 | this.detachMousewheelListener(); 64 | } 65 | 66 | isPassiveSupported() { 67 | let passive = false; 68 | 69 | const testOptions = { 70 | get passive() { 71 | passive = true; 72 | } 73 | }; 74 | 75 | try { 76 | document.addEventListener('test', null, testOptions); 77 | document.removeEventListener('test', null, testOptions); 78 | } catch (e) { 79 | // ignore 80 | } 81 | return passive; 82 | } 83 | 84 | eventListenerOptions() { 85 | let options = this.props.useCapture; 86 | 87 | if (this.isPassiveSupported()) { 88 | options = { 89 | useCapture: this.props.useCapture, 90 | passive: true 91 | }; 92 | } else { 93 | options = { 94 | passive: false 95 | }; 96 | } 97 | return options; 98 | } 99 | 100 | // Set a defaut loader for all your `InfiniteScroll` components 101 | setDefaultLoader(loader) { 102 | this.defaultLoader = loader; 103 | } 104 | 105 | detachMousewheelListener() { 106 | let scrollEl = window; 107 | if (this.props.useWindow === false) { 108 | scrollEl = this.scrollComponent.parentNode; 109 | } 110 | 111 | scrollEl.removeEventListener( 112 | 'mousewheel', 113 | this.mousewheelListener, 114 | this.options ? this.options : this.props.useCapture 115 | ); 116 | } 117 | 118 | detachScrollListener() { 119 | let scrollEl = window; 120 | if (this.props.useWindow === false) { 121 | scrollEl = this.getParentElement(this.scrollComponent); 122 | } 123 | 124 | scrollEl.removeEventListener( 125 | 'scroll', 126 | this.scrollListener, 127 | this.options ? this.options : this.props.useCapture 128 | ); 129 | scrollEl.removeEventListener( 130 | 'resize', 131 | this.scrollListener, 132 | this.options ? this.options : this.props.useCapture 133 | ); 134 | } 135 | 136 | getParentElement(el) { 137 | const scrollParent = 138 | this.props.getScrollParent && this.props.getScrollParent(); 139 | if (scrollParent != null) { 140 | return scrollParent; 141 | } 142 | return el && el.parentNode; 143 | } 144 | 145 | filterProps(props) { 146 | return props; 147 | } 148 | 149 | attachScrollListener() { 150 | const parentElement = this.getParentElement(this.scrollComponent); 151 | 152 | if (!this.props.hasMore || !parentElement) { 153 | return; 154 | } 155 | 156 | let scrollEl = window; 157 | if (this.props.useWindow === false) { 158 | scrollEl = parentElement; 159 | } 160 | 161 | scrollEl.addEventListener( 162 | 'mousewheel', 163 | this.mousewheelListener, 164 | this.options ? this.options : this.props.useCapture 165 | ); 166 | scrollEl.addEventListener( 167 | 'scroll', 168 | this.scrollListener, 169 | this.options ? this.options : this.props.useCapture 170 | ); 171 | scrollEl.addEventListener( 172 | 'resize', 173 | this.scrollListener, 174 | this.options ? this.options : this.props.useCapture 175 | ); 176 | 177 | if (this.props.initialLoad) { 178 | this.scrollListener(); 179 | } 180 | } 181 | 182 | mousewheelListener(e) { 183 | // Prevents Chrome hangups 184 | // See: https://stackoverflow.com/questions/47524205/random-high-content-download-time-in-chrome/47684257#47684257 185 | if (e.deltaY === 1 && !this.isPassiveSupported()) { 186 | e.preventDefault(); 187 | } 188 | } 189 | 190 | scrollListener() { 191 | const el = this.scrollComponent; 192 | const scrollEl = window; 193 | const parentNode = this.getParentElement(el); 194 | 195 | let offset; 196 | if (this.props.useWindow) { 197 | const doc = 198 | document.documentElement || document.body.parentNode || document.body; 199 | const scrollTop = 200 | scrollEl.pageYOffset !== undefined 201 | ? scrollEl.pageYOffset 202 | : doc.scrollTop; 203 | if (this.props.isReverse) { 204 | offset = scrollTop; 205 | } else { 206 | offset = this.calculateOffset(el, scrollTop); 207 | } 208 | } else if (this.props.isReverse) { 209 | offset = parentNode.scrollTop; 210 | } else { 211 | offset = el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight; 212 | } 213 | 214 | // Here we make sure the element is visible as well as checking the offset 215 | if ( 216 | offset < Number(this.props.threshold) && 217 | (el && el.offsetParent !== null) 218 | ) { 219 | this.detachScrollListener(); 220 | this.beforeScrollHeight = parentNode.scrollHeight; 221 | this.beforeScrollTop = parentNode.scrollTop; 222 | // Call loadMore after detachScrollListener to allow for non-async loadMore functions 223 | if (typeof this.props.loadMore === 'function') { 224 | this.props.loadMore((this.pageLoaded += 1)); 225 | this.loadMore = true; 226 | } 227 | } 228 | } 229 | 230 | calculateOffset(el, scrollTop) { 231 | if (!el) { 232 | return 0; 233 | } 234 | 235 | return ( 236 | this.calculateTopPosition(el) + 237 | (el.offsetHeight - scrollTop - window.innerHeight) 238 | ); 239 | } 240 | 241 | calculateTopPosition(el) { 242 | if (!el) { 243 | return 0; 244 | } 245 | return el.offsetTop + this.calculateTopPosition(el.offsetParent); 246 | } 247 | 248 | render() { 249 | const renderProps = this.filterProps(this.props); 250 | const { 251 | children, 252 | element, 253 | hasMore, 254 | initialLoad, 255 | isReverse, 256 | loader, 257 | loadMore, 258 | pageStart, 259 | ref, 260 | threshold, 261 | useCapture, 262 | useWindow, 263 | getScrollParent, 264 | ...props 265 | } = renderProps; 266 | 267 | props.ref = node => { 268 | this.scrollComponent = node; 269 | if (ref) { 270 | ref(node); 271 | } 272 | }; 273 | 274 | const childrenArray = [children]; 275 | if (hasMore) { 276 | if (loader) { 277 | isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader); 278 | } else if (this.defaultLoader) { 279 | isReverse 280 | ? childrenArray.unshift(this.defaultLoader) 281 | : childrenArray.push(this.defaultLoader); 282 | } 283 | } 284 | return React.createElement(element, props, childrenArray); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /test/infiniteScroll_test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, act } from '@testing-library/react'; 3 | import { expect } from 'chai'; 4 | import { stub, spy } from 'sinon'; 5 | import InfiniteScroll from '../src/InfiniteScroll'; 6 | 7 | describe('InfiniteScroll component', () => { 8 | it('should render', () => { 9 | const loadMore = stub(); 10 | const children = ( 11 |
12 |
1
13 |
2
14 |
3
15 |
16 | ); 17 | 18 | const { container } = render( 19 |
20 | 21 |
{children}
22 |
23 |
24 | ); 25 | expect(container.querySelectorAll('.child-class').length).to.equal(3); 26 | }); 27 | 28 | it('should render componentDidMount', () => { 29 | spy(InfiniteScroll.prototype, 'componentDidMount'); 30 | const loadMore = stub(); 31 | const children = ( 32 |
33 |
1
34 |
2
35 |
3
36 |
37 | ); 38 | render( 39 |
40 | 41 |
{children}
42 |
43 |
44 | ); 45 | expect(InfiniteScroll.prototype.componentDidMount.callCount).to.equal(1); 46 | InfiniteScroll.prototype.componentDidMount.restore(); 47 | }); 48 | 49 | it('should attach scroll listeners', () => { 50 | spy(InfiniteScroll.prototype, 'attachScrollListener'); 51 | spy(InfiniteScroll.prototype, 'scrollListener'); 52 | const loadMore = stub(); 53 | const children = ( 54 |
55 |
1
56 |
2
57 |
3
58 |
59 | ); 60 | render( 61 |
62 | 69 |
{children}
70 |
71 |
72 | ); 73 | expect(InfiniteScroll.prototype.attachScrollListener.callCount).to.equal(1); 74 | expect(InfiniteScroll.prototype.scrollListener.callCount).to.equal(1); 75 | InfiniteScroll.prototype.attachScrollListener.restore(); 76 | InfiniteScroll.prototype.scrollListener.restore(); 77 | }); 78 | 79 | it('should handle when the scrollElement is removed from the DOM', () => { 80 | const componentRef = React.createRef(); 81 | 82 | const loadMore = stub(); 83 | 84 | const { container } = render( 85 |
86 | 92 |
Child Text
93 |
94 |
95 | ); 96 | 97 | // The component has now mounted, but the scrollComponent is null 98 | componentRef.current.scrollComponent = null; 99 | 100 | // Invoke the scroll listener which depends on the scrollComponent to 101 | // verify it executes properly, and safely navigates when the 102 | // scrollComponent is null. 103 | componentRef.current.scrollListener(); 104 | 105 | expect(container.textContent).to.equal('Child Text'); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | require('babel-register')(); 3 | var chai = require('chai'); 4 | var JSDOM = require('jsdom').JSDOM; 5 | var exposedProperties = ['window', 'navigator', 'document']; 6 | 7 | global.dom = new JSDOM(''); 8 | global.window = dom.window.document.defaultView; 9 | 10 | Object.keys(dom.window.document.defaultView).forEach((property) => { 11 | if (typeof global[property] === 'undefined') { 12 | exposedProperties.push(property); 13 | global[property] = dom.window.document.defaultView[property]; 14 | } 15 | }); 16 | 17 | global.navigator = { 18 | userAgent: 'node.js', 19 | }; 20 | 21 | global.IS_REACT_ACT_ENVIRONMENT = true; 22 | --------------------------------------------------------------------------------