diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e85b535..e49c882a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ Changelog ------------ +### 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) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..0569d0a7 --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md index 87da7279..bbdac329 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > React components for efficiently rendering large lists and tabular data -[![NPM registry](https://img.shields.io/npm/v/react-window.svg?style=for-the-badge)](https://yarnpkg.com/en/package/react-window) [![Travis](https://img.shields.io/badge/ci-travis-green.svg?style=for-the-badge)](https://travis-ci.org/bvaughn/react-window) [![NPM license](https://img.shields.io/badge/license-mit-red.svg?style=for-the-badge)](LICENSE) +[![NPM registry](https://img.shields.io/npm/v/react-window.svg?style=for-the-badge)](https://yarnpkg.com/en/package/react-window) [![Travis](https://img.shields.io/badge/ci-travis-green.svg?style=for-the-badge)](https://travis-ci.org/bvaughn/react-window) [![NPM license](https://img.shields.io/badge/license-mit-red.svg?style=for-the-badge)](LICENSE.md) ## Install @@ -16,11 +16,16 @@ npm install --save react-window ## Usage -Learn more at [react-window.now.sh](https://react-window.now.sh/). +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`? +### 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**1 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). @@ -32,6 +37,54 @@ If `react-window` provides the functionality your project needs, I would strongl 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. +### 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): + +screen shot 2019-03-07 at 7 29 08 pm + +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... + + + +...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! + +screen shot 2019-03-07 at 7 21 48 pm + +### 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): + +screen shot 2019-03-07 at 7 32 32 pm + +Here's a [Code Sandbox demo](https://codesandbox.io/s/5wqo7z2np4). + +### Can I attach custom properties or event handlers? + +Yes, using the `outerElementType` prop. + +Screen Shot 2019-03-12 at 8 58 09 AM + +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. + +Screen Shot 2019-03-26 at 6 33 56 PM + +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 are Code Sandbox demos for list and grids: +* [List demo](https://codesandbox.io/s/0mk3qwpl4l) +* [Grid demo](https://codesandbox.io/s/vqk32863wy) + ## License MIT © [bvaughn](https://github.com/bvaughn) diff --git a/package.json b/package.json index f56c7095..427d5ccb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-window", - "version": "1.6.0-alpha.1", + "version": "1.8.0-alpha.1", "description": "React components for efficiently rendering large, scrollable lists and tabular data", "author": @@ -61,7 +61,7 @@ }, "dependencies": { "@babel/runtime": "^7.0.0", - "memoize-one": "^3.1.1" + "memoize-one": ">=3.1.1 <6" }, "peerDependencies": { "react": "^15.0.0 || ^16.0.0", @@ -95,13 +95,14 @@ "gh-pages": "^1.1.0", "lint-staged": "^7.0.5", "prettier": "^1.12.1", - "react": "^16.7.0", - "react-dom": "^16.7.0", + "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": "^0.65.0", - "rollup-plugin-babel": "^4.0.2", - "rollup-plugin-commonjs": "^8.2.1", - "rollup-plugin-node-resolve": "^3.0.2" + "rollup": "^1.4.1", + "rollup-plugin-babel": "^4.3.2", + "rollup-plugin-commonjs": "^9.2.1", + "rollup-plugin-node-resolve": "^4.0.1" } } diff --git a/rollup.config.js b/rollup.config.js index 5d167d14..c05d722a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,6 +1,7 @@ import babel from 'rollup-plugin-babel'; import commonjs from 'rollup-plugin-commonjs'; import nodeResolve from 'rollup-plugin-node-resolve'; + import pkg from './package.json'; const input = './src/index.js'; diff --git a/src/DynamicSizeList.js b/src/DynamicSizeList.js index 6ec34f0f..26149e65 100644 --- a/src/DynamicSizeList.js +++ b/src/DynamicSizeList.js @@ -152,7 +152,7 @@ const DynamicSizeList = createListComponent({ scrollOffset: number, instanceProps: InstanceProps ): number => { - const { direction, height, width } = props; + const { direction, layout, height, width } = props; if (process.env.NODE_ENV !== 'production') { const { lastMeasuredIndex } = instanceProps; @@ -164,7 +164,9 @@ const DynamicSizeList = createListComponent({ } } - const size = (((direction === 'horizontal' ? width : height): any): number); + const size = (((direction === 'horizontal' || layout === 'horizontal' + ? width + : height): any): number); const itemMetadata = getItemMetadata(props, index, instanceProps); // Get estimated total size after ItemMetadata is computed, @@ -225,9 +227,11 @@ const DynamicSizeList = createListComponent({ scrollOffset: number, instanceProps: InstanceProps ): number => { - const { direction, height, itemCount, width } = props; + const { direction, layout, height, itemCount, width } = props; - const size = (((direction === 'horizontal' ? width : height): any): number); + const size = (((direction === 'horizontal' || layout === 'horizontal' + ? width + : height): any): number); const itemMetadata = getItemMetadata(props, startIndex, instanceProps); const maxOffset = scrollOffset + size; @@ -325,7 +329,7 @@ const DynamicSizeList = createListComponent({ instance.forceUpdate(); } else { const { scrollOffset } = instance.state; - const { direction } = instance.props; + const { direction, layout } = instance.props; // Adjusting scroll offset directly interrupts smooth scrolling for some browsers (e.g. Firefox). // The relative scrollBy() method doesn't interrupt (or at least it won't as of Firefox v65). @@ -335,10 +339,17 @@ const DynamicSizeList = createListComponent({ // $FlowFixMe Property scrollBy is missing in HTMLDivElement if (typeof element.scrollBy === 'function') { element.scrollBy( - direction === 'horizontal' ? sizeDeltaForStateUpdate : 0, - direction === 'horizontal' ? 0 : sizeDeltaForStateUpdate + direction === 'horizontal' || layout === 'horizontal' + ? sizeDeltaForStateUpdate + : 0, + direction === 'horizontal' || layout === 'horizontal' + ? 0 + : sizeDeltaForStateUpdate ); - } else if (direction === 'horizontal') { + } else if ( + direction === 'horizontal' || + layout === 'horizontal' + ) { element.scrollLeft = scrollOffset; } else { element.scrollTop = scrollOffset; @@ -418,6 +429,7 @@ const DynamicSizeList = createListComponent({ const { children, direction, + layout, itemCount, itemData, itemKey = defaultItemKey, @@ -451,6 +463,7 @@ const DynamicSizeList = createListComponent({ items.push( createElement(ItemMeasurer, { direction, + layout, handleNewMeasurements, index, item, diff --git a/src/FixedSizeGrid.js b/src/FixedSizeGrid.js index f446acb8..6224f71d 100644 --- a/src/FixedSizeGrid.js +++ b/src/FixedSizeGrid.js @@ -27,7 +27,9 @@ const FixedSizeGrid = createGridComponent({ { columnCount, columnWidth, width }: Props, columnIndex: number, align: ScrollToAlign, - scrollLeft: number + scrollLeft: number, + instanceProps: typeof undefined, + scrollbarSize: number ): number => { const maxOffset = Math.max( 0, @@ -40,6 +42,7 @@ const FixedSizeGrid = createGridComponent({ 0, columnIndex * ((columnWidth: any): number) - width + + scrollbarSize + ((columnWidth: any): number) ); @@ -66,7 +69,9 @@ const FixedSizeGrid = createGridComponent({ { rowHeight, height, rowCount }: Props, rowIndex: number, align: ScrollToAlign, - scrollTop: number + scrollTop: number, + instanceProps: typeof undefined, + scrollbarSize: number ): number => { const maxOffset = Math.max( 0, @@ -79,6 +84,7 @@ const FixedSizeGrid = createGridComponent({ 0, rowIndex * ((rowHeight: any): number) - height + + scrollbarSize + ((rowHeight: any): number) ); diff --git a/src/FixedSizeList.js b/src/FixedSizeList.js index 7491d91d..498a8c9b 100644 --- a/src/FixedSizeList.js +++ b/src/FixedSizeList.js @@ -15,12 +15,14 @@ const FixedSizeList = createListComponent({ ((itemSize: any): number) * itemCount, getOffsetForIndexAndAlignment: ( - { direction, height, itemCount, itemSize, width }: Props, + { direction, height, itemCount, itemSize, layout, width }: Props, index: number, align: ScrollToAlign, scrollOffset: number ): number => { - const size = (((direction === 'horizontal' ? width : height): any): 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( @@ -62,12 +64,14 @@ const FixedSizeList = createListComponent({ ), getStopIndexForStartIndex: ( - { direction, height, itemCount, itemSize, width }: Props, + { direction, height, itemCount, itemSize, layout, width }: Props, startIndex: number, scrollOffset: number ): number => { + // TODO Deprecate direction "horizontal" + const isHorizontal = direction === 'horizontal' || layout === 'horizontal'; const offset = startIndex * ((itemSize: any): number); - const size = (((direction === 'horizontal' ? width : height): any): number); + const size = (((isHorizontal ? width : height): any): number); return Math.max( 0, Math.min( diff --git a/src/ItemMeasurer.js b/src/ItemMeasurer.js index 86ad6c8d..763e1aba 100644 --- a/src/ItemMeasurer.js +++ b/src/ItemMeasurer.js @@ -3,7 +3,7 @@ import { cloneElement, Component } from 'react'; import { findDOMNode } from 'react-dom'; -import type { Direction } from './createListComponent'; +import type { Direction, Layout } from './createListComponent'; import type { HandleNewMeasurements } from './DynamicSizeList'; class DOMRectReadOnly { @@ -38,6 +38,7 @@ declare class ResizeObserver { type ItemMeasurerProps = {| direction: Direction, + layout: Layout, handleNewMeasurements: HandleNewMeasurements, index: number, item: React$Element, @@ -107,6 +108,7 @@ export default class ItemMeasurer extends Component { _measureItem = (isCommitPhase: boolean) => { const { direction, + layout, handleNewMeasurements, index, size: oldSize, @@ -121,7 +123,7 @@ export default class ItemMeasurer extends Component { node instanceof node.ownerDocument.defaultView.HTMLElement ) { const newSize = - direction === 'horizontal' + direction === 'horizontal' || layout === 'horizontal' ? Math.ceil(node.offsetWidth) : Math.ceil(node.offsetHeight); diff --git a/src/VariableSizeGrid.js b/src/VariableSizeGrid.js index bf31f207..061b3064 100644 --- a/src/VariableSizeGrid.js +++ b/src/VariableSizeGrid.js @@ -35,6 +35,12 @@ const getEstimatedTotalHeight = ( ) => { 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; @@ -56,6 +62,12 @@ const getEstimatedTotalWidth = ( ) => { 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; @@ -221,7 +233,8 @@ const getOffsetForIndexAndAlignment = ( index: number, align: ScrollToAlign, scrollOffset: number, - instanceProps: InstanceProps + instanceProps: InstanceProps, + scrollbarSize: number ): number => { const size = itemType === 'column' ? props.width : props.height; const itemMetadata = getItemMetadata(itemType, props, index, instanceProps); @@ -237,7 +250,10 @@ const getOffsetForIndexAndAlignment = ( 0, Math.min(estimatedTotalSize - size, itemMetadata.offset) ); - const minOffset = Math.max(0, itemMetadata.offset - size + itemMetadata.size); + const minOffset = Math.max( + 0, + itemMetadata.offset - size + scrollbarSize + itemMetadata.size + ); switch (align) { case 'start': @@ -312,7 +328,8 @@ const VariableSizeGrid = createGridComponent({ index: number, align: ScrollToAlign, scrollOffset: number, - instanceProps: InstanceProps + instanceProps: InstanceProps, + scrollbarSize: number ): number => getOffsetForIndexAndAlignment( 'column', @@ -320,7 +337,8 @@ const VariableSizeGrid = createGridComponent({ index, align, scrollOffset, - instanceProps + instanceProps, + scrollbarSize ), getOffsetForRowAndAlignment: ( @@ -328,7 +346,8 @@ const VariableSizeGrid = createGridComponent({ index: number, align: ScrollToAlign, scrollOffset: number, - instanceProps: InstanceProps + instanceProps: InstanceProps, + scrollbarSize: number ): number => getOffsetForIndexAndAlignment( 'row', @@ -336,7 +355,8 @@ const VariableSizeGrid = createGridComponent({ index, align, scrollOffset, - instanceProps + instanceProps, + scrollbarSize ), getRowOffset: ( diff --git a/src/VariableSizeList.js b/src/VariableSizeList.js index 4bec6b78..806beac1 100644 --- a/src/VariableSizeList.js +++ b/src/VariableSizeList.js @@ -146,6 +146,12 @@ const getEstimatedTotalSize = ( ) => { 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; @@ -179,9 +185,11 @@ const VariableSizeList = createListComponent({ scrollOffset: number, instanceProps: InstanceProps ): number => { - const { direction, height, width } = props; + const { direction, height, layout, width } = props; - const size = (((direction === 'horizontal' ? width : height): any): number); + // 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, @@ -228,9 +236,11 @@ const VariableSizeList = createListComponent({ scrollOffset: number, instanceProps: InstanceProps ): number => { - const { direction, height, itemCount, width } = props; + const { direction, height, itemCount, layout, width } = props; - const size = (((direction === 'horizontal' ? width : height): any): number); + // 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; diff --git a/src/__tests__/FixedSizeGrid.js b/src/__tests__/FixedSizeGrid.js index 14e0de1f..3a2325f9 100644 --- a/src/__tests__/FixedSizeGrid.js +++ b/src/__tests__/FixedSizeGrid.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import ReactTestRenderer from 'react-test-renderer'; import ReactTestUtils from 'react-dom/test-utils'; import { FixedSizeGrid } from '..'; +import * as domHelpers from '../domHelpers'; const findScrollContainer = rendered => rendered.root.children[0].children[0]; @@ -13,7 +14,7 @@ const simulateScroll = (instance, { scrollLeft, scrollTop }) => { }; describe('FixedSizeGrid', () => { - let itemRenderer, defaultProps, onItemsRendered; + let defaultProps, getScrollbarSize, itemRenderer, onItemsRendered; // Use PureComponent to test memoization. // Pass through to itemRenderer mock for easier test assertions. @@ -26,6 +27,9 @@ describe('FixedSizeGrid', () => { beforeEach(() => { jest.useFakeTimers(); + // Mock the DOM helper util for testing purposes. + getScrollbarSize = domHelpers.getScrollbarSize = jest.fn(() => 0); + onItemsRendered = jest.fn(); itemRenderer = jest.fn(({ style, ...rest }) => ( @@ -172,6 +176,41 @@ describe('FixedSizeGrid', () => { }); }); + describe('direction', () => { + it('should set the appropriate CSS direction style', () => { + const renderer = ReactTestRenderer.create( + + ); + expect(renderer.toJSON().props.style.direction).toBe('ltr'); + renderer.update(); + expect(renderer.toJSON().props.style.direction).toBe('rtl'); + }); + + it('should position items correctly', () => { + const renderer = ReactTestRenderer.create( + + ); + + let params = itemRenderer.mock.calls[0][0]; + expect(params.columnIndex).toBe(0); + expect(params.rowIndex).toBe(0); + let style = params.style; + expect(style.left).toBe(0); + expect(style.right).toBeUndefined(); + + itemRenderer.mockClear(); + + renderer.update(); + + params = itemRenderer.mock.calls[0][0]; + expect(params.columnIndex).toBe(0); + expect(params.rowIndex).toBe(0); + style = params.style; + expect(style.left).toBeUndefined(); + expect(style.right).toBe(0); + }); + }); + describe('overscanColumnsCount and overscanRowsCount', () => { it('should require a minimum of 1 overscan to support tabbing', () => { ReactTestRenderer.create( @@ -248,7 +287,8 @@ describe('FixedSizeGrid', () => { describe('overscanCount', () => { it('should warn about deprecated overscanCount prop', () => { spyOn(console, 'warn'); - ReactTestRenderer.create( + + const renderer = ReactTestRenderer.create( ); expect(console.warn).toHaveBeenCalledTimes(1); @@ -256,10 +296,16 @@ describe('FixedSizeGrid', () => { 'The overscanCount prop has been deprecated. ' + 'Please use the overscanColumnsCount and overscanRowsCount props instead.' ); + + renderer.update(); + + // But it should only warn once. + expect(console.warn).toHaveBeenCalledTimes(1); }); it('should use overscanColumnsCount if both it and overscanCount are provided', () => { spyOn(console, 'warn'); + ReactTestRenderer.create( { it('should use overscanRowsCount if both it and overscanCount are provided', () => { spyOn(console, 'warn'); + ReactTestRenderer.create( { it('should support deprecated overscanCount', () => { spyOn(console, 'warn'); + ReactTestRenderer.create( { rendered .getInstance() .scrollToItem({ columnIndex: 2, rowIndex: 2, align: 'auto' }); + // Scroll down to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'auto' }); + // Scroll left to column 0, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 0, align: 'auto' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); @@ -449,6 +501,10 @@ describe('FixedSizeGrid', () => { rendered .getInstance() .scrollToItem({ columnIndex: 99, rowIndex: 99, align: 'start' }); + // Scroll up to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'start' }); + // Scroll left to column 0, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 0, align: 'start' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); @@ -471,6 +527,10 @@ describe('FixedSizeGrid', () => { rendered .getInstance() .scrollToItem({ columnIndex: 1, rowIndex: 1, align: 'end' }); + // Scroll down to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'end' }); + // Scroll right to column 9, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 9, align: 'end' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); @@ -499,6 +559,10 @@ describe('FixedSizeGrid', () => { rendered .getInstance() .scrollToItem({ columnIndex: 99, rowIndex: 99, align: 'center' }); + // Scroll up to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'center' }); + // Scroll left to column 3, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 3, align: 'center' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); @@ -512,6 +576,109 @@ describe('FixedSizeGrid', () => { instance.scrollToItem({ columnIndex: 15, rowIndex: 20 }); expect(itemRenderer.mock.calls[0][0].isScrolling).toBe(false); }); + + it('should account for scrollbar size', () => { + const onScroll = jest.fn(); + const rendered = ReactTestRenderer.create( + + ); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 15, rowIndex: 10, align: 'end' }); + + // With hidden scrollbars (size === 0) we would expect... + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'forward', + scrollLeft: 1300, + scrollTop: 125, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'forward', + }); + + getScrollbarSize.mockImplementation(() => 20); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 15, rowIndex: 10, align: 'end' }); + + // With scrollbars of size 20 we would expect those values ot increase by 20px + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'forward', + scrollLeft: 1320, + scrollTop: 145, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'forward', + }); + }); + + it('should not account for scrollbar size when no scrollbar is visible for a particular direction', () => { + getScrollbarSize.mockImplementation(() => 20); + + const onScroll = jest.fn(); + const rendered = ReactTestRenderer.create( + + ); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 0, rowIndex: 10, align: 'end' }); + + // Since there aren't enough columns to require horizontal scrolling, + // the additional 20px for the scrollbar should not be taken into consideration. + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'backward', + scrollLeft: 0, + scrollTop: 125, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'forward', + }); + + rendered.update( + + ); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 15, rowIndex: 0, align: 'end' }); + + // Since there aren't enough rows to require vertical scrolling, + // the additional 20px for the scrollbar should not be taken into consideration. + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'forward', + scrollLeft: 1300, + scrollTop: 0, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'backward', + }); + }); }); // onItemsRendered is pretty well covered by other snapshot tests @@ -721,19 +888,29 @@ describe('FixedSizeGrid', () => { it('should warn if legacy innerTagName or outerTagName props are used', () => { spyOn(console, 'warn'); - ReactDOM.render( + const renderer = ReactTestRenderer.create( , - document.createElement('div') + /> ); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenLastCalledWith( 'The innerTagName and outerTagName props have been deprecated. ' + 'Please use the innerElementType and outerElementType props instead.' ); + + renderer.update( + + ); + + // But it should only warn once. + expect(console.warn).toHaveBeenCalledTimes(1); }); }); @@ -814,10 +991,22 @@ describe('FixedSizeGrid', () => { ); }); + it('should fail if an invalid direction is provided', () => { + expect(() => + ReactTestRenderer.create( + + ) + ).toThrow( + 'An invalid "direction" prop has been specified. ' + + 'Value should be either "ltr" or "rtl". ' + + '"null" was specified.' + ); + }); + it('should fail if a string height is provided', () => { expect(() => ReactTestRenderer.create( - + ) ).toThrow( 'An invalid "height" prop has been specified. ' + @@ -829,11 +1018,7 @@ describe('FixedSizeGrid', () => { it('should fail if a string width is provided', () => { expect(() => ReactTestRenderer.create( - + ) ).toThrow( 'An invalid "width" prop has been specified. ' + diff --git a/src/__tests__/FixedSizeList.js b/src/__tests__/FixedSizeList.js index d71b9f39..f591c3d0 100644 --- a/src/__tests__/FixedSizeList.js +++ b/src/__tests__/FixedSizeList.js @@ -58,13 +58,32 @@ describe('FixedSizeList', () => { it('should render a list of columns', () => { ReactTestRenderer.create( - + ); expect(itemRenderer).toHaveBeenCalledTimes(5); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); + it('should re-render items if layout changes', () => { + const rendered = ReactTestRenderer.create( + + ); + expect(itemRenderer).toHaveBeenCalled(); + itemRenderer.mockClear(); + + // Re-rendering should not affect pure sCU children: + rendered.update(); + expect(itemRenderer).not.toHaveBeenCalled(); + + // Re-rendering with new layout should re-render children: + rendered.update(); + expect(itemRenderer).toHaveBeenCalled(); + }); + + // TODO Deprecate direction "horizontal" it('should re-render items if direction changes', () => { + spyOn(console, 'warn'); // Ingore legacy prop warning + const rendered = ReactTestRenderer.create( ); @@ -75,7 +94,7 @@ describe('FixedSizeList', () => { rendered.update(); expect(itemRenderer).not.toHaveBeenCalled(); - // Re-rendering with new direction should re-render children: + // Re-rendering with new layout should re-render children: rendered.update(); expect(itemRenderer).toHaveBeenCalled(); }); @@ -97,7 +116,7 @@ describe('FixedSizeList', () => { ReactDOM.render( , document.createElement('div') @@ -202,6 +221,39 @@ describe('FixedSizeList', () => { }); }); + describe('direction', () => { + it('should set the appropriate CSS direction style', () => { + const renderer = ReactTestRenderer.create( + + ); + expect(renderer.toJSON().props.style.direction).toBe('ltr'); + renderer.update(); + expect(renderer.toJSON().props.style.direction).toBe('rtl'); + }); + + it('should position items correctly', () => { + const renderer = ReactTestRenderer.create( + + ); + + let params = itemRenderer.mock.calls[0][0]; + expect(params.index).toBe(0); + let style = params.style; + expect(style.left).toBe(0); + expect(style.right).toBeUndefined(); + + itemRenderer.mockClear(); + + renderer.update(); + + params = itemRenderer.mock.calls[0][0]; + expect(params.index).toBe(0); + style = params.style; + expect(style.left).toBeUndefined(); + expect(style.right).toBe(0); + }); + }); + describe('overscanCount', () => { it('should require a minimum of 1 overscan to support tabbing', () => { ReactTestRenderer.create( @@ -583,19 +635,68 @@ describe('FixedSizeList', () => { it('should warn if legacy innerTagName or outerTagName props are used', () => { spyOn(console, 'warn'); - ReactDOM.render( + + const renderer = ReactTestRenderer.create( , - document.createElement('div') + /> ); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenLastCalledWith( 'The innerTagName and outerTagName props have been deprecated. ' + 'Please use the innerElementType and outerElementType props instead.' ); + + renderer.update( + + ); + + // But it should only warn once. + expect(console.warn).toHaveBeenCalledTimes(1); + }); + + it('should warn if legacy direction "horizontal" value is used', () => { + spyOn(console, 'warn'); + + const renderer = ReactTestRenderer.create( + + ); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenLastCalledWith( + 'The direction prop should be either "ltr" (default) or "rtl". ' + + 'Please use the layout prop to specify "vertical" (default) or "horizontal" orientation.' + ); + + renderer.update( + + ); + + // But it should only warn once. + expect(console.warn).toHaveBeenCalledTimes(1); + }); + + it('should warn if legacy direction "vertical" value is used', () => { + spyOn(console, 'warn'); + + const renderer = ReactTestRenderer.create( + + ); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenLastCalledWith( + 'The direction prop should be either "ltr" (default) or "rtl". ' + + 'Please use the layout prop to specify "vertical" (default) or "horizontal" orientation.' + ); + + renderer.update(); + + // But it should only warn once. + expect(console.warn).toHaveBeenCalledTimes(1); }); }); @@ -664,6 +765,18 @@ describe('FixedSizeList', () => { ); }); + it('should fail if an invalid layout is provided', () => { + expect(() => + ReactTestRenderer.create( + + ) + ).toThrow( + 'An invalid "layout" prop has been specified. ' + + 'Value should be either "horizontal" or "vertical". ' + + '"null" was specified.' + ); + }); + it('should fail if an invalid direction is provided', () => { expect(() => ReactTestRenderer.create( @@ -671,7 +784,7 @@ describe('FixedSizeList', () => { ) ).toThrow( 'An invalid "direction" prop has been specified. ' + - 'Value should be either "horizontal" or "vertical". ' + + 'Value should be either "ltr" or "rtl". ' + '"null" was specified.' ); }); @@ -679,7 +792,7 @@ describe('FixedSizeList', () => { it('should fail if a string height is provided for a vertical list', () => { expect(() => ReactTestRenderer.create( - + ) ).toThrow( 'An invalid "height" prop has been specified. ' + @@ -691,11 +804,7 @@ describe('FixedSizeList', () => { it('should fail if a string width is provided for a horizontal list', () => { expect(() => ReactTestRenderer.create( - + ) ).toThrow( 'An invalid "width" prop has been specified. ' + diff --git a/src/__tests__/VariableSizeGrid.js b/src/__tests__/VariableSizeGrid.js index 7fdd0175..645e5896 100644 --- a/src/__tests__/VariableSizeGrid.js +++ b/src/__tests__/VariableSizeGrid.js @@ -1,11 +1,25 @@ -import React, { PureComponent } from 'react'; +import React, { createRef, PureComponent } from 'react'; +import { render } from 'react-dom'; +import { Simulate } from 'react-dom/test-utils'; import ReactTestRenderer from 'react-test-renderer'; import { VariableSizeGrid } from '..'; +import * as domHelpers from '../domHelpers'; + +const simulateScroll = (instance, { scrollLeft, scrollTop }) => { + instance._outerRef.scrollLeft = scrollLeft; + instance._outerRef.scrollTop = scrollTop; + Simulate.scroll(instance._outerRef); +}; const findScrollContainer = rendered => rendered.root.children[0].children[0]; describe('VariableSizeGrid', () => { - let columnWidth, defaultProps, itemRenderer, onItemsRendered, rowHeight; + let columnWidth, + defaultProps, + getScrollbarSize, + itemRenderer, + onItemsRendered, + rowHeight; // Use PureComponent to test memoization. // Pass through to itemRenderer mock for easier test assertions. @@ -26,6 +40,9 @@ describe('VariableSizeGrid', () => { beforeEach(() => { jest.useFakeTimers(); + // Mock the DOM helper util for testing purposes. + getScrollbarSize = domHelpers.getScrollbarSize = jest.fn(() => 0); + itemRenderer = jest.fn(({ style, ...rest }) => (
{JSON.stringify(rest, null, 2)}
)); @@ -173,6 +190,10 @@ describe('VariableSizeGrid', () => { rendered .getInstance() .scrollToItem({ columnIndex: 2, rowIndex: 2, align: 'auto' }); + // Scroll down to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'auto' }); + // Scroll left to column 0, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 0, align: 'auto' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); @@ -195,6 +216,10 @@ describe('VariableSizeGrid', () => { rendered .getInstance() .scrollToItem({ columnIndex: 9, rowIndex: 19, align: 'start' }); + // Scroll up to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'start' }); + // Scroll left to column 0, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 0, align: 'start' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); @@ -217,6 +242,10 @@ describe('VariableSizeGrid', () => { rendered .getInstance() .scrollToItem({ columnIndex: 1, rowIndex: 1, align: 'end' }); + // Scroll down to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'end' }); + // Scroll right to column 9, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 9, align: 'end' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); @@ -245,8 +274,96 @@ describe('VariableSizeGrid', () => { rendered .getInstance() .scrollToItem({ columnIndex: 9, rowIndex: 19, align: 'center' }); + // Scroll up to row 10, without changing scrollLeft + rendered.getInstance().scrollToItem({ rowIndex: 10, align: 'center' }); + // Scroll left to column 3, without changing scrollTop + rendered.getInstance().scrollToItem({ columnIndex: 3, align: 'center' }); expect(onItemsRendered.mock.calls).toMatchSnapshot(); }); + + it('should account for scrollbar size', () => { + const onScroll = jest.fn(); + const rendered = ReactTestRenderer.create( + + ); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 15, rowIndex: 10, align: 'end' }); + + // With hidden scrollbars (size === 0) we would expect... + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'forward', + scrollLeft: 720, + scrollTop: 230, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'forward', + }); + + getScrollbarSize.mockImplementation(() => 20); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 15, rowIndex: 10, align: 'end' }); + + // With scrollbars of size 20 we would expect those values ot increase by 20px + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'forward', + scrollLeft: 740, + scrollTop: 250, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'forward', + }); + }); + + it('should not account for scrollbar size when no scrollbar is visible for a particular direction', () => { + getScrollbarSize.mockImplementation(() => 20); + + const onScroll = jest.fn(); + const rendered = ReactTestRenderer.create( + + ); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 0, rowIndex: 10, align: 'end' }); + + // Since there aren't enough columns to require horizontal scrolling, + // the additional 20px for the scrollbar should not be taken into consideration. + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'backward', + scrollLeft: 0, + scrollTop: 230, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'forward', + }); + + rendered.update( + + ); + + onScroll.mockClear(); + rendered + .getInstance() + .scrollToItem({ columnIndex: 15, rowIndex: 0, align: 'end' }); + + // Since there aren't enough rows to require vertical scrolling, + // the additional 20px for the scrollbar should not be taken into consideration. + expect(onScroll).toHaveBeenCalledWith({ + horizontalScrollDirection: 'forward', + scrollLeft: 720, + scrollTop: 0, + scrollUpdateWasRequested: true, + verticalScrollDirection: 'backward', + }); + }); }); describe('resetAfterIndex method', () => { @@ -408,4 +525,36 @@ describe('VariableSizeGrid', () => { ); }); }); + + // https://github.com/bvaughn/react-window/pull/138 + it('should descrease scroll size when itemCount decreases', () => { + const innerRef = createRef(); + const gridRef = createRef(); + + class Wrapper extends PureComponent { + state = { columnCount: 100, rowCount: 200 }; + render() { + return ( + + ); + } + } + + // Use ReactDOM renderer so "scroll" events work correctly. + const instance = render(, document.createElement('div')); + + // Simulate scrolling past several rows. + simulateScroll(gridRef.current, { scrollLeft: 3000, scrollTop: 4000 }); + + // Decrease itemCount a lot and verify the scroll height is descreased as well. + instance.setState({ columnCount: 2, rowCount: 4 }); + expect(innerRef.current.style.height).toEqual('106px'); + expect(innerRef.current.style.width).toEqual('101px'); + }); }); diff --git a/src/__tests__/VariableSizeList.js b/src/__tests__/VariableSizeList.js index b84bd999..ccfcf592 100644 --- a/src/__tests__/VariableSizeList.js +++ b/src/__tests__/VariableSizeList.js @@ -1,7 +1,18 @@ -import React, { PureComponent } from 'react'; +import React, { createRef, PureComponent } from 'react'; +import { render } from 'react-dom'; +import { Simulate } from 'react-dom/test-utils'; import ReactTestRenderer from 'react-test-renderer'; import { VariableSizeList } from '..'; +const simulateScroll = (instance, scrollOffset, direction = 'vertical') => { + if (direction === 'horizontal') { + instance._outerRef.scrollLeft = scrollOffset; + } else { + instance._outerRef.scrollTop = scrollOffset; + } + Simulate.scroll(instance._outerRef); +}; + const findScrollContainer = rendered => rendered.root.children[0].children[0]; describe('VariableSizeList', () => { @@ -295,4 +306,34 @@ describe('VariableSizeList', () => { ); }); }); + + // https://github.com/bvaughn/react-window/pull/138 + it('should descrease scroll size when itemCount decreases', () => { + const innerRef = createRef(); + const listRef = createRef(); + + class Wrapper extends PureComponent { + state = { itemCount: 100 }; + render() { + return ( + + ); + } + } + + // Use ReactDOM renderer so "scroll" events work correctly. + const instance = render(, document.createElement('div')); + + // Simulate scrolling past several rows. + simulateScroll(listRef.current, 3000); + + // Decrease itemCount a lot and verify the scroll height is descreased as well. + instance.setState({ itemCount: 3 }); + expect(innerRef.current.style.height).toEqual('78px'); + }); }); diff --git a/src/__tests__/__snapshots__/FixedSizeGrid.js.snap b/src/__tests__/__snapshots__/FixedSizeGrid.js.snap index b38eaac4..356b3690 100644 --- a/src/__tests__/__snapshots__/FixedSizeGrid.js.snap +++ b/src/__tests__/__snapshots__/FixedSizeGrid.js.snap @@ -311,6 +311,30 @@ Array [ "visibleRowStopIndex": 6, }, ], + Array [ + Object { + "overscanColumnStartIndex": 1, + "overscanColumnStopIndex": 5, + "overscanRowStartIndex": 6, + "overscanRowStopIndex": 12, + "visibleColumnStartIndex": 2, + "visibleColumnStopIndex": 4, + "visibleRowStartIndex": 7, + "visibleRowStopIndex": 11, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 0, + "overscanColumnStopIndex": 3, + "overscanRowStartIndex": 6, + "overscanRowStopIndex": 12, + "visibleColumnStartIndex": 0, + "visibleColumnStopIndex": 2, + "visibleRowStartIndex": 7, + "visibleRowStopIndex": 11, + }, + ], ] `; @@ -376,6 +400,30 @@ Array [ "visibleRowStopIndex": 99, }, ], + Array [ + Object { + "overscanColumnStartIndex": 97, + "overscanColumnStopIndex": 99, + "overscanRowStartIndex": 7, + "overscanRowStopIndex": 13, + "visibleColumnStartIndex": 98, + "visibleColumnStopIndex": 99, + "visibleRowStartIndex": 8, + "visibleRowStopIndex": 12, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 1, + "overscanColumnStopIndex": 5, + "overscanRowStartIndex": 7, + "overscanRowStopIndex": 13, + "visibleColumnStartIndex": 2, + "visibleColumnStopIndex": 4, + "visibleRowStartIndex": 8, + "visibleRowStopIndex": 12, + }, + ], ] `; @@ -429,6 +477,30 @@ Array [ "visibleRowStopIndex": 4, }, ], + Array [ + Object { + "overscanColumnStartIndex": 0, + "overscanColumnStopIndex": 3, + "overscanRowStartIndex": 6, + "overscanRowStopIndex": 12, + "visibleColumnStartIndex": 0, + "visibleColumnStopIndex": 2, + "visibleRowStartIndex": 7, + "visibleRowStopIndex": 11, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 7, + "overscanColumnStopIndex": 11, + "overscanRowStartIndex": 6, + "overscanRowStopIndex": 12, + "visibleColumnStartIndex": 8, + "visibleColumnStopIndex": 10, + "visibleRowStartIndex": 7, + "visibleRowStopIndex": 11, + }, + ], ] `; @@ -482,6 +554,30 @@ Array [ "visibleRowStopIndex": 99, }, ], + Array [ + Object { + "overscanColumnStartIndex": 97, + "overscanColumnStopIndex": 99, + "overscanRowStartIndex": 9, + "overscanRowStopIndex": 15, + "visibleColumnStartIndex": 98, + "visibleColumnStopIndex": 99, + "visibleRowStartIndex": 10, + "visibleRowStopIndex": 14, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 0, + "overscanColumnStopIndex": 3, + "overscanRowStartIndex": 9, + "overscanRowStopIndex": 15, + "visibleColumnStartIndex": 0, + "visibleColumnStopIndex": 2, + "visibleRowStartIndex": 10, + "visibleRowStopIndex": 14, + }, + ], ] `; diff --git a/src/__tests__/__snapshots__/VariableSizeGrid.js.snap b/src/__tests__/__snapshots__/VariableSizeGrid.js.snap index f2c689e7..407f8062 100644 --- a/src/__tests__/__snapshots__/VariableSizeGrid.js.snap +++ b/src/__tests__/__snapshots__/VariableSizeGrid.js.snap @@ -26,6 +26,30 @@ Array [ "visibleRowStopIndex": 5, }, ], + Array [ + Object { + "overscanColumnStartIndex": 1, + "overscanColumnStopIndex": 6, + "overscanRowStartIndex": 7, + "overscanRowStopIndex": 11, + "visibleColumnStartIndex": 2, + "visibleColumnStopIndex": 5, + "visibleRowStartIndex": 8, + "visibleRowStopIndex": 10, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 0, + "overscanColumnStopIndex": 4, + "overscanRowStartIndex": 7, + "overscanRowStopIndex": 11, + "visibleColumnStartIndex": 0, + "visibleColumnStopIndex": 3, + "visibleRowStartIndex": 8, + "visibleRowStopIndex": 10, + }, + ], ] `; @@ -91,6 +115,30 @@ Array [ "visibleRowStopIndex": 19, }, ], + Array [ + Object { + "overscanColumnStartIndex": 5, + "overscanColumnStopIndex": 9, + "overscanRowStartIndex": 8, + "overscanRowStopIndex": 12, + "visibleColumnStartIndex": 6, + "visibleColumnStopIndex": 9, + "visibleRowStartIndex": 9, + "visibleRowStopIndex": 11, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 0, + "overscanColumnStopIndex": 6, + "overscanRowStartIndex": 8, + "overscanRowStopIndex": 12, + "visibleColumnStartIndex": 1, + "visibleColumnStopIndex": 5, + "visibleRowStartIndex": 9, + "visibleRowStopIndex": 11, + }, + ], ] `; @@ -144,6 +192,30 @@ Array [ "visibleRowStopIndex": 3, }, ], + Array [ + Object { + "overscanColumnStartIndex": 0, + "overscanColumnStopIndex": 4, + "overscanRowStartIndex": 7, + "overscanRowStopIndex": 11, + "visibleColumnStartIndex": 0, + "visibleColumnStopIndex": 3, + "visibleRowStartIndex": 8, + "visibleRowStopIndex": 10, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 5, + "overscanColumnStopIndex": 9, + "overscanRowStartIndex": 7, + "overscanRowStopIndex": 11, + "visibleColumnStartIndex": 6, + "visibleColumnStopIndex": 9, + "visibleRowStartIndex": 8, + "visibleRowStopIndex": 10, + }, + ], ] `; @@ -197,5 +269,29 @@ Array [ "visibleRowStopIndex": 19, }, ], + Array [ + Object { + "overscanColumnStartIndex": 5, + "overscanColumnStopIndex": 9, + "overscanRowStartIndex": 9, + "overscanRowStopIndex": 13, + "visibleColumnStartIndex": 6, + "visibleColumnStopIndex": 9, + "visibleRowStartIndex": 10, + "visibleRowStopIndex": 12, + }, + ], + Array [ + Object { + "overscanColumnStartIndex": 0, + "overscanColumnStopIndex": 4, + "overscanRowStartIndex": 9, + "overscanRowStopIndex": 13, + "visibleColumnStartIndex": 0, + "visibleColumnStopIndex": 3, + "visibleRowStartIndex": 10, + "visibleRowStopIndex": 12, + }, + ], ] `; diff --git a/src/createGridComponent.js b/src/createGridComponent.js index e0d704f5..7e8fc85e 100644 --- a/src/createGridComponent.js +++ b/src/createGridComponent.js @@ -3,9 +3,11 @@ 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' | 'center' | 'start' | 'end'; type itemSize = number | ((index: number) => number); @@ -49,6 +51,7 @@ export type Props = {| className?: string, columnCount: number, columnWidth: itemSize, + direction: Direction, height: number, initialScrollLeft?: number, initialScrollTop?: number, @@ -77,6 +80,7 @@ export type Props = {| |}; type State = {| + instance: any, isScrolling: boolean, horizontalScrollDirection: ScrollDirection, scrollLeft: number, @@ -101,7 +105,8 @@ type GetOffsetForItemAndAlignment = ( index: number, align: ScrollToAlign, scrollOffset: number, - instanceProps: any + instanceProps: any, + scrollbarSize: number ) => number; type GetStartIndexForOffset = ( props: Props, @@ -122,6 +127,17 @@ 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 instace. +// 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, @@ -161,11 +177,13 @@ export default function createGridComponent({ _outerRef: ?HTMLDivElement; static defaultProps = { + direction: 'ltr', itemData: undefined, useIsScrolling: false, }; state: State = { + instance: this, isScrolling: false, horizontalScrollDirection: 'forward', scrollLeft: @@ -190,8 +208,8 @@ export default function createGridComponent({ static getDerivedStateFromProps( nextProps: Props, prevState: State - ): $Shape { - validateSharedProps(nextProps); + ): $Shape | null { + validateSharedProps(nextProps, prevState); validateProps(nextProps); return null; } @@ -229,26 +247,53 @@ export default function createGridComponent({ rowIndex, }: { align: ScrollToAlign, - columnIndex: number, - rowIndex: number, + columnIndex?: number, + rowIndex?: number, }): void { + const { height, width } = this.props; const { scrollLeft, scrollTop } = this.state; + const scrollbarSize = getScrollbarSize(); + + 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: getOffsetForColumnAndAlignment( - this.props, - columnIndex, - align, - scrollLeft, - this._instanceProps - ), - scrollTop: getOffsetForRowAndAlignment( - this.props, - rowIndex, - align, - scrollTop, - this._instanceProps - ), + 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, }); } @@ -285,6 +330,7 @@ export default function createGridComponent({ children, className, columnCount, + direction, height, innerRef, innerElementType, @@ -356,6 +402,7 @@ export default function createGridComponent({ overflow: 'auto', WebkitOverflowScrolling: 'touch', willChange: 'transform', + direction, ...style, }, }, @@ -482,20 +529,27 @@ export default function createGridComponent({ // 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 key = `${rowIndex}:${columnIndex}`; + const { columnWidth, direction, rowHeight } = this.props; const itemStyleCache = this._getItemStyleCache( - shouldResetStyleCacheOnItemSizeChange && this.props.columnWidth, - shouldResetStyleCacheOnItemSizeChange && this.props.rowHeight + 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', - left: getColumnOffset(this.props, columnIndex, this._instanceProps), + [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), @@ -505,11 +559,8 @@ export default function createGridComponent({ return style; }; - // TODO This memoized getter doesn't make much sense. - // If all that's really needed is for the impl to be able to reset the cache, - // Then we could expose a better API for that. - _getItemStyleCache: (_: any, __: any) => ItemStyleCache; - _getItemStyleCache = memoizeOne((_: any, __: any) => ({})); + _getItemStyleCache: (_: any, __: any, ___: any) => ItemStyleCache; + _getItemStyleCache = memoizeOne((_: any, __: any, ___: any) => ({})); _getHorizontalRangeToRender(): [number, number, number, number] { const { @@ -606,7 +657,12 @@ export default function createGridComponent({ } _onScroll = (event: ScrollEvent): void => { - const { scrollLeft, scrollTop } = event.currentTarget; + const { + clientWidth, + scrollLeft, + scrollTop, + scrollWidth, + } = event.currentTarget; this.setState(prevState => { if ( prevState.scrollLeft === scrollLeft && @@ -618,11 +674,25 @@ export default function createGridComponent({ 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 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, + scrollLeft: calculatedScrollLeft, scrollTop, verticalScrollDirection: prevState.scrollTop < scrollTop ? 'forward' : 'backward', @@ -670,27 +740,37 @@ export default function createGridComponent({ }; } -const validateSharedProps = ({ - children, - height, - innerTagName, - outerTagName, - overscanCount, - width, -}: Props): void => { +const validateSharedProps = ( + { + children, + direction, + height, + innerTagName, + outerTagName, + overscanCount, + width, + }: Props, + { instance }: State +): void => { if (process.env.NODE_ENV !== 'production') { if (typeof overscanCount === 'number') { - console.warn( - 'The overscanCount prop has been deprecated. ' + - 'Please use the overscanColumnsCount and overscanRowsCount props instead.' - ); + 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) { - console.warn( - 'The innerTagName and outerTagName props have been deprecated. ' + - 'Please use the innerElementType and outerElementType props instead.' - ); + 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) { @@ -701,6 +781,19 @@ const validateSharedProps = ({ ); } + 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. ' + diff --git a/src/createListComponent.js b/src/createListComponent.js index 18249c87..25b4a58e 100644 --- a/src/createListComponent.js +++ b/src/createListComponent.js @@ -9,7 +9,9 @@ import type { TimeoutID } from './timer'; export type ScrollToAlign = 'auto' | 'center' | 'start' | 'end'; type itemSize = number | ((index: number) => number); -export type Direction = 'horizontal' | 'vertical'; +// TODO Deprecate directions "horizontal" and "vertical" +export type Direction = 'ltr' | 'rtl' | 'horizontal' | 'vertical'; +export type Layout = 'horizontal' | 'vertical'; export type RenderComponentProps = {| data: T, @@ -49,6 +51,7 @@ export type Props = {| itemData: T, itemKey?: (index: number, data: T) => any, itemSize: itemSize, + layout: Layout, onItemsRendered?: onItemsRenderedCallback, onScroll?: onScrollCallback, outerRef?: any, @@ -61,6 +64,7 @@ export type Props = {| |}; type State = {| + instance: any, isScrolling: boolean, scrollDirection: ScrollDirection, scrollOffset: number, @@ -103,6 +107,17 @@ const IS_SCROLLING_DEBOUNCE_INTERVAL = 150; export const defaultItemKey = (index: number, data: any) => index; +// In DEV mode, this Set helps us only log a warning once per component instace. +// 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, @@ -130,13 +145,15 @@ export default function createListComponent({ _resetIsScrollingTimeoutId: TimeoutID | null = null; static defaultProps = { - direction: 'vertical', + direction: 'ltr', itemData: undefined, + layout: 'vertical', overscanCount: 2, useIsScrolling: false, }; state: State = { + instance: this, isScrolling: false, scrollDirection: 'forward', scrollOffset: @@ -154,11 +171,11 @@ export default function createListComponent({ } static getDerivedStateFromProps( - props: Props, - state: State - ): $Shape { - validateSharedProps(props); - validateProps(props); + nextProps: Props, + prevState: State + ): $Shape | null { + validateSharedProps(nextProps, prevState); + validateProps(nextProps); return null; } @@ -188,14 +205,16 @@ export default function createListComponent({ } componentDidMount() { - const { initialScrollOffset, direction } = this.props; + const { direction, initialScrollOffset, layout } = this.props; if (typeof initialScrollOffset === 'number' && this._outerRef !== null) { - const element = ((this._outerRef: any): HTMLDivElement); - if (direction === 'horizontal') { - element.scrollLeft = initialScrollOffset; + // TODO Deprecate direction "horizontal" + if (direction === 'horizontal' || layout === 'horizontal') { + ((this + ._outerRef: any): HTMLDivElement).scrollLeft = initialScrollOffset; } else { - element.scrollTop = initialScrollOffset; + ((this + ._outerRef: any): HTMLDivElement).scrollTop = initialScrollOffset; } } @@ -204,15 +223,15 @@ export default function createListComponent({ } componentDidUpdate() { - const { direction } = this.props; + const { direction, layout } = this.props; const { scrollOffset, scrollUpdateWasRequested } = this.state; if (scrollUpdateWasRequested && this._outerRef !== null) { - const element = ((this._outerRef: any): HTMLDivElement); - if (direction === 'horizontal') { - element.scrollLeft = scrollOffset; + // TODO Deprecate direction "horizontal" + if (direction === 'horizontal' || layout === 'horizontal') { + ((this._outerRef: any): HTMLDivElement).scrollLeft = scrollOffset; } else { - element.scrollTop = scrollOffset; + ((this._outerRef: any): HTMLDivElement).scrollTop = scrollOffset; } } @@ -236,6 +255,7 @@ export default function createListComponent({ innerRef, innerElementType, innerTagName, + layout, outerElementType, outerTagName, style, @@ -243,10 +263,13 @@ export default function createListComponent({ } = this.props; const { isScrolling } = this.state; - const onScroll = - direction === 'vertical' - ? this._onScrollVertical - : this._onScrollHorizontal; + // TODO Deprecate direction "horizontal" + const isHorizontal = + direction === 'horizontal' || layout === 'horizontal'; + + const onScroll = isHorizontal + ? this._onScrollHorizontal + : this._onScrollVertical; const items = this._renderItems(); @@ -270,6 +293,7 @@ export default function createListComponent({ position: 'relative', WebkitOverflowScrolling: 'touch', willChange: 'transform', + direction, ...style, }, }, @@ -277,9 +301,9 @@ export default function createListComponent({ children: items, ref: innerRef, style: { - height: direction === 'horizontal' ? '100%' : estimatedTotalSize, + height: isHorizontal ? '100%' : estimatedTotalSize, pointerEvents: isScrolling ? 'none' : '', - width: direction === 'horizontal' ? estimatedTotalSize : '100%', + width: isHorizontal ? estimatedTotalSize : '100%', }, }) ); @@ -371,10 +395,11 @@ export default function createListComponent({ // 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 } = this.props; + const { direction, itemSize, layout } = this.props; const itemStyleCache = this._getItemStyleCache( shouldResetStyleCacheOnItemSizeChange && itemSize, + shouldResetStyleCacheOnItemSizeChange && layout, shouldResetStyleCacheOnItemSizeChange && direction ); @@ -385,12 +410,16 @@ export default function createListComponent({ 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', - left: direction === 'horizontal' ? offset : 0, - top: direction === 'vertical' ? offset : 0, - height: direction === 'vertical' ? size : '100%', - width: direction === 'horizontal' ? size : '100%', + [direction === 'rtl' ? 'right' : 'left']: isHorizontal ? offset : 0, + top: !isHorizontal ? offset : 0, + height: !isHorizontal ? size : '100%', + width: isHorizontal ? size : '100%', }; } @@ -402,8 +431,8 @@ export default function createListComponent({ // TODO This memoized getter doesn't make much sense. // If all that's really needed is for the impl to be able to reset the cache, // Then we could expose a better API for that. - _getItemStyleCache: (_: any, __: any) => ItemStyleCache; - _getItemStyleCache = memoizeOne((_, __) => { + _getItemStyleCache: (_: any, __: any, ___: any) => ItemStyleCache; + _getItemStyleCache = memoizeOne((_: any, __: any, ___: any) => { this._itemStyleCache = {}; return this._itemStyleCache; @@ -478,7 +507,7 @@ export default function createListComponent({ } _onScrollHorizontal = (event: ScrollEvent): void => { - const { scrollLeft } = event.currentTarget; + const { clientWidth, scrollLeft, scrollWidth } = event.currentTarget; this.setState(prevState => { if (prevState.scrollOffset === scrollLeft) { // Scroll position may have been updated by cDM/cDU, @@ -487,11 +516,25 @@ export default function createListComponent({ 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: scrollLeft, + scrollOffset, scrollUpdateWasRequested: false, }; }, this._resetIsScrollingDebounced); @@ -566,28 +609,66 @@ export default function createListComponent({ // 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, - innerTagName, - outerTagName, - width, -}: Props): void => { +const validateSharedProps = ( + { + children, + direction, + height, + layout, + innerTagName, + outerTagName, + width, + }: Props, + { instance }: State +): void => { if (process.env.NODE_ENV !== 'production') { if (innerTagName != null || outerTagName != null) { - console.warn( - 'The innerTagName and outerTagName props have been deprecated. ' + - 'Please use the innerElementType and outerElementType props instead.' - ); + 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 (direction !== 'horizontal' && direction !== 'vertical') { - throw Error( - 'An invalid "direction" prop has been specified. ' + - 'Value should be either "horizontal" or "vertical". ' + - `"${direction}" was specified.` - ); + // 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) { @@ -598,13 +679,13 @@ const validateSharedProps = ({ ); } - if (direction === 'horizontal' && typeof width !== 'number') { + 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 (direction === 'vertical' && typeof height !== 'number') { + } else if (!isHorizontal && typeof height !== 'number') { throw Error( 'An invalid "height" prop has been specified. ' + 'Vertical lists must specify a number for height. ' + diff --git a/src/domHelpers.js b/src/domHelpers.js new file mode 100644 index 00000000..830aba68 --- /dev/null +++ b/src/domHelpers.js @@ -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; +} diff --git a/website/config/webpack.config.dev.js b/website/config/webpack.config.dev.js index b145b81a..db716bb2 100644 --- a/website/config/webpack.config.dev.js +++ b/website/config/webpack.config.dev.js @@ -181,7 +181,7 @@ module.exports = { // Custom CodeMirror loader for syntax highlighting { - test: /code\/[^/]+$/, + test: /[/\\\\]code[/\\\\]/, loader: require.resolve('../webpack/codemirror-loader'), }, diff --git a/website/config/webpack.config.prod.js b/website/config/webpack.config.prod.js index e529fde9..45267b27 100644 --- a/website/config/webpack.config.prod.js +++ b/website/config/webpack.config.prod.js @@ -211,7 +211,7 @@ module.exports = { // Custom CodeMirror loader for syntax highlighting { - test: /code\/[^/]+$/, + test: /[/\\\\]code[/\\\\]/, loader: require.resolve('../webpack/codemirror-loader'), }, diff --git a/website/package.json b/website/package.json index 3102e563..a7ce5463 100644 --- a/website/package.json +++ b/website/package.json @@ -50,10 +50,11 @@ "prop-types": "^15.6.1", "raf": "3.4.0", "random-words": "^1.1.0", - "react": "^16.7.0", + "react": "^16.8.4", "react-codemirror2": "^5.0.1", "react-dev-utils": "^6.0.0-next.66cc7a90", - "react-dom": "^16.7.0", + "react-dom": "^16.8.4", + "react-is": "^16.8.4", "react-router": "^4.2.0", "react-router-dom": "^4.2.2", "react-router-hash-link": "^1.2.0", diff --git a/website/sandboxes/dynamic-size-list-horizontal/index.js b/website/sandboxes/dynamic-size-list-horizontal/index.js index 935c95df..3fe0620f 100644 --- a/website/sandboxes/dynamic-size-list-horizontal/index.js +++ b/website/sandboxes/dynamic-size-list-horizontal/index.js @@ -26,7 +26,7 @@ const Example = () => ( ( const Example = () => ( {Column} diff --git a/website/sandboxes/memoized-list-items/package.json b/website/sandboxes/memoized-list-items/package.json index 1d2a045c..6110232c 100644 --- a/website/sandboxes/memoized-list-items/package.json +++ b/website/sandboxes/memoized-list-items/package.json @@ -2,7 +2,7 @@ "description": "Demo of react-window with advanced memoization techniques", "main": "src/index.js", "dependencies": { - "memoize-one": "^4", + "memoize-one": "^5", "react": "^16.6", "react-dom": "^16.6", "react-window": "^1" diff --git a/website/sandboxes/variable-size-list-horizontal/index.js b/website/sandboxes/variable-size-list-horizontal/index.js index 89f1d54f..cc69d7d1 100644 --- a/website/sandboxes/variable-size-list-horizontal/index.js +++ b/website/sandboxes/variable-size-list-horizontal/index.js @@ -21,10 +21,10 @@ const Column = ({ index, style }) => ( const Example = () => ( {Column} diff --git a/website/src/App.js b/website/src/App.js index f02b9f95..170cf955 100644 --- a/website/src/App.js +++ b/website/src/App.js @@ -11,6 +11,7 @@ import FixedSizeGridApi from './routes/api/FixedSizeGrid'; import FixedSizeGridExample from './routes/examples/FixedSizeGrid'; import FixedSizeListApi from './routes/api/FixedSizeList'; import FixedSizeListExample from './routes/examples/FixedSizeList'; +import RTLLayoutExample from './routes/examples/RTLLayout'; import ListWithScrollingIndicatorExample from './routes/examples/ListWithScrollingIndicator'; import MemoizedListItemsExample from './routes/examples/MemoizedListItemsExample'; import ScrollToItemExample from './routes/examples/ScrollToItem'; @@ -102,6 +103,11 @@ const EXAMPLE_ROUTES = [ title: 'Memoized List items', component: MemoizedListItemsExample, }, + { + path: '/examples/list/fixed-size-rtl', + title: 'RTL layout', + component: RTLLayoutExample, + }, ]; const COMPONENTS_ROUTES = [ diff --git a/website/src/code/DynamicSizeListHorizontal.js b/website/src/code/DynamicSizeListHorizontal.js index e1e69e79..a76b502e 100644 --- a/website/src/code/DynamicSizeListHorizontal.js +++ b/website/src/code/DynamicSizeListHorizontal.js @@ -10,7 +10,7 @@ const Column = ({ index, style }) => ( // Note that no itemSize is required for dyanmic lists! const Example = () => ( ( +
+ بند {rowIndex},{columnIndex} +
+); + +const Example = () => ( + + {Cell} + +); \ No newline at end of file diff --git a/website/src/code/FixedSizeListHorizontal.js b/website/src/code/FixedSizeListHorizontal.js index 72cdefd1..654196df 100644 --- a/website/src/code/FixedSizeListHorizontal.js +++ b/website/src/code/FixedSizeListHorizontal.js @@ -6,10 +6,10 @@ const Column = ({ index, style }) => ( const Example = () => ( {Column} diff --git a/website/src/code/FixedSizeListHorizontalRtl.js b/website/src/code/FixedSizeListHorizontalRtl.js new file mode 100644 index 00000000..b43071e0 --- /dev/null +++ b/website/src/code/FixedSizeListHorizontalRtl.js @@ -0,0 +1,18 @@ +import { FixedSizeList as List } from 'react-window'; + +const Column = ({ index, style }) => ( +
عمود {index}
+); + +const Example = () => ( + + {Column} + +); diff --git a/website/src/code/ScrollToItemGrid.js b/website/src/code/ScrollToItemGrid.js index b9a21cbe..b867c2ec 100644 --- a/website/src/code/ScrollToItemGrid.js +++ b/website/src/code/ScrollToItemGrid.js @@ -19,4 +19,10 @@ gridRef.current.scrollToItem({ align: "start", columnIndex: 150, rowIndex: 300 +}); + +// You can specify only columnIndex or rowIndex if you just want to scroll one axis. +// For example: +gridRef.current.scrollToItem({ + columnIndex: 100, }); \ No newline at end of file diff --git a/website/src/code/VariableSizeListHorizontal.js b/website/src/code/VariableSizeListHorizontal.js index 8548dc79..654d7e17 100644 --- a/website/src/code/VariableSizeListHorizontal.js +++ b/website/src/code/VariableSizeListHorizontal.js @@ -14,10 +14,10 @@ const Column = ({ index, style }) => ( const Example = () => ( {Column} diff --git a/website/src/components/ProfiledExample.js b/website/src/components/ProfiledExample.js index cf108584..54f405c2 100644 --- a/website/src/components/ProfiledExample.js +++ b/website/src/components/ProfiledExample.js @@ -13,7 +13,7 @@ type Props = {| style?: Object, |}; -const isProfilingEnabled = window.location.hash.includes('profile=true'); +const isProfilingEnabled = window.location.hash.indexOf('profile=true') >= 0; export default class ProfiledExample extends PureComponent { _averageTimeRef = createRef(); diff --git a/website/src/routes/api/FixedSizeGrid.js b/website/src/routes/api/FixedSizeGrid.js index fab2bb2c..9709b581 100644 --- a/website/src/routes/api/FixedSizeGrid.js +++ b/website/src/routes/api/FixedSizeGrid.js @@ -74,6 +74,31 @@ const PROPS = [ name: 'columnWidth', type: 'number', }, + { + defaultValue: '"ltr"', + description: ( + +

Determines the direction of text and horizontal scrolling.

+
    +
  • ltr (default)
  • +
  • rtl
  • +
+

+ This property also automatically sets the{' '} + + CSS direction style + {' '} + for the grid component. +

+
+ ), + name: 'direction', + type: 'string', + }, { description: (

@@ -181,7 +206,7 @@ const PROPS = [

Called when the range of items rendered by the grid changes.

- This callback will only be called when item indices change. It will + This callback will only be called when item indices change. It will not be called if items are re-rendered for other reasons (e.g. a change in isScrolling or data params).

@@ -384,19 +409,31 @@ const METHODS = [

By default, the Grid will scroll as little as possible to ensure the item is visible. You can control the alignment of the item though by - specifying a second alignment parameter. Acceptable values are: + specifying an align property. Acceptable values are:

  • - auto (default) - Scroll as little as possible to ensure the item is - visible. (If the item is already visible, it won't scroll at all.) + auto (default) - Scroll as little as possible to ensure + the item is visible. (If the item is already visible, it won't + scroll at all.) +
  • +
  • + center - Center align the item within the grid.
  • -
  • center - Center align the item within the grid.
  • - end - Align the item to the bottom, right hand side of the grid. + end - Align the item to the bottom, right hand side of + the grid. +
  • +
  • + start - Align the item to the top, left hand of the + grid.
  • -
  • start - Align the item to the top, left hand of the grid.
+

+ If either columnIndex or rowIndex are + omitted, scrollLeft or scrollTop will be + unchanged (respectively). +

See here for an example of this API. @@ -405,6 +442,6 @@ const METHODS = [ ), signature: - 'scrollToItem({align: string = "auto", columnIndex: number, rowIndex: number }): void', + 'scrollToItem({align: string = "auto", columnIndex?: number, rowIndex?: number }): void', }, ]; diff --git a/website/src/routes/api/FixedSizeList.js b/website/src/routes/api/FixedSizeList.js index dea1aed3..70be84ca 100644 --- a/website/src/routes/api/FixedSizeList.js +++ b/website/src/routes/api/FixedSizeList.js @@ -58,17 +58,24 @@ const PROPS = [ type: 'string', }, { - defaultValue: '"vertical"', + defaultValue: '"ltr"', description: ( -

Primary scroll direction of the list. Acceptable values are:

+

Determines the direction of text and horizontal scrolling.

    -
  • vertical (default) - Up/down scrolling.
  • -
  • horizontal - Left/right scrolling.
  • +
  • ltr (default)
  • +
  • rtl

- Note that lists may scroll in both directions (depending on CSS) but - content will only be windowed in the primary direction. + This property also automatically sets the{' '} + + CSS direction style + {' '} + for the list component.

), @@ -207,6 +214,24 @@ const PROPS = [ name: 'itemSize', type: 'number', }, + { + defaultValue: '"vertical"', + description: ( + +

Layout/orientation of the list. Acceptable values are:

+
    +
  • vertical (default) - Up/down scrolling.
  • +
  • horizontal - Left/right scrolling.
  • +
+

+ Note that lists may scroll in both directions (depending on CSS) but + content will only be windowed in the layout direction specified. +

+
+ ), + name: 'layout', + type: 'string', + }, { description: ( diff --git a/website/src/routes/examples/DynamicSizeList.js b/website/src/routes/examples/DynamicSizeList.js index ae5742f7..c5bd1114 100644 --- a/website/src/routes/examples/DynamicSizeList.js +++ b/website/src/routes/examples/DynamicSizeList.js @@ -200,7 +200,7 @@ export default class DynamicSizeList extends PureComponent { {Item} diff --git a/website/src/routes/examples/MemoizedListItemsExample.js b/website/src/routes/examples/MemoizedListItemsExample.js index 6eb60e0c..ebbde055 100644 --- a/website/src/routes/examples/MemoizedListItemsExample.js +++ b/website/src/routes/examples/MemoizedListItemsExample.js @@ -3,20 +3,19 @@ import React, { PureComponent, memo } from 'react'; import { FixedSizeList, areEqual } from 'react-window'; import CodeBlock from '../../components/CodeBlock'; import ProfiledExample from '../../components/ProfiledExample'; +import { fillArray } from '../../utils'; import CODE from '../../code/MemoizedListItems.js'; import styles from './shared.module.css'; const generateItems = numItems => - Array(numItems) - .fill(true) - .map(_ => ({ - isActive: false, - label: Math.random() - .toString(36) - .substr(2), - })); + fillArray(numItems, () => ({ + isActive: false, + label: Math.random() + .toString(36) + .substr(2), + })); const Row = memo(({ data, index, style }) => { const { items, toggleItemActive } = data; diff --git a/website/src/routes/examples/RTLLayout.js b/website/src/routes/examples/RTLLayout.js new file mode 100644 index 00000000..26d533f0 --- /dev/null +++ b/website/src/routes/examples/RTLLayout.js @@ -0,0 +1,99 @@ +import React, { PureComponent } from 'react'; +import { FixedSizeGrid, FixedSizeList } from 'react-window'; +import CodeBlock from '../../components/CodeBlock'; +import ProfiledExample from '../../components/ProfiledExample'; + +import CODE_GRID from '../../code/FixedSizeGridRtl.js'; +import CODE_LIST from '../../code/FixedSizeListHorizontalRtl.js'; + +import styles from './shared.module.css'; + +class Cell extends PureComponent { + render() { + const { columnIndex, rowIndex, style } = this.props; + + return ( +
+ بند {rowIndex}, {columnIndex} +
+ ); + } +} + +class Item extends PureComponent { + render() { + const { index, style } = this.props; + + return ( +
+ عمود {index} +
+ ); + } +} + +export default function() { + return ( +
+

RTL List

+
+ + + {Item} + + +
+ +
+
+

RTL Grid

+
+ + + {Cell} + + +
+ +
+
+
+ ); +} diff --git a/website/src/routes/examples/ScrollToItem.js b/website/src/routes/examples/ScrollToItem.js index a2132f1f..bccdecee 100644 --- a/website/src/routes/examples/ScrollToItem.js +++ b/website/src/routes/examples/ScrollToItem.js @@ -4,18 +4,15 @@ import { unstable_trace as trace } from 'scheduler/tracing'; import CodeBlock from '../../components/CodeBlock'; import ProfiledExample from '../../components/ProfiledExample'; +import { fillArray } from '../../utils'; import CODE_GRID from '../../code/ScrollToItemGrid.js'; import CODE_LIST from '../../code/ScrollToItemList.js'; import styles from './shared.module.css'; -const columnWidths = new Array(1000) - .fill(true) - .map(() => 75 + Math.round(Math.random() * 50)); -const rowHeights = new Array(1000) - .fill(true) - .map(() => 25 + Math.round(Math.random() * 50)); +const columnWidths = fillArray(1000, () => 75 + Math.round(Math.random() * 50)); +const rowHeights = fillArray(1000, () => 25 + Math.round(Math.random() * 50)); class ListItemRenderer extends PureComponent { render() { @@ -124,6 +121,18 @@ export default class ScrollToItem extends PureComponent { > Scroll to row 200, column 100 (align: center) + + { + this.gridRef.current.scrollToItem({ + columnIndex: 50, + }); + }; + scrollToRow200Auto = () => trace('scroll to row 200', performance.now(), () => this.listRef.current.scrollToItem(200) @@ -154,6 +169,12 @@ export default class ScrollToItem extends PureComponent { this.listRef.current.scrollToItem(300, 'center') ); + scrollToRow100 = () => { + this.gridRef.current.scrollToItem({ + rowIndex: 100, + }); + }; + scrollToRow100Column50Auto = () => { this.gridRef.current.scrollToItem({ columnIndex: 50, diff --git a/website/src/routes/examples/VariableSizeGrid.js b/website/src/routes/examples/VariableSizeGrid.js index 6322fb63..84c1ded8 100644 --- a/website/src/routes/examples/VariableSizeGrid.js +++ b/website/src/routes/examples/VariableSizeGrid.js @@ -2,17 +2,14 @@ import React, { PureComponent } from 'react'; import { VariableSizeGrid } from 'react-window'; import CodeBlock from '../../components/CodeBlock'; import ProfiledExample from '../../components/ProfiledExample'; +import { fillArray } from '../../utils'; import CODE from '../../code/VariableSizeGrid.js'; import styles from './shared.module.css'; -const columnWidths = new Array(1000) - .fill(true) - .map(() => 75 + Math.round(Math.random() * 50)); -const rowHeights = new Array(1000) - .fill(true) - .map(() => 25 + Math.round(Math.random() * 50)); +const columnWidths = fillArray(1000, () => 75 + Math.round(Math.random() * 50)); +const rowHeights = fillArray(1000, () => 25 + Math.round(Math.random() * 50)); class Cell extends PureComponent { render() { diff --git a/website/src/routes/examples/VariableSizeList.js b/website/src/routes/examples/VariableSizeList.js index 748ce6b9..c5e4b9ef 100644 --- a/website/src/routes/examples/VariableSizeList.js +++ b/website/src/routes/examples/VariableSizeList.js @@ -2,18 +2,15 @@ import React, { PureComponent } from 'react'; import { VariableSizeList } from 'react-window'; import CodeBlock from '../../components/CodeBlock'; import ProfiledExample from '../../components/ProfiledExample'; +import { fillArray } from '../../utils'; import CODE_HORIZONTAL from '../../code/VariableSizeListHorizontal.js'; import CODE_VERTICAL from '../../code/VariableSizeListVertical.js'; import styles from './shared.module.css'; -const columnSizes = new Array(1000) - .fill(true) - .map(() => 75 + Math.round(Math.random() * 50)); -const rowSizes = new Array(1000) - .fill(true) - .map(() => 25 + Math.round(Math.random() * 50)); +const columnSizes = fillArray(1000, () => 75 + Math.round(Math.random() * 50)); +const rowSizes = fillArray(1000, () => 25 + Math.round(Math.random() * 50)); class Item extends PureComponent { render() { @@ -61,11 +58,11 @@ export default function() { > columnSizes[index]} + layout="horizontal" width={300} > {Item} diff --git a/website/src/utils.js b/website/src/utils.js new file mode 100644 index 00000000..9ffba561 --- /dev/null +++ b/website/src/utils.js @@ -0,0 +1,7 @@ +export function fillArray(length, fillFunction): Array { + const array = new Array(length); + for (let i = 0; i < length; i++) { + array[i] = fillFunction(i); + } + return array; +} diff --git a/website/yarn.lock b/website/yarn.lock index 70c13bf9..3e9d3f1c 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -6167,9 +6167,10 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" -memoize-one@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" +"memoize-one@>=3.1.1 <6": + version "5.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e" + integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw== memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" @@ -7562,20 +7563,25 @@ react-dev-utils@6.0.0-next.66cc7a90, react-dev-utils@^6.0.0-next.66cc7a90: strip-ansi "4.0.0" text-table "0.2.0" -react-dom@^16.7.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0.tgz#a17b2a7ca89ee7390bc1ed5eb81783c7461748b8" - integrity sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg== +react-dom@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48" + integrity sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.12.0" + scheduler "^0.13.4" react-error-overlay@5.0.0-next.66cc7a90: version "5.0.0-next.66cc7a90" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.0.0-next.66cc7a90.tgz#68379b131ebe74112a12197504bfe7fa53119b3b" +react-is@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" + integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== + react-router-dom@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" @@ -7664,15 +7670,15 @@ react-scripts@^2.0.0-next.66cc7a90: version "0.0.0" uid "" -react@^16.7.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381" - integrity sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A== +react@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" + integrity sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.12.0" + scheduler "^0.13.4" read-pkg-up@^1.0.1: version "1.0.1" @@ -8165,10 +8171,10 @@ sax@^1.2.1, sax@^1.2.4, sax@~1.2.1, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -scheduler@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0.tgz#8ab17699939c0aedc5a196a657743c496538647b" - integrity sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw== +scheduler@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" + integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" diff --git a/yarn.lock b/yarn.lock index f81bc3cb..2bd7c3fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -586,10 +586,12 @@ "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/node@*": - version "10.1.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.2.tgz#1b928a0baa408fc8ae3ac012cc81375addc147c6" +"@types/node@^11.9.5": + version "11.10.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.10.4.tgz#3f5fc4f0f322805f009e00ab35a2ff3d6b778e42" + integrity sha512-wa09itaLE8L705aXd8F80jnFpxz3Y1/KRHfKsYL2bPc0XF+wEWu8sR9n5bmeu8Ba1N9z2GRNzm/YdHcghLkLKg== abab@^1.0.3, abab@^1.0.4: version "1.0.4" @@ -638,10 +640,15 @@ acorn@^4.0.3, acorn@^4.0.4: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.5.0: +acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: version "5.5.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" +acorn@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== + address@1.0.3, address@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" @@ -1888,9 +1895,10 @@ builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" -builtin-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e" +builtin-modules@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.0.0.tgz#1e587d44b006620d90286cc7a9238bbc6129cab1" + integrity sha512-hMIeU4K2ilbXV6Uv93ZZ0Avg/M91RaKXucQ+4me2Do1txxBDyDZWCBa5bJSLqoNTRpXTLwEzIk1KmloenDDjhg== builtin-status-codes@^3.0.0: version "3.0.0" @@ -3293,14 +3301,15 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" -estree-walker@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa" - -estree-walker@^0.5.0, estree-walker@^0.5.2: +estree-walker@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39" +estree-walker@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.0.tgz#5d865327c44a618dde5699f763891ae31f257dae" + integrity sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw== + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -5543,11 +5552,12 @@ macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" -magic-string@^0.22.4: - version "0.22.5" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" +magic-string@^0.25.1: + version "0.25.2" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.2.tgz#139c3a729515ec55e96e69e82a11fe890a293ad9" + integrity sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg== dependencies: - vlq "^0.2.2" + sourcemap-codec "^1.4.4" make-dir@^1.0.0: version "1.2.0" @@ -5596,9 +5606,10 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" -memoize-one@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" +"memoize-one@>=3.1.1 <6": + version "5.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e" + integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw== memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" @@ -5652,7 +5663,7 @@ micromatch@^2.1.5, micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.1.4, micromatch@^3.1.8: +micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: @@ -6273,6 +6284,11 @@ path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -6878,15 +6894,15 @@ react-dev-utils@^5.0.1: strip-ansi "3.0.1" text-table "0.2.0" -react-dom@^16.7.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0.tgz#a17b2a7ca89ee7390bc1ed5eb81783c7461748b8" - integrity sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg== +react-dom@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48" + integrity sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.12.0" + scheduler "^0.13.4" react-error-overlay@^4.0.0: version "4.0.0" @@ -6897,6 +6913,11 @@ react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa" integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g== +react-is@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" + integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== + react-scripts@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-1.1.4.tgz#d5c230e707918d6dd2d06f303b10f5222d017c88" @@ -6952,15 +6973,15 @@ react-test-renderer@^16.7.0: react-is "^16.7.0" scheduler "^0.12.0" -react@^16.7.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381" - integrity sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A== +react@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" + integrity sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.12.0" + scheduler "^0.13.4" read-pkg-up@^1.0.1: version "1.0.1" @@ -7297,7 +7318,14 @@ resolve@1.6.0: dependencies: path-parse "^1.0.5" -resolve@^1.1.6, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: +resolve@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" + integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== + dependencies: + path-parse "^1.0.6" + +resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0, resolve@^1.6.0: version "1.7.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" dependencies: @@ -7346,37 +7374,32 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-babel@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.0.2.tgz#c073eeb0cc246324e6f6feaedbb90093841a138c" +rollup-plugin-babel@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.3.2.tgz#8c0e1bd7aa9826e90769cf76895007098ffd1413" + integrity sha512-KfnizE258L/4enADKX61ozfwGHoqYauvoofghFJBhFnpH9Sb9dNPpWg8QHOaAfVASUYV8w0mCx430i9z0LJoJg== dependencies: "@babel/helper-module-imports" "^7.0.0" rollup-pluginutils "^2.3.0" -rollup-plugin-commonjs@^8.2.1: - version "8.4.1" - resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.4.1.tgz#5c9cea2b2c3de322f5fbccd147e07ed5e502d7a0" +rollup-plugin-commonjs@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.1.tgz#bb151ca8fa23600c7a03e25f9f0a45b1ee922dac" + integrity sha512-X0A/Cp/t+zbONFinBhiTZrfuUaVwRIp4xsbKq/2ohA2CDULa/7ONSJTelqxon+Vds2R2t2qJTqJQucKUC8GKkw== dependencies: - acorn "^5.2.1" - estree-walker "^0.5.0" - magic-string "^0.22.4" - resolve "^1.4.0" - rollup-pluginutils "^2.0.1" + estree-walker "^0.5.2" + magic-string "^0.25.1" + resolve "^1.10.0" + rollup-pluginutils "^2.3.3" -rollup-plugin-node-resolve@^3.0.2: - version "3.3.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz#c26d110a36812cbefa7ce117cadcd3439aa1c713" +rollup-plugin-node-resolve@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.0.1.tgz#f95765d174e5daeef9ea6268566141f53aa9d422" + integrity sha512-fSS7YDuCe0gYqKsr5OvxMloeZYUSgN43Ypi1WeRZzQcWtHgFayV5tUSPYpxuaioIIWaBXl6NrVk0T2/sKwueLg== dependencies: - builtin-modules "^2.0.0" + builtin-modules "^3.0.0" is-module "^1.0.0" - resolve "^1.1.6" - -rollup-pluginutils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz#7ec95b3573f6543a46a6461bd9a7c544525d0fc0" - dependencies: - estree-walker "^0.3.0" - micromatch "^2.3.11" + resolve "^1.10.0" rollup-pluginutils@^2.3.0: version "2.3.1" @@ -7385,12 +7408,22 @@ rollup-pluginutils@^2.3.0: estree-walker "^0.5.2" micromatch "^2.3.11" -rollup@^0.65.0: - version "0.65.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.65.0.tgz#280db1252169b68fc3043028346b337dde453fba" +rollup-pluginutils@^2.3.3: + version "2.4.1" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.4.1.tgz#de43ab54965bbf47843599a7f3adceb723de38db" + integrity sha512-wesMQ9/172IJDIW/lYWm0vW0LiKe5Ekjws481R7z9WTRtmO59cqyM/2uUlxvf6yzm/fElFmHUobeQOYz46dZJw== + dependencies: + estree-walker "^0.6.0" + micromatch "^3.1.10" + +rollup@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.4.1.tgz#cc03ef6fb49dd72a878e3da0131c0a3696de14a7" + integrity sha512-YWf5zeR6SWtqZmCnuYs4a+ZJetj8NT4yfBMPXekWHW4L3144jM+J2AWagQVejB0FwCqjEUP9l8o4hg1rPDfQlg== dependencies: "@types/estree" "0.0.39" - "@types/node" "*" + "@types/node" "^11.9.5" + acorn "^6.1.1" run-async@^2.2.0: version "2.3.0" @@ -7456,6 +7489,14 @@ scheduler@^0.12.0: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" + integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" @@ -7717,6 +7758,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +sourcemap-codec@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz#c63ea927c029dd6bd9a2b7fa03b3fec02ad56e9f" + integrity sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg== + spdx-correct@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" @@ -8423,10 +8469,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vlq@^0.2.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" - vm-browserify@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"