├── .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 | [](https://www.npmjs.com/package/react-infinite-scroller)
4 | [](https://www.npmjs.com/package/react-infinite-scroller)
5 | [](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 |
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 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
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 | Name
105 | Type
106 | Default
107 | Description
108 |
109 |
110 |
111 |
112 | element
113 | String
114 | 'div'
115 | Name of the element that the component should render as.
116 |
117 |
118 | hasMore
119 | Boolean
120 | false
121 | Whether there are more items to be loaded. Event listeners are removed if false
.
122 |
123 |
124 |
125 | initialLoad
126 | Boolean
127 | true
128 | Whether the component should load the first set of items.
129 |
130 |
131 | loadMore
132 | Function
133 |
134 | A callback when more items are requested by the user.
135 |
136 |
137 | pageStart
138 | Object
139 | 0
140 | The number of the first page to load, With the default of 0
, the first page is
141 | 1
.
142 |
143 |
144 |
145 | threshold
146 | Number
147 | 250
148 | The distance in pixels before the end of the items that will trigger a call to
149 | loadMore
.
150 |
151 |
152 |
153 | useWindow
154 | Boolean
155 | true
156 | Add scroll listeners to the window, or else, the component's parentNode
.
157 |
158 |
159 |
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 |
--------------------------------------------------------------------------------