├── .all-contributorsrc
├── .codecov.yml
├── .eslintrc.js
├── .github
└── workflows
│ ├── publish.yml
│ └── push.yml
├── .gitignore
├── .npmignore
├── .prettierrc.toml
├── .storybook
├── addons.js
├── config.js
└── webpack.config.js
├── README.md
├── docs
└── README-3.0.2.md
├── jest.config.js
├── license
├── package.json
├── rollup.config.js
├── src
├── __tests__
│ └── index.test.tsx
├── index.tsx
├── stories
│ ├── InfiniteScrollWithHeight.tsx
│ ├── PullDownToRefreshInfScroll.tsx
│ ├── ScrollableTargetInfScroll.tsx
│ ├── ScrolleableTop.tsx
│ ├── WindowInfiniteScrollComponent.tsx
│ └── stories.tsx
└── utils
│ └── threshold.ts
├── tsconfig.json
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "contributorsPerLine": 5,
7 | "skipCi": true,
8 | "contributors": [
9 | {
10 | "login": "ankeetmaini",
11 | "name": "Ankeet Maini",
12 | "avatar_url": "https://avatars.githubusercontent.com/u/6652823?v=4",
13 | "profile": "https://ankeetmaini.dev/",
14 | "contributions": [
15 | "question",
16 | "doc",
17 | "code",
18 | "review",
19 | "maintenance"
20 | ]
21 | },
22 | {
23 | "login": "iamdarshshah",
24 | "name": "Darsh Shah",
25 | "avatar_url": "https://avatars.githubusercontent.com/u/25670841?v=4",
26 | "profile": "https://github.com/iamdarshshah",
27 | "contributions": [
28 | "infra"
29 | ]
30 | }
31 | ],
32 | "projectName": "react-infinite-scroll-component",
33 | "projectOwner": "ankeetmaini",
34 | "repoType": "github",
35 | "repoHost": "https://github.com"
36 | }
37 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | notify:
3 | require_ci_to_pass: yes
4 |
5 | coverage:
6 | precision: 2
7 | round: down
8 | range: '10...100'
9 |
10 | status:
11 | project:
12 | default:
13 | threshold: 1
14 | patch:
15 | default:
16 | threshold: 1
17 | changes: no
18 |
19 | parsers:
20 | gcov:
21 | branch_detection:
22 | conditional: yes
23 | loop: yes
24 | method: no
25 | macro: no
26 |
27 | comment:
28 | layout: 'header, diff'
29 | behavior: default
30 | require_changes: no
31 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | parser: '@typescript-eslint/parser',
4 | parserOptions: {
5 | project: path.resolve(__dirname, './tsconfig.json'),
6 | tsconfigRootDir: __dirname,
7 | },
8 | env: {
9 | browser: true,
10 | node: true,
11 | },
12 | plugins: ['@typescript-eslint'],
13 | extends: [
14 | 'eslint:recommended',
15 | 'plugin:@typescript-eslint/eslint-recommended',
16 | 'plugin:react/recommended',
17 | 'plugin:@typescript-eslint/recommended',
18 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
19 | 'prettier',
20 | 'prettier/@typescript-eslint',
21 | 'prettier/react',
22 | ],
23 | rules: {
24 | '@typescript-eslint/prefer-regexp-exec': 1,
25 | '@typescript-eslint/ban-ts-ignore': 0,
26 | '@typescript-eslint/unbound-method': 1,
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish on release
2 |
3 | on: [release]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v1
11 |
12 | - name: install dependencies
13 | run: yarn
14 |
15 | - name: npm publish
16 | run: |
17 | npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN
18 | npm publish || true
19 | env:
20 | CI: true
21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
22 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: react-infinite-scroll-component
2 |
3 | on: [push]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v1
11 |
12 | - name: install dependencies
13 | run: yarn
14 |
15 | - name: lint
16 | run: yarn lint
17 |
18 | - name: prettier
19 | run: yarn prettier:check
20 |
21 | - name: unit tests
22 | run: yarn test
23 |
24 | - name: ts type checks
25 | run: yarn ts-check
26 |
27 | - uses: codecov/codecov-action@v1.0.3
28 | with:
29 | token: ${{secrets.CODECOV_TOKEN}}
30 | file: ./coverage/lcov.info
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | .DS_Store
3 | .idea
4 | node_modules
5 | /lib
6 | .rts2_*
7 | dist
8 | .vscode
9 | storybook-static/
10 | coverage/
11 | yarn-error.log
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.gitignore
2 | /.npmignore
3 | /demos/
4 | /server.js
5 | /webpack.*
6 | npm-debug.log
7 | __tests__
8 | stories/
9 | .rts2*
10 | .storybook
11 | coverage/
12 |
--------------------------------------------------------------------------------
/.prettierrc.toml:
--------------------------------------------------------------------------------
1 | trailingComma = "es5"
2 | tabWidth = 2
3 | semi = true
4 | singleQuote = true
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register';
2 | import '@storybook/addon-links/register';
3 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from "@storybook/react";
2 |
3 | configure(require.context("../src", true, /\.?stories\.tsx$/), module);
4 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = ({ config }) => {
2 | config.module.rules.push({
3 | test: /\.(ts|tsx)$/,
4 | use: [
5 | {
6 | loader: require.resolve("awesome-typescript-loader")
7 | },
8 | // Optional
9 | {
10 | loader: require.resolve("react-docgen-typescript-loader")
11 | }
12 | ]
13 | });
14 | config.resolve.extensions.push(".ts", ".tsx");
15 | return config;
16 | };
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-infinite-scroll-component [](https://www.npmjs.com/package/react-infinite-scroll-component) [](https://www.npmjs.com/package/react-infinite-scroll-component)
2 |
3 | [](#contributors-)
4 |
5 |
6 | A component to make all your infinite scrolling woes go away with just 4.15 kB! `Pull Down to Refresh` feature
7 | added. An infinite-scroll that actually works and super-simple to integrate!
8 |
9 | ## Install
10 |
11 | ```bash
12 | npm install --save react-infinite-scroll-component
13 |
14 | or
15 |
16 | yarn add react-infinite-scroll-component
17 |
18 | // in code ES6
19 | import InfiniteScroll from 'react-infinite-scroll-component';
20 | // or commonjs
21 | var InfiniteScroll = require('react-infinite-scroll-component');
22 | ```
23 |
24 | ## Using
25 |
26 | ```jsx
27 | Loading...}
32 | endMessage={
33 |
34 | Yay! You have seen it all
35 |
36 | }
37 | // below props only if you need pull down functionality
38 | refreshFunction={this.refresh}
39 | pullDownToRefresh
40 | pullDownToRefreshThreshold={50}
41 | pullDownToRefreshContent={
42 | ↓ Pull down to refresh
43 | }
44 | releaseToRefreshContent={
45 | ↑ Release to refresh
46 | }
47 | >
48 | {items}
49 |
50 | ```
51 |
52 | ## Using scroll on top
53 |
54 | ```jsx
55 |
81 | ```
82 |
83 | The `InfiniteScroll` component can be used in three ways.
84 |
85 | - Specify a value for the `height` prop if you want your **scrollable** content to have a specific height, providing scrollbars for scrolling your content and fetching more data.
86 | - If your **scrollable** content is being rendered within a parent element that is already providing overflow scrollbars, you can set the `scrollableTarget` prop to reference the DOM element and use it's scrollbars for fetching more data.
87 | - Without setting either the `height` or `scrollableTarget` props, the scroll will happen at `document.body` like _Facebook's_ timeline scroll.
88 |
89 | ## docs version wise
90 |
91 | [3.0.2](docs/README-3.0.2.md)
92 |
93 | ## live examples
94 |
95 | - infinite scroll (never ending) example using react (body/window scroll)
96 | - [](https://codesandbox.io/s/yk7637p62z)
97 | - infinte scroll till 500 elements (body/window scroll)
98 | - [](https://codesandbox.io/s/439v8rmqm0)
99 | - infinite scroll in an element (div of height 400px)
100 | - [](https://codesandbox.io/s/w3w89k7x8)
101 | - infinite scroll with `scrollableTarget` (a parent element which is scrollable)
102 | - [](https://codesandbox.io/s/r7rp40n0zm)
103 |
104 | ## props
105 |
106 | | name | type | description |
107 | | ------------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
108 | | **next** | function | a function which must be called after reaching the bottom. It must trigger some sort of action which fetches the next data. **The data is passed as `children` to the `InfiniteScroll` component and the data should contain previous items too.** e.g. _Initial data = [1, 2, 3]_ and then next load of data should be _[1, 2, 3, 4, 5, 6]_. |
109 | | **hasMore** | boolean | it tells the `InfiniteScroll` component on whether to call `next` function on reaching the bottom and shows an `endMessage` to the user |
110 | | **children** | node (list) | the data items which you need to scroll. |
111 | | **dataLength** | number | set the length of the data.This will unlock the subsequent calls to next. |
112 | | **loader** | node | you can send a loader component to show while the component waits for the next load of data. e.g. `Loading...
` or any fancy loader element |
113 | | **scrollThreshold** | number | string | A threshold value defining when `InfiniteScroll` will call `next`. Default value is `0.8`. It means the `next` will be called when user comes below 80% of the total height. If you pass threshold in pixels (`scrollThreshold="200px"`), `next` will be called once you scroll at least (100% - scrollThreshold) pixels down. |
114 | | **onScroll** | function | a function that will listen to the scroll event on the scrolling container. Note that the scroll event is throttled, so you may not receive as many events as you would expect. |
115 | | **endMessage** | node | this message is shown to the user when he has seen all the records which means he's at the bottom and `hasMore` is `false` |
116 | | **className** | string | add any custom class you want |
117 | | **style** | object | any style which you want to override |
118 | | **height** | number | optional, give only if you want to have a fixed height scrolling content |
119 | | **scrollableTarget** | node or string | optional, reference to a (parent) DOM element that is already providing overflow scrollbars to the `InfiniteScroll` component. _You should provide the `id` of the DOM node preferably._ |
120 | | **hasChildren** | bool | `children` is by default assumed to be of type array and it's length is used to determine if loader needs to be shown or not, if your `children` is not an array, specify this prop to tell if your items are 0 or more. |
121 | | **pullDownToRefresh** | bool | to enable **Pull Down to Refresh** feature |
122 | | **pullDownToRefreshContent** | node | any JSX that you want to show the user, `default={Pull down to refresh
}` |
123 | | **releaseToRefreshContent** | node | any JSX that you want to show the user, `default={Release to refresh
}` |
124 | | **pullDownToRefreshThreshold** | number | minimum distance the user needs to pull down to trigger the refresh, `default=100px` , a lower value may be needed to trigger the refresh depending your users browser. |
125 | | **refreshFunction** | function | this function will be called, it should return the fresh data that you want to show the user |
126 | | **initialScrollY** | number | set a scroll y position for the component to render with. |
127 | | **inverse** | bool | set infinite scroll on top |
128 |
129 | ## Contributors ✨
130 |
131 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
132 |
133 |
134 |
135 |
136 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | This project follows the [all-contributors](https://allcontributors.org) specification. Contributions of any kind are welcome!
149 |
150 | ## LICENSE
151 |
152 | [MIT](LICENSE)
153 |
--------------------------------------------------------------------------------
/docs/README-3.0.2.md:
--------------------------------------------------------------------------------
1 | # react-infinite-scroll-component [](https://www.npmjs.com/package/react-infinite-scroll-component) [](https://www.npmjs.com/package/react-infinite-scroll-component)
2 |
3 | A component to make all your infinite scrolling woes go away with just 4.15 kB! `Pull Down to Refresh` feature
4 | added. An infinite-scroll that actually works and super-simple to integrate!
5 |
6 | # install
7 | ```bash
8 | npm install --save react-infinite-scroll-component
9 |
10 | // in code ES6
11 | import InfiniteScroll from 'react-infinite-scroll-component';
12 | // or commonjs
13 | var InfiniteScroll = require('react-infinite-scroll-component');
14 | ```
15 |
16 | # demos
17 | - [See the demo in action at http://ankeetmaini.github.io/react-infinite-scroll-component/](http://ankeetmaini.github.io/react-infinite-scroll-component/). Thanks [@kdenz](https://github.com/kdenz)!
18 | - The code for demos is in the `demos/` directory. You can also clone and open `lib/index.html` in your browser to see the demos in action.
19 |
20 | # using
21 |
22 | ```jsx
23 | ↓ Pull down to refresh
27 | }
28 | releaseToRefreshContent={
29 | ↑ Release to refresh
30 | }
31 | refreshFunction={this.refresh}
32 | next={fetchData}
33 | hasMore={true}
34 | loader={Loading...
}
35 | endMessage={
36 |
37 | Yay! You have seen it all
38 |
39 | }>
40 | {items}
41 |
42 | ```
43 |
44 | The `InfiniteScroll` component can be used in three ways.
45 |
46 | - Specify a value for the `height` prop if you want your **scrollable** content to have a specific height, providing scrollbars for scrolling your content and fetching more data.
47 | - If your **scrollable** content is being rendered within a parent element that is already providing overflow scrollbars, you can set the `scrollableTarget` prop to reference the DOM element and use it's scrollbars for fetching more data.
48 | - Without setting either the `height` or `scrollableTarget` props, the scroll will happen at `document.body` like *Facebook's* timeline scroll.
49 |
50 |
51 | # props
52 | name | type | description
53 | -----|------|------------
54 | **next** | function | a function which must be called after reaching the bottom. It must trigger some sort of action which fetches the next data. **The data is passed as `children` to the `InfiniteScroll` component and the data should contain previous items too.** e.g. *Initial data = [1, 2, 3]* and then next load of data should be *[1, 2, 3, 4, 5, 6]*.
55 | **hasMore** | boolean | it tells the `InfiniteScroll` component on whether to call `next` function on reaching the bottom and shows an `endMessage` to the user
56 | **children** | node (list) | the data items which you need to scroll.
57 | **loader** | node | you can send a loader component to show while the component waits for the next load of data. e.g. `Loading...
` or any fancy loader element
58 | **scrollThreshold** | number | a threshold value after that the `InfiniteScroll` will call `next`. By default it's `0.8`. It means the `next` will be called when the user comes below 80% of the total height.
59 | **onScroll** | function | a function that will listen to the scroll event on the scrolling container. Note that the scroll event is throttled, so you may not receive as many events as you would expect.
60 | **endMessage** | node | this message is shown to the user when he has seen all the records which means he's at the bottom and `hasMore` is `false`
61 | **style** | object | any style which you want to override
62 | **height** | number | optional, give only if you want to have a fixed height scrolling content
63 | **scrollableTarget** | node | optional, reference to a (parent) DOM element that is already providing overflow scrollbars to the `InfiniteScroll` component.
64 | **hasChildren** | bool | `children` is by default assumed to be of type array and it's length is used to determine if loader needs to be shown or not, if your `children` is not an array, specify this prop to tell if your items are 0 or more.
65 | **pullDownToRefresh** | bool | to enable **Pull Down to Refresh** feature
66 | **pullDownToRefreshContent** | node | any JSX that you want to show the user, `default={Pull down to refresh
}`
67 | **releaseToRefreshContent** | node | any JSX that you want to show the user, `default={Release to refresh
}`
68 | **pullDownToRefreshThreshold** | number | minimum distance the user needs to pull down to trigger the refresh, `default=100px`
69 | **refreshFunction** | function | this function will be called, it should return the fresh data that you want to show the user
70 | **initialScrollY** | number | set a scroll y position for the component to render with.
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // All imported modules in your tests should be mocked automatically
3 | // automock: false,
4 |
5 | // Stop running tests after the first failure
6 | // bail: false,
7 |
8 | // Respect "browser" field in package.json when resolving modules
9 | // browser: false,
10 |
11 | // The directory where Jest should store its cached dependency information
12 | // cacheDirectory: "/var/folders/xp/bp966jvd70j25zc1cz6sd905psfs4r/T/jest_ueoec8",
13 |
14 | // Automatically clear mock calls and instances between every test
15 | clearMocks: false,
16 |
17 | // Indicates whether the coverage information should be collected while executing the test
18 | collectCoverage: true,
19 |
20 | // An array of glob patterns indicating a set of files for which coverage information should be collected
21 | collectCoverageFrom: [
22 | '/src/**/*{js,ts,jsx,tsx}',
23 | '!/src/**/*stories',
24 | '!**/node_modules/**',
25 | ],
26 |
27 | // The directory where Jest should output its coverage files
28 | coverageDirectory: 'coverage',
29 |
30 | // An array of regexp pattern strings used to skip coverage collection
31 | coveragePathIgnorePatterns: ['/node_modules/', 'stories', 'dist'],
32 |
33 | // A list of reporter names that Jest uses when writing coverage reports
34 | coverageReporters: ['lcov', 'text-summary'],
35 |
36 | // An object that configures minimum threshold enforcement for coverage results
37 | // coverageThreshold: null,
38 |
39 | // Make calling deprecated APIs throw helpful error messages
40 | // errorOnDeprecated: false,
41 |
42 | // Force coverage collection from ignored files usin a array of glob patterns
43 | // forceCoverageMatch: [],
44 |
45 | // A path to a module which exports an async function that is triggered once before all test suites
46 | // globalSetup: null,
47 |
48 | // A path to a module which exports an async function that is triggered once after all test suites
49 | // globalTeardown: null,
50 |
51 | // A set of global variables that need to be available in all test environments
52 | // globals: {},
53 |
54 | // An array of directory names to be searched recursively up from the requiring module's location
55 | // moduleDirectories: [
56 | // "node_modules"
57 | // ],
58 |
59 | // An array of file extensions your modules use
60 | // moduleFileExtensions: [
61 | // "js",
62 | // "json",
63 | // "jsx",
64 | // "node"
65 | // ],
66 |
67 | // A map from regular expressions to module names that allow to stub out resources with a single module
68 | // moduleNameMapper: {},
69 |
70 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
71 | // modulePathIgnorePatterns: [],
72 |
73 | // Activates notifications for test results
74 | // notify: false,
75 |
76 | // An enum that specifies notification mode. Requires { notify: true }
77 | // notifyMode: "always",
78 |
79 | // A preset that is used as a base for Jest's configuration
80 | preset: 'ts-jest',
81 |
82 | // Run tests from one or more projects
83 | // projects: null,
84 |
85 | // Use this configuration option to add custom reporters to Jest
86 | // reporters: undefined,
87 |
88 | // Automatically reset mock state between every test
89 | // resetMocks: false,
90 |
91 | // Reset the module registry before running each individual test
92 | // resetModules: false,
93 |
94 | // A path to a custom resolver
95 | // resolver: null,
96 |
97 | // Automatically restore mock state between every test
98 | // restoreMocks: false,
99 |
100 | // The root directory that Jest should scan for tests and modules within
101 | // rootDir: null,
102 |
103 | // A list of paths to directories that Jest should use to search for files in
104 | roots: ['src'],
105 |
106 | // Allows you to use a custom runner instead of Jest's default test runner
107 | // runner: "jest-runner",
108 |
109 | // The paths to modules that run some code to configure or set up the testing environment before each test
110 | // setupFiles: [],
111 |
112 | // The path to a module that runs some code to configure or set up the testing framework before each test
113 | // setupTestFrameworkScriptFile: null,
114 |
115 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
116 | // snapshotSerializers: [],
117 |
118 | // The test environment that will be used for testing
119 | testEnvironment: 'jsdom',
120 |
121 | // Options that will be passed to the testEnvironment
122 | // testEnvironmentOptions: {},
123 |
124 | // Adds a location field to test results
125 | // testLocationInResults: false,
126 |
127 | // The glob patterns Jest uses to detect test files
128 | // testMatch: [
129 | // "**/__tests__/**/*.js?(x)",
130 | // "**/?(*.)+(spec|test).js?(x)"
131 | // ],
132 |
133 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
134 | // testPathIgnorePatterns: [
135 | // "/node_modules/"
136 | // ],
137 |
138 | // The regexp pattern Jest uses to detect test files
139 | // testRegex: "",
140 |
141 | // This option allows the use of a custom results processor
142 | // testResultsProcessor: null,
143 |
144 | // This option allows use of a custom test runner
145 | // testRunner: "jasmine2",
146 |
147 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
148 | // testURL: "http://localhost",
149 |
150 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
151 | // timers: "real",
152 |
153 | // A map from regular expressions to paths to transformers
154 | // transform: null,
155 |
156 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
157 | // transformIgnorePatterns: [
158 | // "/node_modules/"
159 | // ],
160 |
161 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
162 | // unmockedModulePathPatterns: undefined,
163 |
164 | // Indicates whether each individual test should be reported during the run
165 | // verbose: null,
166 |
167 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
168 | // watchPathIgnorePatterns: [],
169 |
170 | // Whether to use watchman for file crawling
171 | // watchman: true,
172 | };
173 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 react-infinite-scroll-component
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-infinite-scroll-component",
3 | "version": "6.1.0",
4 | "description": "An Infinite Scroll component in react.",
5 | "source": "src/index.tsx",
6 | "main": "dist/index.js",
7 | "unpkg": "dist/index.umd.js",
8 | "module": "dist/index.es.js",
9 | "types": "dist/index.d.ts",
10 | "scripts": {
11 | "build": "rimraf dist && rollup -c",
12 | "prepublish": "yarn build",
13 | "storybook": "start-storybook -p 6006",
14 | "build-storybook": "build-storybook",
15 | "codecov": "codecov",
16 | "lint": "eslint 'src/**/*.{ts,tsx,js,jsx}'",
17 | "lint:fix": "yarn lint --fix",
18 | "prettier:check": "prettier --check 'src/**/*'",
19 | "prettify": "prettier --write 'src/**/*'",
20 | "ts-check": "tsc -p tsconfig.json --noEmit",
21 | "test": "jest"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/ankeetmaini/react-infinite-scroll-component.git"
26 | },
27 | "keywords": [
28 | "react",
29 | "infinite-scroll",
30 | "infinite",
31 | "scroll",
32 | "component",
33 | "react-component"
34 | ],
35 | "author": "Ankeet Maini ",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/ankeetmaini/react-infinite-scroll-component/issues"
39 | },
40 | "homepage": "https://github.com/ankeetmaini/react-infinite-scroll-component#readme",
41 | "peerDependencies": {
42 | "react": ">=16.0.0"
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.6.2",
46 | "@storybook/addon-actions": "^5.2.1",
47 | "@storybook/addon-info": "^5.2.1",
48 | "@storybook/addon-links": "^5.2.1",
49 | "@storybook/addons": "^5.2.1",
50 | "@storybook/react": "^5.2.1",
51 | "@testing-library/react": "^9.2.0",
52 | "@types/jest": "^24.0.18",
53 | "@types/react": "^16.9.2",
54 | "@types/react-dom": "^16.9.1",
55 | "@types/storybook__react": "^4.0.2",
56 | "@types/throttle-debounce": "^2.1.0",
57 | "@typescript-eslint/eslint-plugin": "^2.3.2",
58 | "@typescript-eslint/parser": "^2.3.2",
59 | "awesome-typescript-loader": "^5.2.1",
60 | "babel-loader": "^8.0.6",
61 | "eslint": "^6.5.1",
62 | "eslint-config-prettier": "^6.3.0",
63 | "eslint-plugin-react": "^7.15.0",
64 | "husky": ">=1",
65 | "jest": "^24.9.0",
66 | "lint-staged": ">=8",
67 | "prettier": "1.18.2",
68 | "react": "^16.10.1",
69 | "react-docgen-typescript-loader": "^3.2.1",
70 | "react-dom": "^16.10.1",
71 | "rimraf": "^3.0.0",
72 | "rollup": "^1.26.3",
73 | "rollup-plugin-node-resolve": "^5.2.0",
74 | "rollup-plugin-typescript2": "^0.25.2",
75 | "ts-jest": "^24.1.0",
76 | "typescript": "^3.7.2"
77 | },
78 | "dependencies": {
79 | "throttle-debounce": "^2.1.0"
80 | },
81 | "husky": {
82 | "hooks": {
83 | "pre-commit": "yarn run ts-check && lint-staged"
84 | }
85 | },
86 | "lint-staged": {
87 | "*.{js,css,json,md}": [
88 | "prettier --write",
89 | "git add"
90 | ],
91 | "*.js": [
92 | "prettier --write",
93 | "eslint --fix",
94 | "git add"
95 | ],
96 | "*.{ts,tsx}": [
97 | "prettier --write",
98 | "eslint --fix",
99 | "git add"
100 | ]
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import typescript from 'rollup-plugin-typescript2';
3 | import pkg from './package.json';
4 | export default {
5 | input: './src/index.tsx',
6 | output: [
7 | {
8 | file: pkg.main,
9 | format: 'cjs',
10 | sourcemap: true,
11 | },
12 | {
13 | file: pkg.module,
14 | format: 'es',
15 | sourcemap: true,
16 | },
17 | {
18 | file: pkg.unpkg,
19 | format: 'iife',
20 | sourcemap: true,
21 | name: 'InfiniteScroll',
22 | },
23 | ],
24 | external: [...Object.keys(pkg.peerDependencies || {})],
25 | plugins: [resolve(), typescript({ useTsconfigDeclarationDir: true })],
26 | };
27 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, cleanup } from '@testing-library/react';
3 | import InfiniteScroll from '../index';
4 |
5 | describe('React Infinite Scroll Component', () => {
6 | const originalConsoleError = console.error;
7 |
8 | afterEach(() => {
9 | cleanup();
10 | console.error = originalConsoleError;
11 | });
12 |
13 | it('renders .infinite-scroll-component', () => {
14 | const { container } = render(
15 | {}}
20 | >
21 |
22 |
23 | );
24 | expect(
25 | container.querySelectorAll('.infinite-scroll-component').length
26 | ).toBe(1);
27 | });
28 |
29 | it('renders custom class', () => {
30 | const { container } = render(
31 | {}}
37 | >
38 |
39 |
40 | );
41 | expect(container.querySelectorAll('.custom-class').length).toBe(1);
42 | });
43 |
44 | it('renders children when passed in', () => {
45 | const { container } = render(
46 | {}}
51 | >
52 |
53 |
54 | );
55 | expect(container.querySelectorAll('.child').length).toBe(1);
56 | });
57 |
58 | it('calls scroll handler if provided, when user scrolls', () => {
59 | jest.useFakeTimers();
60 | const onScrollMock = jest.fn();
61 |
62 | const { container } = render(
63 | {}}
70 | >
71 |
72 |
73 | );
74 |
75 | const scrollEvent = new Event('scroll');
76 | const node = container.querySelector(
77 | '.infinite-scroll-component'
78 | ) as HTMLElement;
79 |
80 | node.dispatchEvent(scrollEvent);
81 | jest.runOnlyPendingTimers();
82 | expect(setTimeout).toHaveBeenCalledTimes(1);
83 | expect(onScrollMock).toHaveBeenCalled();
84 | });
85 |
86 | describe('When missing the dataLength prop', () => {
87 | it('throws an error', () => {
88 | console.error = jest.fn();
89 | const props = { loader: 'Loading...', hasMore: false, next: () => {} };
90 |
91 | // @ts-ignore
92 | expect(() => render()).toThrow(Error);
93 | // @ts-ignore
94 | expect(console.error.mock.calls[0][0]).toContain(
95 | '"dataLength" is missing'
96 | );
97 | });
98 | });
99 |
100 | describe('When user scrolls to the bottom', () => {
101 | it('does not show loader if hasMore is false', () => {
102 | const { container, queryByText } = render(
103 | {}}
109 | >
110 |
111 |
112 | );
113 |
114 | const scrollEvent = new Event('scroll');
115 | const node = container.querySelector(
116 | '.infinite-scroll-component'
117 | ) as HTMLElement;
118 | node.dispatchEvent(scrollEvent);
119 | expect(queryByText('Loading...')).toBeFalsy();
120 | });
121 |
122 | it('shows loader if hasMore is true', () => {
123 | const { container, getByText } = render(
124 | {}}
130 | height={100}
131 | >
132 |
133 |
134 | );
135 |
136 | const scrollEvent = new Event('scroll');
137 | const node = container.querySelector(
138 | '.infinite-scroll-component'
139 | ) as HTMLElement;
140 | node.dispatchEvent(scrollEvent);
141 | expect(getByText('Loading...')).toBeTruthy();
142 | });
143 | });
144 |
145 | it('shows end message', () => {
146 | const { queryByText } = render(
147 | {}}
153 | >
154 |
155 |
156 | );
157 | expect(queryByText('Reached end.')).toBeTruthy();
158 | });
159 |
160 | it('adds a classname to the outer div', () => {
161 | const { container } = render(
162 | {}}
166 | loader={Loading...
}
167 | >
168 |
169 |
170 | );
171 | const outerDiv = container.getElementsByClassName(
172 | 'infinite-scroll-component__outerdiv'
173 | );
174 | expect(outerDiv.length).toBeTruthy();
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ReactNode, CSSProperties } from 'react';
2 | import { throttle } from 'throttle-debounce';
3 | import { ThresholdUnits, parseThreshold } from './utils/threshold';
4 |
5 | type Fn = () => any;
6 | export interface Props {
7 | next: Fn;
8 | hasMore: boolean;
9 | children: ReactNode;
10 | loader: ReactNode;
11 | scrollThreshold?: number | string;
12 | endMessage?: ReactNode;
13 | style?: CSSProperties;
14 | height?: number | string;
15 | scrollableTarget?: ReactNode;
16 | hasChildren?: boolean;
17 | inverse?: boolean;
18 | pullDownToRefresh?: boolean;
19 | pullDownToRefreshContent?: ReactNode;
20 | releaseToRefreshContent?: ReactNode;
21 | pullDownToRefreshThreshold?: number;
22 | refreshFunction?: Fn;
23 | onScroll?: (e: MouseEvent) => any;
24 | dataLength: number;
25 | initialScrollY?: number;
26 | className?: string;
27 | }
28 |
29 | interface State {
30 | showLoader: boolean;
31 | pullToRefreshThresholdBreached: boolean;
32 | prevDataLength: number | undefined;
33 | }
34 |
35 | export default class InfiniteScroll extends Component {
36 | constructor(props: Props) {
37 | super(props);
38 |
39 | this.state = {
40 | showLoader: false,
41 | pullToRefreshThresholdBreached: false,
42 | prevDataLength: props.dataLength,
43 | };
44 |
45 | this.throttledOnScrollListener = throttle(150, this.onScrollListener).bind(
46 | this
47 | );
48 | this.onStart = this.onStart.bind(this);
49 | this.onMove = this.onMove.bind(this);
50 | this.onEnd = this.onEnd.bind(this);
51 | }
52 |
53 | private throttledOnScrollListener: (e: MouseEvent) => void;
54 | private _scrollableNode: HTMLElement | undefined | null;
55 | private el: HTMLElement | undefined | Window & typeof globalThis;
56 | private _infScroll: HTMLDivElement | undefined;
57 | private lastScrollTop = 0;
58 | private actionTriggered = false;
59 | private _pullDown: HTMLDivElement | undefined;
60 |
61 | // variables to keep track of pull down behaviour
62 | private startY = 0;
63 | private currentY = 0;
64 | private dragging = false;
65 |
66 | // will be populated in componentDidMount
67 | // based on the height of the pull down element
68 | private maxPullDownDistance = 0;
69 |
70 | componentDidMount() {
71 | if (typeof this.props.dataLength === 'undefined') {
72 | throw new Error(
73 | `mandatory prop "dataLength" is missing. The prop is needed` +
74 | ` when loading more content. Check README.md for usage`
75 | );
76 | }
77 |
78 | this._scrollableNode = this.getScrollableTarget();
79 | this.el = this.props.height
80 | ? this._infScroll
81 | : this._scrollableNode || window;
82 |
83 | if (this.el) {
84 | this.el.addEventListener('scroll', this
85 | .throttledOnScrollListener as EventListenerOrEventListenerObject);
86 | }
87 |
88 | if (
89 | typeof this.props.initialScrollY === 'number' &&
90 | this.el &&
91 | this.el instanceof HTMLElement &&
92 | this.el.scrollHeight > this.props.initialScrollY
93 | ) {
94 | this.el.scrollTo(0, this.props.initialScrollY);
95 | }
96 |
97 | if (this.props.pullDownToRefresh && this.el) {
98 | this.el.addEventListener('touchstart', this.onStart);
99 | this.el.addEventListener('touchmove', this.onMove);
100 | this.el.addEventListener('touchend', this.onEnd);
101 |
102 | this.el.addEventListener('mousedown', this.onStart);
103 | this.el.addEventListener('mousemove', this.onMove);
104 | this.el.addEventListener('mouseup', this.onEnd);
105 |
106 | // get BCR of pullDown element to position it above
107 | this.maxPullDownDistance =
108 | (this._pullDown &&
109 | this._pullDown.firstChild &&
110 | (this._pullDown.firstChild as HTMLDivElement).getBoundingClientRect()
111 | .height) ||
112 | 0;
113 | this.forceUpdate();
114 |
115 | if (typeof this.props.refreshFunction !== 'function') {
116 | throw new Error(
117 | `Mandatory prop "refreshFunction" missing.
118 | Pull Down To Refresh functionality will not work
119 | as expected. Check README.md for usage'`
120 | );
121 | }
122 | }
123 | }
124 |
125 | componentWillUnmount() {
126 | if (this.el) {
127 | this.el.removeEventListener('scroll', this
128 | .throttledOnScrollListener as EventListenerOrEventListenerObject);
129 |
130 | if (this.props.pullDownToRefresh) {
131 | this.el.removeEventListener('touchstart', this.onStart);
132 | this.el.removeEventListener('touchmove', this.onMove);
133 | this.el.removeEventListener('touchend', this.onEnd);
134 |
135 | this.el.removeEventListener('mousedown', this.onStart);
136 | this.el.removeEventListener('mousemove', this.onMove);
137 | this.el.removeEventListener('mouseup', this.onEnd);
138 | }
139 | }
140 | }
141 |
142 | componentDidUpdate(prevProps: Props) {
143 | // do nothing when dataLength is unchanged
144 | if (this.props.dataLength === prevProps.dataLength) return;
145 |
146 | this.actionTriggered = false;
147 |
148 | // update state when new data was sent in
149 | this.setState({
150 | showLoader: false,
151 | });
152 | }
153 |
154 | static getDerivedStateFromProps(nextProps: Props, prevState: State) {
155 | const dataLengthChanged = nextProps.dataLength !== prevState.prevDataLength;
156 |
157 | // reset when data changes
158 | if (dataLengthChanged) {
159 | return {
160 | ...prevState,
161 | prevDataLength: nextProps.dataLength,
162 | };
163 | }
164 | return null;
165 | }
166 |
167 | getScrollableTarget = () => {
168 | if (this.props.scrollableTarget instanceof HTMLElement)
169 | return this.props.scrollableTarget;
170 | if (typeof this.props.scrollableTarget === 'string') {
171 | return document.getElementById(this.props.scrollableTarget);
172 | }
173 | if (this.props.scrollableTarget === null) {
174 | console.warn(`You are trying to pass scrollableTarget but it is null. This might
175 | happen because the element may not have been added to DOM yet.
176 | See https://github.com/ankeetmaini/react-infinite-scroll-component/issues/59 for more info.
177 | `);
178 | }
179 | return null;
180 | };
181 |
182 | onStart: EventListener = (evt: Event) => {
183 | if (this.lastScrollTop) return;
184 |
185 | this.dragging = true;
186 |
187 | if (evt instanceof MouseEvent) {
188 | this.startY = evt.pageY;
189 | } else if (evt instanceof TouchEvent) {
190 | this.startY = evt.touches[0].pageY;
191 | }
192 | this.currentY = this.startY;
193 |
194 | if (this._infScroll) {
195 | this._infScroll.style.willChange = 'transform';
196 | this._infScroll.style.transition = `transform 0.2s cubic-bezier(0,0,0.31,1)`;
197 | }
198 | };
199 |
200 | onMove: EventListener = (evt: Event) => {
201 | if (!this.dragging) return;
202 |
203 | if (evt instanceof MouseEvent) {
204 | this.currentY = evt.pageY;
205 | } else if (evt instanceof TouchEvent) {
206 | this.currentY = evt.touches[0].pageY;
207 | }
208 |
209 | // user is scrolling down to up
210 | if (this.currentY < this.startY) return;
211 |
212 | if (
213 | this.currentY - this.startY >=
214 | Number(this.props.pullDownToRefreshThreshold)
215 | ) {
216 | this.setState({
217 | pullToRefreshThresholdBreached: true,
218 | });
219 | }
220 |
221 | // so you can drag upto 1.5 times of the maxPullDownDistance
222 | if (this.currentY - this.startY > this.maxPullDownDistance * 1.5) return;
223 |
224 | if (this._infScroll) {
225 | this._infScroll.style.overflow = 'visible';
226 | this._infScroll.style.transform = `translate3d(0px, ${this.currentY -
227 | this.startY}px, 0px)`;
228 | }
229 | };
230 |
231 | onEnd: EventListener = () => {
232 | this.startY = 0;
233 | this.currentY = 0;
234 |
235 | this.dragging = false;
236 |
237 | if (this.state.pullToRefreshThresholdBreached) {
238 | this.props.refreshFunction && this.props.refreshFunction();
239 | this.setState({
240 | pullToRefreshThresholdBreached: false,
241 | });
242 | }
243 |
244 | requestAnimationFrame(() => {
245 | // this._infScroll
246 | if (this._infScroll) {
247 | this._infScroll.style.overflow = 'auto';
248 | this._infScroll.style.transform = 'none';
249 | this._infScroll.style.willChange = 'unset';
250 | }
251 | });
252 | };
253 |
254 | isElementAtTop(target: HTMLElement, scrollThreshold: string | number = 0.8) {
255 | const clientHeight =
256 | target === document.body || target === document.documentElement
257 | ? window.screen.availHeight
258 | : target.clientHeight;
259 |
260 | const threshold = parseThreshold(scrollThreshold);
261 |
262 | if (threshold.unit === ThresholdUnits.Pixel) {
263 | return (
264 | target.scrollTop <=
265 | threshold.value + clientHeight - target.scrollHeight + 1
266 | );
267 | }
268 |
269 | return (
270 | target.scrollTop <=
271 | threshold.value / 100 + clientHeight - target.scrollHeight + 1
272 | );
273 | }
274 |
275 | isElementAtBottom(
276 | target: HTMLElement,
277 | scrollThreshold: string | number = 0.8
278 | ) {
279 | const clientHeight =
280 | target === document.body || target === document.documentElement
281 | ? window.screen.availHeight
282 | : target.clientHeight;
283 |
284 | const threshold = parseThreshold(scrollThreshold);
285 |
286 | if (threshold.unit === ThresholdUnits.Pixel) {
287 | return (
288 | target.scrollTop + clientHeight >= target.scrollHeight - threshold.value
289 | );
290 | }
291 |
292 | return (
293 | target.scrollTop + clientHeight >=
294 | (threshold.value / 100) * target.scrollHeight
295 | );
296 | }
297 |
298 | onScrollListener = (event: MouseEvent) => {
299 | if (typeof this.props.onScroll === 'function') {
300 | // Execute this callback in next tick so that it does not affect the
301 | // functionality of the library.
302 | setTimeout(() => this.props.onScroll && this.props.onScroll(event), 0);
303 | }
304 |
305 | const target =
306 | this.props.height || this._scrollableNode
307 | ? (event.target as HTMLElement)
308 | : document.documentElement.scrollTop
309 | ? document.documentElement
310 | : document.body;
311 |
312 | // return immediately if the action has already been triggered,
313 | // prevents multiple triggers.
314 | if (this.actionTriggered) return;
315 |
316 | const atBottom = this.props.inverse
317 | ? this.isElementAtTop(target, this.props.scrollThreshold)
318 | : this.isElementAtBottom(target, this.props.scrollThreshold);
319 |
320 | // call the `next` function in the props to trigger the next data fetch
321 | if (atBottom && this.props.hasMore) {
322 | this.actionTriggered = true;
323 | this.setState({ showLoader: true });
324 | this.props.next && this.props.next();
325 | }
326 |
327 | this.lastScrollTop = target.scrollTop;
328 | };
329 |
330 | render() {
331 | const style = {
332 | height: this.props.height || 'auto',
333 | overflow: 'auto',
334 | WebkitOverflowScrolling: 'touch',
335 | ...this.props.style,
336 | } as CSSProperties;
337 | const hasChildren =
338 | this.props.hasChildren ||
339 | !!(
340 | this.props.children &&
341 | this.props.children instanceof Array &&
342 | this.props.children.length
343 | );
344 |
345 | // because heighted infiniteScroll visualy breaks
346 | // on drag down as overflow becomes visible
347 | const outerDivStyle =
348 | this.props.pullDownToRefresh && this.props.height
349 | ? { overflow: 'auto' }
350 | : {};
351 | return (
352 |
356 |
(this._infScroll = infScroll)}
359 | style={style}
360 | >
361 | {this.props.pullDownToRefresh && (
362 |
(this._pullDown = pullDown)}
365 | >
366 |
374 | {this.state.pullToRefreshThresholdBreached
375 | ? this.props.releaseToRefreshContent
376 | : this.props.pullDownToRefreshContent}
377 |
378 |
379 | )}
380 | {this.props.children}
381 | {!this.state.showLoader &&
382 | !hasChildren &&
383 | this.props.hasMore &&
384 | this.props.loader}
385 | {this.state.showLoader && this.props.hasMore && this.props.loader}
386 | {!this.props.hasMore && this.props.endMessage}
387 |
388 |
389 | );
390 | }
391 | }
392 |
--------------------------------------------------------------------------------
/src/stories/InfiniteScrollWithHeight.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import InfiniteScroll from '../index';
4 |
5 | const style = {
6 | height: 30,
7 | border: '1px solid green',
8 | margin: 6,
9 | padding: 8,
10 | };
11 |
12 | export default class App extends React.Component {
13 | state = {
14 | items: Array.from({ length: 20 }),
15 | hasMore: true,
16 | };
17 |
18 | fetchMoreData = () => {
19 | if (this.state.items.length >= 500) {
20 | this.setState({ hasMore: false });
21 | return;
22 | }
23 | // a fake async api call like which sends
24 | // 20 more records in .5 secs
25 | setTimeout(() => {
26 | this.setState({
27 | items: this.state.items.concat(Array.from({ length: 20 })),
28 | });
29 | }, 500);
30 | };
31 |
32 | render() {
33 | return (
34 |
35 |
demo: Infinite Scroll with fixed height
36 |
37 |
Loading...}
42 | height={400}
43 | endMessage={
44 |
45 | Yay! You have seen it all
46 |
47 | }
48 | >
49 | {this.state.items.map((_, index) => (
50 |
51 | div - #{index}
52 |
53 | ))}
54 |
55 |
56 | );
57 | }
58 | }
59 |
60 | render(, document.getElementById('root'));
61 |
--------------------------------------------------------------------------------
/src/stories/PullDownToRefreshInfScroll.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import InfiniteScroll from '../index';
4 |
5 | const style = {
6 | height: 30,
7 | border: '1px solid green',
8 | margin: 6,
9 | padding: 8,
10 | };
11 |
12 | export default class App extends React.Component {
13 | state = {
14 | items: Array.from({ length: 20 }),
15 | };
16 |
17 | fetchMoreData = () => {
18 | // a fake async api call like which sends
19 | // 20 more records in 1.5 secs
20 | setTimeout(() => {
21 | this.setState({
22 | items: this.state.items.concat(Array.from({ length: 20 })),
23 | });
24 | }, 1500);
25 | };
26 |
27 | render() {
28 | return (
29 |
30 |
demo: Pull down to refresh
31 |
32 |
Loading...}
37 | pullDownToRefresh
38 | pullDownToRefreshContent={
39 |
40 | ↓ Pull down to refresh
41 |
42 | }
43 | releaseToRefreshContent={
44 | ↑ Release to refresh
45 | }
46 | refreshFunction={this.fetchMoreData}
47 | >
48 | {this.state.items.map((_, index) => (
49 |
50 | div - #{index}
51 |
52 | ))}
53 |
54 |
55 | );
56 | }
57 | }
58 |
59 | render(, document.getElementById('root'));
60 |
--------------------------------------------------------------------------------
/src/stories/ScrollableTargetInfScroll.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import InfiniteScroll from '../index';
4 |
5 | const style = {
6 | height: 30,
7 | border: '1px solid green',
8 | margin: 6,
9 | padding: 8,
10 | };
11 |
12 | export default class App extends React.Component {
13 | state = {
14 | items: Array.from({ length: 20 }),
15 | };
16 |
17 | fetchMoreData = () => {
18 | // a fake async api call like which sends
19 | // 20 more records in 1.5 secs
20 | setTimeout(() => {
21 | this.setState({
22 | items: this.state.items.concat(Array.from({ length: 20 })),
23 | });
24 | }, 1500);
25 | };
26 |
27 | render() {
28 | return (
29 |
30 |
demo: Infinite Scroll with scrollable target
31 |
32 |
47 |
48 | );
49 | }
50 | }
51 |
52 | render(, document.getElementById('root'));
53 |
--------------------------------------------------------------------------------
/src/stories/ScrolleableTop.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import InfiniteScroll from '../index';
4 |
5 | const style = {
6 | height: 30,
7 | border: '1px solid green',
8 | margin: 6,
9 | padding: 8,
10 | };
11 |
12 | export default class App extends React.Component {
13 | state = {
14 | items: Array.from({ length: 20 }),
15 | };
16 |
17 | fetchMoreData = () => {
18 | // a fake async api call like which sends
19 | // 20 more records in 1.5 secs
20 | setTimeout(() => {
21 | this.setState({
22 | items: this.state.items.concat(Array.from({ length: 20 })),
23 | });
24 | }, 1500);
25 | };
26 |
27 | render() {
28 | return (
29 |
30 |
demo: Infinite Scroll on top
31 |
32 |
57 |
58 | );
59 | }
60 | }
61 |
62 | render(, document.getElementById('root'));
63 |
--------------------------------------------------------------------------------
/src/stories/WindowInfiniteScrollComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InfiniteScroll from '../index';
3 | type State = {
4 | data: number[];
5 | };
6 | export default class WindowInfiniteScrollComponent extends React.Component<
7 | {},
8 | State
9 | > {
10 | state = {
11 | data: new Array(100).fill(1),
12 | };
13 |
14 | next = () => {
15 | setTimeout(() => {
16 | const newData = [...this.state.data, new Array(100).fill(1)];
17 | this.setState({ data: newData });
18 | }, 2000);
19 | };
20 | render() {
21 | return (
22 | <>
23 | Loading...}
27 | dataLength={this.state.data.length}
28 | >
29 | {this.state.data.map((_, i) => (
30 |
34 | #{i + 1} row
35 |
36 | ))}
37 |
38 | >
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/stories/stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import WindowInf from './WindowInfiniteScrollComponent';
5 | import PullDownToRefreshInfScroll from './PullDownToRefreshInfScroll';
6 | import InfiniteScrollWithHeight from './InfiniteScrollWithHeight';
7 | import ScrollableTargetInfiniteScroll from './ScrollableTargetInfScroll';
8 | import ScrolleableTop from './ScrolleableTop';
9 |
10 | const stories = storiesOf('Components', module);
11 |
12 | stories.add('InfiniteScroll', () => , {
13 | info: { inline: true },
14 | });
15 |
16 | stories.add('PullDownToRefresh', () => , {
17 | info: { inline: true },
18 | });
19 |
20 | stories.add('InfiniteScrollWithHeight', () => , {
21 | info: { inline: true },
22 | });
23 |
24 | stories.add(
25 | 'ScrollableTargetInfiniteScroll',
26 | () => ,
27 | {
28 | info: { inline: true },
29 | }
30 | );
31 |
32 | stories.add('InfiniteScrollTop', () => , {
33 | info: { inline: true },
34 | });
35 |
--------------------------------------------------------------------------------
/src/utils/threshold.ts:
--------------------------------------------------------------------------------
1 | export const ThresholdUnits = {
2 | Pixel: 'Pixel',
3 | Percent: 'Percent',
4 | };
5 |
6 | const defaultThreshold = {
7 | unit: ThresholdUnits.Percent,
8 | value: 0.8,
9 | };
10 |
11 | export function parseThreshold(scrollThreshold: string | number) {
12 | if (typeof scrollThreshold === 'number') {
13 | return {
14 | unit: ThresholdUnits.Percent,
15 | value: scrollThreshold * 100,
16 | };
17 | }
18 |
19 | if (typeof scrollThreshold === 'string') {
20 | if (scrollThreshold.match(/^(\d*(\.\d+)?)px$/)) {
21 | return {
22 | unit: ThresholdUnits.Pixel,
23 | value: parseFloat(scrollThreshold),
24 | };
25 | }
26 |
27 | if (scrollThreshold.match(/^(\d*(\.\d+)?)%$/)) {
28 | return {
29 | unit: ThresholdUnits.Percent,
30 | value: parseFloat(scrollThreshold),
31 | };
32 | }
33 |
34 | console.warn(
35 | 'scrollThreshold format is invalid. Valid formats: "120px", "50%"...'
36 | );
37 |
38 | return defaultThreshold;
39 | }
40 |
41 | console.warn('scrollThreshold should be string or number');
42 |
43 | return defaultThreshold;
44 | }
45 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
5 | "module": "ES2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6 | // "lib": [], /* Specify library files to be included in the compilation. */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
10 | "declaration": true /* Generates corresponding '.d.ts' file. */,
11 | "declarationDir": "./dist",
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | // "outDir": "./", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "removeComments": true, /* Do not emit comments to output. */
19 | // "noEmit": true, /* Do not emit outputs. */
20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
23 |
24 | /* Strict Type-Checking Options */
25 | "strict": true /* Enable all strict type-checking options. */,
26 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
27 | "strictNullChecks": true /* Enable strict null checks. */,
28 | "strictFunctionTypes": true /* Enable strict checking of function types. */,
29 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
30 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
31 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
32 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
33 |
34 | /* Additional Checks */
35 | "noUnusedLocals": true /* Report errors on unused locals. */,
36 | "noUnusedParameters": true /* Report errors on unused parameters. */,
37 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
38 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
39 |
40 | /* Module Resolution Options */
41 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
45 | // "typeRoots": [], /* List of folders to include type definitions from. */
46 | // "types": [], /* Type declaration files to be included in compilation. */
47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
48 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
50 |
51 | /* Source Map Options */
52 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
53 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
56 |
57 | /* Experimental Options */
58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
60 | },
61 | "include": [
62 | "src/**/*",
63 | "lint-staged.config.js",
64 | "jest.config.js",
65 | "rollup.config.js"
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------