mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Vendored react-window
This commit is contained in:
@@ -137,7 +137,6 @@
|
||||
"react-is": "^0.0.0-50b50c26f",
|
||||
"react-test-renderer": "^0.0.0-50b50c26f",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"rimraf": "^2.6.3",
|
||||
"scheduler": "^0.0.0-50b50c26f",
|
||||
|
||||
@@ -26,6 +26,7 @@ module.exports = {
|
||||
resolve: {
|
||||
alias: {
|
||||
src: resolve(__dirname, '../../../src'),
|
||||
'react-window': resolve(__dirname, '../../../vendor/react-window'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@@ -31,6 +31,7 @@ module.exports = {
|
||||
resolve: {
|
||||
alias: {
|
||||
src: resolve(__dirname, '../../../src'),
|
||||
'react-window': resolve(__dirname, '../../../vendor/react-window'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@@ -30,6 +30,7 @@ const config = {
|
||||
resolve: {
|
||||
alias: {
|
||||
src: resolve(__dirname, '../../src'),
|
||||
'react-window': resolve(__dirname, '../../vendor/react-window'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
Changelog
|
||||
------------
|
||||
|
||||
### 1.8.0
|
||||
* 🎉 Added new "smart" align option for grid and list scroll-to-item methods ([gaearon](https://github.com/gaearon) - [#209](https://github.com/bvaughn/react-window/pull/209))
|
||||
|
||||
### 1.7.2
|
||||
* 🐛 Add guards to avoid invalid scroll offsets when `scrollTo()` is called with a negative offset or when `scrollToItem` is called with invalid indices (negative or too large).
|
||||
|
||||
### 1.7.1
|
||||
* 🐛 Fix SSR regression introduced in 1.7.0 - ([Betree](https://github.com/Betree) - [#185](https://github.com/bvaughn/react-window/pull/185))
|
||||
|
||||
### 1.7.0
|
||||
* 🎉 Grid `scrollToItem` supports optional `rowIndex` and `columnIndex` params ([jgoz](https://github.com/jgoz) - [#174](https://github.com/bvaughn/react-window/pull/174))
|
||||
* DEV mode checks for `WeakSet` support before using it to avoid requiring a polyfill for IE11 - ([jgoz](https://github.com/jgoz) - [#167](https://github.com/bvaughn/react-window/pull/167))
|
||||
|
||||
### 1.6.2
|
||||
* 🐛 Bugfix for RTL when scrolling back towards the beginning (right) of the list.
|
||||
|
||||
### 1.6.1
|
||||
* 🐛 Bugfix to account for differences between Chrome and non-Chrome browsers with regard to RTL and "scroll" events.
|
||||
|
||||
### 1.6.0
|
||||
* 🎉 RTL support added for lists and grids. Special thanks to [davidgarsan](https://github.com/davidgarsan) for his support. - [#156](https://github.com/bvaughn/react-window/pull/156)
|
||||
* 🐛 Grid `scrollToItem` methods take scrollbar size into account when aligning items - [#153](https://github.com/bvaughn/react-window/issues/153)
|
||||
|
||||
### 1.5.2
|
||||
* 🐛 Edge case bug fix for `VariableSizeList` and `VariableSizeGrid` when the number of items decreases while a scroll is in progress. - ([iamsolankiamit](https://github.com/iamsolankiamit) - [#138](https://github.com/bvaughn/react-window/pull/138))
|
||||
|
||||
### 1.5.1
|
||||
* 🐛 Updated `getDerivedState` Flow annotations to address a warning in a newer version of Flow.
|
||||
|
||||
### 1.5.0
|
||||
* 🎉 Added advanced memoization helpers methods `areEqual` and `shouldComponentUpdate` for item renderers. - [#114](https://github.com/bvaughn/react-window/issues/114)
|
||||
|
||||
### 1.4.0
|
||||
* 🎉 List and Grid components now "overscan" (pre-render) in both directions when scrolling is not active. When scrolling is in progress, cells are only pre-rendered in the direction being scrolled. This change has been made in an effort to reduce visible flicker when scrolling starts without adding additional overhead during scroll (which is the most performance sensitive time).
|
||||
* 🎉 Grid components now support separate `overscanColumnsCount` and `overscanRowsCount` props. Legacy `overscanCount` prop will continue to work, but with a deprecation warning in DEV mode.
|
||||
* 🐛 Replaced `setTimeout` with `requestAnimationFrame` based timer, to avoid starvation issue for `isScrolling` reset. - [#106](https://github.com/bvaughn/react-window/issues/106)
|
||||
* 🎉 Renamed List and Grid `innerTagName` and `outerTagName` props to `innerElementType` and `outerElementType` to formalize support for attaching arbitrary props (e.g. test ids) to List and Grid inner and outer DOM elements. Legacy `innerTagName` and `outerTagName` props will continue to work, but with a deprecation warning in DEV mode.
|
||||
* 🐛 List re-renders items if `direction` prop changes. - [#104](https://github.com/bvaughn/react-window/issues/104)
|
||||
|
||||
### 1.3.1
|
||||
* 🎉 Pass `itemData` value to custom `itemKey` callbacks when present - [#90](https://github.com/bvaughn/react-window/issues/90))
|
||||
|
||||
### 1.3.0
|
||||
* (Skipped)
|
||||
|
||||
### 1.2.4
|
||||
* 🐛 Added Flow annotations to memoized methods to avoid a Flow warning for newer versions of Flow
|
||||
|
||||
### 1.2.3
|
||||
* 🐛 Relaxed `children` validation checks. They were too strict and didn't support new React APIs like `memo`.
|
||||
|
||||
### 1.2.2
|
||||
* 🐛 Improved Flow types for class component item renderers - ([nicholas-l](https://github.com/nicholas-l) - [#77](https://github.com/bvaughn/react-window/pull/77))
|
||||
|
||||
### 1.2.1
|
||||
* 🎉 Improved Flow types to include optional `itemData` parameter. ([TrySound](https://github.com/TrySound) - [#66](https://github.com/bvaughn/react-window/pull/66))
|
||||
* 🐛 `VariableSizeList` and `VariableSizeGrid` no longer call size getter functions with invalid index when item count is zero.
|
||||
|
||||
### 1.2.0
|
||||
* 🎉 Flow types added to NPM package. ([TrySound](https://github.com/TrySound) - [#40](https://github.com/bvaughn/react-window/pull/40))
|
||||
* 🎉 Relaxed grid `scrollTo` method to make `scrollLeft` and `scrollTop` params _optional_ (so you can only update one axis if desired). - [#63](https://github.com/bvaughn/react-window/pull/63))
|
||||
* 🐛 Fixed invalid `this` pointer in `VariableSizeGrid` that broke the `resetAfter*` methods - [#58](https://github.com/bvaughn/react-window/pull/58))
|
||||
* Upgraded to babel 7 and used shared runtime helpers to reduce package size slightly. ([TrySound](https://github.com/TrySound) - [#48](https://github.com/bvaughn/react-window/pull/48))
|
||||
* Remove `overflow:hidden` from inner container ([souporserious](https://github.com/souporserious) - [#56](https://github.com/bvaughn/react-window/pull/56))
|
||||
|
||||
### 1.1.2
|
||||
* 🐛 Fixed edge case `scrollToItem` bug that caused lists/grids with very few items to have negative scroll offsets.
|
||||
|
||||
### 1.1.1
|
||||
* 🐛 `FixedSizeGrid` and `FixedSizeList` automatically clear style cache when item size props change.
|
||||
|
||||
### 1.1.0
|
||||
* 🎉 Use explicit `constructor` and `super` to generate cleaner component code. ([Andarist](https://github.com/Andarist) - [#26](https://github.com/bvaughn/react-window/pull/26))
|
||||
* 🎉 Add optional `shouldForceUpdate` param reset-index methods to specify `forceUpdate` behavior. ([nihgwu](https://github.com/nihgwu) - [#32](https://github.com/bvaughn/react-window/pull/32))
|
||||
|
||||
### 1.0.3
|
||||
* 🐛 Avoid unnecessary scrollbars for lists (e.g. no horizontal scrollbar for a vertical list) unless content requires them.
|
||||
|
||||
### 1.0.2
|
||||
|
||||
* 🎉 Enable Babel `annotate-pure-calls` option so that classes compiled by "transform-es2015-classes" are annotated with `#__PURE__`. This enables [UglifyJS to remove them if they are not referenced](https://github.com/mishoo/UglifyJS2/pull/1448), improving dead code elimination in application code. ([Andarist](https://github.com/Andarist) - [#20](https://github.com/bvaughn/react-window/pull/20))
|
||||
* 🎉 Update "rollup-plugin-peer-deps-external" and use new `includeDependencies` flag so that the "memoize-one" dependency does not get inlined into the Rollup bundle. ([Andarist](https://github.com/Andarist) - [#19](https://github.com/bvaughn/react-window/pull/19))
|
||||
* 🎉 Enable [Babel "loose" mode](https://babeljs.io/docs/en/babel-preset-env#loose) to reduce package size (-8%). ([Andarist](https://github.com/Andarist) - [#18](https://github.com/bvaughn/react-window/pull/18))
|
||||
|
||||
### 1.0.1
|
||||
Updated `README.md` file to remove `@alpha` tag from NPM installation instructions.
|
||||
|
||||
# 1.0.0
|
||||
Initial release of library. Includes the following components:
|
||||
* `FixedSizeGrid`
|
||||
* `FixedSizeList`
|
||||
* `VariableSizeGrid`
|
||||
* `VariableSizeList`
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Brian Vaughn
|
||||
|
||||
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.
|
||||
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
# react-window
|
||||
|
||||
> React components for efficiently rendering large lists and tabular data
|
||||
|
||||
[](https://yarnpkg.com/en/package/react-window) [](https://travis-ci.org/bvaughn/react-window) [](LICENSE.md)
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Yarn
|
||||
yarn add react-window
|
||||
|
||||
# NPM
|
||||
npm install --save react-window
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Learn more at [react-window.now.sh](https://react-window.now.sh/):
|
||||
|
||||
## Related libraries
|
||||
|
||||
* [`react-virtualized-auto-sizer`](https://npmjs.com/package/react-virtualized-auto-sizer): HOC that grows to fit all of the available space and passes the width and height values to its child.
|
||||
* [`react-window-infinite-loader`](https://npmjs.com/package/react-window-infinite-loader): Helps break large data sets down into chunks that can be just-in-time loaded as they are scrolled into view. It can also be used to create infinite loading lists (e.g. Facebook or Twitter).
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### How is `react-window` different from `react-virtualized`?
|
||||
I wrote `react-virtualized` several years ago. At the time, I was new to both React and the concept of windowing. Because of this, I made a few API decisions that I later came to regret. One of these was adding too many non-essential features and components. Once you add something to an open source project, removing it is pretty painful for users.
|
||||
|
||||
`react-window` is a complete rewrite of `react-virtualized`. I didn't try to solve as many problems or support as many use cases. Instead I focused on making the package **smaller**<sup>1</sup> and **faster**. I also put a lot of thought into making the API (and documentation) as beginner-friendly as possible (with the caveat that windowing is still kind of an advanced use case).
|
||||
|
||||
If `react-window` provides the functionality your project needs, I would strongly recommend using it instead of `react-virtualized`. However if you need features that only `react-virtualized` provides, you have two options:
|
||||
|
||||
1. Use `react-virtualized`. (It's still widely used by a lot of successful projects!)
|
||||
2. Create a component that decorates one of the `react-window` primitives and adds the functionality you need. You may even want to release this component to NPM (as its own, standalone package)! 🙂
|
||||
|
||||
<sup>1 - Adding a `react-virtualized` list to a CRA project increases the (gzipped) build size by ~33.5 KB. Adding a `react-window` list to a CRA project increases the (gzipped) build size by <2 KB.</sup>
|
||||
|
||||
### Can a list or a grid fill 100% the width or height of a page?
|
||||
|
||||
Yes. I recommend using the [`react-virtualized-auto-sizer` package](https://npmjs.com/package/react-virtualized-auto-sizer):
|
||||
|
||||
<img width="336" alt="screen shot 2019-03-07 at 7 29 08 pm" src="https://user-images.githubusercontent.com/29597/54005716-50f41880-410f-11e9-864f-a65bbdf49e07.png">
|
||||
|
||||
Here's a [Code Sandbox demo](https://codesandbox.io/s/3vnx878jk5).
|
||||
|
||||
### Why is my list blank when I scroll?
|
||||
|
||||
If your list looks something like this...
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/29597/54005352-eb535c80-410d-11e9-80b2-d3d02db1f599.gif" width="302" height="152">
|
||||
|
||||
...then you probably forgot to use the `style` parameter! Libraries like react-window work by absolutely positioning the list items (via an inline style), so don't forget to attach it to the DOM element you render!
|
||||
|
||||
<img width="257" alt="screen shot 2019-03-07 at 7 21 48 pm" src="https://user-images.githubusercontent.com/29597/54005433-45ecb880-410e-11e9-8721-420ff1a153da.png">
|
||||
|
||||
### Can I lazy load data for my list?
|
||||
|
||||
Yes. I recommend using the [`react-window-infinite-loader` package](https://npmjs.com/package/react-window-infinite-loader):
|
||||
|
||||
<img width="368" alt="screen shot 2019-03-07 at 7 32 32 pm" src="https://user-images.githubusercontent.com/29597/54006733-653a1480-4113-11e9-907b-08ca5e27b3f9.png">
|
||||
|
||||
Here's a [Code Sandbox demo](https://codesandbox.io/s/5wqo7z2np4).
|
||||
|
||||
### Can I attach custom properties or event handlers?
|
||||
|
||||
Yes, using the `outerElementType` prop.
|
||||
|
||||
<img width="412" alt="Screen Shot 2019-03-12 at 8 58 09 AM" src="https://user-images.githubusercontent.com/29597/54215333-f9ee9a80-44a4-11e9-9142-34c67026d950.png">
|
||||
|
||||
Here's a [Code Sandbox demo](https://codesandbox.io/s/4zqx79nww0).
|
||||
|
||||
### Can I add gutter or padding between items?
|
||||
|
||||
Yes, although it requires a bit of inline styling.
|
||||
|
||||
<img width="416" alt="Screen Shot 2019-03-26 at 6 33 56 PM" src="https://user-images.githubusercontent.com/29597/55043972-c14ad700-4ff5-11e9-9caa-2e9f4d85f96c.png">
|
||||
|
||||
Here's a [Code Sandbox demo](https://codesandbox.io/s/2w8wmlm89p).
|
||||
|
||||
### Does this library support "sticky" items?
|
||||
|
||||
Yes, although it requires a small amount of user code. Here's a [Code Sandbox demo](https://codesandbox.io/s/0mk3qwpl4l).
|
||||
|
||||
## License
|
||||
|
||||
MIT © [bvaughn](https://github.com/bvaughn)
|
||||
+1894
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from '../src';
|
||||
+1883
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from '../src';
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"name": "react-window",
|
||||
"version": "1.8.0",
|
||||
"description":
|
||||
"React components for efficiently rendering large, scrollable lists and tabular data",
|
||||
"author":
|
||||
"Brian Vaughn <brian.david.vaughn@gmail.com> (https://github.com/bvaughn/)",
|
||||
"contributors": [
|
||||
"Brian Vaughn <brian.david.vaughn@gmail.com> (https://github.com/bvaughn/)"
|
||||
],
|
||||
"license": "MIT",
|
||||
"homepage": "http://react-window.now.sh/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bvaughn/react-window.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/bvaughn/react-window/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"reactjs",
|
||||
"virtual",
|
||||
"window",
|
||||
"windowed",
|
||||
"list",
|
||||
"scrolling",
|
||||
"infinite",
|
||||
"virtualized",
|
||||
"table",
|
||||
"grid",
|
||||
"spreadsheet"
|
||||
],
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"files": ["dist", "src/*.js"],
|
||||
"scripts": {
|
||||
"flow": "flow check --max-warnings=0 src && flow check website",
|
||||
"precommit": "lint-staged",
|
||||
"prettier": "prettier --write '**/*.{js,json,css}'",
|
||||
"linc": "lint-staged",
|
||||
"lint": "eslint '**/*.js'",
|
||||
"test": "cross-env CI=1 react-scripts test --env=jsdom",
|
||||
"test:watch": "react-scripts test --env=jsdom",
|
||||
"build:flow":
|
||||
"cp flow-template dist/index.cjs.js.flow && cp flow-template dist/index.esm.js.flow",
|
||||
"build:source": "rollup -c",
|
||||
"build": "del dist && mkdir dist && yarn build:flow && yarn build:source",
|
||||
"start": "rollup -c -w",
|
||||
"prepare": "yarn run build",
|
||||
"website:build": "cd website && yarn build",
|
||||
"website:deploy": "cd website && yarn deploy",
|
||||
"website:run": "cd website && yarn start"
|
||||
},
|
||||
"lint-staged": {
|
||||
"{website,src}/**/*.{js,json,css}": ["prettier --write", "git add"],
|
||||
"**/*.js": "eslint --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"babel-plugin-annotate-pure-calls": "^0.3.0",
|
||||
"cross-env": "^5.1.4",
|
||||
"del-cli": "^1.1.0",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-config-react-app": "^2.1.0",
|
||||
"eslint-config-standard": "^11.0.0",
|
||||
"eslint-config-standard-react": "^6.0.0",
|
||||
"eslint-plugin-flowtype": "^2.47.1",
|
||||
"eslint-plugin-import": "^2.11.0",
|
||||
"eslint-plugin-jsx-a11y": "^5",
|
||||
"eslint-plugin-node": "^6.0.1",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"eslint-plugin-promise": "^3.7.0",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"flow-bin": "^0.80.0",
|
||||
"gh-pages": "^1.1.0",
|
||||
"lint-staged": "^7.0.5",
|
||||
"prettier": "^1.12.1",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-is": "^16.8.4",
|
||||
"react-scripts": "^1.1.1",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
"rollup": "^1.4.1",
|
||||
"rollup-plugin-babel": "^4.3.2",
|
||||
"rollup-plugin-commonjs": "^9.2.1",
|
||||
"rollup-plugin-node-resolve": "^4.0.1"
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// @flow
|
||||
|
||||
import createGridComponent from './createGridComponent';
|
||||
|
||||
import type { Props, ScrollToAlign } from './createGridComponent';
|
||||
|
||||
const FixedSizeGrid = createGridComponent({
|
||||
getColumnOffset: ({ columnWidth }: Props<any>, index: number): number =>
|
||||
index * ((columnWidth: any): number),
|
||||
|
||||
getColumnWidth: ({ columnWidth }: Props<any>, index: number): number =>
|
||||
((columnWidth: any): number),
|
||||
|
||||
getRowOffset: ({ rowHeight }: Props<any>, index: number): number =>
|
||||
index * ((rowHeight: any): number),
|
||||
|
||||
getRowHeight: ({ rowHeight }: Props<any>, index: number): number =>
|
||||
((rowHeight: any): number),
|
||||
|
||||
getEstimatedTotalHeight: ({ rowCount, rowHeight }: Props<any>) =>
|
||||
((rowHeight: any): number) * rowCount,
|
||||
|
||||
getEstimatedTotalWidth: ({ columnCount, columnWidth }: Props<any>) =>
|
||||
((columnWidth: any): number) * columnCount,
|
||||
|
||||
getOffsetForColumnAndAlignment: (
|
||||
{ columnCount, columnWidth, width }: Props<any>,
|
||||
columnIndex: number,
|
||||
align: ScrollToAlign,
|
||||
scrollLeft: number,
|
||||
instanceProps: typeof undefined,
|
||||
scrollbarSize: number
|
||||
): number => {
|
||||
const maxOffset = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
columnCount * ((columnWidth: any): number) - width,
|
||||
columnIndex * ((columnWidth: any): number)
|
||||
)
|
||||
);
|
||||
const minOffset = Math.max(
|
||||
0,
|
||||
columnIndex * ((columnWidth: any): number) -
|
||||
width +
|
||||
scrollbarSize +
|
||||
((columnWidth: any): number)
|
||||
);
|
||||
|
||||
if (align === 'smart') {
|
||||
if (scrollLeft >= minOffset - width && scrollLeft <= maxOffset + width) {
|
||||
align = 'auto';
|
||||
} else {
|
||||
align = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
switch (align) {
|
||||
case 'start':
|
||||
return maxOffset;
|
||||
case 'end':
|
||||
return minOffset;
|
||||
case 'center':
|
||||
return Math.round(minOffset + (maxOffset - minOffset) / 2);
|
||||
case 'auto':
|
||||
default:
|
||||
if (scrollLeft >= minOffset && scrollLeft <= maxOffset) {
|
||||
return scrollLeft;
|
||||
} else if (scrollLeft - minOffset < maxOffset - scrollLeft) {
|
||||
return minOffset;
|
||||
} else {
|
||||
return maxOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getOffsetForRowAndAlignment: (
|
||||
{ rowHeight, height, rowCount }: Props<any>,
|
||||
rowIndex: number,
|
||||
align: ScrollToAlign,
|
||||
scrollTop: number,
|
||||
instanceProps: typeof undefined,
|
||||
scrollbarSize: number
|
||||
): number => {
|
||||
const maxOffset = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
rowCount * ((rowHeight: any): number) - height,
|
||||
rowIndex * ((rowHeight: any): number)
|
||||
)
|
||||
);
|
||||
const minOffset = Math.max(
|
||||
0,
|
||||
rowIndex * ((rowHeight: any): number) -
|
||||
height +
|
||||
scrollbarSize +
|
||||
((rowHeight: any): number)
|
||||
);
|
||||
|
||||
if (align === 'smart') {
|
||||
if (scrollTop >= minOffset - height && scrollTop <= maxOffset + height) {
|
||||
align = 'auto';
|
||||
} else {
|
||||
align = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
switch (align) {
|
||||
case 'start':
|
||||
return maxOffset;
|
||||
case 'end':
|
||||
return minOffset;
|
||||
case 'center':
|
||||
return Math.round(minOffset + (maxOffset - minOffset) / 2);
|
||||
case 'auto':
|
||||
default:
|
||||
if (scrollTop >= minOffset && scrollTop <= maxOffset) {
|
||||
return scrollTop;
|
||||
} else if (scrollTop - minOffset < maxOffset - scrollTop) {
|
||||
return minOffset;
|
||||
} else {
|
||||
return maxOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getColumnStartIndexForOffset: (
|
||||
{ columnWidth, columnCount }: Props<any>,
|
||||
scrollLeft: number
|
||||
): number =>
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
columnCount - 1,
|
||||
Math.floor(scrollLeft / ((columnWidth: any): number))
|
||||
)
|
||||
),
|
||||
|
||||
getColumnStopIndexForStartIndex: (
|
||||
{ columnWidth, columnCount, width }: Props<any>,
|
||||
startIndex: number,
|
||||
scrollLeft: number
|
||||
): number => {
|
||||
const left = startIndex * ((columnWidth: any): number);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
columnCount - 1,
|
||||
startIndex +
|
||||
Math.floor(
|
||||
(width + (scrollLeft - left)) / ((columnWidth: any): number)
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
getRowStartIndexForOffset: (
|
||||
{ rowHeight, rowCount }: Props<any>,
|
||||
scrollTop: number
|
||||
): number =>
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(rowCount - 1, Math.floor(scrollTop / ((rowHeight: any): number)))
|
||||
),
|
||||
|
||||
getRowStopIndexForStartIndex: (
|
||||
{ rowHeight, rowCount, height }: Props<any>,
|
||||
startIndex: number,
|
||||
scrollTop: number
|
||||
): number => {
|
||||
const left = startIndex * ((rowHeight: any): number);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
rowCount - 1,
|
||||
startIndex +
|
||||
Math.floor((height + (scrollTop - left)) / ((rowHeight: any): number))
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
initInstanceProps(props: Props<any>): any {
|
||||
// Noop
|
||||
},
|
||||
|
||||
shouldResetStyleCacheOnItemSizeChange: true,
|
||||
|
||||
validateProps: ({ columnWidth, rowHeight }: Props<any>): void => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof columnWidth !== 'number') {
|
||||
throw Error(
|
||||
'An invalid "columnWidth" prop has been specified. ' +
|
||||
'Value should be a number. ' +
|
||||
`"${
|
||||
columnWidth === null ? 'null' : typeof columnWidth
|
||||
}" was specified.`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof rowHeight !== 'number') {
|
||||
throw Error(
|
||||
'An invalid "rowHeight" prop has been specified. ' +
|
||||
'Value should be a number. ' +
|
||||
`"${rowHeight === null ? 'null' : typeof rowHeight}" was specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default FixedSizeGrid;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// @flow
|
||||
|
||||
import createListComponent from './createListComponent';
|
||||
|
||||
import type { Props, ScrollToAlign } from './createListComponent';
|
||||
|
||||
const FixedSizeList = createListComponent({
|
||||
getItemOffset: ({ itemSize, size }: Props<any>, index: number): number =>
|
||||
index * ((itemSize: any): number),
|
||||
|
||||
getItemSize: ({ itemSize, size }: Props<any>, index: number): number =>
|
||||
((itemSize: any): number),
|
||||
|
||||
getEstimatedTotalSize: ({ itemCount, itemSize }: Props<any>) =>
|
||||
((itemSize: any): number) * itemCount,
|
||||
|
||||
getOffsetForIndexAndAlignment: (
|
||||
{ direction, height, itemCount, itemSize, layout, width }: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number
|
||||
): number => {
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
|
||||
const size = (((isHorizontal ? width : height): any): number);
|
||||
const maxOffset = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
itemCount * ((itemSize: any): number) - size,
|
||||
index * ((itemSize: any): number)
|
||||
)
|
||||
);
|
||||
const minOffset = Math.max(
|
||||
0,
|
||||
index * ((itemSize: any): number) - size + ((itemSize: any): number)
|
||||
);
|
||||
|
||||
if (align === 'smart') {
|
||||
if (
|
||||
scrollOffset >= minOffset - size &&
|
||||
scrollOffset <= maxOffset + size
|
||||
) {
|
||||
align = 'auto';
|
||||
} else {
|
||||
align = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
switch (align) {
|
||||
case 'start':
|
||||
return maxOffset;
|
||||
case 'end':
|
||||
return minOffset;
|
||||
case 'center':
|
||||
return Math.round(minOffset + (maxOffset - minOffset) / 2);
|
||||
case 'auto':
|
||||
default:
|
||||
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
|
||||
return scrollOffset;
|
||||
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
|
||||
return minOffset;
|
||||
} else {
|
||||
return maxOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getStartIndexForOffset: (
|
||||
{ itemCount, itemSize }: Props<any>,
|
||||
offset: number
|
||||
): number =>
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(itemCount - 1, Math.floor(offset / ((itemSize: any): number)))
|
||||
),
|
||||
|
||||
getStopIndexForStartIndex: (
|
||||
{ direction, height, itemCount, itemSize, layout, width }: Props<any>,
|
||||
startIndex: number,
|
||||
scrollOffset: number
|
||||
): number => {
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
|
||||
const offset = startIndex * ((itemSize: any): number);
|
||||
const size = (((isHorizontal ? width : height): any): number);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
itemCount - 1,
|
||||
startIndex +
|
||||
Math.floor(
|
||||
(size + (scrollOffset - offset)) / ((itemSize: any): number)
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
initInstanceProps(props: Props<any>): any {
|
||||
// Noop
|
||||
},
|
||||
|
||||
shouldResetStyleCacheOnItemSizeChange: true,
|
||||
|
||||
validateProps: ({ itemSize }: Props<any>): void => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof itemSize !== 'number') {
|
||||
throw Error(
|
||||
'An invalid "itemSize" prop has been specified. ' +
|
||||
'Value should be a number. ' +
|
||||
`"${itemSize === null ? 'null' : typeof itemSize}" was specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default FixedSizeList;
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
// @flow
|
||||
|
||||
import createGridComponent from './createGridComponent';
|
||||
|
||||
import type { Props, ScrollToAlign } from './createGridComponent';
|
||||
|
||||
const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
|
||||
|
||||
type VariableSizeProps = {|
|
||||
estimatedColumnWidth: number,
|
||||
estimatedRowHeight: number,
|
||||
...Props<any>,
|
||||
|};
|
||||
|
||||
type itemSizeGetter = (index: number) => number;
|
||||
type ItemType = 'column' | 'row';
|
||||
|
||||
type ItemMetadata = {|
|
||||
offset: number,
|
||||
size: number,
|
||||
|};
|
||||
type ItemMetadataMap = { [index: number]: ItemMetadata };
|
||||
type InstanceProps = {|
|
||||
columnMetadataMap: ItemMetadataMap,
|
||||
estimatedColumnWidth: number,
|
||||
estimatedRowHeight: number,
|
||||
lastMeasuredColumnIndex: number,
|
||||
lastMeasuredRowIndex: number,
|
||||
rowMetadataMap: ItemMetadataMap,
|
||||
|};
|
||||
|
||||
const getEstimatedTotalHeight = (
|
||||
{ rowCount }: Props<any>,
|
||||
{ rowMetadataMap, estimatedRowHeight, lastMeasuredRowIndex }: InstanceProps
|
||||
) => {
|
||||
let totalSizeOfMeasuredRows = 0;
|
||||
|
||||
// Edge case check for when the number of items decreases while a scroll is in progress.
|
||||
// https://github.com/bvaughn/react-window/pull/138
|
||||
if (lastMeasuredRowIndex >= rowCount) {
|
||||
lastMeasuredRowIndex = rowCount - 1;
|
||||
}
|
||||
|
||||
if (lastMeasuredRowIndex >= 0) {
|
||||
const itemMetadata = rowMetadataMap[lastMeasuredRowIndex];
|
||||
totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size;
|
||||
}
|
||||
|
||||
const numUnmeasuredItems = rowCount - lastMeasuredRowIndex - 1;
|
||||
const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedRowHeight;
|
||||
|
||||
return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems;
|
||||
};
|
||||
|
||||
const getEstimatedTotalWidth = (
|
||||
{ columnCount }: Props<any>,
|
||||
{
|
||||
columnMetadataMap,
|
||||
estimatedColumnWidth,
|
||||
lastMeasuredColumnIndex,
|
||||
}: InstanceProps
|
||||
) => {
|
||||
let totalSizeOfMeasuredRows = 0;
|
||||
|
||||
// Edge case check for when the number of items decreases while a scroll is in progress.
|
||||
// https://github.com/bvaughn/react-window/pull/138
|
||||
if (lastMeasuredColumnIndex >= columnCount) {
|
||||
lastMeasuredColumnIndex = columnCount - 1;
|
||||
}
|
||||
|
||||
if (lastMeasuredColumnIndex >= 0) {
|
||||
const itemMetadata = columnMetadataMap[lastMeasuredColumnIndex];
|
||||
totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size;
|
||||
}
|
||||
|
||||
const numUnmeasuredItems = columnCount - lastMeasuredColumnIndex - 1;
|
||||
const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedColumnWidth;
|
||||
|
||||
return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems;
|
||||
};
|
||||
|
||||
const getItemMetadata = (
|
||||
itemType: ItemType,
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): ItemMetadata => {
|
||||
let itemMetadataMap, itemSize, lastMeasuredIndex;
|
||||
if (itemType === 'column') {
|
||||
itemMetadataMap = instanceProps.columnMetadataMap;
|
||||
itemSize = ((props.columnWidth: any): itemSizeGetter);
|
||||
lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex;
|
||||
} else {
|
||||
itemMetadataMap = instanceProps.rowMetadataMap;
|
||||
itemSize = ((props.rowHeight: any): itemSizeGetter);
|
||||
lastMeasuredIndex = instanceProps.lastMeasuredRowIndex;
|
||||
}
|
||||
|
||||
if (index > lastMeasuredIndex) {
|
||||
let offset = 0;
|
||||
if (lastMeasuredIndex >= 0) {
|
||||
const itemMetadata = itemMetadataMap[lastMeasuredIndex];
|
||||
offset = itemMetadata.offset + itemMetadata.size;
|
||||
}
|
||||
|
||||
for (let i = lastMeasuredIndex + 1; i <= index; i++) {
|
||||
let size = itemSize(i);
|
||||
|
||||
itemMetadataMap[i] = {
|
||||
offset,
|
||||
size,
|
||||
};
|
||||
|
||||
offset += size;
|
||||
}
|
||||
|
||||
if (itemType === 'column') {
|
||||
instanceProps.lastMeasuredColumnIndex = index;
|
||||
} else {
|
||||
instanceProps.lastMeasuredRowIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
return itemMetadataMap[index];
|
||||
};
|
||||
|
||||
const findNearestItem = (
|
||||
itemType: ItemType,
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
offset: number
|
||||
) => {
|
||||
let itemMetadataMap, lastMeasuredIndex;
|
||||
if (itemType === 'column') {
|
||||
itemMetadataMap = instanceProps.columnMetadataMap;
|
||||
lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex;
|
||||
} else {
|
||||
itemMetadataMap = instanceProps.rowMetadataMap;
|
||||
lastMeasuredIndex = instanceProps.lastMeasuredRowIndex;
|
||||
}
|
||||
|
||||
const lastMeasuredItemOffset =
|
||||
lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0;
|
||||
|
||||
if (lastMeasuredItemOffset >= offset) {
|
||||
// If we've already measured items within this range just use a binary search as it's faster.
|
||||
return findNearestItemBinarySearch(
|
||||
itemType,
|
||||
props,
|
||||
instanceProps,
|
||||
lastMeasuredIndex,
|
||||
0,
|
||||
offset
|
||||
);
|
||||
} else {
|
||||
// If we haven't yet measured this high, fallback to an exponential search with an inner binary search.
|
||||
// The exponential search avoids pre-computing sizes for the full set of items as a binary search would.
|
||||
// The overall complexity for this approach is O(log n).
|
||||
return findNearestItemExponentialSearch(
|
||||
itemType,
|
||||
props,
|
||||
instanceProps,
|
||||
Math.max(0, lastMeasuredIndex),
|
||||
offset
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestItemBinarySearch = (
|
||||
itemType: ItemType,
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
high: number,
|
||||
low: number,
|
||||
offset: number
|
||||
): number => {
|
||||
while (low <= high) {
|
||||
const middle = low + Math.floor((high - low) / 2);
|
||||
const currentOffset = getItemMetadata(
|
||||
itemType,
|
||||
props,
|
||||
middle,
|
||||
instanceProps
|
||||
).offset;
|
||||
|
||||
if (currentOffset === offset) {
|
||||
return middle;
|
||||
} else if (currentOffset < offset) {
|
||||
low = middle + 1;
|
||||
} else if (currentOffset > offset) {
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (low > 0) {
|
||||
return low - 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestItemExponentialSearch = (
|
||||
itemType: ItemType,
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
index: number,
|
||||
offset: number
|
||||
): number => {
|
||||
const itemCount = itemType === 'column' ? props.columnCount : props.rowCount;
|
||||
let interval = 1;
|
||||
|
||||
while (
|
||||
index < itemCount &&
|
||||
getItemMetadata(itemType, props, index, instanceProps).offset < offset
|
||||
) {
|
||||
index += interval;
|
||||
interval *= 2;
|
||||
}
|
||||
|
||||
return findNearestItemBinarySearch(
|
||||
itemType,
|
||||
props,
|
||||
instanceProps,
|
||||
Math.min(index, itemCount - 1),
|
||||
Math.floor(index / 2),
|
||||
offset
|
||||
);
|
||||
};
|
||||
|
||||
const getOffsetForIndexAndAlignment = (
|
||||
itemType: ItemType,
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number,
|
||||
instanceProps: InstanceProps,
|
||||
scrollbarSize: number
|
||||
): number => {
|
||||
const size = itemType === 'column' ? props.width : props.height;
|
||||
const itemMetadata = getItemMetadata(itemType, props, index, instanceProps);
|
||||
|
||||
// Get estimated total size after ItemMetadata is computed,
|
||||
// To ensure it reflects actual measurements instead of just estimates.
|
||||
const estimatedTotalSize =
|
||||
itemType === 'column'
|
||||
? getEstimatedTotalWidth(props, instanceProps)
|
||||
: getEstimatedTotalHeight(props, instanceProps);
|
||||
|
||||
const maxOffset = Math.max(
|
||||
0,
|
||||
Math.min(estimatedTotalSize - size, itemMetadata.offset)
|
||||
);
|
||||
const minOffset = Math.max(
|
||||
0,
|
||||
itemMetadata.offset - size + scrollbarSize + itemMetadata.size
|
||||
);
|
||||
|
||||
if (align === 'smart') {
|
||||
if (scrollOffset >= minOffset - size && scrollOffset <= maxOffset + size) {
|
||||
align = 'auto';
|
||||
} else {
|
||||
align = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
switch (align) {
|
||||
case 'start':
|
||||
return maxOffset;
|
||||
case 'end':
|
||||
return minOffset;
|
||||
case 'center':
|
||||
return Math.round(minOffset + (maxOffset - minOffset) / 2);
|
||||
case 'auto':
|
||||
default:
|
||||
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
|
||||
return scrollOffset;
|
||||
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
|
||||
return minOffset;
|
||||
} else {
|
||||
return maxOffset;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const VariableSizeGrid = createGridComponent({
|
||||
getColumnOffset: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => getItemMetadata('column', props, index, instanceProps).offset,
|
||||
|
||||
getColumnStartIndexForOffset: (
|
||||
props: Props<any>,
|
||||
scrollLeft: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => findNearestItem('column', props, instanceProps, scrollLeft),
|
||||
|
||||
getColumnStopIndexForStartIndex: (
|
||||
props: Props<any>,
|
||||
startIndex: number,
|
||||
scrollLeft: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => {
|
||||
const { columnCount, width } = props;
|
||||
|
||||
const itemMetadata = getItemMetadata(
|
||||
'column',
|
||||
props,
|
||||
startIndex,
|
||||
instanceProps
|
||||
);
|
||||
const maxOffset = scrollLeft + width;
|
||||
|
||||
let offset = itemMetadata.offset + itemMetadata.size;
|
||||
let stopIndex = startIndex;
|
||||
|
||||
while (stopIndex < columnCount - 1 && offset < maxOffset) {
|
||||
stopIndex++;
|
||||
offset += getItemMetadata('column', props, stopIndex, instanceProps).size;
|
||||
}
|
||||
|
||||
return stopIndex;
|
||||
},
|
||||
|
||||
getColumnWidth: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => instanceProps.columnMetadataMap[index].size,
|
||||
|
||||
getEstimatedTotalHeight,
|
||||
getEstimatedTotalWidth,
|
||||
|
||||
getOffsetForColumnAndAlignment: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number,
|
||||
instanceProps: InstanceProps,
|
||||
scrollbarSize: number
|
||||
): number =>
|
||||
getOffsetForIndexAndAlignment(
|
||||
'column',
|
||||
props,
|
||||
index,
|
||||
align,
|
||||
scrollOffset,
|
||||
instanceProps,
|
||||
scrollbarSize
|
||||
),
|
||||
|
||||
getOffsetForRowAndAlignment: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number,
|
||||
instanceProps: InstanceProps,
|
||||
scrollbarSize: number
|
||||
): number =>
|
||||
getOffsetForIndexAndAlignment(
|
||||
'row',
|
||||
props,
|
||||
index,
|
||||
align,
|
||||
scrollOffset,
|
||||
instanceProps,
|
||||
scrollbarSize
|
||||
),
|
||||
|
||||
getRowOffset: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => getItemMetadata('row', props, index, instanceProps).offset,
|
||||
|
||||
getRowHeight: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => instanceProps.rowMetadataMap[index].size,
|
||||
|
||||
getRowStartIndexForOffset: (
|
||||
props: Props<any>,
|
||||
scrollTop: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => findNearestItem('row', props, instanceProps, scrollTop),
|
||||
|
||||
getRowStopIndexForStartIndex: (
|
||||
props: Props<any>,
|
||||
startIndex: number,
|
||||
scrollTop: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => {
|
||||
const { rowCount, height } = props;
|
||||
|
||||
const itemMetadata = getItemMetadata(
|
||||
'row',
|
||||
props,
|
||||
startIndex,
|
||||
instanceProps
|
||||
);
|
||||
const maxOffset = scrollTop + height;
|
||||
|
||||
let offset = itemMetadata.offset + itemMetadata.size;
|
||||
let stopIndex = startIndex;
|
||||
|
||||
while (stopIndex < rowCount - 1 && offset < maxOffset) {
|
||||
stopIndex++;
|
||||
offset += getItemMetadata('row', props, stopIndex, instanceProps).size;
|
||||
}
|
||||
|
||||
return stopIndex;
|
||||
},
|
||||
|
||||
initInstanceProps(props: Props<any>, instance: any): InstanceProps {
|
||||
const {
|
||||
estimatedColumnWidth,
|
||||
estimatedRowHeight,
|
||||
} = ((props: any): VariableSizeProps);
|
||||
|
||||
const instanceProps = {
|
||||
columnMetadataMap: {},
|
||||
estimatedColumnWidth: estimatedColumnWidth || DEFAULT_ESTIMATED_ITEM_SIZE,
|
||||
estimatedRowHeight: estimatedRowHeight || DEFAULT_ESTIMATED_ITEM_SIZE,
|
||||
lastMeasuredColumnIndex: -1,
|
||||
lastMeasuredRowIndex: -1,
|
||||
rowMetadataMap: {},
|
||||
};
|
||||
|
||||
instance.resetAfterColumnIndex = (
|
||||
columnIndex: number,
|
||||
shouldForceUpdate?: boolean = true
|
||||
) => {
|
||||
instance.resetAfterIndices({ columnIndex, shouldForceUpdate });
|
||||
};
|
||||
|
||||
instance.resetAfterRowIndex = (
|
||||
rowIndex: number,
|
||||
shouldForceUpdate?: boolean = true
|
||||
) => {
|
||||
instance.resetAfterIndices({ rowIndex, shouldForceUpdate });
|
||||
};
|
||||
|
||||
instance.resetAfterIndices = ({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
shouldForceUpdate = true,
|
||||
}: {
|
||||
columnIndex?: number,
|
||||
rowIndex?: number,
|
||||
shouldForceUpdate: boolean,
|
||||
}) => {
|
||||
if (typeof columnIndex === 'number') {
|
||||
instanceProps.lastMeasuredColumnIndex = Math.min(
|
||||
instanceProps.lastMeasuredColumnIndex,
|
||||
columnIndex - 1
|
||||
);
|
||||
}
|
||||
if (typeof rowIndex === 'number') {
|
||||
instanceProps.lastMeasuredRowIndex = Math.min(
|
||||
instanceProps.lastMeasuredRowIndex,
|
||||
rowIndex - 1
|
||||
);
|
||||
}
|
||||
|
||||
// We could potentially optimize further by only evicting styles after this index,
|
||||
// But since styles are only cached while scrolling is in progress-
|
||||
// It seems an unnecessary optimization.
|
||||
// It's unlikely that resetAfterIndex() will be called while a user is scrolling.
|
||||
instance._getItemStyleCache(-1);
|
||||
|
||||
if (shouldForceUpdate) {
|
||||
instance.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
return instanceProps;
|
||||
},
|
||||
|
||||
shouldResetStyleCacheOnItemSizeChange: false,
|
||||
|
||||
validateProps: ({ columnWidth, rowHeight }: Props<any>): void => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof columnWidth !== 'function') {
|
||||
throw Error(
|
||||
'An invalid "columnWidth" prop has been specified. ' +
|
||||
'Value should be a function. ' +
|
||||
`"${
|
||||
columnWidth === null ? 'null' : typeof columnWidth
|
||||
}" was specified.`
|
||||
);
|
||||
} else if (typeof rowHeight !== 'function') {
|
||||
throw Error(
|
||||
'An invalid "rowHeight" prop has been specified. ' +
|
||||
'Value should be a function. ' +
|
||||
`"${rowHeight === null ? 'null' : typeof rowHeight}" was specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default VariableSizeGrid;
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
// @flow
|
||||
|
||||
import createListComponent from './createListComponent';
|
||||
|
||||
import type { Props, ScrollToAlign } from './createListComponent';
|
||||
|
||||
const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
|
||||
|
||||
type VariableSizeProps = {|
|
||||
estimatedItemSize: number,
|
||||
...Props<any>,
|
||||
|};
|
||||
|
||||
type itemSizeGetter = (index: number) => number;
|
||||
|
||||
type ItemMetadata = {|
|
||||
offset: number,
|
||||
size: number,
|
||||
|};
|
||||
type InstanceProps = {|
|
||||
itemMetadataMap: { [index: number]: ItemMetadata },
|
||||
estimatedItemSize: number,
|
||||
lastMeasuredIndex: number,
|
||||
|};
|
||||
|
||||
const getItemMetadata = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): ItemMetadata => {
|
||||
const { itemSize } = ((props: any): VariableSizeProps);
|
||||
const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
|
||||
|
||||
if (index > lastMeasuredIndex) {
|
||||
let offset = 0;
|
||||
if (lastMeasuredIndex >= 0) {
|
||||
const itemMetadata = itemMetadataMap[lastMeasuredIndex];
|
||||
offset = itemMetadata.offset + itemMetadata.size;
|
||||
}
|
||||
|
||||
for (let i = lastMeasuredIndex + 1; i <= index; i++) {
|
||||
let size = ((itemSize: any): itemSizeGetter)(i);
|
||||
|
||||
itemMetadataMap[i] = {
|
||||
offset,
|
||||
size,
|
||||
};
|
||||
|
||||
offset += size;
|
||||
}
|
||||
|
||||
instanceProps.lastMeasuredIndex = index;
|
||||
}
|
||||
|
||||
return itemMetadataMap[index];
|
||||
};
|
||||
|
||||
const findNearestItem = (
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
offset: number
|
||||
) => {
|
||||
const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
|
||||
|
||||
const lastMeasuredItemOffset =
|
||||
lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0;
|
||||
|
||||
if (lastMeasuredItemOffset >= offset) {
|
||||
// If we've already measured items within this range just use a binary search as it's faster.
|
||||
return findNearestItemBinarySearch(
|
||||
props,
|
||||
instanceProps,
|
||||
lastMeasuredIndex,
|
||||
0,
|
||||
offset
|
||||
);
|
||||
} else {
|
||||
// If we haven't yet measured this high, fallback to an exponential search with an inner binary search.
|
||||
// The exponential search avoids pre-computing sizes for the full set of items as a binary search would.
|
||||
// The overall complexity for this approach is O(log n).
|
||||
return findNearestItemExponentialSearch(
|
||||
props,
|
||||
instanceProps,
|
||||
Math.max(0, lastMeasuredIndex),
|
||||
offset
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestItemBinarySearch = (
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
high: number,
|
||||
low: number,
|
||||
offset: number
|
||||
): number => {
|
||||
while (low <= high) {
|
||||
const middle = low + Math.floor((high - low) / 2);
|
||||
const currentOffset = getItemMetadata(props, middle, instanceProps).offset;
|
||||
|
||||
if (currentOffset === offset) {
|
||||
return middle;
|
||||
} else if (currentOffset < offset) {
|
||||
low = middle + 1;
|
||||
} else if (currentOffset > offset) {
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (low > 0) {
|
||||
return low - 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestItemExponentialSearch = (
|
||||
props: Props<any>,
|
||||
instanceProps: InstanceProps,
|
||||
index: number,
|
||||
offset: number
|
||||
): number => {
|
||||
const { itemCount } = props;
|
||||
let interval = 1;
|
||||
|
||||
while (
|
||||
index < itemCount &&
|
||||
getItemMetadata(props, index, instanceProps).offset < offset
|
||||
) {
|
||||
index += interval;
|
||||
interval *= 2;
|
||||
}
|
||||
|
||||
return findNearestItemBinarySearch(
|
||||
props,
|
||||
instanceProps,
|
||||
Math.min(index, itemCount - 1),
|
||||
Math.floor(index / 2),
|
||||
offset
|
||||
);
|
||||
};
|
||||
|
||||
const getEstimatedTotalSize = (
|
||||
{ itemCount }: Props<any>,
|
||||
{ itemMetadataMap, estimatedItemSize, lastMeasuredIndex }: InstanceProps
|
||||
) => {
|
||||
let totalSizeOfMeasuredItems = 0;
|
||||
|
||||
// Edge case check for when the number of items decreases while a scroll is in progress.
|
||||
// https://github.com/bvaughn/react-window/pull/138
|
||||
if (lastMeasuredIndex >= itemCount) {
|
||||
lastMeasuredIndex = itemCount - 1;
|
||||
}
|
||||
|
||||
if (lastMeasuredIndex >= 0) {
|
||||
const itemMetadata = itemMetadataMap[lastMeasuredIndex];
|
||||
totalSizeOfMeasuredItems = itemMetadata.offset + itemMetadata.size;
|
||||
}
|
||||
|
||||
const numUnmeasuredItems = itemCount - lastMeasuredIndex - 1;
|
||||
const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedItemSize;
|
||||
|
||||
return totalSizeOfMeasuredItems + totalSizeOfUnmeasuredItems;
|
||||
};
|
||||
|
||||
const VariableSizeList = createListComponent({
|
||||
getItemOffset: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => getItemMetadata(props, index, instanceProps).offset,
|
||||
|
||||
getItemSize: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => instanceProps.itemMetadataMap[index].size,
|
||||
|
||||
getEstimatedTotalSize,
|
||||
|
||||
getOffsetForIndexAndAlignment: (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => {
|
||||
const { direction, height, layout, width } = props;
|
||||
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
|
||||
const size = (((isHorizontal ? width : height): any): number);
|
||||
const itemMetadata = getItemMetadata(props, index, instanceProps);
|
||||
|
||||
// Get estimated total size after ItemMetadata is computed,
|
||||
// To ensure it reflects actual measurements instead of just estimates.
|
||||
const estimatedTotalSize = getEstimatedTotalSize(props, instanceProps);
|
||||
|
||||
const maxOffset = Math.max(
|
||||
0,
|
||||
Math.min(estimatedTotalSize - size, itemMetadata.offset)
|
||||
);
|
||||
const minOffset = Math.max(
|
||||
0,
|
||||
itemMetadata.offset - size + itemMetadata.size
|
||||
);
|
||||
|
||||
if (align === 'smart') {
|
||||
if (
|
||||
scrollOffset >= minOffset - size &&
|
||||
scrollOffset <= maxOffset + size
|
||||
) {
|
||||
align = 'auto';
|
||||
} else {
|
||||
align = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
switch (align) {
|
||||
case 'start':
|
||||
return maxOffset;
|
||||
case 'end':
|
||||
return minOffset;
|
||||
case 'center':
|
||||
return Math.round(minOffset + (maxOffset - minOffset) / 2);
|
||||
case 'auto':
|
||||
default:
|
||||
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
|
||||
return scrollOffset;
|
||||
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
|
||||
return minOffset;
|
||||
} else {
|
||||
return maxOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getStartIndexForOffset: (
|
||||
props: Props<any>,
|
||||
offset: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => findNearestItem(props, instanceProps, offset),
|
||||
|
||||
getStopIndexForStartIndex: (
|
||||
props: Props<any>,
|
||||
startIndex: number,
|
||||
scrollOffset: number,
|
||||
instanceProps: InstanceProps
|
||||
): number => {
|
||||
const { direction, height, itemCount, layout, width } = props;
|
||||
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
|
||||
const size = (((isHorizontal ? width : height): any): number);
|
||||
const itemMetadata = getItemMetadata(props, startIndex, instanceProps);
|
||||
const maxOffset = scrollOffset + size;
|
||||
|
||||
let offset = itemMetadata.offset + itemMetadata.size;
|
||||
let stopIndex = startIndex;
|
||||
|
||||
while (stopIndex < itemCount - 1 && offset < maxOffset) {
|
||||
stopIndex++;
|
||||
offset += getItemMetadata(props, stopIndex, instanceProps).size;
|
||||
}
|
||||
|
||||
return stopIndex;
|
||||
},
|
||||
|
||||
initInstanceProps(props: Props<any>, instance: any): InstanceProps {
|
||||
const { estimatedItemSize } = ((props: any): VariableSizeProps);
|
||||
|
||||
const instanceProps = {
|
||||
itemMetadataMap: {},
|
||||
estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_ITEM_SIZE,
|
||||
lastMeasuredIndex: -1,
|
||||
};
|
||||
|
||||
instance.resetAfterIndex = (
|
||||
index: number,
|
||||
shouldForceUpdate?: boolean = true
|
||||
) => {
|
||||
instanceProps.lastMeasuredIndex = Math.min(
|
||||
instanceProps.lastMeasuredIndex,
|
||||
index - 1
|
||||
);
|
||||
|
||||
// We could potentially optimize further by only evicting styles after this index,
|
||||
// But since styles are only cached while scrolling is in progress-
|
||||
// It seems an unnecessary optimization.
|
||||
// It's unlikely that resetAfterIndex() will be called while a user is scrolling.
|
||||
instance._getItemStyleCache(-1);
|
||||
|
||||
if (shouldForceUpdate) {
|
||||
instance.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
return instanceProps;
|
||||
},
|
||||
|
||||
shouldResetStyleCacheOnItemSizeChange: false,
|
||||
|
||||
validateProps: ({ itemSize }: Props<any>): void => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof itemSize !== 'function') {
|
||||
throw Error(
|
||||
'An invalid "itemSize" prop has been specified. ' +
|
||||
'Value should be a function. ' +
|
||||
`"${itemSize === null ? 'null' : typeof itemSize}" was specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default VariableSizeList;
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import shallowDiffers from './shallowDiffers';
|
||||
|
||||
// Custom comparison function for React.memo().
|
||||
// It knows to compare individual style props and ignore the wrapper object.
|
||||
// See https://reactjs.org/docs/react-api.html#reactmemo
|
||||
export default function areEqual(
|
||||
prevProps: Object,
|
||||
nextProps: Object
|
||||
): boolean {
|
||||
const { style: prevStyle, ...prevRest } = prevProps;
|
||||
const { style: nextStyle, ...nextRest } = nextProps;
|
||||
|
||||
return (
|
||||
!shallowDiffers(prevStyle, nextStyle) && !shallowDiffers(prevRest, nextRest)
|
||||
);
|
||||
}
|
||||
+834
@@ -0,0 +1,834 @@
|
||||
// @flow
|
||||
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { createElement, PureComponent } from 'react';
|
||||
import { cancelTimeout, requestTimeout } from './timer';
|
||||
import { getScrollbarSize } from './domHelpers';
|
||||
|
||||
import type { TimeoutID } from './timer';
|
||||
|
||||
type Direction = 'ltr' | 'rtl';
|
||||
export type ScrollToAlign = 'auto' | 'smart' | 'center' | 'start' | 'end';
|
||||
|
||||
type itemSize = number | ((index: number) => number);
|
||||
|
||||
type RenderComponentProps<T> = {|
|
||||
columnIndex: number,
|
||||
data: T,
|
||||
isScrolling?: boolean,
|
||||
rowIndex: number,
|
||||
style: Object,
|
||||
|};
|
||||
export type RenderComponent<T> = React$ComponentType<
|
||||
$Shape<RenderComponentProps<T>>
|
||||
>;
|
||||
|
||||
type ScrollDirection = 'forward' | 'backward';
|
||||
|
||||
type OnItemsRenderedCallback = ({
|
||||
overscanColumnStartIndex: number,
|
||||
overscanColumnStopIndex: number,
|
||||
overscanRowStartIndex: number,
|
||||
overscanRowStopIndex: number,
|
||||
visibleColumnStartIndex: number,
|
||||
visibleColumnStopIndex: number,
|
||||
visibleRowStartIndex: number,
|
||||
visibleRowStopIndex: number,
|
||||
}) => void;
|
||||
type OnScrollCallback = ({
|
||||
horizontalScrollDirection: ScrollDirection,
|
||||
scrollLeft: number,
|
||||
scrollTop: number,
|
||||
scrollUpdateWasRequested: boolean,
|
||||
verticalScrollDirection: ScrollDirection,
|
||||
}) => void;
|
||||
|
||||
type ScrollEvent = SyntheticEvent<HTMLDivElement>;
|
||||
type ItemStyleCache = { [key: string]: Object };
|
||||
|
||||
export type Props<T> = {|
|
||||
children: RenderComponent<T>,
|
||||
className?: string,
|
||||
columnCount: number,
|
||||
columnWidth: itemSize,
|
||||
direction: Direction,
|
||||
height: number,
|
||||
initialScrollLeft?: number,
|
||||
initialScrollTop?: number,
|
||||
innerRef?: any,
|
||||
innerElementType?: React$ElementType,
|
||||
innerTagName?: string, // deprecated
|
||||
itemData: T,
|
||||
itemKey?: (params: {|
|
||||
columnIndex: number,
|
||||
data: T,
|
||||
rowIndex: number,
|
||||
|}) => any,
|
||||
onItemsRendered?: OnItemsRenderedCallback,
|
||||
onScroll?: OnScrollCallback,
|
||||
outerRef?: any,
|
||||
outerElementType?: React$ElementType,
|
||||
outerTagName?: string, // deprecated
|
||||
overscanColumnsCount?: number,
|
||||
overscanCount?: number, // deprecated
|
||||
overscanRowsCount?: number,
|
||||
rowCount: number,
|
||||
rowHeight: itemSize,
|
||||
style?: Object,
|
||||
useIsScrolling: boolean,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
instance: any,
|
||||
isScrolling: boolean,
|
||||
horizontalScrollDirection: ScrollDirection,
|
||||
scrollLeft: number,
|
||||
scrollTop: number,
|
||||
scrollUpdateWasRequested: boolean,
|
||||
verticalScrollDirection: ScrollDirection,
|
||||
|};
|
||||
|
||||
type getItemOffset = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type getItemSize = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type getEstimatedTotalSize = (props: Props<any>, instanceProps: any) => number;
|
||||
type GetOffsetForItemAndAlignment = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number,
|
||||
instanceProps: any,
|
||||
scrollbarSize: number
|
||||
) => number;
|
||||
type GetStartIndexForOffset = (
|
||||
props: Props<any>,
|
||||
offset: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type GetStopIndexForStartIndex = (
|
||||
props: Props<any>,
|
||||
startIndex: number,
|
||||
scrollOffset: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type InitInstanceProps = (props: Props<any>, instance: any) => any;
|
||||
type ValidateProps = (props: Props<any>) => void;
|
||||
|
||||
const IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
|
||||
|
||||
const defaultItemKey = ({ columnIndex, data, rowIndex }) =>
|
||||
`${rowIndex}:${columnIndex}`;
|
||||
|
||||
// In DEV mode, this Set helps us only log a warning once per component instance.
|
||||
// This avoids spamming the console every time a render happens.
|
||||
let devWarningsOverscanCount = null;
|
||||
let devWarningsTagName = null;
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof window !== 'undefined' && typeof window.WeakSet !== 'undefined') {
|
||||
devWarningsOverscanCount = new WeakSet();
|
||||
devWarningsTagName = new WeakSet();
|
||||
}
|
||||
}
|
||||
|
||||
export default function createGridComponent({
|
||||
getColumnOffset,
|
||||
getColumnStartIndexForOffset,
|
||||
getColumnStopIndexForStartIndex,
|
||||
getColumnWidth,
|
||||
getEstimatedTotalHeight,
|
||||
getEstimatedTotalWidth,
|
||||
getOffsetForColumnAndAlignment,
|
||||
getOffsetForRowAndAlignment,
|
||||
getRowHeight,
|
||||
getRowOffset,
|
||||
getRowStartIndexForOffset,
|
||||
getRowStopIndexForStartIndex,
|
||||
initInstanceProps,
|
||||
shouldResetStyleCacheOnItemSizeChange,
|
||||
validateProps,
|
||||
}: {|
|
||||
getColumnOffset: getItemOffset,
|
||||
getColumnStartIndexForOffset: GetStartIndexForOffset,
|
||||
getColumnStopIndexForStartIndex: GetStopIndexForStartIndex,
|
||||
getColumnWidth: getItemSize,
|
||||
getEstimatedTotalHeight: getEstimatedTotalSize,
|
||||
getEstimatedTotalWidth: getEstimatedTotalSize,
|
||||
getOffsetForColumnAndAlignment: GetOffsetForItemAndAlignment,
|
||||
getOffsetForRowAndAlignment: GetOffsetForItemAndAlignment,
|
||||
getRowOffset: getItemOffset,
|
||||
getRowHeight: getItemSize,
|
||||
getRowStartIndexForOffset: GetStartIndexForOffset,
|
||||
getRowStopIndexForStartIndex: GetStopIndexForStartIndex,
|
||||
initInstanceProps: InitInstanceProps,
|
||||
shouldResetStyleCacheOnItemSizeChange: boolean,
|
||||
validateProps: ValidateProps,
|
||||
|}) {
|
||||
return class Grid<T> extends PureComponent<Props<T>, State> {
|
||||
_instanceProps: any = initInstanceProps(this.props, this);
|
||||
_resetIsScrollingTimeoutId: TimeoutID | null = null;
|
||||
_outerRef: ?HTMLDivElement;
|
||||
|
||||
static defaultProps = {
|
||||
direction: 'ltr',
|
||||
itemData: undefined,
|
||||
useIsScrolling: false,
|
||||
};
|
||||
|
||||
state: State = {
|
||||
instance: this,
|
||||
isScrolling: false,
|
||||
horizontalScrollDirection: 'forward',
|
||||
scrollLeft:
|
||||
typeof this.props.initialScrollLeft === 'number'
|
||||
? this.props.initialScrollLeft
|
||||
: 0,
|
||||
scrollTop:
|
||||
typeof this.props.initialScrollTop === 'number'
|
||||
? this.props.initialScrollTop
|
||||
: 0,
|
||||
scrollUpdateWasRequested: false,
|
||||
verticalScrollDirection: 'forward',
|
||||
};
|
||||
|
||||
// Always use explicit constructor for React components.
|
||||
// It produces less code after transpilation. (#26)
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(
|
||||
nextProps: Props<T>,
|
||||
prevState: State
|
||||
): $Shape<State> | null {
|
||||
validateSharedProps(nextProps, prevState);
|
||||
validateProps(nextProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
scrollTo({
|
||||
scrollLeft,
|
||||
scrollTop,
|
||||
}: {
|
||||
scrollLeft: number,
|
||||
scrollTop: number,
|
||||
}): void {
|
||||
if (scrollLeft !== undefined) {
|
||||
scrollLeft = Math.max(0, scrollLeft);
|
||||
}
|
||||
if (scrollTop !== undefined) {
|
||||
scrollTop = Math.max(0, scrollTop);
|
||||
}
|
||||
|
||||
this.setState(prevState => {
|
||||
if (scrollLeft === undefined) {
|
||||
scrollLeft = prevState.scrollLeft;
|
||||
}
|
||||
if (scrollTop === undefined) {
|
||||
scrollTop = prevState.scrollTop;
|
||||
}
|
||||
|
||||
if (
|
||||
prevState.scrollLeft === scrollLeft &&
|
||||
prevState.scrollTop === scrollTop
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
horizontalScrollDirection:
|
||||
prevState.scrollLeft < scrollLeft ? 'forward' : 'backward',
|
||||
scrollLeft: scrollLeft,
|
||||
scrollTop: scrollTop,
|
||||
scrollUpdateWasRequested: true,
|
||||
verticalScrollDirection:
|
||||
prevState.scrollTop < scrollTop ? 'forward' : 'backward',
|
||||
};
|
||||
}, this._resetIsScrollingDebounced);
|
||||
}
|
||||
|
||||
scrollToItem({
|
||||
align = 'auto',
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
}: {
|
||||
align: ScrollToAlign,
|
||||
columnIndex?: number,
|
||||
rowIndex?: number,
|
||||
}): void {
|
||||
const { columnCount, height, rowCount, width } = this.props;
|
||||
const { scrollLeft, scrollTop } = this.state;
|
||||
const scrollbarSize = getScrollbarSize();
|
||||
|
||||
if (columnIndex !== undefined) {
|
||||
columnIndex = Math.max(0, Math.min(columnIndex, columnCount - 1));
|
||||
}
|
||||
if (rowIndex !== undefined) {
|
||||
rowIndex = Math.max(0, Math.min(rowIndex, rowCount - 1));
|
||||
}
|
||||
|
||||
const estimatedTotalHeight = getEstimatedTotalHeight(
|
||||
this.props,
|
||||
this._instanceProps
|
||||
);
|
||||
const estimatedTotalWidth = getEstimatedTotalWidth(
|
||||
this.props,
|
||||
this._instanceProps
|
||||
);
|
||||
|
||||
// The scrollbar size should be considered when scrolling an item into view,
|
||||
// to ensure it's fully visible.
|
||||
// But we only need to account for its size when it's actually visible.
|
||||
const horizontalScrollbarSize =
|
||||
estimatedTotalWidth > width ? scrollbarSize : 0;
|
||||
const verticalScrollbarSize =
|
||||
estimatedTotalHeight > height ? scrollbarSize : 0;
|
||||
|
||||
this.scrollTo({
|
||||
scrollLeft:
|
||||
columnIndex !== undefined
|
||||
? getOffsetForColumnAndAlignment(
|
||||
this.props,
|
||||
columnIndex,
|
||||
align,
|
||||
scrollLeft,
|
||||
this._instanceProps,
|
||||
verticalScrollbarSize
|
||||
)
|
||||
: scrollLeft,
|
||||
scrollTop:
|
||||
rowIndex !== undefined
|
||||
? getOffsetForRowAndAlignment(
|
||||
this.props,
|
||||
rowIndex,
|
||||
align,
|
||||
scrollTop,
|
||||
this._instanceProps,
|
||||
horizontalScrollbarSize
|
||||
)
|
||||
: scrollTop,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { initialScrollLeft, initialScrollTop } = this.props;
|
||||
if (typeof initialScrollLeft === 'number' && this._outerRef != null) {
|
||||
((this._outerRef: any): HTMLDivElement).scrollLeft = initialScrollLeft;
|
||||
}
|
||||
if (typeof initialScrollTop === 'number' && this._outerRef != null) {
|
||||
((this._outerRef: any): HTMLDivElement).scrollTop = initialScrollTop;
|
||||
}
|
||||
|
||||
this._callPropsCallbacks();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { scrollLeft, scrollTop, scrollUpdateWasRequested } = this.state;
|
||||
if (scrollUpdateWasRequested && this._outerRef !== null) {
|
||||
((this._outerRef: any): HTMLDivElement).scrollLeft = scrollLeft;
|
||||
((this._outerRef: any): HTMLDivElement).scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
this._callPropsCallbacks();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._resetIsScrollingTimeoutId !== null) {
|
||||
cancelTimeout(this._resetIsScrollingTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
columnCount,
|
||||
direction,
|
||||
height,
|
||||
innerRef,
|
||||
innerElementType,
|
||||
innerTagName,
|
||||
itemData,
|
||||
itemKey = defaultItemKey,
|
||||
outerElementType,
|
||||
outerTagName,
|
||||
rowCount,
|
||||
style,
|
||||
useIsScrolling,
|
||||
width,
|
||||
} = this.props;
|
||||
const { isScrolling } = this.state;
|
||||
|
||||
const [
|
||||
columnStartIndex,
|
||||
columnStopIndex,
|
||||
] = this._getHorizontalRangeToRender();
|
||||
const [rowStartIndex, rowStopIndex] = this._getVerticalRangeToRender();
|
||||
|
||||
const items = [];
|
||||
if (columnCount > 0 && rowCount) {
|
||||
for (
|
||||
let rowIndex = rowStartIndex;
|
||||
rowIndex <= rowStopIndex;
|
||||
rowIndex++
|
||||
) {
|
||||
for (
|
||||
let columnIndex = columnStartIndex;
|
||||
columnIndex <= columnStopIndex;
|
||||
columnIndex++
|
||||
) {
|
||||
items.push(
|
||||
createElement(children, {
|
||||
columnIndex,
|
||||
data: itemData,
|
||||
isScrolling: useIsScrolling ? isScrolling : undefined,
|
||||
key: itemKey({ columnIndex, data: itemData, rowIndex }),
|
||||
rowIndex,
|
||||
style: this._getItemStyle(rowIndex, columnIndex),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read this value AFTER items have been created,
|
||||
// So their actual sizes (if variable) are taken into consideration.
|
||||
const estimatedTotalHeight = getEstimatedTotalHeight(
|
||||
this.props,
|
||||
this._instanceProps
|
||||
);
|
||||
const estimatedTotalWidth = getEstimatedTotalWidth(
|
||||
this.props,
|
||||
this._instanceProps
|
||||
);
|
||||
|
||||
return createElement(
|
||||
outerElementType || outerTagName || 'div',
|
||||
{
|
||||
className,
|
||||
onScroll: this._onScroll,
|
||||
ref: this._outerRefSetter,
|
||||
style: {
|
||||
position: 'relative',
|
||||
height,
|
||||
width,
|
||||
overflow: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
willChange: 'transform',
|
||||
direction,
|
||||
...style,
|
||||
},
|
||||
},
|
||||
createElement(innerElementType || innerTagName || 'div', {
|
||||
children: items,
|
||||
ref: innerRef,
|
||||
style: {
|
||||
height: estimatedTotalHeight,
|
||||
pointerEvents: isScrolling ? 'none' : '',
|
||||
width: estimatedTotalWidth,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_callOnItemsRendered: (
|
||||
overscanColumnStartIndex: number,
|
||||
overscanColumnStopIndex: number,
|
||||
overscanRowStartIndex: number,
|
||||
overscanRowStopIndex: number,
|
||||
visibleColumnStartIndex: number,
|
||||
visibleColumnStopIndex: number,
|
||||
visibleRowStartIndex: number,
|
||||
visibleRowStopIndex: number
|
||||
) => void;
|
||||
_callOnItemsRendered = memoizeOne(
|
||||
(
|
||||
overscanColumnStartIndex: number,
|
||||
overscanColumnStopIndex: number,
|
||||
overscanRowStartIndex: number,
|
||||
overscanRowStopIndex: number,
|
||||
visibleColumnStartIndex: number,
|
||||
visibleColumnStopIndex: number,
|
||||
visibleRowStartIndex: number,
|
||||
visibleRowStopIndex: number
|
||||
) =>
|
||||
((this.props.onItemsRendered: any): OnItemsRenderedCallback)({
|
||||
overscanColumnStartIndex,
|
||||
overscanColumnStopIndex,
|
||||
overscanRowStartIndex,
|
||||
overscanRowStopIndex,
|
||||
visibleColumnStartIndex,
|
||||
visibleColumnStopIndex,
|
||||
visibleRowStartIndex,
|
||||
visibleRowStopIndex,
|
||||
})
|
||||
);
|
||||
|
||||
_callOnScroll: (
|
||||
scrollLeft: number,
|
||||
scrollTop: number,
|
||||
horizontalScrollDirection: ScrollDirection,
|
||||
verticalScrollDirection: ScrollDirection,
|
||||
scrollUpdateWasRequested: boolean
|
||||
) => void;
|
||||
_callOnScroll = memoizeOne(
|
||||
(
|
||||
scrollLeft: number,
|
||||
scrollTop: number,
|
||||
horizontalScrollDirection: ScrollDirection,
|
||||
verticalScrollDirection: ScrollDirection,
|
||||
scrollUpdateWasRequested: boolean
|
||||
) =>
|
||||
((this.props.onScroll: any): OnScrollCallback)({
|
||||
horizontalScrollDirection,
|
||||
scrollLeft,
|
||||
scrollTop,
|
||||
verticalScrollDirection,
|
||||
scrollUpdateWasRequested,
|
||||
})
|
||||
);
|
||||
|
||||
_callPropsCallbacks() {
|
||||
const { columnCount, onItemsRendered, onScroll, rowCount } = this.props;
|
||||
|
||||
if (typeof onItemsRendered === 'function') {
|
||||
if (columnCount > 0 && rowCount > 0) {
|
||||
const [
|
||||
overscanColumnStartIndex,
|
||||
overscanColumnStopIndex,
|
||||
visibleColumnStartIndex,
|
||||
visibleColumnStopIndex,
|
||||
] = this._getHorizontalRangeToRender();
|
||||
const [
|
||||
overscanRowStartIndex,
|
||||
overscanRowStopIndex,
|
||||
visibleRowStartIndex,
|
||||
visibleRowStopIndex,
|
||||
] = this._getVerticalRangeToRender();
|
||||
this._callOnItemsRendered(
|
||||
overscanColumnStartIndex,
|
||||
overscanColumnStopIndex,
|
||||
overscanRowStartIndex,
|
||||
overscanRowStopIndex,
|
||||
visibleColumnStartIndex,
|
||||
visibleColumnStopIndex,
|
||||
visibleRowStartIndex,
|
||||
visibleRowStopIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof onScroll === 'function') {
|
||||
const {
|
||||
horizontalScrollDirection,
|
||||
scrollLeft,
|
||||
scrollTop,
|
||||
scrollUpdateWasRequested,
|
||||
verticalScrollDirection,
|
||||
} = this.state;
|
||||
this._callOnScroll(
|
||||
scrollLeft,
|
||||
scrollTop,
|
||||
horizontalScrollDirection,
|
||||
verticalScrollDirection,
|
||||
scrollUpdateWasRequested
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Lazily create and cache item styles while scrolling,
|
||||
// So that pure component sCU will prevent re-renders.
|
||||
// We maintain this cache, and pass a style prop rather than index,
|
||||
// So that List can clear cached styles and force item re-render if necessary.
|
||||
_getItemStyle: (rowIndex: number, columnIndex: number) => Object;
|
||||
_getItemStyle = (rowIndex: number, columnIndex: number): Object => {
|
||||
const { columnWidth, direction, rowHeight } = this.props;
|
||||
|
||||
const itemStyleCache = this._getItemStyleCache(
|
||||
shouldResetStyleCacheOnItemSizeChange && columnWidth,
|
||||
shouldResetStyleCacheOnItemSizeChange && direction,
|
||||
shouldResetStyleCacheOnItemSizeChange && rowHeight
|
||||
);
|
||||
|
||||
const key = `${rowIndex}:${columnIndex}`;
|
||||
|
||||
let style;
|
||||
if (itemStyleCache.hasOwnProperty(key)) {
|
||||
style = itemStyleCache[key];
|
||||
} else {
|
||||
itemStyleCache[key] = style = {
|
||||
position: 'absolute',
|
||||
[direction === 'rtl' ? 'right' : 'left']: getColumnOffset(
|
||||
this.props,
|
||||
columnIndex,
|
||||
this._instanceProps
|
||||
),
|
||||
top: getRowOffset(this.props, rowIndex, this._instanceProps),
|
||||
height: getRowHeight(this.props, rowIndex, this._instanceProps),
|
||||
width: getColumnWidth(this.props, columnIndex, this._instanceProps),
|
||||
};
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
_getItemStyleCache: (_: any, __: any, ___: any) => ItemStyleCache;
|
||||
_getItemStyleCache = memoizeOne((_: any, __: any, ___: any) => ({}));
|
||||
|
||||
_getHorizontalRangeToRender(): [number, number, number, number] {
|
||||
const {
|
||||
columnCount,
|
||||
overscanColumnsCount,
|
||||
overscanCount,
|
||||
rowCount,
|
||||
} = this.props;
|
||||
const { horizontalScrollDirection, isScrolling, scrollLeft } = this.state;
|
||||
|
||||
const overscanCountResolved: number =
|
||||
overscanColumnsCount || overscanCount || 1;
|
||||
|
||||
if (columnCount === 0 || rowCount === 0) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
const startIndex = getColumnStartIndexForOffset(
|
||||
this.props,
|
||||
scrollLeft,
|
||||
this._instanceProps
|
||||
);
|
||||
const stopIndex = getColumnStopIndexForStartIndex(
|
||||
this.props,
|
||||
startIndex,
|
||||
scrollLeft,
|
||||
this._instanceProps
|
||||
);
|
||||
|
||||
// Overscan by one item in each direction so that tab/focus works.
|
||||
// If there isn't at least one extra item, tab loops back around.
|
||||
const overscanBackward =
|
||||
!isScrolling || horizontalScrollDirection === 'backward'
|
||||
? Math.max(1, overscanCountResolved)
|
||||
: 1;
|
||||
const overscanForward =
|
||||
!isScrolling || horizontalScrollDirection === 'forward'
|
||||
? Math.max(1, overscanCountResolved)
|
||||
: 1;
|
||||
|
||||
return [
|
||||
Math.max(0, startIndex - overscanBackward),
|
||||
Math.max(0, Math.min(columnCount - 1, stopIndex + overscanForward)),
|
||||
startIndex,
|
||||
stopIndex,
|
||||
];
|
||||
}
|
||||
|
||||
_getVerticalRangeToRender(): [number, number, number, number] {
|
||||
const {
|
||||
columnCount,
|
||||
overscanCount,
|
||||
overscanRowsCount,
|
||||
rowCount,
|
||||
} = this.props;
|
||||
const { isScrolling, verticalScrollDirection, scrollTop } = this.state;
|
||||
|
||||
const overscanCountResolved: number =
|
||||
overscanRowsCount || overscanCount || 1;
|
||||
|
||||
if (columnCount === 0 || rowCount === 0) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
const startIndex = getRowStartIndexForOffset(
|
||||
this.props,
|
||||
scrollTop,
|
||||
this._instanceProps
|
||||
);
|
||||
const stopIndex = getRowStopIndexForStartIndex(
|
||||
this.props,
|
||||
startIndex,
|
||||
scrollTop,
|
||||
this._instanceProps
|
||||
);
|
||||
|
||||
// Overscan by one item in each direction so that tab/focus works.
|
||||
// If there isn't at least one extra item, tab loops back around.
|
||||
const overscanBackward =
|
||||
!isScrolling || verticalScrollDirection === 'backward'
|
||||
? Math.max(1, overscanCountResolved)
|
||||
: 1;
|
||||
const overscanForward =
|
||||
!isScrolling || verticalScrollDirection === 'forward'
|
||||
? Math.max(1, overscanCountResolved)
|
||||
: 1;
|
||||
|
||||
return [
|
||||
Math.max(0, startIndex - overscanBackward),
|
||||
Math.max(0, Math.min(rowCount - 1, stopIndex + overscanForward)),
|
||||
startIndex,
|
||||
stopIndex,
|
||||
];
|
||||
}
|
||||
|
||||
_onScroll = (event: ScrollEvent): void => {
|
||||
const {
|
||||
clientWidth,
|
||||
scrollLeft,
|
||||
scrollTop,
|
||||
scrollWidth,
|
||||
} = event.currentTarget;
|
||||
this.setState(prevState => {
|
||||
if (
|
||||
prevState.scrollLeft === scrollLeft &&
|
||||
prevState.scrollTop === scrollTop
|
||||
) {
|
||||
// Scroll position may have been updated by cDM/cDU,
|
||||
// In which case we don't need to trigger another render,
|
||||
// And we don't want to update state.isScrolling.
|
||||
return null;
|
||||
}
|
||||
|
||||
const { direction } = this.props;
|
||||
|
||||
// HACK According to the spec, scrollLeft should be negative for RTL aligned elements.
|
||||
// Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
|
||||
let calculatedScrollLeft = scrollLeft;
|
||||
if (direction === 'rtl') {
|
||||
if (scrollLeft <= 0) {
|
||||
calculatedScrollLeft = -scrollLeft;
|
||||
} else {
|
||||
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isScrolling: true,
|
||||
horizontalScrollDirection:
|
||||
prevState.scrollLeft < scrollLeft ? 'forward' : 'backward',
|
||||
scrollLeft: calculatedScrollLeft,
|
||||
scrollTop,
|
||||
verticalScrollDirection:
|
||||
prevState.scrollTop < scrollTop ? 'forward' : 'backward',
|
||||
scrollUpdateWasRequested: false,
|
||||
};
|
||||
}, this._resetIsScrollingDebounced);
|
||||
};
|
||||
|
||||
_outerRefSetter = (ref: any): void => {
|
||||
const { outerRef } = this.props;
|
||||
|
||||
this._outerRef = ((ref: any): HTMLDivElement);
|
||||
|
||||
if (typeof outerRef === 'function') {
|
||||
outerRef(ref);
|
||||
} else if (
|
||||
outerRef != null &&
|
||||
typeof outerRef === 'object' &&
|
||||
outerRef.hasOwnProperty('current')
|
||||
) {
|
||||
outerRef.current = ref;
|
||||
}
|
||||
};
|
||||
|
||||
_resetIsScrollingDebounced = () => {
|
||||
if (this._resetIsScrollingTimeoutId !== null) {
|
||||
cancelTimeout(this._resetIsScrollingTimeoutId);
|
||||
}
|
||||
|
||||
this._resetIsScrollingTimeoutId = requestTimeout(
|
||||
this._resetIsScrolling,
|
||||
IS_SCROLLING_DEBOUNCE_INTERVAL
|
||||
);
|
||||
};
|
||||
|
||||
_resetIsScrolling = () => {
|
||||
this._resetIsScrollingTimeoutId = null;
|
||||
|
||||
this.setState({ isScrolling: false }, () => {
|
||||
// Clear style cache after state update has been committed.
|
||||
// This way we don't break pure sCU for items that don't use isScrolling param.
|
||||
this._getItemStyleCache(-1);
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const validateSharedProps = (
|
||||
{
|
||||
children,
|
||||
direction,
|
||||
height,
|
||||
innerTagName,
|
||||
outerTagName,
|
||||
overscanCount,
|
||||
width,
|
||||
}: Props<any>,
|
||||
{ instance }: State
|
||||
): void => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof overscanCount === 'number') {
|
||||
if (devWarningsOverscanCount && !devWarningsOverscanCount.has(instance)) {
|
||||
devWarningsOverscanCount.add(instance);
|
||||
console.warn(
|
||||
'The overscanCount prop has been deprecated. ' +
|
||||
'Please use the overscanColumnsCount and overscanRowsCount props instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (innerTagName != null || outerTagName != null) {
|
||||
if (devWarningsTagName && !devWarningsTagName.has(instance)) {
|
||||
devWarningsTagName.add(instance);
|
||||
console.warn(
|
||||
'The innerTagName and outerTagName props have been deprecated. ' +
|
||||
'Please use the innerElementType and outerElementType props instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (children == null) {
|
||||
throw Error(
|
||||
'An invalid "children" prop has been specified. ' +
|
||||
'Value should be a React component. ' +
|
||||
`"${children === null ? 'null' : typeof children}" was specified.`
|
||||
);
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case 'ltr':
|
||||
case 'rtl':
|
||||
// Valid values
|
||||
break;
|
||||
default:
|
||||
throw Error(
|
||||
'An invalid "direction" prop has been specified. ' +
|
||||
'Value should be either "ltr" or "rtl". ' +
|
||||
`"${direction}" was specified.`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof width !== 'number') {
|
||||
throw Error(
|
||||
'An invalid "width" prop has been specified. ' +
|
||||
'Grids must specify a number for width. ' +
|
||||
`"${width === null ? 'null' : typeof width}" was specified.`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof height !== 'number') {
|
||||
throw Error(
|
||||
'An invalid "height" prop has been specified. ' +
|
||||
'Grids must specify a number for height. ' +
|
||||
`"${height === null ? 'null' : typeof height}" was specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
+670
@@ -0,0 +1,670 @@
|
||||
// @flow
|
||||
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { createElement, PureComponent } from 'react';
|
||||
import { cancelTimeout, requestTimeout } from './timer';
|
||||
|
||||
import type { TimeoutID } from './timer';
|
||||
|
||||
export type ScrollToAlign = 'auto' | 'smart' | 'center' | 'start' | 'end';
|
||||
|
||||
type itemSize = number | ((index: number) => number);
|
||||
// TODO Deprecate directions "horizontal" and "vertical"
|
||||
type Direction = 'ltr' | 'rtl' | 'horizontal' | 'vertical';
|
||||
type Layout = 'horizontal' | 'vertical';
|
||||
|
||||
type RenderComponentProps<T> = {|
|
||||
data: T,
|
||||
index: number,
|
||||
isScrolling?: boolean,
|
||||
style: Object,
|
||||
|};
|
||||
type RenderComponent<T> = React$ComponentType<$Shape<RenderComponentProps<T>>>;
|
||||
|
||||
type ScrollDirection = 'forward' | 'backward';
|
||||
|
||||
type onItemsRenderedCallback = ({
|
||||
overscanStartIndex: number,
|
||||
overscanStopIndex: number,
|
||||
visibleStartIndex: number,
|
||||
visibleStopIndex: number,
|
||||
}) => void;
|
||||
type onScrollCallback = ({
|
||||
scrollDirection: ScrollDirection,
|
||||
scrollOffset: number,
|
||||
scrollUpdateWasRequested: boolean,
|
||||
}) => void;
|
||||
|
||||
type ScrollEvent = SyntheticEvent<HTMLDivElement>;
|
||||
type ItemStyleCache = { [index: number]: Object };
|
||||
|
||||
export type Props<T> = {|
|
||||
children: RenderComponent<T>,
|
||||
className?: string,
|
||||
direction: Direction,
|
||||
height: number | string,
|
||||
initialScrollOffset?: number,
|
||||
innerRef?: any,
|
||||
innerElementType?: React$ElementType,
|
||||
innerTagName?: string, // deprecated
|
||||
itemCount: number,
|
||||
itemData: T,
|
||||
itemKey?: (index: number, data: T) => any,
|
||||
itemSize: itemSize,
|
||||
layout: Layout,
|
||||
onItemsRendered?: onItemsRenderedCallback,
|
||||
onScroll?: onScrollCallback,
|
||||
outerRef?: any,
|
||||
outerElementType?: React$ElementType,
|
||||
outerTagName?: string, // deprecated
|
||||
overscanCount: number,
|
||||
style?: Object,
|
||||
useIsScrolling: boolean,
|
||||
width: number | string,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
instance: any,
|
||||
isScrolling: boolean,
|
||||
scrollDirection: ScrollDirection,
|
||||
scrollOffset: number,
|
||||
scrollUpdateWasRequested: boolean,
|
||||
|};
|
||||
|
||||
type GetItemOffset = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type GetItemSize = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type GetEstimatedTotalSize = (props: Props<any>, instanceProps: any) => number;
|
||||
type GetOffsetForIndexAndAlignment = (
|
||||
props: Props<any>,
|
||||
index: number,
|
||||
align: ScrollToAlign,
|
||||
scrollOffset: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type GetStartIndexForOffset = (
|
||||
props: Props<any>,
|
||||
offset: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type GetStopIndexForStartIndex = (
|
||||
props: Props<any>,
|
||||
startIndex: number,
|
||||
scrollOffset: number,
|
||||
instanceProps: any
|
||||
) => number;
|
||||
type InitInstanceProps = (props: Props<any>, instance: any) => any;
|
||||
type ValidateProps = (props: Props<any>) => void;
|
||||
|
||||
const IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
|
||||
|
||||
const defaultItemKey = (index: number, data: any) => index;
|
||||
|
||||
// In DEV mode, this Set helps us only log a warning once per component instance.
|
||||
// This avoids spamming the console every time a render happens.
|
||||
let devWarningsDirection = null;
|
||||
let devWarningsTagName = null;
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof window !== 'undefined' && typeof window.WeakSet !== 'undefined') {
|
||||
devWarningsDirection = new WeakSet();
|
||||
devWarningsTagName = new WeakSet();
|
||||
}
|
||||
}
|
||||
|
||||
export default function createListComponent({
|
||||
getItemOffset,
|
||||
getEstimatedTotalSize,
|
||||
getItemSize,
|
||||
getOffsetForIndexAndAlignment,
|
||||
getStartIndexForOffset,
|
||||
getStopIndexForStartIndex,
|
||||
initInstanceProps,
|
||||
shouldResetStyleCacheOnItemSizeChange,
|
||||
validateProps,
|
||||
}: {|
|
||||
getItemOffset: GetItemOffset,
|
||||
getEstimatedTotalSize: GetEstimatedTotalSize,
|
||||
getItemSize: GetItemSize,
|
||||
getOffsetForIndexAndAlignment: GetOffsetForIndexAndAlignment,
|
||||
getStartIndexForOffset: GetStartIndexForOffset,
|
||||
getStopIndexForStartIndex: GetStopIndexForStartIndex,
|
||||
initInstanceProps: InitInstanceProps,
|
||||
shouldResetStyleCacheOnItemSizeChange: boolean,
|
||||
validateProps: ValidateProps,
|
||||
|}) {
|
||||
return class List<T> extends PureComponent<Props<T>, State> {
|
||||
_instanceProps: any = initInstanceProps(this.props, this);
|
||||
_outerRef: ?HTMLDivElement;
|
||||
_resetIsScrollingTimeoutId: TimeoutID | null = null;
|
||||
|
||||
static defaultProps = {
|
||||
direction: 'ltr',
|
||||
itemData: undefined,
|
||||
layout: 'vertical',
|
||||
overscanCount: 2,
|
||||
useIsScrolling: false,
|
||||
};
|
||||
|
||||
state: State = {
|
||||
instance: this,
|
||||
isScrolling: false,
|
||||
scrollDirection: 'forward',
|
||||
scrollOffset:
|
||||
typeof this.props.initialScrollOffset === 'number'
|
||||
? this.props.initialScrollOffset
|
||||
: 0,
|
||||
scrollUpdateWasRequested: false,
|
||||
};
|
||||
|
||||
// Always use explicit constructor for React components.
|
||||
// It produces less code after transpilation. (#26)
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(
|
||||
nextProps: Props<T>,
|
||||
prevState: State
|
||||
): $Shape<State> | null {
|
||||
validateSharedProps(nextProps, prevState);
|
||||
validateProps(nextProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
scrollTo(scrollOffset: number): void {
|
||||
scrollOffset = Math.max(0, scrollOffset);
|
||||
|
||||
this.setState(prevState => {
|
||||
if (prevState.scrollOffset === scrollOffset) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
scrollDirection:
|
||||
prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
|
||||
scrollOffset: scrollOffset,
|
||||
scrollUpdateWasRequested: true,
|
||||
};
|
||||
}, this._resetIsScrollingDebounced);
|
||||
}
|
||||
|
||||
scrollToItem(index: number, align: ScrollToAlign = 'auto'): void {
|
||||
const { itemCount } = this.props;
|
||||
const { scrollOffset } = this.state;
|
||||
|
||||
index = Math.max(0, Math.min(index, itemCount - 1));
|
||||
|
||||
this.scrollTo(
|
||||
getOffsetForIndexAndAlignment(
|
||||
this.props,
|
||||
index,
|
||||
align,
|
||||
scrollOffset,
|
||||
this._instanceProps
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { direction, initialScrollOffset, layout } = this.props;
|
||||
|
||||
if (typeof initialScrollOffset === 'number' && this._outerRef !== null) {
|
||||
// TODO Deprecate direction "horizontal"
|
||||
if (direction === 'horizontal' || layout === 'horizontal') {
|
||||
((this
|
||||
._outerRef: any): HTMLDivElement).scrollLeft = initialScrollOffset;
|
||||
} else {
|
||||
((this
|
||||
._outerRef: any): HTMLDivElement).scrollTop = initialScrollOffset;
|
||||
}
|
||||
}
|
||||
|
||||
this._callPropsCallbacks();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { direction, layout } = this.props;
|
||||
const { scrollOffset, scrollUpdateWasRequested } = this.state;
|
||||
|
||||
if (scrollUpdateWasRequested && this._outerRef !== null) {
|
||||
// TODO Deprecate direction "horizontal"
|
||||
if (direction === 'horizontal' || layout === 'horizontal') {
|
||||
((this._outerRef: any): HTMLDivElement).scrollLeft = scrollOffset;
|
||||
} else {
|
||||
((this._outerRef: any): HTMLDivElement).scrollTop = scrollOffset;
|
||||
}
|
||||
}
|
||||
|
||||
this._callPropsCallbacks();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._resetIsScrollingTimeoutId !== null) {
|
||||
cancelTimeout(this._resetIsScrollingTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
direction,
|
||||
height,
|
||||
innerRef,
|
||||
innerElementType,
|
||||
innerTagName,
|
||||
itemCount,
|
||||
itemData,
|
||||
itemKey = defaultItemKey,
|
||||
layout,
|
||||
outerElementType,
|
||||
outerTagName,
|
||||
style,
|
||||
useIsScrolling,
|
||||
width,
|
||||
} = this.props;
|
||||
const { isScrolling } = this.state;
|
||||
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal =
|
||||
direction === 'horizontal' || layout === 'horizontal';
|
||||
|
||||
const onScroll = isHorizontal
|
||||
? this._onScrollHorizontal
|
||||
: this._onScrollVertical;
|
||||
|
||||
const [startIndex, stopIndex] = this._getRangeToRender();
|
||||
|
||||
const items = [];
|
||||
if (itemCount > 0) {
|
||||
for (let index = startIndex; index <= stopIndex; index++) {
|
||||
items.push(
|
||||
createElement(children, {
|
||||
data: itemData,
|
||||
key: itemKey(index, itemData),
|
||||
index,
|
||||
isScrolling: useIsScrolling ? isScrolling : undefined,
|
||||
style: this._getItemStyle(index),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Read this value AFTER items have been created,
|
||||
// So their actual sizes (if variable) are taken into consideration.
|
||||
const estimatedTotalSize = getEstimatedTotalSize(
|
||||
this.props,
|
||||
this._instanceProps
|
||||
);
|
||||
|
||||
return createElement(
|
||||
outerElementType || outerTagName || 'div',
|
||||
{
|
||||
className,
|
||||
onScroll,
|
||||
ref: this._outerRefSetter,
|
||||
style: {
|
||||
position: 'relative',
|
||||
height,
|
||||
width,
|
||||
overflow: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
willChange: 'transform',
|
||||
direction,
|
||||
...style,
|
||||
},
|
||||
},
|
||||
createElement(innerElementType || innerTagName || 'div', {
|
||||
children: items,
|
||||
ref: innerRef,
|
||||
style: {
|
||||
height: isHorizontal ? '100%' : estimatedTotalSize,
|
||||
pointerEvents: isScrolling ? 'none' : '',
|
||||
width: isHorizontal ? estimatedTotalSize : '100%',
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_callOnItemsRendered: (
|
||||
overscanStartIndex: number,
|
||||
overscanStopIndex: number,
|
||||
visibleStartIndex: number,
|
||||
visibleStopIndex: number
|
||||
) => void;
|
||||
_callOnItemsRendered = memoizeOne(
|
||||
(
|
||||
overscanStartIndex: number,
|
||||
overscanStopIndex: number,
|
||||
visibleStartIndex: number,
|
||||
visibleStopIndex: number
|
||||
) =>
|
||||
((this.props.onItemsRendered: any): onItemsRenderedCallback)({
|
||||
overscanStartIndex,
|
||||
overscanStopIndex,
|
||||
visibleStartIndex,
|
||||
visibleStopIndex,
|
||||
})
|
||||
);
|
||||
|
||||
_callOnScroll: (
|
||||
scrollDirection: ScrollDirection,
|
||||
scrollOffset: number,
|
||||
scrollUpdateWasRequested: boolean
|
||||
) => void;
|
||||
_callOnScroll = memoizeOne(
|
||||
(
|
||||
scrollDirection: ScrollDirection,
|
||||
scrollOffset: number,
|
||||
scrollUpdateWasRequested: boolean
|
||||
) =>
|
||||
((this.props.onScroll: any): onScrollCallback)({
|
||||
scrollDirection,
|
||||
scrollOffset,
|
||||
scrollUpdateWasRequested,
|
||||
})
|
||||
);
|
||||
|
||||
_callPropsCallbacks() {
|
||||
if (typeof this.props.onItemsRendered === 'function') {
|
||||
const { itemCount } = this.props;
|
||||
if (itemCount > 0) {
|
||||
const [
|
||||
overscanStartIndex,
|
||||
overscanStopIndex,
|
||||
visibleStartIndex,
|
||||
visibleStopIndex,
|
||||
] = this._getRangeToRender();
|
||||
this._callOnItemsRendered(
|
||||
overscanStartIndex,
|
||||
overscanStopIndex,
|
||||
visibleStartIndex,
|
||||
visibleStopIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof this.props.onScroll === 'function') {
|
||||
const {
|
||||
scrollDirection,
|
||||
scrollOffset,
|
||||
scrollUpdateWasRequested,
|
||||
} = this.state;
|
||||
this._callOnScroll(
|
||||
scrollDirection,
|
||||
scrollOffset,
|
||||
scrollUpdateWasRequested
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Lazily create and cache item styles while scrolling,
|
||||
// So that pure component sCU will prevent re-renders.
|
||||
// We maintain this cache, and pass a style prop rather than index,
|
||||
// So that List can clear cached styles and force item re-render if necessary.
|
||||
_getItemStyle: (index: number) => Object;
|
||||
_getItemStyle = (index: number): Object => {
|
||||
const { direction, itemSize, layout } = this.props;
|
||||
|
||||
const itemStyleCache = this._getItemStyleCache(
|
||||
shouldResetStyleCacheOnItemSizeChange && itemSize,
|
||||
shouldResetStyleCacheOnItemSizeChange && layout,
|
||||
shouldResetStyleCacheOnItemSizeChange && direction
|
||||
);
|
||||
|
||||
let style;
|
||||
if (itemStyleCache.hasOwnProperty(index)) {
|
||||
style = itemStyleCache[index];
|
||||
} else {
|
||||
const offset = getItemOffset(this.props, index, this._instanceProps);
|
||||
const size = getItemSize(this.props, index, this._instanceProps);
|
||||
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal =
|
||||
direction === 'horizontal' || layout === 'horizontal';
|
||||
|
||||
itemStyleCache[index] = style = {
|
||||
position: 'absolute',
|
||||
[direction === 'rtl' ? 'right' : 'left']: isHorizontal ? offset : 0,
|
||||
top: !isHorizontal ? offset : 0,
|
||||
height: !isHorizontal ? size : '100%',
|
||||
width: isHorizontal ? size : '100%',
|
||||
};
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
_getItemStyleCache: (_: any, __: any, ___: any) => ItemStyleCache;
|
||||
_getItemStyleCache = memoizeOne((_: any, __: any, ___: any) => ({}));
|
||||
|
||||
_getRangeToRender(): [number, number, number, number] {
|
||||
const { itemCount, overscanCount } = this.props;
|
||||
const { isScrolling, scrollDirection, scrollOffset } = this.state;
|
||||
|
||||
if (itemCount === 0) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
const startIndex = getStartIndexForOffset(
|
||||
this.props,
|
||||
scrollOffset,
|
||||
this._instanceProps
|
||||
);
|
||||
const stopIndex = getStopIndexForStartIndex(
|
||||
this.props,
|
||||
startIndex,
|
||||
scrollOffset,
|
||||
this._instanceProps
|
||||
);
|
||||
|
||||
// Overscan by one item in each direction so that tab/focus works.
|
||||
// If there isn't at least one extra item, tab loops back around.
|
||||
const overscanBackward =
|
||||
!isScrolling || scrollDirection === 'backward'
|
||||
? Math.max(1, overscanCount)
|
||||
: 1;
|
||||
const overscanForward =
|
||||
!isScrolling || scrollDirection === 'forward'
|
||||
? Math.max(1, overscanCount)
|
||||
: 1;
|
||||
|
||||
return [
|
||||
Math.max(0, startIndex - overscanBackward),
|
||||
Math.max(0, Math.min(itemCount - 1, stopIndex + overscanForward)),
|
||||
startIndex,
|
||||
stopIndex,
|
||||
];
|
||||
}
|
||||
|
||||
_onScrollHorizontal = (event: ScrollEvent): void => {
|
||||
const { clientWidth, scrollLeft, scrollWidth } = event.currentTarget;
|
||||
this.setState(prevState => {
|
||||
if (prevState.scrollOffset === scrollLeft) {
|
||||
// Scroll position may have been updated by cDM/cDU,
|
||||
// In which case we don't need to trigger another render,
|
||||
// And we don't want to update state.isScrolling.
|
||||
return null;
|
||||
}
|
||||
|
||||
const { direction } = this.props;
|
||||
|
||||
// HACK According to the spec, scrollLeft should be negative for RTL aligned elements.
|
||||
// Chrome does not seem to adhere; its scrolLeft values are positive (measured relative to the left).
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
|
||||
let scrollOffset = scrollLeft;
|
||||
if (direction === 'rtl') {
|
||||
if (scrollLeft <= 0) {
|
||||
scrollOffset = -scrollOffset;
|
||||
} else {
|
||||
scrollOffset = scrollWidth - clientWidth - scrollLeft;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isScrolling: true,
|
||||
scrollDirection:
|
||||
prevState.scrollOffset < scrollLeft ? 'forward' : 'backward',
|
||||
scrollOffset,
|
||||
scrollUpdateWasRequested: false,
|
||||
};
|
||||
}, this._resetIsScrollingDebounced);
|
||||
};
|
||||
|
||||
_onScrollVertical = (event: ScrollEvent): void => {
|
||||
const { scrollTop } = event.currentTarget;
|
||||
this.setState(prevState => {
|
||||
if (prevState.scrollOffset === scrollTop) {
|
||||
// Scroll position may have been updated by cDM/cDU,
|
||||
// In which case we don't need to trigger another render,
|
||||
// And we don't want to update state.isScrolling.
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
isScrolling: true,
|
||||
scrollDirection:
|
||||
prevState.scrollOffset < scrollTop ? 'forward' : 'backward',
|
||||
scrollOffset: scrollTop,
|
||||
scrollUpdateWasRequested: false,
|
||||
};
|
||||
}, this._resetIsScrollingDebounced);
|
||||
};
|
||||
|
||||
_outerRefSetter = (ref: any): void => {
|
||||
const { outerRef } = this.props;
|
||||
|
||||
this._outerRef = ((ref: any): HTMLDivElement);
|
||||
|
||||
if (typeof outerRef === 'function') {
|
||||
outerRef(ref);
|
||||
} else if (
|
||||
outerRef != null &&
|
||||
typeof outerRef === 'object' &&
|
||||
outerRef.hasOwnProperty('current')
|
||||
) {
|
||||
outerRef.current = ref;
|
||||
}
|
||||
};
|
||||
|
||||
_resetIsScrollingDebounced = () => {
|
||||
if (this._resetIsScrollingTimeoutId !== null) {
|
||||
cancelTimeout(this._resetIsScrollingTimeoutId);
|
||||
}
|
||||
|
||||
this._resetIsScrollingTimeoutId = requestTimeout(
|
||||
this._resetIsScrolling,
|
||||
IS_SCROLLING_DEBOUNCE_INTERVAL
|
||||
);
|
||||
};
|
||||
|
||||
_resetIsScrolling = () => {
|
||||
this._resetIsScrollingTimeoutId = null;
|
||||
|
||||
this.setState({ isScrolling: false }, () => {
|
||||
// Clear style cache after state update has been committed.
|
||||
// This way we don't break pure sCU for items that don't use isScrolling param.
|
||||
this._getItemStyleCache(-1, null);
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// NOTE: I considered further wrapping individual items with a pure ListItem component.
|
||||
// This would avoid ever calling the render function for the same index more than once,
|
||||
// But it would also add the overhead of a lot of components/fibers.
|
||||
// I assume people already do this (render function returning a class component),
|
||||
// So my doing it would just unnecessarily double the wrappers.
|
||||
|
||||
const validateSharedProps = (
|
||||
{
|
||||
children,
|
||||
direction,
|
||||
height,
|
||||
layout,
|
||||
innerTagName,
|
||||
outerTagName,
|
||||
width,
|
||||
}: Props<any>,
|
||||
{ instance }: State
|
||||
): void => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (innerTagName != null || outerTagName != null) {
|
||||
if (devWarningsTagName && !devWarningsTagName.has(instance)) {
|
||||
devWarningsTagName.add(instance);
|
||||
console.warn(
|
||||
'The innerTagName and outerTagName props have been deprecated. ' +
|
||||
'Please use the innerElementType and outerElementType props instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Deprecate direction "horizontal"
|
||||
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
|
||||
|
||||
switch (direction) {
|
||||
case 'horizontal':
|
||||
case 'vertical':
|
||||
if (devWarningsDirection && !devWarningsDirection.has(instance)) {
|
||||
devWarningsDirection.add(instance);
|
||||
console.warn(
|
||||
'The direction prop should be either "ltr" (default) or "rtl". ' +
|
||||
'Please use the layout prop to specify "vertical" (default) or "horizontal" orientation.'
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ltr':
|
||||
case 'rtl':
|
||||
// Valid values
|
||||
break;
|
||||
default:
|
||||
throw Error(
|
||||
'An invalid "direction" prop has been specified. ' +
|
||||
'Value should be either "ltr" or "rtl". ' +
|
||||
`"${direction}" was specified.`
|
||||
);
|
||||
}
|
||||
|
||||
switch (layout) {
|
||||
case 'horizontal':
|
||||
case 'vertical':
|
||||
// Valid values
|
||||
break;
|
||||
default:
|
||||
throw Error(
|
||||
'An invalid "layout" prop has been specified. ' +
|
||||
'Value should be either "horizontal" or "vertical". ' +
|
||||
`"${layout}" was specified.`
|
||||
);
|
||||
}
|
||||
|
||||
if (children == null) {
|
||||
throw Error(
|
||||
'An invalid "children" prop has been specified. ' +
|
||||
'Value should be a React component. ' +
|
||||
`"${children === null ? 'null' : typeof children}" was specified.`
|
||||
);
|
||||
}
|
||||
|
||||
if (isHorizontal && typeof width !== 'number') {
|
||||
throw Error(
|
||||
'An invalid "width" prop has been specified. ' +
|
||||
'Horizontal lists must specify a number for width. ' +
|
||||
`"${width === null ? 'null' : typeof width}" was specified.`
|
||||
);
|
||||
} else if (!isHorizontal && typeof height !== 'number') {
|
||||
throw Error(
|
||||
'An invalid "height" prop has been specified. ' +
|
||||
'Vertical lists must specify a number for height. ' +
|
||||
`"${height === null ? 'null' : typeof height}" was specified.`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
|
||||
let size: number = -1;
|
||||
|
||||
// This utility copied from "dom-helpers" package.
|
||||
export function getScrollbarSize(recalculate?: boolean = false): number {
|
||||
if (size === -1 || recalculate) {
|
||||
const div = document.createElement('div');
|
||||
const style = div.style;
|
||||
style.width = '50px';
|
||||
style.height = '50px';
|
||||
style.overflow = 'scroll';
|
||||
|
||||
((document.body: any): HTMLBodyElement).appendChild(div);
|
||||
|
||||
size = div.offsetWidth - div.clientWidth;
|
||||
|
||||
((document.body: any): HTMLBodyElement).removeChild(div);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
|
||||
export { default as VariableSizeGrid } from './VariableSizeGrid';
|
||||
export { default as VariableSizeList } from './VariableSizeList';
|
||||
export { default as FixedSizeGrid } from './FixedSizeGrid';
|
||||
export { default as FixedSizeList } from './FixedSizeList';
|
||||
|
||||
export { default as areEqual } from './areEqual';
|
||||
export { default as shouldComponentUpdate } from './shouldComponentUpdate';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
|
||||
// Pulled from react-compat
|
||||
// https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349
|
||||
export default function shallowDiffers(prev: Object, next: Object): boolean {
|
||||
for (let attribute in prev) {
|
||||
if (!(attribute in next)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (let attribute in next) {
|
||||
if (prev[attribute] !== next[attribute]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import areEqual from './areEqual';
|
||||
import shallowDiffers from './shallowDiffers';
|
||||
|
||||
// Custom shouldComponentUpdate for class components.
|
||||
// It knows to compare individual style props and ignore the wrapper object.
|
||||
// See https://reactjs.org/docs/react-component.html#shouldcomponentupdate
|
||||
export default function shouldComponentUpdate(
|
||||
nextProps: Object,
|
||||
nextState: Object
|
||||
): boolean {
|
||||
return (
|
||||
!areEqual(this.props, nextProps) || shallowDiffers(this.state, nextState)
|
||||
);
|
||||
}
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
// @flow
|
||||
|
||||
// Animation frame based implementation of setTimeout.
|
||||
// Inspired by Joe Lambert, https://gist.github.com/joelambert/1002116#file-requesttimeout-js
|
||||
|
||||
const hasNativePerformanceNow =
|
||||
typeof performance === 'object' && typeof performance.now === 'function';
|
||||
|
||||
const now = hasNativePerformanceNow
|
||||
? () => performance.now()
|
||||
: () => Date.now();
|
||||
|
||||
export type TimeoutID = {|
|
||||
id: AnimationFrameID,
|
||||
|};
|
||||
|
||||
export function cancelTimeout(timeoutID: TimeoutID) {
|
||||
cancelAnimationFrame(timeoutID.id);
|
||||
}
|
||||
|
||||
export function requestTimeout(callback: Function, delay: number): TimeoutID {
|
||||
const start = now();
|
||||
|
||||
function tick() {
|
||||
if (now() - start >= delay) {
|
||||
callback.call(null);
|
||||
} else {
|
||||
timeoutID.id = requestAnimationFrame(tick);
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutID: TimeoutID = {
|
||||
id: requestAnimationFrame(tick),
|
||||
};
|
||||
|
||||
return timeoutID;
|
||||
}
|
||||
@@ -758,7 +758,7 @@
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.0.0"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.0.0"
|
||||
|
||||
"@babel/runtime@7.3.1", "@babel/runtime@^7.0.0":
|
||||
"@babel/runtime@7.3.1":
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a"
|
||||
integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==
|
||||
@@ -8119,11 +8119,6 @@ mem@^4.0.0:
|
||||
mimic-fn "^1.0.0"
|
||||
p-is-promise "^2.0.0"
|
||||
|
||||
"memoize-one@>=3.1.1 <6":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.4.tgz#005928aced5c43d890a4dfab18ca908b0ec92cbc"
|
||||
integrity sha512-P0z5IeAH6qHHGkJIXWw0xC2HNEgkx/9uWWBQw64FJj3/ol14VYdfVGWWr0fXfjhhv3TKVIqUq65os6O4GUNksA==
|
||||
|
||||
memoize-one@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
|
||||
@@ -9754,14 +9749,6 @@ react-virtualized-auto-sizer@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
|
||||
integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
|
||||
|
||||
react-window@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.0.tgz#08196d1e0749fe60d0b29bb94ae682d37666e620"
|
||||
integrity sha512-impIDEhm3+9/Y8ctiFaUI07a8Y/1jG0U4ONRUmYe7IsU+UQ2B0Ayyfbr+3R3nwAJpVGHx5pfI5SoDBt1o07YFw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
memoize-one ">=3.1.1 <6"
|
||||
|
||||
react@^0.0.0-50b50c26f:
|
||||
version "0.0.0-50b50c26f"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-50b50c26f.tgz#b782b579ce1f5d8bd696c5e45c744714ebecb111"
|
||||
|
||||
Reference in New Issue
Block a user