Repository: researchgate/react-intersection-list
Branch: main
Commit: e873ee45e215
Files: 40
Total size: 70.9 KB
Directory structure:
gitextract_h1ndfqvp/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .prettierignore
├── .spire/
│ └── spire-plugin-tslint.js
├── .storybook/
│ ├── addons.js
│ ├── config.js
│ └── preview-head.html
├── AUTHORS
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs/
│ ├── README.md
│ ├── docs/
│ │ ├── components/
│ │ │ ├── AsyncList/
│ │ │ │ └── index.js
│ │ │ ├── Axis/
│ │ │ │ └── index.js
│ │ │ ├── InfiniteList/
│ │ │ │ └── index.js
│ │ │ └── style.css
│ │ └── index.js
│ └── recipes/
│ └── README.md
├── package.json
├── renovate.json
├── src/
│ ├── List.js
│ ├── Sentinel.js
│ ├── __mocks__/
│ │ └── Sentinel.js
│ ├── __tests__/
│ │ ├── List.spec.js
│ │ ├── Sentinel.spec.js
│ │ ├── __snapshots__/
│ │ │ └── List.spec.js.snap
│ │ └── mockSentinel.js
│ ├── index.js
│ └── utils.js
└── types/
├── index.d.ts
├── test.tsx
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["@researchgate/babel-preset"]
}
================================================
FILE: .editorconfig
================================================
# http://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = false
[*.md]
trim_trailing_whitespace = false
[*.{js,scss}]
charset = utf-8
indent_style = space
insert_final_newline = true
[*.js]
indent_size = 4
[*.scss]
indent_size = 2
[{*.json,.travis.yml,.*rc}]
indent_style = space
indent_size = 2
================================================
FILE: .eslintignore
================================================
node_modules
lib
.docs
types
================================================
FILE: .eslintrc.js
================================================
'use strict';
// This file was created by spire-plugin-eslint for editor support
module.exports = require('@researchgate/spire-config/eslint/react-typescript');
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Code of Conduct
## Our Pledge
In the interest of fostering an open, inclusive, and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, nationality, personal appearance, race, religion, sexual identity and orientation, level of experience, or technical ability.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of different viewpoints and experiences
* Gracefully accepting constructive criticism
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Sustained disruption of discussion
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
The ResearchGate community prioritizes (marginalized) people's safety over (privileged) people's comfort.
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instance of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behavior that they deem inappropriate, threatening, offensive, or harmful.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [researchgate@github.com](mailto:researchgate@github.com). All complaints will be reviewed and investigated, and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regards to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is based on [Contributor Covenant][homepage].
[homepage]: http://contributor-covenant.org
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing Guide
Thanks for taking the time to contribute!
### Table of contents
[Code of Conduct](#code-of-conduct)
[Contribution prerequisites](#contribution-prerequisites)
[Project Setup](#project-setup)
* [How to run the project](#how-to-run-the-project)
* [How to run tests](#how-to-run-tests)
[How can you contribute](#how-can-you-contribute)
* [Reporting bugs](#reporting-bugs)
* [Suggesting enhancements](#suggesting-enhancements)
* [Writing documentation](#writing-documentation)
* [Your first code contribution](#your-first-code-contribution)
[Additional Notes](#additional-notes)
* [Issue and Pull Request Templates](#issue-and-pull-request-templates)
* [Need help?](#need-help?)
## Code of Conduct
This project and everyone contributing to it adheres to the [ResearchGate Code of Conduct](CODE_OF_CONDUCT.md).
By participating you are expected to uphold this code. Please report any behavior you find unacceptable to [researchgate@github.com](mailto:researchgate@github.com).
## Contribution prerequisites
Before you start your work, make sure that you:
* have `node` installed at v6.10.0
* have `npm` installed at 5.3.0
* have `yarn` installed at v0.21.3
* are familiar with `git`
* are familiar with [conventional commits](http://conventionalcommits.org)
* have read and agree to abide by the [ResearchGate Code of Conduct](CODE_OF_CONDUCT.md)
## Project setup
### How to run the project
Create a clone of the repository:
```
git clone https://github.com/researchgate/react-intersection-list.git
cd react-intersection-list
```
Install the dependencies with:
```
npm install
```
Now run storybook:
```
npm run storybook
```
### How to run tests
This project uses [jest](http://facebook.github.io/jest/) for JavaScript testing.
To run tests, execute:
```
yarn test
```
Since coverage is not collected by default when running yarn test, run:
```
yarn coverage
```
To run linters use:
```
yarn lint
```
## How you can contribute
### Reporting bugs
This section guides you through the steps to follow when you submit a bug report for a ResearchGate project.
Following these guidelines makes it easy for the maintainers and community to understand your report, reproduce the behavior, and find related reports.
Before creating a bug report, please check Open Issues as you may find that there is already an issue open for the bug you’ve found. When you create a bug report, be sure to include as much detail as possible and fill out [the required template](ISSUE_TEMPLATE.md), the information it asks for helps us resolve issues faster.
After you have submitted the issue, we'll try to get back to you as soon as possible.
### Suggesting enhancements
This section guides you through submitting a feature suggestion.
All enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/) and, when approved by a core team member or project maintainer, are given the green light to then be turned into a Pull Request.
For the best possible experience, please provide us with:
* **A clear and descriptive title**
* **A step-by-step description of the suggested enhancement** and what it should do
* **Specific examples to demonstrate the steps**. Ideally, you should support it with code snippets, screenshots, and/or animated GIFs
* **Explanation of why this feature would be useful** to this project
* **Your development environment** and context for creating the feature
After you have submitted your Pull Request, we'll try to get back to you as soon as possible.
### Writing documentation
All great projects require good documentation.
There is __always__ room for (better) docs, so why not to contribute to the project by enhancing them?
Please do so via Pull Request.
### Your first code contribution
Unsure where you can start contributing?
We strive to make all our projects easy for beginners to contribute to. Just look out for issues labeled `help-wanted` and `beginner-friendly`, then get stuck in!
If you still need some guidance, consider [this resource](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) and/or contact us.
## Additional notes
### Issue and pull request templates
When filing an issue or pull request, please take the time to fill out the templates we provide in as much detail as you can. This helps ensure that we have all the information we need to provide you with the right support so that your experience contributing to our open-source project runs as smoothly and easily as you would like it to.
### Need help?
If you need any help or require additional information, don't hesitate to contact the project maintainer or any of the contributors.
Thank you for contributing!
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
Thanks for taking the time to file an issue with us.
Please note that this issue template is used **ONLY** for reporting bugs.
If you have an issue that isn’t a bug, please follow the steps listed in the [Contributing](CONTRIBUTING.md).
Thanks!
## Expected behavior
<!--- What should happen -->
## Current behavior
<!--- What is happening instead of the expected behavior -->
## Steps to reproduce
<!--- Provide a link to a live example, a code snippet, or a set of steps to -->
<!--- reproduce this bug. -->
1.
2.
3.
4.
## Context (environment)
<!--- Please provide technical context, as well as possible background -->
<!--- information that can help us identify the problem -->
* **Version**: <!-- compulsory. you must provide your version -->
* **Platform**: <!-- either `uname -a` output, or if Windows, version and 32-bit or
64-bit -->
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Thanks for taking the time to file a pull request with us.
Please take a moment to provide the following details:
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Motivation and context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
## How has this been tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
## Screenshots (if relevant):
## Type of change
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project
- [ ] My change requires a change to the documentation
- [ ] I have updated the documentation accordingly
- [ ] I have read the [CONTRIBUTING](CONTRIBUTING.md) document
- [ ] I have added tests to cover my changes
- [ ] All new and existing tests passed
================================================
FILE: .github/workflows/build.yml
================================================
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: 14
jobs:
tests:
name: Unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache node_modules
uses: actions/cache@v3
id: cache-nodemodules
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --non-interactive
- name: Unit tests
run: yarn test --ci --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
files: ./src/coverage/coverage-final.json
fail_ci_if_error: true
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache node_modules
uses: actions/cache@v3
id: cache-nodemodules
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --non-interactive
- name: Lint
run: yarn lint
docs:
needs: [tests, lint]
if: github.ref == 'refs/heads/main'
name: Update docs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache node_modules
uses: actions/cache@v3
id: cache-nodemodules
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --non-interactive
- name: Build latest app
run: yarn build
- name: Generate storybook documentation
run: yarn build:storybook
- name: Deploy storybook documentation to gh-pages
uses: JamesIves/github-pages-deploy-action@v4.2.5
with:
branch: gh-pages # The branch the action should deploy to.
folder: .docs # The folder the action should deploy.
release:
needs: [tests, lint, docs]
if: github.ref == 'refs/heads/main'
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache node_modules
uses: actions/cache@v3
id: cache-nodemodules
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --non-interactive
- name: Release
run: yarn release
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
npm-debug.log*
yarn-error.log
/node_modules
/lib
/src/coverage
/.docs
.DS_Store
================================================
FILE: .prettierignore
================================================
node_modules/
yarn.lock
================================================
FILE: .spire/spire-plugin-tslint.js
================================================
function tslint({ setState }) {
return {
name: 'spire-plugin-tslint',
async precommit() {
setState(state => ({
linters: [
...state.linters,
{ '**/*.tsx?': ['tsc', '--project', 'types'] },
],
}));
},
};
}
module.exports = tslint;
================================================
FILE: .storybook/addons.js
================================================
import '@storybook/addon-options/register';
================================================
FILE: .storybook/config.js
================================================
import { addParameters, configure } from '@storybook/react';
import { withOptions } from '@storybook/addon-options';
withOptions({
name: 'React Intersection List',
url: 'https://github.com/researchgate/react-intersection-list',
showDownPanel: false,
});
addParameters({ options: { theme: {} } });
configure(() => require('../docs/docs/index.js'), module);
================================================
FILE: .storybook/preview-head.html
================================================
<link
href="https://fonts.googleapis.com/css?family=Roboto&display=swap"
rel="stylesheet"
/>
================================================
FILE: AUTHORS
================================================
Luis Merino <mail@luismerino.name>
Daniel Tschinder <daniel.tschinder@researchgate.net>
Sergey Tatarintsev <sergey.tatarintsev@researchgate.net>
================================================
FILE: CHANGELOG.md
================================================
## [3.0.12](https://github.com/researchgate/react-intersection-list/compare/v3.0.11...v3.0.12) (2021-05-11)
### Bug Fixes
* **renovate:** Force default branch to be main ([74fa774](https://github.com/researchgate/react-intersection-list/commit/74fa77474de0c95d94f4bfcf24d7b198bfda98e6))
## [3.0.11](https://github.com/researchgate/react-intersection-list/compare/v3.0.10...v3.0.11) (2020-08-14)
### Bug Fixes
* **security:** Fix security issue with serialize-javascript ([8c3265e](https://github.com/researchgate/react-intersection-list/commit/8c3265e10a4de29a7717edc53f6be7158f41b77d))
## [3.0.10](https://github.com/researchgate/react-intersection-list/compare/v3.0.9...v3.0.10) (2020-05-04)
### Bug Fixes
* fix import path of intersection-observer dependency in sentinel ([#124](https://github.com/researchgate/react-intersection-list/issues/124)) ([659afa8](https://github.com/researchgate/react-intersection-list/commit/659afa8835ede0b07bd49d3d23f8e652312c1dff))
## [3.0.9](https://github.com/researchgate/react-intersection-list/compare/v3.0.8...v3.0.9) (2020-03-26)
### Bug Fixes
* **intersection-observer:** upgrade to latest version and adapt ([1a5fa7c](https://github.com/researchgate/react-intersection-list/commit/1a5fa7c60e1aaab3a4056b4bd1e5049fbb3c7294))
## [3.0.8](https://github.com/researchgate/react-intersection-list/compare/v3.0.7...v3.0.8) (2019-12-01)
### Bug Fixes
* **deps:** lock file maintenance ([#79](https://github.com/researchgate/react-intersection-list/issues/79)) ([fd19604](https://github.com/researchgate/react-intersection-list/commit/fd1960478c2dadb8518c45fc113216168325061a))
## [3.0.7](https://github.com/researchgate/react-intersection-list/compare/v3.0.6...v3.0.7) (2019-11-23)
### Bug Fixes
* **publish:** Do not publish tests ([ef19403](https://github.com/researchgate/react-intersection-list/commit/ef19403259e91b9fd27109d3c56cae5f84679167))
## [3.0.6](https://github.com/researchgate/react-intersection-list/compare/v3.0.5...v3.0.6) (2019-11-23)
### Bug Fixes
* **spire:** change tooling and build system to spire ([36bdcd7](https://github.com/researchgate/react-intersection-list/commit/36bdcd7bc9f61d5c3f33e0e73f0afbdba6cc353f))
# Change Log
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
<a name="3.0.5"></a>
## [3.0.5](https://github.com/researchgate/react-intersection-list/compare/v3.0.4...v3.0.5) (2019-05-16)
<a name="3.0.4"></a>
## [3.0.4](https://github.com/researchgate/react-intersection-list/compare/v3.0.3...v3.0.4) (2019-04-25)
### Bug Fixes
* **reobserve:** using externalUnobserver to operate on the right target ([f6d9dc2](https://github.com/researchgate/react-intersection-list/commit/f6d9dc2))
<a name="3.0.3"></a>
## [3.0.3](https://github.com/researchgate/react-intersection-list/compare/v3.0.2...v3.0.3) (2019-04-23)
<a name="3.0.2"></a>
## [3.0.2](https://github.com/researchgate/react-intersection-list/compare/v3.0.1...v3.0.2) (2018-06-18)
<a name="3.0.1"></a>
## [3.0.1](https://github.com/researchgate/react-intersection-list/compare/v3.0.0...v3.0.1) (2018-05-28)
<a name="3.0.0"></a>
# [3.0.0](https://github.com/researchgate/react-intersection-list/compare/v1.0.2...v3.0.0) (2018-05-15)
### Features
* **prop_api_update:** introducing API for 2.0.0 ([276ca7b](https://github.com/researchgate/react-intersection-list/commit/276ca7b))
* **React16.4:** Migrated codebase to stop using legacy lifecycles ([ab7fc85](https://github.com/researchgate/react-intersection-list/commit/ab7fc85))
### BREAKING CHANGES
* **prop_api_update:** * Replace `currentLength` with more descriptive `itemCount` prop
* New render prop `renderItem` to completement `children` as render prop
* New prop `items` as a unique identifier/symbol to provide an easier integration of reusable lists
* TypeScript Module definitions
<a name="2.0.0"></a>
# [2.0.0](https://github.com/researchgate/react-intersection-list/compare/v1.0.2...v2.0.0) (2018-05-09)
### Features
* **prop_api_update:** introducing API for 2.0.0
### BREAKING CHANGES
* **prop_api_update:** * Replace `currentLength` with more descriptive `itemCount` prop
* New render prop `renderItem` to completement `children` as render prop
* New prop `items` as a unique identifier/symbol to provide an easier integration of reusable lists
* TypeScript Module definitions
<a name="1.0.2"></a>
## [1.0.2](https://github.com/researchgate/react-intersection-list/compare/v1.0.1...v1.0.2) (2018-04-12)
<a name="1.0.1"></a>
## [1.0.1](https://github.com/researchgate/react-intersection-list/compare/v1.0.0...v1.0.1) (2018-02-26)
<a name="1.0.0"></a>
# [1.0.0](https://github.com/researchgate/react-intersection-list/compare/v0.4.1...v1.0.0) (2017-11-24)
### Features
* **React16:** support for React 16 ([12ba423](https://github.com/researchgate/react-intersection-list/commit/12ba423))
* **setRootNode:** save call to getComputedStyle if root node is unchanged ([80659dd](https://github.com/researchgate/react-intersection-list/commit/80659dd))
### BREAKING CHANGES
* **React16:** <sentinel> tag replaced by a <span> tag.
* **React16:** deprecation warning will not longer appear for itemsLength prop.
<a name="0.4.1"></a>
## [0.4.1](https://github.com/researchgate/react-intersection-list/compare/v0.4.0...v0.4.1) (2017-10-11)
* Update @researchgate/react-intersection-observer dependency to the latest version
<a name="0.4.0"></a>
# [0.4.0](https://github.com/researchgate/react-intersection-list/compare/v0.3.2...v0.4.0) (2017-10-10)
### Features
* **currentLength:** replace itemsLength with more explicit currentLength prop name ([de19504](https://github.com/researchgate/react-intersection-list/commit/de19504))
<a name="0.3.2"></a>
## [0.3.2](https://github.com/researchgate/react-intersection-list/compare/v0.3.1...v0.3.2) (2017-09-23)
### Bug Fixes
* **dependencies:** Remove rimraf from dependencies ([b78adde](https://github.com/researchgate/react-intersection-list/commit/b78adde))
<a name="0.3.1"></a>
## [0.3.1](https://github.com/researchgate/react-intersection-list/compare/v0.3.0...v0.3.1) (2017-09-20)
<a name="0.3.0"></a>
# 0.3.0 (2017-09-14)
### Features
* **core_dep_update:** update react-intersection-observer to latest version ([201c2d1](https://github.com/researchgate/react-intersection-list/commit/201c2d1))
<a name="0.1.1"></a>
## [0.1.1](https://github.com/researchgate/react-intersection-list/compare/v0.1.0...v0.1.1) (2017-09-12)
### Bug Fixes
* **sentinel:** ensure sentinel re-observes after items render ([e6d0fa5](https://github.com/researchgate/react-intersection-list/commit/e6d0fa5))
<a name="0.1.0"></a>
# 0.1.0 (2017-09-11)
### Features
* **awaitMore:** improved API with awaitMore ([6c58e95](https://github.com/researchgate/react-intersection-list/commit/6c58e95))
* **hasMore:** prop that allows to bypass the length-size check ([f67ab3d](https://github.com/researchgate/react-intersection-list/commit/f67ab3d))
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) @researchgate/react-intersection-list authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
⚠️ This repository is not as actively maintained as we wish it to be. Feel free to fork this project and fix any outstanding issues you might have, and we'll try to merge relevant changes eventually. We apologize for the inconvenience.
<p align="center">
<img alt="React Intersection List" src=".github/logo.svg" width="888">
</p>
<p align="center">
<a href="https://travis-ci.com/researchgate/react-intersection-list"><img alt="Build Status" src="https://travis-ci.com/researchgate/react-intersection-list.svg?branch=master"></a>
<a href="https://codecov.io/gh/researchgate/react-intersection-list"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/researchgate/react-intersection-list.svg"></a>
<a href="https://www.npmjs.com/package/@researchgate/react-intersection-list"><img alt="NPM version" src="https://img.shields.io/npm/v/@researchgate/react-intersection-list.svg"></a>
<a href="https://github.com/prettier/prettier"><img alt="styled with prettier" src="https://img.shields.io/badge/styled_with-prettier-ff69b4.svg"></a>
</p>
<br>
> **Agent Smith:** ...we have no choice but to continue as planned. Deploy the
> sentinels. Immediately.
**React Intersection List** builds on top of
**[React Intersection Observer](https://github.com/researchgate/react-intersection-observer)**,
using a [sentinel](https://en.wikipedia.org/wiki/Sentinel_value) in the DOM to
deliver a high-performance and smooth scrolling experience, even on low-end
devices.
<br>
<details>
<summary><strong>Table of Contents</strong></summary>
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Getting Started](#getting-started)
- [Why React Intersection List?](#why-react-intersection-list)
- [Documentation](#documentation)
- [How to](#how-to)
- [FAQ](#faq)
- [Props](#props)
- [Examples](#examples)
- [Contributing](#contributing)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
</details>
## Getting Started
```
$ npm install --save @researchgate/react-intersection-list
```
And optionally the
[polyfill](https://github.com/w3c/IntersectionObserver/tree/gh-pages/polyfill):
```
$ npm install --save intersection-observer
```
Next create a `<List>` and two instance methods as props `children` and
`itemRenderer`:
```jsx
import React, { Component } from 'react';
import List from '@researchgate/react-intersection-list';
export default class InfiniteList extends Component {
itemsRenderer = (items, ref) => (
<ul className="list" ref={ref}>
{items}
</ul>
);
itemRenderer = (index, key) => <li key={key}>{index}</li>;
render() {
return (
<List
itemCount={1000}
itemsRenderer={this.itemsRenderer}
renderItem={this.itemRenderer}
/>
);
}
}
```
Note that `<List>` is a `PureComponent` so it can keep itself from re-rendering.
It's highly recommended to avoid creating new functions for `renderItem` and
`itemsRenderer` so that it can successfully shallow compare props on re-render.
## Why React Intersection List?
The approach to infinite scrolling was commonly done by devs implementing
throttled `scroll` event callbacks. This keeps the main thread unnecessarily
busy... No more! `IntersectionObservers` invoke callbacks in a **low-priority
and asynchronous** way by design.
> **Agent Smith:** Never send a human to do a machine's job.
The implementation follows these steps:
1. Add a sentinel close to the last item in the list
2. Update the list moving the internal cursor
3. Trigger a callback when the sentinel comes into view
4. Reposition the recycled sentinel at the end
5. Repeat (∞) ?
## Documentation
### How to
Provided an `itemsRenderer` prop you must attach the `ref` argument to your
scrollable DOM element:
```jsx
<div ref={ref}>{items}</div>
```
This element specifies `overflow: auto|scroll` and it'll become the
`IntersectionObserver root`. If the `overflow` property isn't found, then
`window` will be used as the `root` instead.
The `sentinel` element is by default detached from the list when the current
size reaches the available length, unless you're using `awaitMore`. In case your
list is in memory and you rely on the list for incremental rendering only, the
default detaching behavior suffices. If you're loading more items in an
asynchoronous way, make sure you switch `awaitMore` once you reach the total
length (bottom of the list).
The prop `itemCount` must be used if the prop `items` is not provided, and
viceversa. Calculating the list size is done by adding the current size and the
page size until the items' length is reached.
### FAQ
<details>
<summary>Why am I receiving too many `onIntersection` callbacks</summary>
We extend `PureComponent`. That means, if the parent component re-renders and the _props_ passed to your `<List />` don't
hold the same reference anymore, the list re-renders and we accidentally restart the `IntersectionObserver` of the `Sentinel`.
</details>
<br />
<details>
<summary>Do I always need to assign the `ref`?</summary>
Yes, the ref callback will be used as the `root` and is forwarded to the `IntersectionObserver` within the `Sentinel`.
</details>
<br />
<details>
<summary>What's the `threshold` value, and why does it need a *unit*?</summary>
The `threshold` value is the amount of space needed before the `sentinel` intersects with the root. The prop is
transformed into a valid `rootMargin` property for the `IntersectionObserver`, depending on the `axis` you select. As a
sidenote, we believe that a percentage unit works best for responsive layouts.
</details>
<br />
<details>
<summary>I am getting a console warning when I first load the list</summary>
<blockquote>The sentinel detected a viewport with a bigger size than the size of its items...</blockquote>
The prop `pageSize` is `10` by default, so make sure you're not falling short on items when you first render the
component. The idea of an infinite scrolling list is that items overflow the viewport, so that users have the impression that there're always more items available.
</details>
<br />
<details>
<summary>Why doesn't the list render my updated list element(s)?</summary>
The list renders items based on its props. An update somewhere else in your app (or within your list item) might update
your list element(s), but if your list's `currentLength` prop for instance, remains unchanged, the list prevents a
re-render. Updating the entire infinite list when one of its items has changed is far from optimal. Instead, update each item individually with some form of `connect()` function or observables.
</details>
<br />
<details>
<summary>Are you planning to implement a "virtual list mode" like react-virtualized?</summary>
Yes, there's already an [open issue](https://github.com/researchgate/react-intersection-list/issues/2) to implement a
mode using occlusion culling. It will be implemented in a future release. If you can't wait, you could help us out by opening a Pull Request :)
</details>
### Props
| property | type | default | description |
| --------------------- | ------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `renderItem/children` | `(index: number, key: number) => React.Element` | `(index, key) => <div key={key}>{index}</div>` | render function as children or render props;<br />gets call once for each item. |
| `itemsRenderer` | `(items: Array(React.Element), ref: HTMLElement) => React.Element` | `(items, ref) => <div ref={ref}>{items}</div>` | render function for the list's<br />root element, often returning a scrollable element. |
| `itemCount/items` | `number/Array (or Iterable Object)` | `0` | item count to render. |
| `awaitMore` | `boolean` | | if true keeps the sentinel from detaching. |
| `onIntersection` | `(size: number, pageSize: number) => void` | | invoked when the sentinel comes into view. |
| `threshold` | `string` | `100px` | value in absolute `px` or `%`<br />as spacing before the sentinel hits the edge of the list's viewport. |
| `axis` | `string` | `y` | scroll direction: `y` == vertical and `x` == horizontal |
| `pageSize` | `number` | `10` | number of items to render each hit. |
| `initialIndex` | `number` | `0` | start position of iterator of items. |
### Examples
Find multiple examples under:
[https://researchgate.github.io/react-intersection-list/](https://researchgate.github.io/react-intersection-list/)
## Contributing
We'd love your help on creating React Intersection List!
Before you do, please read our [Code of Conduct](.github/CODE_OF_CONDUCT.md) so
you know what we expect when you contribute to our projects.
Our [Contributing Guide](.github/CONTRIBUTING.md) tells you about our
development process and what we're looking for, gives you instructions on how to
issue bugs and suggest features, and explains how you can build and test your
changes.
**Haven't contributed to an open source project before?** No problem!
[Contributing Guide](.github/CONTRIBUTING.md) has you covered as well.
================================================
FILE: docs/README.md
================================================
## Documentation
Welcome to the code docs section! Here you'll find some implementation use cases:
* Visit our [examples](https://researchgate.github.io/react-intersection-list/) page
* Take a look at the [recipes](./recipes) page
================================================
FILE: docs/docs/components/AsyncList/index.js
================================================
import React, { Component } from 'react';
import List from '../../../../src';
const PAGE_SIZE = 20;
export default class AsyncList extends Component {
state = {
awaitMore: true,
isLoading: false,
currentPage: 0,
repos: [],
};
feedList(repos) {
this.setState({
awaitMore: repos.length > 0,
isLoading: false,
repos: [...this.state.repos, ...repos],
});
}
handleLoadMore = () => {
const currentPage = this.state.currentPage + 1;
this.setState({
isLoading: true,
currentPage,
});
const url = 'https://api.github.com/users/researchgate/repos';
const qs = `?type=public&per_page=${PAGE_SIZE}&page=${currentPage}`;
const headers = {
'Accept-Encoding': '',
};
const ifNoneMatch = sessionStorage.getItem(`etag_${currentPage}`);
if (ifNoneMatch) {
headers['If-None-Match'] = ifNoneMatch.match(/(?:\d|[a-z])+/)[0];
}
let hasError = false;
fetch(url + qs, { headers })
.then((response) => {
if (!(response.status !== 200)) {
const etag = response.headers.get('etag');
if (etag) {
sessionStorage.setItem(`etag_${currentPage}`, etag);
}
} else {
hasError = true;
}
return response.json();
})
.then((repos) => {
if (hasError) {
throw new Error(repos.message);
}
this.feedList(
repos.filter((repo) => repo.fork === false && repo.language)
);
})
.catch((err) => {
console.error(err); // eslint-disable-line
this.feedList([]);
});
};
renderItems = (items, ref) => (
<div className="list list--compact" ref={ref}>
{items}
</div>
);
renderItem = (index, key) => {
const repo = this.state.repos[index];
return (
<div key={key}>
<a href={repo.html_url} target="_blank">
<strong>{repo.name}</strong>
</a>
<{repo.language}>
</div>
);
};
render() {
return (
<div>
{this.state.isLoading && <div className="loading">Loading</div>}
<List
awaitMore={this.state.awaitMore}
itemsRenderer={this.renderItems}
itemCount={this.state.repos.length}
onIntersection={this.handleLoadMore}
pageSize={PAGE_SIZE}
renderItem={this.renderItem}
/>
</div>
);
}
}
================================================
FILE: docs/docs/components/Axis/index.js
================================================
import React from 'react';
import List from '../../../../src';
const itemsRenderer = (items, ref) => (
<div className="list list--horizontal" ref={ref}>
{items}
</div>
);
// eslint-disable-next-line react/no-multi-comp
const Axis = () => (
<List
axis="x"
itemCount={Infinity}
itemsRenderer={itemsRenderer}
pageSize={40}
/>
);
export default Axis;
================================================
FILE: docs/docs/components/InfiniteList/index.js
================================================
import React from 'react';
import List from '../../../../src';
const itemsRenderer = (items, ref) => (
<div className="list" ref={ref}>
{items}
</div>
);
// eslint-disable-next-line react/no-multi-comp
const InfiniteList = () => (
<List itemCount={Infinity} itemsRenderer={itemsRenderer} pageSize={40} />
);
export default InfiniteList;
================================================
FILE: docs/docs/components/style.css
================================================
body {
font-family: -apple-system, '.SFNSText-Regular', 'San Francisco', Roboto, 'Segoe UI', 'Helvetica Neue',
'Lucida Grande', sans-serif;
margin: 0;
padding: 0;
color: #555;
-webkit-font-smoothing: antialiased;
min-height: 100%;
}
.loading {
background: #0080ff;
color: #fff;
height: 40px;
font-size: 20px;
line-height: 40px;
text-transform: capitalize;
font-weight: bold;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
pointer-events: none;
}
.list {
box-sizing: border-box;
height: 100vh;
background-color: #b2d2ed;
overflow: auto;
position: relative;
}
.list--horizontal {
width: 100%;
display: flex;
overflow-y: visible;
flex-direction: row;
}
.list--horizontal > div {
min-width: 100px;
text-align: center;
}
.list--compact {
height: 333px;
}
.list > div {
background-color: #4b9deb;
color: #fff;
margin: 15px;
padding: 15px;
font-size: 16px;
text-transform: capitalize;
cursor: default;
white-space: nowrap;
}
.list > div > a {
color: white;
text-decoration: none;
margin-right: 8px;
}
.list > div > a:hover {
text-decoration: underline;
}
================================================
FILE: docs/docs/index.js
================================================
import 'intersection-observer'; // eslint-disable-line import/no-extraneous-dependencies
import 'whatwg-fetch'; // eslint-disable-line import/no-extraneous-dependencies
import React from 'react';
import { storiesOf } from '@storybook/react'; // eslint-disable-line import/no-extraneous-dependencies
import InfiniteList from './components/InfiniteList';
import AsyncList from './components/AsyncList';
import Axis from './components/Axis';
import './components/style.css';
storiesOf('Examples', module)
.add('Synchoronous', InfiniteList)
.add('Asynchoronous', () => <AsyncList />)
.add('X-Axis', Axis);
================================================
FILE: docs/recipes/README.md
================================================
## Recipes
### Asynchonous Repo List
When the sentinel comes into view, you can use the callback to load data, create the next items, and attach them. For
this case we're loading Github repositories with pagination. We assume that we don't know the total length and we'll
want to keep fetching until the (unknown) end of the list. The solution here is to pass the prop `awaitMore:bool =
true`, so that the sentinel awaits for more items.
```jsx
import React from 'react';
import List from '@researchgate/react-intersection-list';
const PAGE_SIZE = 20;
export default class extends React.Component {
state = {
awaitMore: true,
isLoading: false,
currentPage: 0,
repos: [],
};
feedList = repos => {
this.setState({
awaitMore: repos.length > 0,
isLoading: false,
repos: [...this.state.repos, ...repos],
});
};
handleLoadMore = () => {
const currentPage = this.state.currentPage + 1;
this.setState({
isLoading: true,
currentPage,
});
const url = 'https://api.github.com/users/researchgate/repos';
const qs = `?type=public&per_page=${PAGE_SIZE}&page=${currentPage}`;
fetch(url + qs)
.then(response => response.json())
.then(this.feedList)
.catch(err => {
throw err;
});
};
renderItems = (items, ref) => (
<div className="list" ref={ref}>
{items}
</div>
);
renderItem = (index, key) => {
const repo = this.state.repos[index];
return (
<div key={key}>
<strong>{repo.name}</strong>
</div>
);
};
render() {
return (
<div>
{this.state.isLoading && <div className="loading">Loading</div>}
<List
awaitMore={this.state.awaitMore}
itemsRenderer={this.renderItems}
itemCount={this.state.repos.length}
onIntersection={this.handleLoadMore}
pageSize={PAGE_SIZE}
renderItem={this.renderItem}
/>
</div>
);
}
}
```
If the total amount of items are prefetched and available, we won't need `awaitMore` and the `pageSize` will be used to
paginate results until we reach the bottom of the list.
### Infinite Synchronous List
```jsx
import React from 'react';
import List from '@researchgate/react-intersection-list';
export default () => <List itemCount={Infinity}>{(index, key) => <div key={key}>{index}</div>}</List>;
```
### Can I submit a new recipe?
Yes, of course!
1. Fork the code repo.
2. Create your new recipe in the correct subfolder within `./docs/docs/components/` (create a new folder if it doesn't
already exist).
3. Make sure you have included a README as well as your source file.
4. Submit a PR.
_If you haven't yet, please read our
[contribution guidelines](https://github.com/researchgate/react-intersection-list/blob/master/.github/CONTRIBUTING.md)._
### What license are the recipes released under?
By default, all newly submitted code is licensed under the MIT license.
### How else can I contribute?
Recipes don't always have to be code - great documentation, tutorials, general tips and even general improvements to our
examples folder are greatly appreciated.
================================================
FILE: package.json
================================================
{
"name": "@researchgate/react-intersection-list",
"description": "React List Component using the Intersection Observer API",
"version": "3.0.12",
"author": "Luis Merino <mail@luismerino.name>",
"engines": {
"node": ">=10.0.0"
},
"bugs": {
"url": "https://github.com/researchgate/react-intersection-list/issues"
},
"dependencies": {
"@researchgate/react-intersection-observer": "^1.1.2",
"prop-types": "^15.7.2",
"warning": "^4.0.3"
},
"devDependencies": {
"@babel/cli": "7.20.7",
"@babel/core": "7.20.12",
"@researchgate/babel-preset": "2.0.14",
"@researchgate/spire-config": "7.0.0",
"@storybook/addon-options": "5.3.21",
"@storybook/react": "6.3.12",
"@types/react": "17.0.53",
"babel-loader": "8.3.0",
"cross-env": "7.0.3",
"intersection-observer": "0.12.2",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-test-renderer": "16.14.0",
"spire": "4.1.2",
"spire-plugin-semantic-release": "4.1.0",
"typescript": "4.9.5",
"whatwg-fetch": "3.6.2"
},
"files": [
"lib",
"types/index.d.ts"
],
"types": "types/index.d.ts",
"homepage": "https://github.com/researchgate/react-intersection-list#readme",
"keywords": [
"Intersection",
"Observer",
"react",
"component",
"list",
"infinite",
"scrollable",
"researchgate"
],
"license": "MIT",
"main": "lib/js/index.js",
"module": "lib/es/index.js",
"peerDependencies": {
"react": "^16.3.2",
"react-dom": "^16.3.2"
},
"repository": {
"type": "git",
"url": "https://github.com/researchgate/react-intersection-list.git"
},
"jest": {
"rootDir": "src",
"testMatch": [
"**/__tests__/**/*.spec.js"
],
"testURL": "http://localhost/"
},
"prettier": "@researchgate/prettier-config",
"spire": {
"extends": [
[
"@researchgate/spire-config",
{
"eslint": "react-typescript"
}
]
],
"plugins": [
"spire-plugin-semantic-release",
"<rootDir>/.spire/spire-plugin-tslint"
]
},
"scripts": {
"build": "yarn build:js && yarn build:es",
"build:js": "cross-env BABEL_ENV=production BABEL_OUTPUT=cjs babel src --out-dir lib/js --ignore **/__tests__,**/__mocks__",
"build:es": "cross-env BABEL_ENV=production BABEL_OUTPUT=esm babel src --out-dir lib/es --ignore **/__tests__,**/__mocks__",
"build:storybook": "build-storybook -o .docs",
"lint": "spire lint",
"prepublishOnly": "yarn build",
"release": "spire release --branches main",
"storybook": "start-storybook -p 9001 -c .storybook",
"test": "spire test",
"ts:check": "tsc --project types"
}
}
================================================
FILE: renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["@researchgate:lib"],
"automergeType": "branch",
"labels": ["dependencies"],
"baseBranches": ["main"]
}
================================================
FILE: src/List.js
================================================
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import warning from 'warning';
import Sentinel from './Sentinel';
import { getItemCount, computeSize } from './utils';
const AXIS_CSS_MAP = { x: 'overflowX', y: 'overflowY' };
class List extends PureComponent {
static propTypes = {
awaitMore: PropTypes.bool,
axis: PropTypes.oneOf(['x', 'y']),
children: PropTypes.func,
initialIndex: PropTypes.number,
items(props, propName) {
const object = props[propName];
if (
object != null &&
!(
Array.isArray(object) ||
typeof object[Symbol.iterator] === 'function'
)
) {
return new Error(
`\`${propName}\` must be of type Array or a native type implementing the iterable interface`
);
}
return undefined;
},
itemCount: PropTypes.number,
itemsRenderer: PropTypes.func,
onIntersection: PropTypes.func,
pageSize: PropTypes.number,
renderItem: PropTypes.func,
threshold: PropTypes.string,
};
static defaultProps = {
axis: 'y',
initialIndex: 0,
itemsRenderer: (items, ref) => <div ref={ref}>{items}</div>,
pageSize: 10,
threshold: '100px',
};
state = {
size: 0,
};
static getDerivedStateFromProps({ pageSize, ...props }, prevState) {
const itemCount = getItemCount(props, true);
let newSize;
if (prevState.size === 0) {
newSize = pageSize;
} else {
if (prevState.itemCount < itemCount) {
newSize = prevState.size + pageSize;
} else {
newSize = prevState.size + -(prevState.pageSize - pageSize);
}
}
return {
pageSize,
itemCount,
size: computeSize(newSize, itemCount),
};
}
componentDidMount() {
this.prematureIntersectionChecked = this.state.size === 0;
}
setRef = (callback) => {
let prevRootNode;
this.setRootNode = (node) => {
if (node !== prevRootNode) {
prevRootNode = node;
const overflow = window.getComputedStyle(node)[
AXIS_CSS_MAP[this.props.axis]
];
callback(
['auto', 'scroll', 'overlay'].indexOf(overflow) !== -1
? node
: null
);
}
};
};
handleUpdate = ({ isIntersecting }) => {
const { awaitMore, onIntersection } = this.props;
const { size, itemCount, pageSize } = this.state;
if (!this.prematureIntersectionChecked) {
this.prematureIntersectionChecked = true;
warning(
!isIntersecting,
'ReactIntersectionList: the sentinel detected a viewport with a bigger size than the size of its items. ' +
'This could lead to detrimental behavior, e.g.: triggering more than one onIntersection callback at the start.\n' +
'To prevent this, use either a bigger `pageSize` value or avoid using the prop awaitMore initially.'
);
}
if (isIntersecting) {
const nextSize = computeSize(size + pageSize, itemCount);
if (size !== nextSize) {
this.setState({ size: nextSize });
}
if (onIntersection && (!awaitMore || this.awaitIntersection)) {
if (this.awaitIntersection) {
this.awaitIntersection = false;
}
onIntersection(nextSize, pageSize);
}
}
};
getItemRenderer() {
const { children, renderItem } = this.props;
const hasChildren = typeof children !== 'undefined';
const hasRender = typeof renderItem !== 'undefined';
warning(
!(hasChildren && hasRender),
'ReactIntersectionList: cannot use children and renderItem props as render function at the same time.'
);
if (hasChildren) {
return children;
}
return hasRender
? renderItem
: (index, key) => <div key={key}>{index}</div>;
}
renderItems() {
const {
awaitMore,
axis,
initialIndex,
itemsRenderer,
threshold,
} = this.props;
const { size, itemCount } = this.state;
const itemRenderer = this.getItemRenderer();
const items = [];
for (let i = 0; i < size; ++i) {
items.push(itemRenderer(initialIndex + i, i));
}
let sentinel;
if (size < itemCount || awaitMore) {
sentinel = (
<Sentinel
key="sentinel"
threshold={threshold}
axis={axis}
setRef={this.setRef}
onChange={this.handleUpdate}
/>
);
items.push(sentinel);
if (awaitMore) {
this.awaitIntersection = true;
}
}
return itemsRenderer(items, (node) => {
if (node && sentinel) {
this.setRootNode(node);
}
});
}
render() {
return this.renderItems();
}
}
export default List;
================================================
FILE: src/Sentinel.js
================================================
import React, { Component } from 'react';
import Observer from '@researchgate/react-intersection-observer';
import PropTypes from 'prop-types';
import { computeRootMargin } from './utils';
class Sentinel extends Component {
static propTypes = {
axis: PropTypes.oneOf(['x', 'y']).isRequired,
threshold: PropTypes.string.isRequired,
setRef: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
rootElement: undefined,
rootMargin: computeRootMargin(props),
};
this.target = (
<template style={{ height: 1, minWidth: 1, display: 'block' }} />
);
props.setRef(this.setRootElement);
}
static getDerivedStateFromProps({ threshold, axis }, prevState) {
let newState = null;
if (threshold !== prevState.threshold || axis !== prevState.axis) {
newState = {
threshold,
axis,
rootMargin: computeRootMargin({ threshold, axis }),
};
}
return newState;
}
shouldComponentUpdate(nextProps, { rootMargin, rootElement }) {
const {
rootMargin: currentRootMargin,
rootElement: currentRootElement,
} = this.state;
// When the rootMargin stays the same but the sentinel is repositioned, it can fall within
// its threshold prematurely. In this case we don't get any update from the Observer instance.
// We need to guarantee an update, and re-observing is a cheap way to accomplish this.
if (
currentRootMargin === rootMargin &&
currentRootElement === rootElement
) {
this.observer.externalUnobserve();
this.observer.observe();
return false;
}
return true;
}
setRootElement = (rootElement) => {
this.setState({ rootElement });
};
render() {
const { onChange } = this.props;
const { rootElement, rootMargin } = this.state;
return (
<Observer
ref={(instance) => {
this.observer = instance;
}}
disabled={typeof rootElement === 'undefined'}
root={rootElement}
rootMargin={rootMargin}
onChange={onChange}
>
{this.target}
</Observer>
);
}
}
export default Sentinel;
================================================
FILE: src/__mocks__/Sentinel.js
================================================
/* eslint-env jest */
export default jest.genMockFromModule(
'@researchgate/react-intersection-observer'
).default;
================================================
FILE: src/__tests__/List.spec.js
================================================
/* eslint-env jest */
import 'intersection-observer';
import React from 'react';
import renderer from 'react-test-renderer';
import List from '../';
import { getItemCount } from '../utils';
import mockSentinel, { mockCallback } from './mockSentinel';
jest.mock('../Sentinel', () => (props) => {
if (props.setRef) {
props.setRef(mockCallback);
}
return mockSentinel;
});
const target = { nodeType: 1 };
const createTree = (props = {}) =>
renderer.create(<List {...props} />, { createNodeMock: () => target });
beforeEach(() => {
jest.clearAllMocks();
window.getComputedStyle = jest.fn(() => ({
overflowX: 'auto',
overflowY: 'auto',
}));
});
test('renders without crashing', () => {
createTree();
});
test('pure component avoids unnecessary re-rendering', () => {
const props = { pageSize: 5, itemCount: 25 };
const tree = createTree(props);
const spy = jest.spyOn(tree.getInstance(), 'render');
tree.update(<List {...props} />);
expect(spy).not.toHaveBeenCalled();
});
test('throws with two different render function props', () => {
const spy = global.spyOn(console, 'error');
createTree({ children() {}, renderItem() {}, itemCount: 2 });
expect(spy.calls.mostRecent().args[0]).toMatchSnapshot();
});
test('render children function', () => {
const props = { children() {}, itemCount: 2 };
const spy = jest.spyOn(props, 'children');
createTree(props);
expect(spy).toHaveBeenCalledTimes(2);
});
test('render prop function', () => {
const props = { renderItem() {}, itemCount: 2 };
const spy = jest.spyOn(props, 'renderItem');
createTree(props);
expect(spy).toHaveBeenCalledTimes(2);
});
test('throws with both itemCount and items props set', () => {
const spy = global.spyOn(console, 'error');
createTree({ items: 666 });
expect(spy.calls.mostRecent().args[0]).toMatchSnapshot();
});
test('render with items instead of itemCount', () => {
const json = createTree({ items: [1, 2] }).toJSON();
const children = json.children;
expect(children.length).toBe(2);
});
test('render zero items if no props are given for count calculation', () => {
expect(getItemCount({})).toBe(0);
});
test('render with iterable Set', () => {
const items = new Set([1, 2, 3]);
const json = createTree({ items }).toJSON();
const children = json.children;
expect(children.length).toBe(3);
});
describe('renderItem', () => {
test('renderItem given default props', () => {
const spy = jest.fn();
createTree({ itemCount: 10, renderItem: spy });
expect(spy).toHaveBeenCalledTimes(10);
expect(spy).lastCalledWith(9, 9);
});
test('renderItem given `pageSize` and `initialIndex` props', () => {
const spy = jest.fn();
createTree({
initialIndex: 10,
itemCount: 30,
pageSize: 15,
renderItem: spy,
});
expect(spy).toHaveBeenCalledTimes(15);
expect(spy).lastCalledWith(24, 14);
});
});
describe('sentinel', () => {
test('sentinel present if items are available in view', () => {
const json = createTree({ itemCount: 10, pageSize: 5 }).toJSON();
const children = json.children;
expect(children.length).toBe(6);
expect(children[children.length - 1].type).toBe('span');
});
test('sentinel not present if no items are available in view', () => {
const json = createTree({ itemCount: 5, pageSize: 5 }).toJSON();
const children = json.children;
expect(children.length).toBe(5);
expect(children[children.length - 1].type).toBe('div');
});
test('sentinel present again if `itemCount` is increased', () => {
const tree = createTree({ itemCount: 5, pageSize: 5 });
expect(tree.toJSON().children.length).toBe(5);
tree.update(<List pageSize={5} itemCount={20} />);
expect(tree.toJSON().children.length).toBe(11);
});
test('sentinel present again if `pageSize` is increased', () => {
const tree = createTree({ itemCount: 100, pageSize: 10 });
expect(tree.toJSON().children.length).toBe(11);
tree.update(<List itemCount={100} pageSize={20} />);
expect(tree.toJSON().children.length).toBe(21);
});
test('sentinel not present if `itemCount` is lower than `pageSize`', () => {
const tree = createTree({ itemCount: 0 });
const children = tree.toJSON().children;
expect(children).toBeNull();
tree.update(<List itemCount={8} />);
const newChildren = tree.toJSON().children;
expect(newChildren.length).toBe(8);
expect(newChildren[newChildren.length - 1].type).toBe('div');
});
test('sentinel observes with `awaitMore` bypassing the `itemCount` check', () => {
const tree = createTree({ itemCount: 5 });
const children = tree.toJSON().children;
expect(children.length).toBe(5);
tree.update(<List itemCount={5} awaitMore={true} />);
const newChildren = tree.toJSON().children;
expect(newChildren.length).toBe(6);
expect(newChildren[newChildren.length - 1].type).toBe('span');
});
});
describe('root node', () => {
test('ref callback sets root node', () => {
createTree({ itemCount: 20 });
expect(mockCallback).toBeCalledWith(target);
});
test('ref callback does sets root node if unmounting', () => {
renderer.create(<List itemCount={20} />, {
createNodeMock: () => undefined,
});
expect(mockCallback).not.toBeCalled();
});
test('ref callback does sets root node without sentinel', () => {
const tree = createTree({ itemCount: 10 });
expect(tree.getInstance().setRootNode).toBeUndefined();
});
test('ref callback sets a null root if it does not have overflow', () => {
window.getComputedStyle = jest.fn(() => ({
overflowY: 'visible',
}));
createTree({ itemCount: 20 });
expect(mockCallback).toBeCalledWith(null);
});
test('having same root node prevents call getComputedStyle', () => {
const tree = createTree({ itemCount: 20 });
tree.getInstance().setRootNode(target);
expect(window.getComputedStyle).toHaveBeenCalledTimes(1);
});
});
describe('intersection', () => {
test('does not throw if sentinel intersects with zero items on mount', () => {
const spy = global.spyOn(console, 'error');
const instance = createTree({ itemCount: 0 }).getInstance();
instance.handleUpdate({ isIntersecting: true });
expect(spy.calls.count()).toBe(0);
});
test('throws once if sentinel intersects with items on mount', () => {
const spy = global.spyOn(console, 'error');
const instance = createTree({ itemCount: 10 }).getInstance();
instance.handleUpdate({ isIntersecting: true });
expect(spy.calls.mostRecent().args[0]).toMatchSnapshot();
instance.handleUpdate({ isIntersecting: true });
expect(spy.calls.count()).toBe(1);
createTree({ itemCount: 0 })
.getInstance()
.handleUpdate({ isIntersecting: true });
expect(spy.calls.count()).toBe(1);
});
test('sets next size value computed into `pageSize`', () => {
const instance = createTree({ itemCount: 20 }).getInstance();
instance.handleUpdate({ isIntersecting: false });
instance.handleUpdate({ isIntersecting: true });
expect(instance.state.size).toBe(20);
});
test('sets next size value computed into `itemCount`', () => {
const instance = createTree({ itemCount: 15 }).getInstance();
instance.handleUpdate({ isIntersecting: false });
instance.handleUpdate({ isIntersecting: true });
expect(instance.state.size).toBe(15);
});
test('invokes `onIntersection` each time when it is not awaiting intersection', () => {
const spy = jest.fn();
const instance = createTree({
itemCount: 30,
onIntersection: spy,
}).getInstance();
instance.handleUpdate({ isIntersecting: false });
instance.handleUpdate({ isIntersecting: true });
instance.handleUpdate({ isIntersecting: true });
expect(instance.state.size).toBe(30);
expect(spy).toHaveBeenCalledTimes(2);
});
test('invokes `onIntersection` only once when it is awaiting intersection', () => {
const spy = jest.fn();
const props = {
awaitMore: true,
itemCount: 10,
onIntersection: spy,
};
const tree = createTree(props);
tree.getInstance().handleUpdate({ isIntersecting: false });
tree.getInstance().handleUpdate({ isIntersecting: true });
tree.getInstance().handleUpdate({ isIntersecting: true });
expect(tree.getInstance().state.size).toBe(10);
tree.update(<List {...props} itemCount={20} />);
tree.getInstance().handleUpdate({ isIntersecting: true });
expect(spy).toHaveBeenCalledTimes(2);
});
});
describe('getDerivedStateFromProps', () => {
test('pageSize increases', () => {
const tree = createTree({ itemCount: 40 });
tree.update(<List itemCount={40} pageSize={20} />);
expect(tree.getInstance().state.size).toBe(20);
});
test('pageSize decreases', () => {
const tree = createTree({ itemCount: 40 });
tree.update(<List itemCount={40} pageSize={5} />);
expect(tree.getInstance().state.size).toBe(5);
});
test('itemCount increases', () => {
const tree = createTree({ itemCount: 20 });
tree.update(<List itemCount={40} pageSize={10} />);
expect(tree.getInstance().state.size).toBe(20);
});
test('itemCount decreases', () => {
const tree = createTree({ itemCount: 20 });
tree.update(<List itemCount={5} pageSize={10} />);
expect(tree.getInstance().state.size).toBe(5);
});
test('both pageSize and itemCount update', () => {
const tree = createTree({ itemCount: 20 });
tree.update(<List itemCount={30} pageSize={15} />);
expect(tree.getInstance().state.size).toBe(25);
tree.update(<List itemCount={20} pageSize={15} />);
expect(tree.getInstance().state.size).toBe(20);
tree.update(<List itemCount={30} pageSize={5} />);
expect(tree.getInstance().state.size).toBe(25);
tree.update(<List itemCount={40} pageSize={10} />);
expect(tree.getInstance().state.size).toBe(35);
});
});
================================================
FILE: src/__tests__/Sentinel.spec.js
================================================
/* eslint-env jest */
import 'intersection-observer';
import React from 'react';
import renderer from 'react-test-renderer';
import { IntersectionObserver as Observer } from '@researchgate/react-intersection-observer/lib/js/IntersectionObserver';
import Sentinel from '../Sentinel';
import { computeRootMargin } from '../utils';
const defaultProps = {
axis: 'y',
threshold: '100px',
setRef: jest.fn(),
onChange() {},
};
const createTree = (props = defaultProps) =>
renderer.create(<Sentinel {...props} />);
describe('constructor', () => {
const propTypes = Sentinel.propTypes;
beforeAll(() => {
Sentinel.propTypes = {};
});
afterAll(() => {
Sentinel.propTypes = propTypes;
});
test('calls computeRootMargin and setRef', () => {
const utils = require('../utils');
const original = utils.computeRootMargin;
const computeRootMarginSpy = jest.fn();
utils.computeRootMargin = computeRootMarginSpy;
const TestSentinel = require('../Sentinel').default;
const renderMock = jest
.spyOn(TestSentinel.prototype, 'render')
.mockImplementation(() => {});
const setRefMock = jest.fn();
const props = { setRef: setRefMock };
const tree = renderer.create(<TestSentinel {...props} />);
expect(computeRootMarginSpy).toBeCalledWith(props);
expect(setRefMock).toBeCalledWith(tree.getInstance().setRootElement);
computeRootMarginSpy.mockRestore();
renderMock.mockRestore();
utils.computeRootMargin = original;
});
});
describe('render', () => {
test('first time sets a disabled observer', () => {
const testRenderer = createTree().root;
const { props } = testRenderer.findByType(Observer);
expect(props).toHaveProperty('disabled', true);
expect(props).toHaveProperty('root', undefined);
});
test('re-renders when setRef callback is called', () => {
const instance = createTree().getInstance();
const spy = (instance.setState = jest.fn());
const setRefMock = instance.props.setRef.mock;
const setRefCallback = setRefMock.calls[setRefMock.calls.length - 1][0];
setRefCallback(null);
expect(spy).toHaveBeenCalledTimes(1);
});
test('prevents re-render if root and rootMargin stay the same', () => {
const tree = createTree();
const spy = jest.spyOn(tree.getInstance(), 'render');
tree.update(<Sentinel {...defaultProps} />);
expect(spy).not.toBeCalled();
});
test('does re-observer if root and rootMargin stay the same', () => {
const tree = createTree();
const instance = tree.getInstance();
const spy1 = jest.spyOn(instance.observer, 'externalUnobserve');
const spy2 = jest.spyOn(instance.observer, 'observe');
tree.update(<Sentinel {...defaultProps} />);
expect(spy1).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledTimes(1);
});
});
describe('compute', () => {
test('returns computed rootMargin', () => {
expect(computeRootMargin({ threshold: '50%', axis: 'x' })).toBe(
'0% 50%'
);
expect(computeRootMargin({ threshold: '50px', axis: 'y' })).toBe(
'50px 0px'
);
expect(computeRootMargin({ threshold: '50', axis: 'y' })).toBe('50 0');
});
test('new axis or threshold props update rootMargin', () => {
const tree = createTree();
const instance = tree.getInstance();
let prevRootMargin = instance.state.rootMargin;
tree.update(<Sentinel {...{ ...defaultProps, axis: 'x' }} />);
expect(prevRootMargin).not.toBe(instance.state.rootMargin);
prevRootMargin = instance.state.rootMargin;
tree.update(<Sentinel {...{ ...defaultProps, threshold: '200px' }} />);
expect(prevRootMargin).not.toBe(instance.state.rootMargin);
});
});
================================================
FILE: src/__tests__/__snapshots__/List.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`intersection throws once if sentinel intersects with items on mount 1`] = `
"Warning: ReactIntersectionList: the sentinel detected a viewport with a bigger size than the size of its items. This could lead to detrimental behavior, e.g.: triggering more than one onIntersection callback at the start.
To prevent this, use either a bigger \`pageSize\` value or avoid using the prop awaitMore initially."
`;
exports[`throws with both itemCount and items props set 1`] = `
"Warning: Failed prop type: \`items\` must be of type Array or a native type implementing the iterable interface
in List"
`;
exports[`throws with two different render function props 1`] = `"Warning: ReactIntersectionList: cannot use children and renderItem props as render function at the same time."`;
================================================
FILE: src/__tests__/mockSentinel.js
================================================
/* eslint-env jest */
import React from 'react';
const mockCallback = jest.fn();
export { mockCallback };
export default <span />;
================================================
FILE: src/index.js
================================================
export { default } from './List';
================================================
FILE: src/utils.js
================================================
import warning from 'warning';
export function computeRootMargin({ threshold, axis }) {
const margins = [threshold];
const unit = threshold.match(/^-?\d*\.?\d+(px|%)$/) || [''];
const value = `0${unit.pop()}`;
if (axis === 'y') {
margins.push(value);
} else {
margins.unshift(value);
}
return margins.join(' ');
}
export function getItemCount({ itemCount, items }, warnIfConflict) {
const hasItemCount = typeof itemCount !== 'undefined';
const hasItems = typeof items !== 'undefined';
const defaultValue = 0;
if (warnIfConflict) {
warning(
!(hasItemCount && hasItems),
'ReactIntersectionList: cannot use itemCount and items props at the same time, choose one to determine the number of items to render.'
);
}
if (hasItemCount) {
return itemCount;
}
return hasItems ? items.length || items.size || defaultValue : defaultValue;
}
export function computeSize(pageSize, itemCount) {
return Math.max(0, Math.min(pageSize, itemCount));
}
================================================
FILE: types/index.d.ts
================================================
import * as React from 'react';
export default class ListClass extends React.PureComponent<ListProps> {}
type RenderFunction = (index: number, key: number) => JSX.Element | string;
type IterableType =
| {
length: number;
}
| {
size: number;
};
interface ChildrenAsFunction {
children: RenderFunction;
renderItem?: never;
}
interface RenderAsProp {
renderItem: RenderFunction;
children?: never;
}
interface ItemCountScalar {
itemCount: number;
items?: never;
}
interface ItemCountIterable {
items: IterableType;
itemCount?: never;
}
interface OptionalProps {
awaitMore?: boolean;
axis?: 'x' | 'y';
initialIndex?: number;
itemsRenderer?: (
items: IterableType,
ref: (instance: React.ReactInstance) => void
) => JSX.Element;
onIntersection?: (nextSize: number, pageSize: number) => void;
pageSize?: number;
threshold?: string;
}
type RenderPropType = ChildrenAsFunction | RenderAsProp;
type ItemCountPropType = ItemCountScalar | ItemCountIterable;
type ListProps = RenderPropType & ItemCountPropType & OptionalProps;
================================================
FILE: types/test.tsx
================================================
import * as React from 'react';
import List from '..';
<List items={[]}>{() => ''}</List>;
<List itemCount={Infinity} renderItem={() => <b>bold</b>} />;
<List
itemCount={100}
awaitMore={false}
axis="y"
initialIndex={50}
pageSize={25}
threshold="50%"
itemsRenderer={(items, ref) => <div ref={ref}>{items}</div>}
onIntersection={() => {}}
>
{(index, key) => `${index}${key}`}
</List>;
================================================
FILE: types/tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react",
"noEmit": true
}
}
gitextract_h1ndfqvp/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .prettierignore
├── .spire/
│ └── spire-plugin-tslint.js
├── .storybook/
│ ├── addons.js
│ ├── config.js
│ └── preview-head.html
├── AUTHORS
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs/
│ ├── README.md
│ ├── docs/
│ │ ├── components/
│ │ │ ├── AsyncList/
│ │ │ │ └── index.js
│ │ │ ├── Axis/
│ │ │ │ └── index.js
│ │ │ ├── InfiniteList/
│ │ │ │ └── index.js
│ │ │ └── style.css
│ │ └── index.js
│ └── recipes/
│ └── README.md
├── package.json
├── renovate.json
├── src/
│ ├── List.js
│ ├── Sentinel.js
│ ├── __mocks__/
│ │ └── Sentinel.js
│ ├── __tests__/
│ │ ├── List.spec.js
│ │ ├── Sentinel.spec.js
│ │ ├── __snapshots__/
│ │ │ └── List.spec.js.snap
│ │ └── mockSentinel.js
│ ├── index.js
│ └── utils.js
└── types/
├── index.d.ts
├── test.tsx
└── tsconfig.json
SYMBOL INDEX (37 symbols across 8 files)
FILE: .spire/spire-plugin-tslint.js
function tslint (line 1) | function tslint({ setState }) {
FILE: docs/docs/components/AsyncList/index.js
constant PAGE_SIZE (line 4) | const PAGE_SIZE = 20;
class AsyncList (line 6) | class AsyncList extends Component {
method feedList (line 14) | feedList(repos) {
method render (line 87) | render() {
FILE: src/List.js
constant AXIS_CSS_MAP (line 7) | const AXIS_CSS_MAP = { x: 'overflowX', y: 'overflowY' };
class List (line 9) | class List extends PureComponent {
method items (line 15) | items(props, propName) {
method getDerivedStateFromProps (line 50) | static getDerivedStateFromProps({ pageSize, ...props }, prevState) {
method componentDidMount (line 71) | componentDidMount() {
method getItemRenderer (line 120) | getItemRenderer() {
method renderItems (line 139) | renderItems() {
method render (line 180) | render() {
FILE: src/Sentinel.js
class Sentinel (line 6) | class Sentinel extends Component {
method constructor (line 14) | constructor(props) {
method getDerivedStateFromProps (line 29) | static getDerivedStateFromProps({ threshold, axis }, prevState) {
method shouldComponentUpdate (line 41) | shouldComponentUpdate(nextProps, { rootMargin, rootElement }) {
method render (line 64) | render() {
FILE: src/__tests__/List.spec.js
method children (line 43) | children() {}
method renderItem (line 43) | renderItem() {}
method children (line 48) | children() {}
method renderItem (line 55) | renderItem() {}
FILE: src/__tests__/Sentinel.spec.js
method onChange (line 13) | onChange() {}
FILE: src/utils.js
function computeRootMargin (line 3) | function computeRootMargin({ threshold, axis }) {
function getItemCount (line 15) | function getItemCount({ itemCount, items }, warnIfConflict) {
function computeSize (line 34) | function computeSize(pageSize, itemCount) {
FILE: types/index.d.ts
class ListClass (line 3) | class ListClass extends React.PureComponent<ListProps> {}
type RenderFunction (line 5) | type RenderFunction = (index: number, key: number) => JSX.Element | string;
type IterableType (line 7) | type IterableType =
type ChildrenAsFunction (line 15) | interface ChildrenAsFunction {
type RenderAsProp (line 20) | interface RenderAsProp {
type ItemCountScalar (line 25) | interface ItemCountScalar {
type ItemCountIterable (line 30) | interface ItemCountIterable {
type OptionalProps (line 35) | interface OptionalProps {
type RenderPropType (line 48) | type RenderPropType = ChildrenAsFunction | RenderAsProp;
type ItemCountPropType (line 50) | type ItemCountPropType = ItemCountScalar | ItemCountIterable;
type ListProps (line 52) | type ListProps = RenderPropType & ItemCountPropType & OptionalProps;
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (77K chars).
[
{
"path": ".babelrc",
"chars": 48,
"preview": "{\n \"presets\": [\"@researchgate/babel-preset\"]\n}\n"
},
{
"path": ".editorconfig",
"chars": 387,
"preview": "# http://EditorConfig.org\n# top-most EditorConfig file\nroot = true\n\n[*]\nend_of_line = lf\ntrim_trailing_whitespace = true"
},
{
"path": ".eslintignore",
"chars": 28,
"preview": "node_modules\nlib\n.docs\ntypes"
},
{
"path": ".eslintrc.js",
"chars": 161,
"preview": "'use strict';\n// This file was created by spire-plugin-eslint for editor support\nmodule.exports = require('@researchgate"
},
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 2757,
"preview": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open, inclusive, and welcoming environment, we as cont"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 4749,
"preview": "# Contributing Guide\n\nThanks for taking the time to contribute!\n\n### Table of contents\n\n[Code of Conduct](#code-of-condu"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 849,
"preview": "Thanks for taking the time to file an issue with us.\nPlease note that this issue template is used **ONLY** for reporting"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1507,
"preview": "Thanks for taking the time to file a pull request with us.\nPlease take a moment to provide the following details:\n\n<!---"
},
{
"path": ".github/workflows/build.yml",
"chars": 3637,
"preview": "name: CI\n\non:\n push:\n branches: [ main ]\n pull_request:\n branches: [ main ]\n\nenv:\n NODE_VERSION: 14\n\njobs:\n te"
},
{
"path": ".gitignore",
"chars": 79,
"preview": "npm-debug.log*\nyarn-error.log\n/node_modules\n/lib\n/src/coverage\n/.docs\n.DS_Store"
},
{
"path": ".prettierignore",
"chars": 23,
"preview": "node_modules/\nyarn.lock"
},
{
"path": ".spire/spire-plugin-tslint.js",
"chars": 358,
"preview": "function tslint({ setState }) {\n return {\n name: 'spire-plugin-tslint',\n async precommit() {\n "
},
{
"path": ".storybook/addons.js",
"chars": 44,
"preview": "import '@storybook/addon-options/register';\n"
},
{
"path": ".storybook/config.js",
"chars": 371,
"preview": "import { addParameters, configure } from '@storybook/react';\nimport { withOptions } from '@storybook/addon-options';\n\nwi"
},
{
"path": ".storybook/preview-head.html",
"chars": 97,
"preview": "<link\n href=\"https://fonts.googleapis.com/css?family=Roboto&display=swap\"\n rel=\"stylesheet\"\n/>\n"
},
{
"path": "AUTHORS",
"chars": 144,
"preview": "Luis Merino <mail@luismerino.name>\nDaniel Tschinder <daniel.tschinder@researchgate.net>\nSergey Tatarintsev <sergey.tatar"
},
{
"path": "CHANGELOG.md",
"chars": 7096,
"preview": "## [3.0.12](https://github.com/researchgate/react-intersection-list/compare/v3.0.11...v3.0.12) (2021-05-11)\n\n\n### Bug Fi"
},
{
"path": "LICENSE",
"chars": 1107,
"preview": "The MIT License (MIT)\n\nCopyright (c) @researchgate/react-intersection-list authors\n\nPermission is hereby granted, free o"
},
{
"path": "README.md",
"chars": 10633,
"preview": "⚠️ This repository is not as actively maintained as we wish it to be. Feel free to fork this project and fix any outstan"
},
{
"path": "docs/README.md",
"chars": 232,
"preview": "## Documentation\n\nWelcome to the code docs section! Here you'll find some implementation use cases:\n\n* Visit our [exampl"
},
{
"path": "docs/docs/components/AsyncList/index.js",
"chars": 2938,
"preview": "import React, { Component } from 'react';\nimport List from '../../../../src';\n\nconst PAGE_SIZE = 20;\n\nexport default cla"
},
{
"path": "docs/docs/components/Axis/index.js",
"chars": 406,
"preview": "import React from 'react';\nimport List from '../../../../src';\n\nconst itemsRenderer = (items, ref) => (\n <div classNa"
},
{
"path": "docs/docs/components/InfiniteList/index.js",
"chars": 360,
"preview": "import React from 'react';\nimport List from '../../../../src';\n\nconst itemsRenderer = (items, ref) => (\n <div classNa"
},
{
"path": "docs/docs/components/style.css",
"chars": 1180,
"preview": "body {\n font-family: -apple-system, '.SFNSText-Regular', 'San Francisco', Roboto, 'Segoe UI', 'Helvetica Neue',\n 'Lu"
},
{
"path": "docs/docs/index.js",
"chars": 615,
"preview": "import 'intersection-observer'; // eslint-disable-line import/no-extraneous-dependencies\nimport 'whatwg-fetch'; // eslin"
},
{
"path": "docs/recipes/README.md",
"chars": 3456,
"preview": "## Recipes\n\n### Asynchonous Repo List\n\nWhen the sentinel comes into view, you can use the callback to load data, create "
},
{
"path": "package.json",
"chars": 2705,
"preview": "{\n \"name\": \"@researchgate/react-intersection-list\",\n \"description\": \"React List Component using the Intersection Obser"
},
{
"path": "renovate.json",
"chars": 192,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\"@researchgate:lib\"],\n \"automergeType"
},
{
"path": "src/List.js",
"chars": 5581,
"preview": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\nimport warning from 'warning';\nimport "
},
{
"path": "src/Sentinel.js",
"chars": 2544,
"preview": "import React, { Component } from 'react';\nimport Observer from '@researchgate/react-intersection-observer';\nimport PropT"
},
{
"path": "src/__mocks__/Sentinel.js",
"chars": 120,
"preview": "/* eslint-env jest */\nexport default jest.genMockFromModule(\n '@researchgate/react-intersection-observer'\n).default;\n"
},
{
"path": "src/__tests__/List.spec.js",
"chars": 10644,
"preview": "/* eslint-env jest */\nimport 'intersection-observer';\nimport React from 'react';\nimport renderer from 'react-test-render"
},
{
"path": "src/__tests__/Sentinel.spec.js",
"chars": 3971,
"preview": "/* eslint-env jest */\nimport 'intersection-observer';\nimport React from 'react';\nimport renderer from 'react-test-render"
},
{
"path": "src/__tests__/__snapshots__/List.spec.js.snap",
"chars": 830,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`intersection throws once if sentinel intersects with items on mount"
},
{
"path": "src/__tests__/mockSentinel.js",
"chars": 134,
"preview": "/* eslint-env jest */\nimport React from 'react';\n\nconst mockCallback = jest.fn();\n\nexport { mockCallback };\n\nexport defa"
},
{
"path": "src/index.js",
"chars": 34,
"preview": "export { default } from './List';\n"
},
{
"path": "src/utils.js",
"chars": 1066,
"preview": "import warning from 'warning';\n\nexport function computeRootMargin({ threshold, axis }) {\n const margins = [threshold]"
},
{
"path": "types/index.d.ts",
"chars": 1089,
"preview": "import * as React from 'react';\n\nexport default class ListClass extends React.PureComponent<ListProps> {}\n\ntype RenderFu"
},
{
"path": "types/test.tsx",
"chars": 404,
"preview": "import * as React from 'react';\nimport List from '..';\n\n<List items={[]}>{() => ''}</List>;\n\n<List itemCount={Infinity} "
},
{
"path": "types/tsconfig.json",
"chars": 70,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"react\",\n \"noEmit\": true\n }\n}\n"
}
]
About this extraction
This page contains the full source code of the researchgate/react-intersection-list GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (70.9 KB), approximately 18.4k tokens, and a symbol index with 37 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.