UNPKG

react-virtualized

Version:

React components for efficiently rendering large, scrollable lists and tabular data

450 lines (445 loc) 19.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var React = _interopRequireWildcard(require("react")); var _reactDom = require("react-dom"); var _testUtils = require("react-dom/test-utils"); var _TestUtils = require("../TestUtils"); var _createCellPositioner = _interopRequireDefault(require("./createCellPositioner")); var _Masonry = _interopRequireDefault(require("./Masonry")); var _CellMeasurer = require("../CellMeasurer"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var ALTERNATING_CELL_HEIGHTS = [100, 50, 100, 150]; var CELL_SIZE_MULTIPLIER = 50; var COLUMN_COUNT = 3; function assertVisibleCells(rendered, text) { expect(Array.from(rendered.querySelectorAll('.cell')).map(function (node) { return node.textContent; }).sort().join(',')).toEqual(text); } function createCellMeasurerCache() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return new _CellMeasurer.CellMeasurerCache(_objectSpread({ defaultHeight: CELL_SIZE_MULTIPLIER, defaultWidth: CELL_SIZE_MULTIPLIER, fixedWidth: true, keyMapper: function keyMapper(index) { return index; } }, props)); } function createCellPositioner(cache) { return (0, _createCellPositioner["default"])({ cellMeasurerCache: cache, columnCount: COLUMN_COUNT, columnWidth: CELL_SIZE_MULTIPLIER }); } function createCellRenderer(cache, renderCallback) { renderCallback = typeof renderCallback === 'function' ? renderCallback : function (index) { return index; }; return function cellRenderer(_ref) { var index = _ref.index, isScrolling = _ref.isScrolling, key = _ref.key, parent = _ref.parent, style = _ref.style; var height = ALTERNATING_CELL_HEIGHTS[index % ALTERNATING_CELL_HEIGHTS.length]; var width = CELL_SIZE_MULTIPLIER; return /*#__PURE__*/React.createElement(_CellMeasurer.CellMeasurer, { cache: cache, index: index, key: key, parent: parent }, /*#__PURE__*/React.createElement("div", { className: "cell", ref: function ref(_ref2) { if (_ref2) { // Accounts for the fact that JSDom doesn't support measurements. Object.defineProperty(_ref2, 'offsetHeight', { configurable: true, value: height }); Object.defineProperty(_ref2, 'offsetWidth', { configurable: true, value: width }); } }, style: _objectSpread(_objectSpread({}, style), {}, { minHeight: height, minWidth: width }) }, renderCallback(index, { index: index, isScrolling: isScrolling, key: key, parent: parent, style: style }))); }; } function getMarkup() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var cellMeasurerCache = props.cellMeasurerCache || createCellMeasurerCache(); return /*#__PURE__*/React.createElement(_Masonry["default"], (0, _extends2["default"])({ cellCount: 1000, cellMeasurerCache: cellMeasurerCache, cellPositioner: createCellPositioner(cellMeasurerCache), cellRenderer: createCellRenderer(cellMeasurerCache), columnCount: COLUMN_COUNT, height: CELL_SIZE_MULTIPLIER * 2, overscanByPixels: CELL_SIZE_MULTIPLIER, width: CELL_SIZE_MULTIPLIER * COLUMN_COUNT }, props)); } function simulateScroll(masonry) { var scrollTop = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var target = { scrollTop: scrollTop }; masonry._scrollingContainer = target; // HACK to work around _onScroll target check var masonryNode = (0, _reactDom.findDOMNode)(masonry); masonryNode.scrollTop = scrollTop; _testUtils.Simulate.scroll(masonryNode); } describe('Masonry', function () { beforeEach(_TestUtils.render.unmount); describe('layout and measuring', function () { it('should measure only enough cells required for initial render', function () { // avg cell size: CELL_SIZE_MULTIPLIER // width: CELL_SIZE_MULTIPLIER * 3 // height: CELL_SIZE_MULTIPLIER * 2 // overcsan by: CELL_SIZE_MULTIPLIER // Expected to measure 9 cells var cellMeasurerCache = createCellMeasurerCache(); (0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache })); for (var i = 0; i <= 8; i++) { expect(cellMeasurerCache.has(i)).toBe(true); } expect(cellMeasurerCache.has(9)).toBe(false); }); it('should not measure cells while scrolling until they are needed', function () { // Expected to measure 9 cells var cellMeasurerCache = createCellMeasurerCache(); var renderCallback = jest.fn().mockImplementation(function (index) { return index; }); var cellRenderer = createCellRenderer(cellMeasurerCache, renderCallback); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache, cellRenderer: cellRenderer }))); renderCallback.mockClear(); // Scroll a little bit, but not so much to require re-measuring simulateScroll(rendered, 51); // Verify that render was only called enough times to fill view port (no extra for measuring) expect(renderCallback).toHaveBeenCalledTimes(9); }); it('should measure additional cells on scroll when it runs out of measured cells', function () { var cellMeasurerCache = createCellMeasurerCache(); var renderCallback = jest.fn().mockImplementation(function (index) { return index; }); var cellRenderer = createCellRenderer(cellMeasurerCache, renderCallback); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellRenderer: cellRenderer, cellMeasurerCache: cellMeasurerCache }))); expect(cellMeasurerCache.has(9)).toBe(false); renderCallback.mockClear(); simulateScroll(rendered, 101); expect(cellMeasurerCache.has(9)).toBe(true); expect(cellMeasurerCache.has(10)).toBe(false); }); // Masonry used to do a render pass for only unmeasured cells, // But this resulting in removing (and later re-adding) measured cells from the DOM, // Which was bad for performance. See GitHub issue #875 it('should not remove previously-measured cells when measuring new ones', function () { var log = []; var cellMeasurerCache = createCellMeasurerCache(); var renderCallback = function renderCallback(index) { log.push(index); }; var cellRenderer = createCellRenderer(cellMeasurerCache, renderCallback); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache, cellRenderer: cellRenderer }))); // Expected to have rendered twice: // 1st time to measure 9 cells (b'c of esimated size) // 2nd time to render and position 9 cells (b'c of actual size) expect(log).toHaveLength(18); log.splice(0); simulateScroll(rendered, 101); // Expected to have rendered twice: // 1st time to measure additional cells (based on estimated size) // 2nd time to render and position with new cells // The 1st render should also have included the pre-measured cells, // To prevent them from being removed, recreated, and re-added to the DOM. expect(log).toHaveLength(18); }); it('should only render enough cells to fill the viewport', function () { var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ overscanByPixels: 0 }))); assertVisibleCells(rendered, '0,1,2,3,4,5'); simulateScroll(rendered, 51); assertVisibleCells(rendered, '0,2,3,4,5,6'); simulateScroll(rendered, 101); assertVisibleCells(rendered, '3,4,5,6,7,8'); simulateScroll(rendered, 1001); assertVisibleCells(rendered, '30,31,32,33,34,35'); }); it('should only render enough cells to fill the viewport plus overscanByPixels', function () { var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ overscanByPixels: 100 }))); assertVisibleCells(rendered, '0,1,10,11,2,3,4,5,6,7,8,9'); simulateScroll(rendered, 51); assertVisibleCells(rendered, '0,1,10,11,2,3,4,5,6,7,8,9'); simulateScroll(rendered, 101); assertVisibleCells(rendered, '0,1,10,11,2,3,4,5,6,7,8,9'); simulateScroll(rendered, 1001); assertVisibleCells(rendered, '26,27,28,29,30,31,32,33,34,35,36,37'); }); it('should still render correctly when autoHeight is true (eg WindowScroller)', function () { // Share instances between renders to avoid resetting state in ways we don't intend var cellMeasurerCache = createCellMeasurerCache(); var cellPositioner = createCellPositioner(cellMeasurerCache); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ autoHeight: true, cellMeasurerCache: cellMeasurerCache, cellPositioner: cellPositioner }))); assertVisibleCells(rendered, '0,1,2,3,4,5,6,7,8'); rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ autoHeight: true, cellMeasurerCache: cellMeasurerCache, cellPositioner: cellPositioner, scrollTop: 51 }))); assertVisibleCells(rendered, '0,1,2,3,4,5,6,7,8'); rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ autoHeight: true, cellMeasurerCache: cellMeasurerCache, cellPositioner: cellPositioner, scrollTop: 101 }))); assertVisibleCells(rendered, '0,2,3,4,5,6,7,8,9'); rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ autoHeight: true, cellMeasurerCache: cellMeasurerCache, cellPositioner: cellPositioner, scrollTop: 1001 }))); assertVisibleCells(rendered, '27,29,30,31,32,33,34,35,36'); }); it('should set right instead of left in a cell styles for rtl row direction', function () { // Share instances between renders to avoid resetting state in ways we don't intend var cellMeasurerCache = createCellMeasurerCache(); var cellPositioner = createCellPositioner(cellMeasurerCache); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache, cellPositioner: cellPositioner, rowDirection: 'rtl' }))); Array.from(rendered.querySelectorAll('.cell')).map(function (node) { expect(node.style.right).toMatch(/px/); }); }); it('should consider scroll only of the container element and not of any ancestor element', function () { var cellMeasurerCache = createCellMeasurerCache(); var renderScrollableCell = function renderScrollableCell(index) { return /*#__PURE__*/React.createElement("div", { style: { height: '50px', overflow: 'visible' }, id: "scrollable-cell-".concat(index) }, /*#__PURE__*/React.createElement("div", { style: { height: '500px' } }, index)); }; var cellRenderer = createCellRenderer(cellMeasurerCache, renderScrollableCell); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ overscanByPixels: 0, cellMeasurerCache: cellMeasurerCache, cellRenderer: cellRenderer }))); assertVisibleCells(rendered, '0,1,2,3,4,5'); var cellEl = rendered.querySelector('#scrollable-cell-1'); _testUtils.Simulate.scroll(cellEl, { target: { scrollTop: 100 } }); assertVisibleCells(rendered, '0,1,2,3,4,5'); }); }); describe('recomputeCellPositions', function () { it('should refresh all cell positions', function () { // Share instances between renders to avoid resetting state in ways we don't intend var cellMeasurerCache = createCellMeasurerCache(); var cellPositioner = jest.fn().mockImplementation(createCellPositioner(cellMeasurerCache)); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache, cellPositioner: cellPositioner }))); assertVisibleCells(rendered, '0,1,2,3,4,5,6,7,8'); cellPositioner.mockImplementation(function (index) { return { left: 0, top: index * CELL_SIZE_MULTIPLIER }; }); var component = (0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache, cellPositioner: cellPositioner })); rendered = (0, _reactDom.findDOMNode)(component); assertVisibleCells(rendered, '0,1,2,3,4,5,6,7,8'); component.recomputeCellPositions(); assertVisibleCells(rendered, '0,1,2,3,4'); }); it('should not reset measurement cache', function () { var cellMeasurerCache = createCellMeasurerCache(); var component = (0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache })); var rendered = (0, _reactDom.findDOMNode)(component); simulateScroll(rendered, 101); expect(cellMeasurerCache.has(9)).toBe(true); simulateScroll(rendered, 0); component.recomputeCellPositions(); for (var i = 0; i <= 9; i++) { expect(cellMeasurerCache.has(i)).toBe(true); } }); }); describe('isScrolling', function () { it('should be true for cellRenderer while scrolling is in progress', function () { var cellMeasurerCache = createCellMeasurerCache(); var renderCallback = jest.fn().mockImplementation(function (index) { return index; }); var cellRenderer = createCellRenderer(cellMeasurerCache, renderCallback); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache, cellRenderer: cellRenderer }))); renderCallback.mockClear(); simulateScroll(rendered, 51); expect(renderCallback.mock.calls[0][1].isScrolling).toEqual(true); }); it('should be reset after a small debounce when scrolling stops', function () { var cellMeasurerCache = createCellMeasurerCache(); var renderCallback = jest.fn().mockImplementation(function (index) { return index; }); var cellRenderer = createCellRenderer(cellMeasurerCache, renderCallback); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellMeasurerCache: cellMeasurerCache, cellRenderer: cellRenderer }))); simulateScroll(rendered, 51); renderCallback.mockClear(); setTimeout(function () { expect(renderCallback.mock.calls[0][1].isScrolling).toEqual(false); }, 0); }); }); describe('callbacks', function () { it('should call onCellsRendered when rendered cells change', function () { var onCellsRendered = jest.fn(); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ onCellsRendered: onCellsRendered }))); expect(onCellsRendered.mock.calls).toEqual([[{ startIndex: 0, stopIndex: 8 }]]); simulateScroll(rendered, 51); expect(onCellsRendered.mock.calls).toEqual([[{ startIndex: 0, stopIndex: 8 }]]); simulateScroll(rendered, 101); expect(onCellsRendered.mock.calls).toEqual([[{ startIndex: 0, stopIndex: 8 }], [{ startIndex: 0, stopIndex: 9 }]]); }); it('should call onScroll when scroll position changes', function () { var onScroll = jest.fn(); var rendered = (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ onScroll: onScroll }))); expect(onScroll.mock.calls).toEqual([[{ clientHeight: 100, scrollHeight: 16900, scrollTop: 0 }]]); simulateScroll(rendered, 51); expect(onScroll.mock.calls).toEqual([[{ clientHeight: 100, scrollHeight: 16900, scrollTop: 0 }], [{ clientHeight: 100, scrollHeight: 16900, scrollTop: 51 }]]); simulateScroll(rendered, 0); expect(onScroll.mock.calls).toEqual([[{ clientHeight: 100, scrollHeight: 16900, scrollTop: 0 }], [{ clientHeight: 100, scrollHeight: 16900, scrollTop: 51 }], [{ clientHeight: 100, scrollHeight: 16900, scrollTop: 0 }]]); }); }); describe('keyMapper', function () { it('should pass the correct key to rendered cells', function () { var keyMapper = jest.fn().mockImplementation(function (index) { return "key:".concat(index); }); var cellRenderer = jest.fn().mockImplementation(function (_ref3) { var index = _ref3.index, key = _ref3.key, style = _ref3.style; return /*#__PURE__*/React.createElement("div", { key: key, style: style }, index); }); (0, _reactDom.findDOMNode)((0, _TestUtils.render)(getMarkup({ cellRenderer: cellRenderer, keyMapper: keyMapper }))); expect(keyMapper).toHaveBeenCalled(); expect(cellRenderer).toHaveBeenCalled(); expect(cellRenderer.mock.calls[0][0].key).toEqual('key:0'); }); }); });