From ee0c966cae4b1443333f37e3d11bf479773ca9de Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Tue, 16 Apr 2019 16:30:09 -0400 Subject: [PATCH 1/6] feat(virtualized): add virtualized table extensions --- jest.config.js | 61 ++-- .../patternfly-4/react-docs/gatsby-node.js | 29 +- .../react-table/src/components/Table/Body.js | 19 +- .../src/components/Table/BodyWrapper.js | 19 +- .../Table/__snapshots__/Table.test.js.snap | 18 ++ .../react-virtualized-extension/.babelrc | 29 ++ .../react-virtualized-extension/.npmignore | 1 + .../react-virtualized-extension/README.md | 5 + .../build/copyStyles.js | 42 +++ .../build/snapshot-serializer.js | 8 + .../react-virtualized-extension/package.json | 58 ++++ .../scripts/copyTS.js | 15 + .../src/components/Virtualized/Body.js | 294 ++++++++++++++++++ .../src/components/Virtualized/BodyWrapper.js | 86 +++++ .../src/components/Virtualized/RowWrapper.js | 78 +++++ .../src/components/Virtualized/Virtualized.md | 292 +++++++++++++++++ .../components/Virtualized/WindowScroller.js | 262 ++++++++++++++++ .../components/Virtualized/WindowScroller.md | 93 ++++++ .../Virtualized/css/virtualized-css.js | 53 ++++ .../src/components/Virtualized/index.js | 4 + .../src/components/Virtualized/types.js | 21 ++ .../Virtualized/utils/animationFrame.js | 41 +++ .../utils/calculateAverageHeight.js | 18 ++ .../Virtualized/utils/calculateRows.js | 100 ++++++ .../Virtualized/utils/detectElementResize.js | 225 ++++++++++++++ .../Virtualized/utils/dimensions.js | 69 ++++ .../components/Virtualized/utils/onScroll.js | 75 +++++ .../utils/requestAnimationTimeout.js | 39 +++ .../src/components/index.js | 1 + .../react-virtualized-extension/src/index.js | 1 + 30 files changed, 1995 insertions(+), 61 deletions(-) create mode 100644 packages/patternfly-4/react-virtualized-extension/.babelrc create mode 100644 packages/patternfly-4/react-virtualized-extension/.npmignore create mode 100644 packages/patternfly-4/react-virtualized-extension/README.md create mode 100644 packages/patternfly-4/react-virtualized-extension/build/copyStyles.js create mode 100644 packages/patternfly-4/react-virtualized-extension/build/snapshot-serializer.js create mode 100644 packages/patternfly-4/react-virtualized-extension/package.json create mode 100644 packages/patternfly-4/react-virtualized-extension/scripts/copyTS.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/BodyWrapper.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/css/virtualized-css.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/index.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/types.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/animationFrame.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/detectElementResize.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/dimensions.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/onScroll.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/requestAnimationTimeout.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/components/index.js create mode 100644 packages/patternfly-4/react-virtualized-extension/src/index.js diff --git a/jest.config.js b/jest.config.js index cbf2f177de7..756665b1601 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,53 +1,46 @@ module.exports = { collectCoverage: true, clearMocks: true, - coverageReporters: [ - "lcov" - ], + coverageReporters: ['lcov'], modulePathIgnorePatterns: [ - "/packages/*.*/dist/*.*", - "/packages/*.*/public/*.*", - "/packages/*.*/.cache/*.*" + '/packages/*.*/dist/*.*', + '/packages/*.*/public/*.*', + '/packages/*.*/.cache/*.*' ], coveragePathIgnorePatterns: [ - "/packages/*.*/dist/*.*", - "/packages/*.*/examples/*.*", - "/packages/*.docs.*", - "/packages/react-docs/*.*" + '/packages/*.*/dist/*.*', + '/packages/*.*/examples/*.*', + '/packages/*.docs.*', + '/packages/react-docs/*.*' ], modulePaths: [ - "/**/node_modules/", - "/packages/", - "/packages/patternfly-3/", - "/packages/patternfly-4/" - ], - roots: [ - "/packages" - ], - setupFiles: [ - "./test.env.js" + '/**/node_modules/', + '/packages/', + '/packages/patternfly-3/', + '/packages/patternfly-4/' ], + roots: ['/packages'], + setupFiles: ['./test.env.js'], snapshotSerializers: [ - "enzyme-to-json/serializer", - "/packages/patternfly-4/react-core/build/snapshot-serializer" + 'enzyme-to-json/serializer', + '/packages/patternfly-4/react-core/build/snapshot-serializer' ], transform: { - "^.+\\.(ts|tsx)?$": "ts-jest", - "^.+\\.jsx?$": "babel-jest", - "\\.(css)$": "/packages/patternfly-4/react-styles/jest-transform.js" + '^.+\\.(ts|tsx)?$': 'ts-jest', + '^.+\\.jsx?$': 'babel-jest', + '\\.(css)$': '/packages/patternfly-4/react-styles/jest-transform.js' }, testPathIgnorePatterns: [ - "/scripts/generators/", - "/packages/patternfly-4/react-integration/" - ], - transformIgnorePatterns: [ - "node_modules/(?!@patternfly|@novnc|tippy.js)" + '/scripts/generators/', + '/packages/patternfly-4/react-integration/', + '/node_modules/(?!lodash-es/.*)' ], + transformIgnorePatterns: ['node_modules/(?!@patternfly|@novnc|tippy.js)'], // https://github.com/kulshekhar/ts-jest/blob/master/docs/user/config/index.md - preset: "ts-jest/presets/js-with-babel", + preset: 'ts-jest/presets/js-with-babel', globals: { - "ts-jest": { - tsConfig: "packages/patternfly-4/react-core/tsconfig.jest.json" + 'ts-jest': { + tsConfig: 'packages/patternfly-4/react-core/tsconfig.jest.json' } } -}; \ No newline at end of file +}; diff --git a/packages/patternfly-4/react-docs/gatsby-node.js b/packages/patternfly-4/react-docs/gatsby-node.js index 9623502f2e4..02ac6a3820d 100644 --- a/packages/patternfly-4/react-docs/gatsby-node.js +++ b/packages/patternfly-4/react-docs/gatsby-node.js @@ -1,5 +1,5 @@ -const navHelpers = require("./src/helpers/navHelpers"); -const path = require("path"); +const navHelpers = require('./src/helpers/navHelpers'); +const path = require('path'); // Add map PR-related environment variables to gatsby nodes exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { @@ -8,17 +8,17 @@ exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { // Docs https://www.gatsbyjs.org/docs/actions/#createNode actions.createNode({ name: 'PR_INFO', - num: num ? num : '', - url: url ? url : '', + num: num || '', + url: url || '', id: createNodeId(`PR_INFO`), parent: null, children: [], internal: { contentDigest: createContentDigest({ a: 'PR_INFO' }), - type: `EnvVars`, - }, + type: `EnvVars` + } }); -}; +} // Create pages for markdown files exports.createPages = ({ graphql, actions }) => { @@ -35,7 +35,6 @@ exports.createPages = ({ graphql, actions }) => { } } } - } `); return mdx.then(({ data }) => { @@ -55,7 +54,7 @@ exports.createPages = ({ graphql, actions }) => { component: path.resolve('./src/templates/mdxFullscreenTemplate.js'), context: { title: node.frontmatter.title, - fileAbsolutePath: node.fileAbsolutePath, // Helps us get the markdown + fileAbsolutePath: node.fileAbsolutePath // Helps us get the markdown } }); } else { @@ -74,9 +73,8 @@ exports.createPages = ({ graphql, actions }) => { }); } }); - }); -}; - + }) +} exports.onCreateWebpackConfig = ({ stage, actions }) => { actions.setWebpackConfig({ @@ -92,6 +90,7 @@ exports.onCreateWebpackConfig = ({ stage, actions }) => { '@patternfly/react-core': path.resolve(__dirname, '../react-core'), '@patternfly/react-icons': path.resolve(__dirname, '../../react-icons'), '@patternfly/react-inline-edit-extension': path.resolve(__dirname, '../react-inline-edit-extension'), + '@patternfly/react-virtualized-extension': path.resolve(__dirname, '../react-virtualized-extension'), '@patternfly/react-styled-system': path.resolve(__dirname, '../react-styled-system'), '@patternfly/react-styles': path.resolve(__dirname, '../react-styles'), '@patternfly/react-table': path.resolve(__dirname, '../react-table'), @@ -99,6 +98,6 @@ exports.onCreateWebpackConfig = ({ stage, actions }) => { // Hack to work downstream in https://github.com/patternfly/patternfly-org '@content': path.resolve(__dirname, 'src/components/componentDocs'), } - }, - }) -}; + } + }); +} diff --git a/packages/patternfly-4/react-table/src/components/Table/Body.js b/packages/patternfly-4/react-table/src/components/Table/Body.js index b658b32c0ea..07799ea13f2 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Body.js +++ b/packages/patternfly-4/react-table/src/components/Table/Body.js @@ -10,13 +10,16 @@ const propTypes = { /** Specify key which should be used for labeling each row. */ rowKey: PropTypes.string, /** Function that is fired when user clicks on row. */ - onRowClick: PropTypes.func + onRowClick: PropTypes.func, + /** Virtualized rows (optional provided in place of rows) */ + rowsToRender: PropTypes.array }; const defaultProps = { rowKey: 'id', className: '', - onRowClick: () => undefined + onRowClick: () => undefined, + rowsToRender: undefined }; const flagVisibility = rows => { @@ -29,10 +32,14 @@ const flagVisibility = rows => { class ContextBody extends React.Component { onRow = (row, rowProps) => { - const { onRowClick } = this.props; + const { onRowClick, onRow } = this.props; + const extendedRowProps = { + ...rowProps, + ...(onRow ? onRow(row, rowProps) : {}) + }; return { row, - rowProps, + rowProps: extendedRowProps, onMouseDown: event => { const computedData = { isInput: event.target.tagName !== 'INPUT', @@ -78,7 +85,7 @@ class ContextBody extends React.Component { }; render() { - const { className, headerData, rows, rowKey, children, onRowClick, ...props } = this.props; + const { className, headerData, rows, rowKey, rowsToRender, children, onRowClick, ...props } = this.props; let mappedRows; if (headerData.length > 0) { @@ -113,7 +120,7 @@ class ContextBody extends React.Component { const TableBody = props => ( - {({ headerData, rows }) => } + {({ headerData, rows }) => } ); diff --git a/packages/patternfly-4/react-table/src/components/Table/BodyWrapper.js b/packages/patternfly-4/react-table/src/components/Table/BodyWrapper.js index 4a0167a7b63..b9c99041ffa 100644 --- a/packages/patternfly-4/react-table/src/components/Table/BodyWrapper.js +++ b/packages/patternfly-4/react-table/src/components/Table/BodyWrapper.js @@ -6,29 +6,36 @@ import PropTypes from 'prop-types'; class BodyWrapper extends Component { render() { - const { mappedRows, ...props } = this.props; - if (mappedRows.some(row => row.hasOwnProperty('parent'))) { + const { mappedRows, tbodyRef, ...props } = this.props; + if (mappedRows && mappedRows.some(row => row.hasOwnProperty('parent'))) { return ( {mapOpenedRows(mappedRows, this.props.children).map((oneRow, key) => ( - + {oneRow.rows} ))} ); } - return ; + return ; } } BodyWrapper.propTypes = { rows: PropTypes.array, - onCollapse: PropTypes.func + onCollapse: PropTypes.func, + tbodyRef: PropTypes.func }; BodyWrapper.defaultProps = { - rows: [] + rows: [], + tbodyRef: null }; export default BodyWrapper; diff --git a/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap b/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap index 6b7b09a01fa..4015c5cf143 100644 --- a/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap +++ b/packages/patternfly-4/react-table/src/components/Table/__snapshots__/Table.test.js.snap @@ -2399,6 +2399,7 @@ exports[`Actions table 1`] = ` ] } rows={Array []} + tbodyRef={null} > { + switch (rule.type) { + case 'rule': + return !rule.selectors.some(sel => unusedSelectorRegEx.test(sel)); + case 'keyframes': + return !unusedKeyFramesRegEx.test(rule.name); + case 'charset': + case 'comment': + return false; + case 'font-face': + const fontFamilyDecl = rule.declarations.find(decl => decl.property === 'font-family'); + return !unusedFontFamilyRegEx.test(fontFamilyDecl.value); + default: + return true; + } +}); + +copySync(join(pfDir, 'assets/images'), join(stylesDir, 'assets/images')); +copySync(join(pfDir, 'assets/fonts'), join(stylesDir, 'assets/fonts'), { + filter(src) { + return !ununsedFontFilesRegExt.test(src); + } +}); +writeFileSync(join(stylesDir, 'base.css'), stringifyCSS(ast)); diff --git a/packages/patternfly-4/react-virtualized-extension/build/snapshot-serializer.js b/packages/patternfly-4/react-virtualized-extension/build/snapshot-serializer.js new file mode 100644 index 00000000000..0edb3cb5b4c --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/build/snapshot-serializer.js @@ -0,0 +1,8 @@ +const fs = require('fs'); +const { createSerializer } = require('@patternfly/react-styles/snapshot-serializer'); + +const pf4CSS = fs.readFileSync(require.resolve('@patternfly/patternfly/patternfly-base.css'), 'utf8'); + +module.exports = createSerializer({ + globalCSS: pf4CSS.match(/:root\W?\{(.|\n)*?\}/)[0] +}); diff --git a/packages/patternfly-4/react-virtualized-extension/package.json b/packages/patternfly-4/react-virtualized-extension/package.json new file mode 100644 index 00000000000..2fa04ad50ff --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/package.json @@ -0,0 +1,58 @@ +{ + "name": "@patternfly/react-virtualized-extension", + "version": "2.1.0", + "description": "This library provides efficient rendering extensions for PatternFly 4 React tables and lists.", + "main": "dist/js/index.js", + "module": "dist/esm/index.js", + "types": "dist/js/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public", + "tag": "prerelease" + }, + "repository": { + "type": "git", + "url": "https://github.com/patternfly/patternfly-react.git" + }, + "keywords": [ + "react", + "patternfly", + "table", + "reacttabular" + ], + "author": "Red Hat", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/patternfly/patternfly-react/issues" + }, + "homepage": "https://github.com/patternfly/patternfly-react/tree/master/packages/patternfly-4/", + "dependencies": { + "@patternfly/patternfly": "2.6.5", + "@patternfly/react-core": "^3.16.10", + "@patternfly/react-icons": "^3.8.1", + "@patternfly/react-styles": "^3.2.0", + "exenv": "^1.2.2", + "reactabular-table": "^8.14.0" + }, + "peerDependencies": { + "@patternfly/react-table": "^2.5.11", + "lodash-es": "4.x", + "prop-types": "^15.6.1", + "react": "^16.4.0", + "react-dom": "^15.6.2 || ^16.4.0" + }, + "scripts": { + "build": "yarn build:babel && yarn build:ts", + "build:babel": "concurrently \"yarn build:babel:cjs\" \"yarn build:babel:esm\"", + "build:babel:cjs": "cross-env BABEL_ENV=production:cjs babel src --out-dir dist/js", + "build:babel:esm": "cross-env BABEL_ENV=production:esm babel src --out-dir dist/esm", + "build:ts": "node ./scripts/copyTS.js", + "postbuild": "node ./build/copyStyles.js" + }, + "devDependencies": { + "css": "^2.2.3", + "fs-extra": "^6.0.1", + "glob": "^7.1.2", + "uuid": "^3.3.2" + } +} diff --git a/packages/patternfly-4/react-virtualized-extension/scripts/copyTS.js b/packages/patternfly-4/react-virtualized-extension/scripts/copyTS.js new file mode 100644 index 00000000000..569890d173f --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/scripts/copyTS.js @@ -0,0 +1,15 @@ +const path = require('path'); +const glob = require('glob'); +const fse = require('fs-extra'); + +const srcDir = path.join('./src'); +const distDir = path.join('./dist/js'); + +const files = glob.sync('**/*.d.ts', { + cwd: srcDir +}); +files.forEach(file => { + const from = path.join(srcDir, file); + const to = path.join(distDir, file); + fse.copySync(from, to); +}); diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js new file mode 100644 index 00000000000..31d438a8f39 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js @@ -0,0 +1,294 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TableBody, TableContext } from '@patternfly/react-table'; +import calculateAverageHeight from './utils/calculateAverageHeight'; +import calculateRows from './utils/calculateRows'; +import createDetectElementResize from './utils/detectElementResize'; + +const initialContext = { + amountOfRowsToRender: 3, // First few rows for initial measurement + startIndex: 0, // Index where to start rendering + startHeight: 0, // Heights for extra rows to mimic scrolling + endHeight: 0, + showExtraRow: false // Show extra row (even/odd issue) +}; +export const VirtualizedBodyContext = React.createContext(initialContext); + +class Body extends React.Component { + state = initialContext; + measuredRows = {}; // row key -> measurement + tbodyRef = null; // tbody ref used for gathering scroll position + initialMeasurement = true; + scrollTop = 0; + timeoutId = 0; + + constructor(props) { + super(props); + this.onScroll = this.onScroll.bind(this); + this.onResize = this.onResize.bind(this); + } + + setTbodyRef = element => { + this.tbodyRef = element; + }; + + scrollTo = index => { + const { rows, rowKey } = this.props; + const startIndex = parseInt(index, 10); + + if (startIndex >= 0) { + const startHeight = calculateAverageHeight(this.measuredRows) * startIndex; + + this.scrollTop = startHeight; + this.tbodyRef.scrollTop = startHeight; + + this.setState(this.calculateRows()); + } + }; + + checkMeasurements = prevProps => { + // If there are no valid measurements or the rows have changed, + // calculate some after waiting a while. Without this some styling solutions + // won't work as you might expect given they can take a while to set container height. + if (this.initialMeasurement || (prevProps && prevProps.rows !== this.props.rows)) { + // If the rows have changed, but the user has not scrolled, maintain the existing + // scroll position + if (this.tbodyRef) { + this.tbodyRef.scrollTop = this.scrollTop; + } + this.timeoutId = setTimeout(() => { + const rows = this.calculateRows(); + + if (!rows) { + // Refresh the rows to trigger measurement. + this.forceUpdate(); + + return; + } + + this.setState(rows, () => { + this.initialMeasurement = false; + }); + }, 100); + } + }; + + getHeight = () => { + const { container, height, style } = this.props; + if (container && container()) { + return container().clientHeight; + } + // If `props.height` is not defined, we use `props.style.maxHeight` instead. + return height || style.maxHeight; + }; + + // Attach information about measuring status. This way we can implement + // proper shouldComponentUpdate + rowsToRender = (rows, startIndex, amountOfRowsToRender, rowKey) => { + const renderedRows = rows.slice(startIndex, startIndex + amountOfRowsToRender).map((rowData, rowIndex) => { + const ariaRowIndex = startIndex + rowIndex + 1; // aria-rowindex should be 1-based, not 0-based. + return { + ...rowData, + 'aria-rowindex': ariaRowIndex, + _measured: !!this.measuredRows[ariaRowIndex] + }; + }); + return renderedRows; + }; + + getBodyOffset = container => + // this is a bug in reactabular-virtualized, return this.tbodyRef.parentElement.offsetTop + this.tbodyRef.offsetTop; + // simply returning the tbodyRef.offsetTop does not account for cases that other parent elements set position:relative + // could be the offset parent. We want the offset from tbody to the passed container element. This can change as the + // user scrolls so it should only be calculated initially or on resize after scroll position is reset. + this.tbodyRef.getBoundingClientRect().top - container.getBoundingClientRect().top; + + registerContainer = () => { + setTimeout(() => { + const element = this.props.container(); + element && element.addEventListener('scroll', this.onScroll); + this._detectElementResize = createDetectElementResize(); + this._detectElementResize.addResizeListener(element, this.onResize); + this.setContainerOffset(); + }, 0); + }; + + setContainerOffset = () => { + const element = this.props.container && this.props.container(); + if (element) { + this.containerOffset = this.getBodyOffset(element); + } + }; + + calculateRows = () => { + const { rows, rowKey } = this.props; + return calculateRows({ + scrollTop: this.scrollTop, + measuredRows: this.measuredRows, + height: this.getHeight(), + rowKey, + rows + }); + }; + + componentDidMount() { + this.checkMeasurements(); + if (this.props.container) { + this.registerContainer(); + } else { + this._detectElementResize = createDetectElementResize(); + this._detectElementResize.addResizeListener(this.tbodyRef, this.onResize); + } + } + + componentDidUpdate(prevProps) { + this.checkMeasurements(prevProps); + } + + componentWillUnmount() { + if (this.tbodyRef && this.tbodyRef.__resizeListeners__) { + this.tbodyRef && this._detectElementResize.removeResizeListener(this.tbodyRef, this.onResize); + } + clearTimeout(this.timeoutId); + } + + onResize() { + // if the containing element resizes reset all measurements & `measuredRows` + this.initialMeasurement = true; + this.scrollTop = 0; + this.setState(initialContext); + this.measuredRows = {}; + this.setContainerOffset(); + this.checkMeasurements(); + } + + onScroll(e) { + const { onScroll, container } = this.props; + onScroll && onScroll(e); + + const { + target: { scrollTop } + } = e; + + // Y didn't change, bail to avoid rendering rows + if (this.scrollTop === scrollTop) { + return; + } + this.scrollTop = container ? scrollTop - this.containerOffset : scrollTop; + this.setState(this.calculateRows()); + } + + render() { + const { onRow, rows, onScroll, container, rowKey, ...props } = this.props; + const { startIndex, amountOfRowsToRender, startHeight, endHeight, showExtraRow } = this.state; + const height = this.getHeight(); + + const rowsToRender = this.rowsToRender(rows, startIndex, amountOfRowsToRender, rowKey); + if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined' && window.LOG_VIRTUALIZED) { + console.log( + // eslint-disable-line no-console + 'rendering', + rowsToRender.length, + '/', + rows.length, + 'rows to render', + rowsToRender, + 'start index', + startIndex, + 'amount of rows to render', + amountOfRowsToRender + ); + } + + const style = { height }; + if (!container) { + // if we do not have a parent container to scroll, set the body to scroll + style.display = 'block'; + style.overflow = 'auto'; + } + const tableBodyProps = { + ...props, + height, + style, + onRow: (row, extra) => ({ + // Pass index so that row heights can be tracked properly + 'data-id': row.id || row['aria-rowindex'], + 'aria-rowindex': row['aria-rowindex'], + ...(onRow ? onRow(row, extra) : {}) + }), + rowsToRender + }; + + // do not listen to tbody onScroll if we are using window scroller + if (!container) { + tableBodyProps.onScroll = this.onScroll; + } + + return ( + { + this.measuredRows[oneRowKey] = rowHeight; + }, + // Capture height data only during the initial measurement or during resize + initialMeasurement: this.initialMeasurement + }} + > + + + ); + } +} +Body.propTypes = { + ...TableBody.propTypes, + height: heightPropCheck, + container: PropTypes.func +}; +Body.defaultProps = { + height: undefined, + container: undefined +}; + +const VirtualizedBody = ({ tableBody, ...props }) => ( + + {({ headerData, rows }) => } + +); + +VirtualizedBody.defaultProps = TableBody.defaultProps; +VirtualizedBody.propTypes = { + /** Additional classes for table body. */ + className: PropTypes.string, + /** Specify key which should be used for labeling each row. */ + rowKey: PropTypes.string, + /** Function that is fired when user clicks on row. */ + onRowClick: PropTypes.func, + /** Virtualized rows (optional provided in place of rows) */ + rowsToRender: PropTypes.array, + /** the height of the body or window container */ + height: heightPropCheck, + /** a callback return the container ref */ + container: PropTypes.func, + /** a react ref that can be used by the consumer to scroll to a given index */ + tableBody: PropTypes.object +}; + +export function heightPropCheck(props, propName, componentName) { + if ( + typeof props[propName] !== 'number' && + (!props.style || typeof props.style.maxHeight !== 'number') && + (!props.container || typeof props.container !== 'function') + ) { + return new Error( + `height or style.maxHeight of type 'number' or container of type 'function' is marked as required in ${componentName}` + ); + } + + return undefined; +} + +export default VirtualizedBody; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/BodyWrapper.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/BodyWrapper.js new file mode 100644 index 00000000000..7b0fbfaf41b --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/BodyWrapper.js @@ -0,0 +1,86 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { VirtualizedBodyContext } from './Body'; +import { BodyWrapper as ReactTableBodyWrapper } from '@patternfly/react-table'; +import { bodyWrapperContextTypes, bodyWrapperTypes } from './types'; + +import { virtualizedCss } from './css/virtualized-css'; + +virtualizedCss.inject(); + +class BodyWrapper extends Component { + tr = props => React.createElement('tr', props); + render() { + const { children, tbodyRef, startHeight, endHeight, showExtraRow, mappedRows, ...props } = this.props; + const startRow = this.tr({ + key: 'start-row', + style: { + height: startHeight + }, + 'aria-hidden': true, + className: 'pf-virtualized-spacer' + }); + const endRow = this.tr({ + key: 'end-row', + style: { + height: endHeight + }, + 'aria-hidden': true, + className: 'pf-virtualized-spacer' + }); + // Extra row to keep onRow indexing stable instead of even/odd. This is important + // for styling. + const rows = [startRow].concat(children).concat(endRow); + + if (showExtraRow) { + rows.unshift( + this.tr({ + key: 'extra-row', + style: { + height: 0 + }, + 'aria-hidden': true, + className: 'pf-virtualized-spacer' + }) + ); + } + + return ( + + {rows} + + ); + } +} +BodyWrapper.contextTypes = bodyWrapperContextTypes; +BodyWrapper.propTypes = { + ...bodyWrapperTypes, + ...ReactTableBodyWrapper.propTypes +}; + +const propTypes = { + rows: PropTypes.array, + tbodyRef: PropTypes.func +}; +const defaultProps = { + rows: [], + tbodyRef: null +}; + +const VirtualizedBodyWrapper = ({ ...props }) => ( + + {({ tbodyRef, startHeight, endHeight, showExtraRow }) => ( + + )} + +); +VirtualizedBodyWrapper.propTypes = propTypes; +VirtualizedBodyWrapper.defaultProps = defaultProps; + +export default VirtualizedBodyWrapper; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js new file mode 100644 index 00000000000..887ec5bfcb2 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js @@ -0,0 +1,78 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { isEqual } from 'lodash-es'; +import { columnsAreEqual } from 'reactabular-table'; +import { RowWrapper } from '@patternfly/react-table'; +import { VirtualizedBodyContext } from './Body'; + +class VirtualizedRowWrapper extends React.Component { + trRef = null; + + setTrRef = element => { + this.trRef = element; + }; + + updateRowHeight = () => { + if (this.trRef) { + const { updateHeight, rowProps } = this.props; + updateHeight(rowProps['aria-rowindex'], this.getAbsoluteHeight(this.trRef)); + } + }; + + // offsetHeight does not include margins, so we use this helper for better accuracy + getAbsoluteHeight = el => { + const styles = window.getComputedStyle(el); + const margin = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom); + return Math.ceil(el.offsetHeight + margin); + }; + + static shouldComponentUpdate(nextProps) { + const { columns, rowData } = this.props; + // Update only if a row has not been measured and either + // columns or rowData hasn't changed + if (nextProps.rowData._measured) { + return !(columnsAreEqual(columns, nextProps.columns) && isEqual(rowData, nextProps.rowData)); + } + return true; + } + + componentDidMount() { + this.updateRowHeight(); + } + componentDidUpdate() { + // update height every update since we have flex css that can change row heights after resize rendering + this.updateRowHeight(); + } + + render() { + const { updateHeight, initialMeasurement, row, rowProps, ...props } = this.props; + return ( + + ); + } +} +VirtualizedRowWrapper.propTypes = { + ...RowWrapper.propTypes, + rowProps: PropTypes.shape({ + 'data-id': PropTypes.string.isRequired, + 'aria-rowindex': PropTypes.number.isRequired + }).isRequired, + updateHeight: PropTypes.func.isRequired, + initialMeasurement: PropTypes.bool.isRequired +}; + +const VirtualizedRowWrapperWithContext = props => ( + + {({ updateHeight, initialMeasurement }) => ( + + )} + +); + +export default VirtualizedRowWrapperWithContext; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md new file mode 100644 index 00000000000..8269ae45b24 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md @@ -0,0 +1,292 @@ +--- +title: 'Table' +section: 'Virtual Scroll' +--- + +import React from 'react'; +import { Table, TableHeader } from '@patternfly/react-table'; +import { +VirtualizedBody, +VirtualizedBodyWrapper, +VirtualizedRowWrapper +} from '@patternfly/react-virtualized-extension'; + +import UUID from 'uuid/v1'; + +## Simple Example + +```js +import React from 'react'; +import { Table, TableHeader } from '@patternfly/react-table'; +import { + VirtualizedBody, + VirtualizedBodyWrapper, + VirtualizedRowWrapper +} from '@patternfly/react-virtualized-extension'; + +import UUID from 'uuid/v1'; + +class VirtualizedExample extends React.Component { + constructor(props) { + super(props); + const rows = []; + for (let i = 0; i < 100; i++) { + rows.push({ + id: UUID(), + cells: [`one-${i}`, `two-${i}`, `three-${i}`, `four-${i}`, `five-${i}`] + }); + } + this.state = { + columns: [ + { title: 'Repositories' }, + { title: 'Branches' }, + { title: 'Pull requests' }, + { title: 'Workspaces' }, + { title: 'Last Commit' } + ], + rows + }; + } + + render() { + const { columns, rows } = this.state; + + return ( + + + +
+ ); + } +} + +export default VirtualizedExample; +``` + +## Sortable Example + +```js +import React from 'react'; +import { Table, TableHeader, sortable, SortByDirection } from '@patternfly/react-table'; +import { + VirtualizedBody, + VirtualizedBodyWrapper, + VirtualizedRowWrapper +} from '@patternfly/react-virtualized-extension'; + +import UUID from 'uuid/v1'; + +class SortableExample extends React.Component { + constructor(props) { + super(props); + + this.tableBody = React.createRef(); + + const rows = []; + for (let i = 0; i < 100; i++) { + rows.push({ + id: UUID(), + cells: [`one-${i}`, `two-${i}`, `three-${i}`, `four-${i}`, `five-${i}`] + }); + } + this.state = { + columns: [ + { title: 'Repositories', transforms: [sortable] }, + { title: 'Branches' }, + { title: 'Pull requests', transforms: [sortable] }, + { title: 'Workspaces' }, + { title: 'Last Commit' } + ], + rows, + sortBy: {} + }; + this.onSort = this.onSort.bind(this); + } + + onSort(_event, index, direction) { + const sortedRows = this.state.rows.sort((a, b) => + a.cells[index] < b.cells[index] ? -1 : a.cells[index] > b.cells[index] ? 1 : 0 + ); + this.tableBody.current.scrollTo(0); + this.setState({ + sortBy: { + index, + direction + }, + rows: direction === SortByDirection.asc ? sortedRows : sortedRows.reverse() + }); + } + + render() { + const { columns, rows, sortBy } = this.state; + + return ( + + + +
+ ); + } +} + +export default SortableExample; +``` + +## Selectable Example + +```js +import React from 'react'; +import { Table, TableHeader, headerCol } from '@patternfly/react-table'; +import { + VirtualizedBody, + VirtualizedBodyWrapper, + VirtualizedRowWrapper +} from '@patternfly/react-virtualized-extension'; + +import UUID from 'uuid/v1'; + +class SelectableExample extends React.Component { + constructor(props) { + super(props); + const rows = []; + for (let i = 0; i < 100; i++) { + rows.push({ + id: UUID(), + cells: [`one-${i}`, `two-${i}`, `three-${i}`, `four-${i}`, `five-${i}`] + }); + } + this.state = { + columns: [ + { title: 'Repositories', cellTransforms: [headerCol()] }, + { title: 'Branches' }, + { title: 'Pull requests' }, + { title: 'Workspaces' }, + { title: 'Last Commit' } + ], + rows + }; + this.onSelect = this.onSelect.bind(this); + } + + onSelect(event, isSelected, virtualRowIndex, rowData) { + let rows; + if (virtualRowIndex === -1) { + rows = this.state.rows.map(oneRow => { + oneRow.selected = isSelected; + return oneRow; + }); + } else { + rows = [...this.state.rows]; + const rowIndex = rows.findIndex(r => r.id === rowData.id); + rows[rowIndex].selected = isSelected; + } + this.setState({ + rows + }); + } + + render() { + const { columns, rows } = this.state; + + return ( + + + +
+ ); + } +} + +export default SelectableExample; +``` + +## Dynamic Height Example + +```js +import React from 'react'; +import { Table, TableHeader } from '@patternfly/react-table'; +import { + VirtualizedBody, + VirtualizedBodyWrapper, + VirtualizedRowWrapper +} from '@patternfly/react-virtualized-extension'; + +import UUID from 'uuid/v1'; + +class DynamicHeightExample extends React.Component { + constructor(props) { + super(props); + const rows = []; + for (let i = 0; i < 100; i++) { + const cells = []; + const num = Math.floor(Math.random() * Math.floor(9)) + 1; + for (let j = 0; j < 5; j++) { + const cellValue = i.toString() + ' Arma virumque cano Troiae qui primus ab oris. '.repeat(num); + cells.push(cellValue); + } + rows.push({ + id: UUID(), + cells + }); + } + this.state = { + columns: [ + { title: 'Repositories' }, + { title: 'Branches' }, + { title: 'Pull requests' }, + { title: 'Workspaces' }, + { title: 'Last Commit' } + ], + rows + }; + } + + render() { + const { columns, rows } = this.state; + + return ( + + + +
+ ); + } +} + +export default DynamicHeightExample; +``` diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.js new file mode 100644 index 00000000000..f4d8051d489 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.js @@ -0,0 +1,262 @@ +/** + * WindowScroller.js + * https://github.com/bvaughn/react-virtualized/blob/9.21.0/source/WindowScroller/WindowScroller.js + * Brian Vaughn + * + * Forked from version 9.21.0; includes the following modifications: + * 1) Allow scrollElement to be queryable as a string using document.querySelector or passed as an element + * */ + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { canUseDOM } from 'exenv'; +import { registerScrollListener, unregisterScrollListener } from './utils/onScroll'; +import { getDimensions, getPositionOffset, getScrollOffset } from './utils/dimensions'; +import createDetectElementResize from './utils/detectElementResize'; + +/** + * Specifies the number of miliseconds during which to disable pointer events while a scroll is in progress. + * This improves performance and makes scrolling smoother. + */ +export const IS_SCROLLING_TIMEOUT = 150; + +const getWindow = () => (typeof window !== 'undefined' ? window : undefined); + +class WindowScroller extends React.PureComponent { + static defaultProps = { + onResize: () => {}, + onScroll: () => {}, + scrollingResetTimeInterval: IS_SCROLLING_TIMEOUT, + scrollElement: getWindow(), + serverHeight: 0, + serverWidth: 0 + }; + + _window = getWindow(); + _isMounted = false; + _positionFromTop = 0; + _positionFromLeft = 0; + _detectElementResize = { + addResizeListener: () => null, + removeResizeListener: () => null + }; + _child = null; + + constructor(props) { + super(props); + this.state = { + ...getDimensions(this._getScrollElement(), this.props), + isScrolling: false, + scrollLeft: 0, + scrollTop: 0 + }; + this.onResize = this.onResize.bind(this); + this.updatePosition = this.updatePosition.bind(this); + } + + onResize() { + this.updatePosition(); + } + + updatePosition(scrollable) { + if (!canUseDOM) { + return null; + } + + const { onResize } = this.props; + const { height, width } = this.state; + const scrollElement = this._getScrollElement(); + + const thisNode = this._child || ReactDOM.findDOMNode(this); + if (thisNode instanceof Element && scrollElement) { + const offset = getPositionOffset(thisNode, scrollElement); + this._positionFromTop = offset.top; + this._positionFromLeft = offset.left; + } + + const dimensions = getDimensions(scrollElement, this.props); + if (height !== dimensions.height || width !== dimensions.width) { + this.setState({ + height: dimensions.height, + width: dimensions.width + }); + onResize({ + height: dimensions.height, + width: dimensions.width + }); + } + } + + componentDidMount() { + const scrollElement = this._getScrollElement(); + this._detectElementResize = createDetectElementResize(); + + this.updatePosition(scrollElement); + + if (scrollElement) { + registerScrollListener(this, scrollElement); + this._registerResizeListener(scrollElement); + } + + this._isMounted = true; + } + + componentDidUpdate(prevProps, prevState) { + if (!canUseDOM) { + return; + } + const prevScrollElement = document.getElementById(prevProps.scrollElement); + const scrollElement = this._getScrollElement(); + + if (prevScrollElement !== scrollElement && prevScrollElement != null && scrollElement != null) { + this.updatePosition(scrollElement); + + unregisterScrollListener(this, prevScrollElement); + registerScrollListener(this, scrollElement); + + this._unregisterResizeListener(prevScrollElement); + this._registerResizeListener(scrollElement); + } + } + + componentWillUnmount() { + const scrollElement = this._getScrollElement(); + if (scrollElement) { + unregisterScrollListener(this, scrollElement); + this._unregisterResizeListener(scrollElement); + } + + this._isMounted = false; + } + + render() { + const { children } = this.props; + const { isScrolling, scrollTop, scrollLeft, height, width } = this.state; + + return children({ + onChildScroll: this._onChildScroll, + registerChild: this._registerChild, + height, + isScrolling, + scrollLeft, + scrollTop, + width + }); + } + + _getScrollElement() { + if (!canUseDOM) { + return null; + } + + const { scrollElement } = this.props; + if (typeof scrollElement === 'string') { + return document.querySelector(scrollElement); + } + // scrollElement defaults to Window + return scrollElement; + } + + _registerChild = element => { + if (element && !(element instanceof Element)) { + console.warn('WindowScroller registerChild expects to be passed Element or null'); + } + this._child = element; + this.updatePosition(); + }; + + _onChildScroll = ({ scrollTop }) => { + if (this.state.scrollTop === scrollTop) { + return; + } + const scrollElement = this._getScrollElement(); + if (scrollElement) { + if (typeof scrollElement.scrollTo === 'function') { + scrollElement.scrollTo(0, scrollTop + this._positionFromTop); + } else { + scrollElement.scrollTop = scrollTop + this._positionFromTop; + } + } + }; + + _registerResizeListener = element => { + if (element === window) { + window.addEventListener('resize', this.onResize, false); + } else { + this._detectElementResize.addResizeListener(element, this.onResize); + } + }; + + _unregisterResizeListener = element => { + if (element === window) { + window.removeEventListener('resize', this.onResize, false); + } else if (element && element.__resizeListeners__) { + this._detectElementResize.removeResizeListener(element, this.onResize); + } + }; + + // Referenced by utils/onScroll + __handleWindowScrollEvent = () => { + if (!this._isMounted) { + return; + } + + const { onScroll } = this.props; + const scrollElement = this._getScrollElement(); + + if (scrollElement) { + const scrollOffset = getScrollOffset(scrollElement); + const scrollLeft = Math.max(0, scrollOffset.left - this._positionFromLeft); + const scrollTop = Math.max(0, scrollOffset.top - this._positionFromTop); + + this.setState({ + isScrolling: true, + scrollLeft, + scrollTop + }); + + onScroll({ + scrollLeft, + scrollTop + }); + } + }; + + // Referenced by utils/onScroll + __resetIsScrolling = () => { + this.setState({ + isScrolling: false + }); + }; +} + +WindowScroller.propTypes = { + /** + * Function responsible for rendering children. + * This function should implement the following signature: + * ({ height, isScrolling, scrollLeft, scrollTop, width }) => PropTypes.element + */ + children: PropTypes.func.isRequired, + + /** Callback to be invoked on-resize: ({ height, width }) */ + onResize: PropTypes.func, + + /** Callback to be invoked on-scroll: ({ scrollLeft, scrollTop }) */ + onScroll: PropTypes.func, + + /** Query string for element to attach scroll event listeners. Defaults to window if no element query string provided. */ + scrollElement: PropTypes.string, + /** + * Wait this amount of time after the last scroll event before resetting child `pointer-events`. + */ + scrollingResetTimeInterval: PropTypes.number, + + /** Height used for server-side rendering */ + serverHeight: PropTypes.number, + + /** Width used for server-side rendering */ + serverWidth: PropTypes.number +}; + +export default WindowScroller; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md new file mode 100644 index 00000000000..4682d4ea4ac --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md @@ -0,0 +1,93 @@ +--- +title: 'Window Scroller' +section: 'Virtual Scroll' +--- + +import React from 'react'; +import { Table, TableHeader } from '@patternfly/react-table'; +import { +VirtualizedBody, +VirtualizedBodyWrapper, +VirtualizedRowWrapper, +WindowScroller +} from '@patternfly/react-virtualized-extension'; +import UUID from 'uuid/v1'; + +## Window Scroller Example + +```js +import React from 'react'; +import { Table, TableHeader } from '@patternfly/react-table'; +import { + VirtualizedBody, + VirtualizedBodyWrapper, + VirtualizedRowWrapper, + WindowScroller +} from '@patternfly/react-virtualized-extension'; + +class WindowScrollerExample extends React.Component { + constructor(props) { + super(props); + this.container = null; + this.setContainer = element => { + this.container = element; + }; + const rows = []; + for (let i = 0; i < 100; i++) { + rows.push({ + id: UUID(), + cells: [`one-${i}`, `two-${i}`, `three-${i}`, `four-${i}`, `five-${i}`] + }); + } + this.state = { + columns: [ + { title: 'Repositories' }, + { title: 'Branches' }, + { title: 'Pull requests' }, + { title: 'Workspaces' }, + { title: 'Last Commit' } + ], + rows + }; + } + + render() { + const { columns, rows } = this.state; + const defaultHeight = 400; + + return ( +
+
+ + {({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => ( + + + this.container} rowKey="id" /> +
+ )} +
+
+
+ ); + } +} + +export default WindowScrollerExample; +``` diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/css/virtualized-css.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/css/virtualized-css.js new file mode 100644 index 00000000000..bf30375f95d --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/css/virtualized-css.js @@ -0,0 +1,53 @@ +import { StyleSheet } from '@patternfly/react-styles'; + +export const virtualizedCss = StyleSheet.parse(` + /* virtualized tables use aria-hidden tr's to offset scrolled rows - + do not add extra spacing to these elements as offset height is important + */ + .pf-virtualized-spacer { + padding: 0 !important; + margin: 0 !important; + border: 0 !important; + } + + /* Based on the following css from reactabular-virtualized: + https://reactabular.js.org/#/features/virtualization?a=using-relative-column-widths + */ + .pf-c-virtualized.pf-c-table { + display: flex; + flex-flow: column; + } + + .pf-c-virtualized.pf-c-table thead, + .pf-c-virtualized.pf-c-table tbody tr { + display: table; + table-layout: fixed; + } + + .pf-c-virtualized.pf-c-table thead { + /* flex: 0 0 auto; */ + width: 100%; + } + + .pf-c-virtualized.pf-c-table thead tr { + /* 0.9em approximates scrollbar width */ + /* width: calc(100% - 0.9em); */ + width: 100%; + } + + .pf-c-virtualized.pf-c-table tbody { + display: block; + /* flex: 1 1 auto; */ + overflow-y: scroll; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; + } + + .pf-c-virtualized.pf-c-table tbody tr { + width: 100%; + } + .pf-c-virtualized.pf-c-table th, + .pf-c-virtualized.pf-c-table td { + width: 20%; + } +`); diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/index.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/index.js new file mode 100644 index 00000000000..474ca69c885 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/index.js @@ -0,0 +1,4 @@ +export { default as VirtualizedBody, VirtualizedBodyContext } from './Body'; +export { default as VirtualizedBodyWrapper } from './BodyWrapper'; +export { default as VirtualizedRowWrapper } from './RowWrapper'; +export { default as WindowScroller } from './WindowScroller'; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/types.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/types.js new file mode 100644 index 00000000000..38e92301118 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/types.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; + +const bodyRowContextTypes = { + initialMeasurement: PropTypes.bool, + updateHeight: PropTypes.func +}; + +const bodyWrapperContextTypes = { + startHeight: PropTypes.number, + endHeight: PropTypes.number, + showExtraRow: PropTypes.bool +}; +const bodyWrapperTypes = { + children: PropTypes.any +}; +const bodyChildContextTypes = { + ...bodyRowContextTypes, + ...bodyWrapperContextTypes +}; + +export { bodyChildContextTypes, bodyRowContextTypes, bodyWrapperContextTypes, bodyWrapperTypes }; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/animationFrame.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/animationFrame.js new file mode 100644 index 00000000000..3282efb3120 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/animationFrame.js @@ -0,0 +1,41 @@ +/** + * animationFrame.js + * https://github.com/bvaughn/react-virtualized/blob/9.21.0/source/utils/animationFrame.js + * Brian Vaughn + * + * Forked from version 9.21.0 + * */ + +// Properly handle server-side rendering. +let win; +if (typeof window !== 'undefined') { + win = window; + // eslint-disable-next-line no-restricted-globals +} else if (typeof self !== 'undefined') { + // eslint-disable-next-line no-restricted-globals + win = self; +} else { + win = {}; +} + +// requestAnimationFrame() shim by Paul Irish +// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ +export const raf = + win.requestAnimationFrame || + win.webkitRequestAnimationFrame || + win.mozRequestAnimationFrame || + win.oRequestAnimationFrame || + win.msRequestAnimationFrame || + function raf(callback) { + return win.setTimeout(callback, 1000 / 60); + }; + +export const caf = + win.cancelAnimationFrame || + win.webkitCancelAnimationFrame || + win.mozCancelAnimationFrame || + win.oCancelAnimationFrame || + win.msCancelAnimationFrame || + function cT(id) { + win.clearTimeout(id); + }; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js new file mode 100644 index 00000000000..0d8809a9df3 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js @@ -0,0 +1,18 @@ +/** + * calculateAverageHeight.js + * https://github.com/reactabular/reactabular/blob/v8.17.0/packages/reactabular-virtualized/src/calculate-average-height.js + * + * Forked from version 8.17.0 + * + * Changes: + * - Use arrow-rowindex based measured amounts for simplicity + * - prevent divide by zero exception + * */ +const calculateAverageHeight = (measuredRows) => { + const measuredAmounts = Object.keys(measuredRows).map(key => measuredRows[key]); + const amountOfMeasuredRows = measuredAmounts.length; + // prevent divide by zero exception + return Math.max(measuredAmounts.reduce((a, b) => a + b / amountOfMeasuredRows, 0), 1); +}; + +export default calculateAverageHeight; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js new file mode 100644 index 00000000000..7af90b722ac --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js @@ -0,0 +1,100 @@ +/** + * calculateRows.js + * https://github.com/reactabular/reactabular/blob/v8.17.0/packages/reactabular-virtualized/src/calculate-rows.js + * + * Forked from version 8.17.0; includes the following modifications: + * 1) Calculate actual row heights in determining startIndex. This allows dynamic row heights. + * */ +import calculateAverageHeight from './calculateAverageHeight'; + +const calculateRows = ({ measuredRows, height, rowKey, rows, scrollTop = 0 }) => { + // used exact measured row heights for determining `startIndex` for smooth scroll + // average heights are not accurate when there is lots of variation in row heights + let startIndex = 0; + let startHeight = 0; + let accruedHeight = 0; + + //default overscan to 10 for now, could be accepted as a prop in the future + const overscan = 10; + + for (let i = 0; i < Object.keys(measuredRows).length; i++) { + // measuredRows use aria-rowindex as identifiers which is 1 based + accruedHeight += measuredRows[i + 1]; + + if (scrollTop < accruedHeight) { + startIndex = i; + break; + } + else if(i + overscan > Object.keys(measuredRows).length){ + // stop accruing after we reach i + overscan + startHeight = accruedHeight; + startIndex = i; + break; + } + else { + // accrue and continue + startHeight = accruedHeight; + } + } + + // averageHeight of measuredRows can still be used to closely approximate amount of rows to render + // if this causes issues w/ row visibility, exact heights can still be used + const averageHeight = calculateAverageHeight(measuredRows); + const amountOfRowsToRender = Math.ceil(height / averageHeight) + overscan; + + // const zeroedIndex = startIndex; + const rowsToRender = rows.slice(startIndex, Math.max(startIndex + amountOfRowsToRender, 0)); + + if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined' && window.LOG_VIRTUALIZED) { + console.log( + // eslint-disable-line no-console + 'update rows to render', + 'scroll top', + scrollTop, + 'measured rows', + measuredRows, + 'amount of rows to render', + amountOfRowsToRender, + 'rows to render', + rowsToRender, + 'start index', + startIndex + ); + } + + // Escape if there are no rows to render for some reason + if (!rowsToRender.length) { + return null; + } + + // Calculate the padding of the last row so we can match whole height. This + // won't be totally accurate if row heights differ but should get close + // enough in most cases. + const endHeight = Math.max((rows.length - amountOfRowsToRender) * averageHeight - startHeight, 0); + + if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined' && window.LOG_VIRTUALIZED) { + console.log( + // eslint-disable-line no-console + 'average height', + averageHeight, + 'body height', + height, + 'scroll top', + scrollTop, + 'start height', + startHeight, + 'end height', + endHeight + ); + } + + return { + amountOfRowsToRender, + startIndex: startIndex, + showExtraRow: !(startIndex % 2), + startHeight, + endHeight + }; +}; + +export default calculateRows; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/detectElementResize.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/detectElementResize.js new file mode 100644 index 00000000000..75551b0b7fe --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/detectElementResize.js @@ -0,0 +1,225 @@ +/* eslint-disable */ +/** + * https://github.com/bvaughn/react-virtualized/blob/9.21.0/source/vendor/detectElementResize.js + * + * Detect Element Resize. + * https://github.com/sdecima/javascript-detect-element-resize + * Sebastian Decima + * + * Forked from version 0.5.3; includes the following modifications: + * 1) Guard against unsafe 'window' and 'document' references (to support SSR). + * 2) Defer initialization code via a top-level function wrapper (to support SSR). + * 3) Avoid unnecessary reflows by not measuring size for scroll events bubbling from children. + * 4) Add nonce for style element. + * */ + +export default function createDetectElementResize(nonce) { + // Check `document` and `window` in case of server-side rendering + var _window; + if (typeof window !== 'undefined') { + _window = window; + } else if (typeof self !== 'undefined') { + _window = self; + } else { + _window = global; + } + + var attachEvent = typeof document !== 'undefined' && document.attachEvent; + + if (!attachEvent) { + var requestFrame = (function() { + var raf = + _window.requestAnimationFrame || + _window.mozRequestAnimationFrame || + _window.webkitRequestAnimationFrame || + function(fn) { + return _window.setTimeout(fn, 20); + }; + return function(fn) { + return raf(fn); + }; + })(); + + var cancelFrame = (function() { + var cancel = + _window.cancelAnimationFrame || + _window.mozCancelAnimationFrame || + _window.webkitCancelAnimationFrame || + _window.clearTimeout; + return function(id) { + return cancel(id); + }; + })(); + + var resetTriggers = function(element) { + var triggers = element.__resizeTriggers__, + expand = triggers.firstElementChild, + contract = triggers.lastElementChild, + expandChild = expand.firstElementChild; + contract.scrollLeft = contract.scrollWidth; + contract.scrollTop = contract.scrollHeight; + expandChild.style.width = expand.offsetWidth + 1 + 'px'; + expandChild.style.height = expand.offsetHeight + 1 + 'px'; + expand.scrollLeft = expand.scrollWidth; + expand.scrollTop = expand.scrollHeight; + }; + + var checkTriggers = function(element) { + return ( + element.offsetWidth != element.__resizeLast__.width || element.offsetHeight != element.__resizeLast__.height + ); + }; + + var scrollListener = function(e) { + // Don't measure (which forces) reflow for scrolls that happen inside of children! + if ( + e.target.className && + typeof e.target.className.indexOf === 'function' && + e.target.className.indexOf('contract-trigger') < 0 && + e.target.className.indexOf('expand-trigger') < 0 + ) { + return; + } + + var element = this; + resetTriggers(this); + if (this.__resizeRAF__) { + cancelFrame(this.__resizeRAF__); + } + this.__resizeRAF__ = requestFrame(function() { + if (checkTriggers(element)) { + element.__resizeLast__.width = element.offsetWidth; + element.__resizeLast__.height = element.offsetHeight; + element.__resizeListeners__.forEach(function(fn) { + fn.call(element, e); + }); + } + }); + }; + + /* Detect CSS Animations support to detect element display/re-attach */ + var animation = false, + keyframeprefix = '', + animationstartevent = 'animationstart', + domPrefixes = 'Webkit Moz O ms'.split(' '), + startEvents = 'webkitAnimationStart animationstart oAnimationStart MSAnimationStart'.split(' '), + pfx = ''; + { + var elm = document.createElement('fakeelement'); + if (elm.style.animationName !== undefined) { + animation = true; + } + + if (animation === false) { + for (var i = 0; i < domPrefixes.length; i++) { + if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) { + pfx = domPrefixes[i]; + keyframeprefix = '-' + pfx.toLowerCase() + '-'; + animationstartevent = startEvents[i]; + animation = true; + break; + } + } + } + } + + var animationName = 'resizeanim'; + var animationKeyframes = + '@' + keyframeprefix + 'keyframes ' + animationName + ' { from { opacity: 0; } to { opacity: 0; } } '; + var animationStyle = keyframeprefix + 'animation: 1ms ' + animationName + '; '; + } + + var createStyles = function(doc) { + if (!doc.getElementById('detectElementResize')) { + //opacity:0 works around a chrome bug https://code.google.com/p/chromium/issues/detail?id=286360 + var css = + (animationKeyframes ? animationKeyframes : '') + + '.resize-triggers { ' + + (animationStyle ? animationStyle : '') + + 'visibility: hidden; opacity: 0; } ' + + '.resize-triggers, .resize-triggers > div, .contract-trigger:before { content: " "; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; z-index: -1; } .resize-triggers > div { background: #eee; overflow: auto; } .contract-trigger:before { width: 200%; height: 200%; }', + head = doc.head || doc.getElementsByTagName('head')[0], + style = doc.createElement('style'); + + style.id = 'detectElementResize'; + style.type = 'text/css'; + + if (nonce != null) { + style.setAttribute('nonce', nonce); + } + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(doc.createTextNode(css)); + } + + head.appendChild(style); + } + }; + + var addResizeListener = function(element, fn) { + if (attachEvent) { + element.attachEvent('onresize', fn); + } else { + if (!element.__resizeTriggers__) { + var doc = element.ownerDocument; + var elementStyle = _window.getComputedStyle(element); + if (elementStyle && elementStyle.position == 'static') { + element.style.position = 'relative'; + } + createStyles(doc); + element.__resizeLast__ = {}; + element.__resizeListeners__ = []; + (element.__resizeTriggers__ = doc.createElement('div')).className = 'resize-triggers'; + element.__resizeTriggers__.innerHTML = + '
' + '
'; + element.appendChild(element.__resizeTriggers__); + resetTriggers(element); + element.addEventListener('scroll', scrollListener, true); + + /* Listen for a css animation to detect element display/re-attach */ + if (animationstartevent) { + element.__resizeTriggers__.__animationListener__ = function animationListener(e) { + if (e.animationName == animationName) { + resetTriggers(element); + } + }; + element.__resizeTriggers__.addEventListener( + animationstartevent, + element.__resizeTriggers__.__animationListener__ + ); + } + } + element.__resizeListeners__.push(fn); + } + }; + + var removeResizeListener = function(element, fn) { + if (attachEvent) { + element.detachEvent('onresize', fn); + } else { + element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); + if (!element.__resizeListeners__.length) { + element.removeEventListener('scroll', scrollListener, true); + if (element.__resizeTriggers__.__animationListener__) { + element.__resizeTriggers__.removeEventListener( + animationstartevent, + element.__resizeTriggers__.__animationListener__ + ); + element.__resizeTriggers__.__animationListener__ = null; + } + try { + element.__resizeTriggers__ = !element.removeChild(element.__resizeTriggers__); + } catch (e) { + // Preact compat; see developit/preact-compat/issues/228 + } + } + } + }; + + return { + addResizeListener, + removeResizeListener + }; +} diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/dimensions.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/dimensions.js new file mode 100644 index 00000000000..bc5703c0eea --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/dimensions.js @@ -0,0 +1,69 @@ +/** + * dimensions.js + * https://github.com/bvaughn/react-virtualized/blob/9.21.0/source/WindowScroller/utils/dimensions.js + * Brian Vaughn + * + * Forked from version 9.21.0 + * */ + +const isWindow = element => element === window; + +const getBoundingBox = element => element.getBoundingClientRect(); + +export function getDimensions(scrollElement, props) { + if (!scrollElement) { + return { + height: props.serverHeight, + width: props.serverWidth + }; + } else if (isWindow(scrollElement)) { + const { innerHeight, innerWidth } = window; + return { + height: typeof innerHeight === 'number' ? innerHeight : 0, + width: typeof innerWidth === 'number' ? innerWidth : 0 + }; + } + return getBoundingBox(scrollElement); +} + +/** + * Gets the vertical and horizontal position of an element within its scroll container. + * Elements that have been “scrolled past” return negative values. + * Handles edge-case where a user is navigating back (history) from an already-scrolled page. + * In this case the body’s top or left position will be a negative number and this element’s top or left will be increased (by that amount). + */ +export function getPositionOffset(element, container) { + if (isWindow(container) && document.documentElement) { + const containerElement = document.documentElement; + const elementRect = getBoundingBox(element); + const containerRect = getBoundingBox(containerElement); + return { + top: elementRect.top - containerRect.top, + left: elementRect.left - containerRect.left + }; + } + const scrollOffset = getScrollOffset(container); + const elementRect = getBoundingBox(element); + const containerRect = getBoundingBox(container); + return { + top: elementRect.top + scrollOffset.top - containerRect.top, + left: elementRect.left + scrollOffset.left - containerRect.left + }; +} + +/** + * Gets the vertical and horizontal scroll amount of the element, accounting for IE compatibility + * and API differences between `window` and other DOM elements. + */ +export function getScrollOffset(element) { + if (isWindow(element) && document.documentElement) { + return { + top: 'scrollY' in window ? window.scrollY : document.documentElement.scrollTop, + left: 'scrollX' in window ? window.scrollX : document.documentElement.scrollLeft + }; + } + return { + top: element.scrollTop, + left: element.scrollLeft + }; +} diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/onScroll.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/onScroll.js new file mode 100644 index 00000000000..9e5173bda6d --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/onScroll.js @@ -0,0 +1,75 @@ +/** + * onScroll.js + * https://github.com/bvaughn/react-virtualized/blob/9.21.0/source/WindowScroller/utils/onScroll.js + * Brian Vaughn + * + * Forked from version 9.21.0 + * */ + +import { requestAnimationTimeout, cancelAnimationTimeout } from './requestAnimationTimeout'; + +let mountedInstances = []; +let originalBodyPointerEvents = null; +let disablePointerEventsTimeoutId = null; + +function enablePointerEventsIfDisabled() { + if (disablePointerEventsTimeoutId) { + disablePointerEventsTimeoutId = null; + + if (document.body && originalBodyPointerEvents != null) { + document.body.style.pointerEvents = originalBodyPointerEvents; + } + + originalBodyPointerEvents = null; + } +} + +function enablePointerEventsAfterDelayCallback() { + enablePointerEventsIfDisabled(); + mountedInstances.forEach(instance => instance.__resetIsScrolling()); +} + +function enablePointerEventsAfterDelay() { + if (disablePointerEventsTimeoutId) { + cancelAnimationTimeout(disablePointerEventsTimeoutId); + } + + let maximumTimeout = 0; + mountedInstances.forEach(instance => { + maximumTimeout = Math.max(maximumTimeout, instance.props.scrollingResetTimeInterval); + }); + + disablePointerEventsTimeoutId = requestAnimationTimeout(enablePointerEventsAfterDelayCallback, maximumTimeout); +} + +function onScrollWindow(event) { + if (event.currentTarget === window && originalBodyPointerEvents == null && document.body) { + originalBodyPointerEvents = document.body.style.pointerEvents; + + document.body.style.pointerEvents = 'none'; + } + enablePointerEventsAfterDelay(); + mountedInstances.forEach(instance => { + if (instance.props.scrollElement === event.currentTarget) { + instance.__handleWindowScrollEvent(); + } + }); +} + +export function registerScrollListener(component, element) { + if (!mountedInstances.some(instance => instance.props.scrollElement === element)) { + element.addEventListener('scroll', onScrollWindow); + } + mountedInstances.push(component); +} + +export function unregisterScrollListener(component, element) { + mountedInstances = mountedInstances.filter(instance => instance !== component); + if (!mountedInstances.length) { + element.removeEventListener('scroll', onScrollWindow); + if (disablePointerEventsTimeoutId) { + cancelAnimationTimeout(disablePointerEventsTimeoutId); + enablePointerEventsIfDisabled(); + } + } +} diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/requestAnimationTimeout.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/requestAnimationTimeout.js new file mode 100644 index 00000000000..79a44fecd44 --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/requestAnimationTimeout.js @@ -0,0 +1,39 @@ +/** + * requestAnimationTimeout.js + * https://github.com/bvaughn/react-virtualized/blob/9.21.0/source/utils/requestAnimationTimeout.js + * Brian Vaughn + * + * Forked from version 9.21.0 + * */ + +import { caf, raf } from './animationFrame'; + +export const cancelAnimationTimeout = frame => caf(frame.id); + +/** + * Recursively calls requestAnimationFrame until a specified delay has been met or exceeded. + * When the delay time has been reached the function you're timing out will be called. + * + * Credit: Joe Lambert (https://gist.github.com/joelambert/1002116#file-requesttimeout-js) + */ +export const requestAnimationTimeout = (callback, delay) => { + let start; + // wait for end of processing current event handler, because event handler may be long + Promise.resolve().then(() => { + start = Date.now(); + }); + + const timeout = () => { + if (Date.now() - start >= delay) { + callback.call(); + } else { + frame.id = raf(timeout); + } + }; + + const frame = { + id: raf(timeout) + }; + + return frame; +}; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/index.js b/packages/patternfly-4/react-virtualized-extension/src/components/index.js new file mode 100644 index 00000000000..1dfee408f4a --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/components/index.js @@ -0,0 +1 @@ +export * from './Virtualized'; diff --git a/packages/patternfly-4/react-virtualized-extension/src/index.js b/packages/patternfly-4/react-virtualized-extension/src/index.js new file mode 100644 index 00000000000..07635cbbc8e --- /dev/null +++ b/packages/patternfly-4/react-virtualized-extension/src/index.js @@ -0,0 +1 @@ +export * from './components'; From 47cbff5854fb84cb85728d04c8f56a665b806fc6 Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Mon, 6 May 2019 11:21:13 -0400 Subject: [PATCH 2/6] bring reactabular-table in house --- jest.config.js | 2 +- .../patternfly-4/react-table/package.json | 7 +- .../react-table/src/components/Table/Body.js | 2 +- .../src/components/Table/Header.js | 2 +- .../react-table/src/components/Table/Table.js | 2 +- .../src/components/Table/base/body-row.js | 85 ++++++++++++++ .../src/components/Table/base/body.js | 67 +++++++++++ .../Table/base/columns-are-equal.js | 19 ++++ .../Table/base/evaluate-formatters.js | 18 +++ .../Table/base/evaluate-transforms.js | 24 ++++ .../src/components/Table/base/header-row.js | 45 ++++++++ .../src/components/Table/base/header.js | 38 +++++++ .../src/components/Table/base/index.js | 9 ++ .../src/components/Table/base/merge-props.js | 36 ++++++ .../src/components/Table/base/provider.js | 56 +++++++++ .../components/Table/base/resolve-row-key.js | 31 +++++ .../src/components/Table/base/types.js | 107 ++++++++++++++++++ .../src/components/Virtualized/Body.js | 20 ++-- .../src/components/Virtualized/Virtualized.md | 2 +- 19 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/body-row.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/body.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/columns-are-equal.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/evaluate-formatters.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/evaluate-transforms.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/header-row.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/header.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/index.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/merge-props.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/provider.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/resolve-row-key.js create mode 100644 packages/patternfly-4/react-table/src/components/Table/base/types.js diff --git a/jest.config.js b/jest.config.js index 756665b1601..5a32e7f90e5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -35,7 +35,7 @@ module.exports = { '/packages/patternfly-4/react-integration/', '/node_modules/(?!lodash-es/.*)' ], - transformIgnorePatterns: ['node_modules/(?!@patternfly|@novnc|tippy.js)'], + transformIgnorePatterns: ['node_modules/(?!@patternfly|@novnc|tippy.js|lodash-es)'], // https://github.com/kulshekhar/ts-jest/blob/master/docs/user/config/index.md preset: 'ts-jest/presets/js-with-babel', globals: { diff --git a/packages/patternfly-4/react-table/package.json b/packages/patternfly-4/react-table/package.json index b5ca251c261..4431c988c5a 100644 --- a/packages/patternfly-4/react-table/package.json +++ b/packages/patternfly-4/react-table/package.json @@ -31,13 +31,14 @@ "@patternfly/react-core": "^3.18.1", "@patternfly/react-icons": "^3.9.2", "@patternfly/react-styles": "^3.2.2", - "exenv": "^1.2.2", - "reactabular-table": "^8.14.0" + "classnames": "^2.2.5", + "exenv": "^1.2.2" }, "peerDependencies": { "prop-types": "^15.6.1", "react": "^16.4.0", - "react-dom": "^15.6.2 || ^16.4.0" + "react-dom": "^15.6.2 || ^16.4.0", + "lodash-es": "4.x" }, "scripts": { "build": "yarn build:babel && node ./scripts/copyTS.js && node ./build/copyStyles.js", diff --git a/packages/patternfly-4/react-table/src/components/Table/Body.js b/packages/patternfly-4/react-table/src/components/Table/Body.js index 07799ea13f2..69b46cfdec8 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Body.js +++ b/packages/patternfly-4/react-table/src/components/Table/Body.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Body } from 'reactabular-table'; +import { Body } from './base'; import PropTypes from 'prop-types'; import { TableContext } from './Table'; import { isRowExpanded } from './utils'; diff --git a/packages/patternfly-4/react-table/src/components/Table/Header.js b/packages/patternfly-4/react-table/src/components/Table/Header.js index af8944f75a8..ff8006dc864 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Header.js +++ b/packages/patternfly-4/react-table/src/components/Table/Header.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Header } from 'reactabular-table'; +import { Header } from './base'; import PropTypes from 'prop-types'; import { TableContext } from './Table'; diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.js b/packages/patternfly-4/react-table/src/components/Table/Table.js index 121d66d0036..b5349662c15 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.js +++ b/packages/patternfly-4/react-table/src/components/Table/Table.js @@ -1,7 +1,7 @@ import React from 'react'; import styles from '@patternfly/patternfly/components/Table/table.css'; import stylesGrid from '@patternfly/patternfly/components/Table/table-grid.css'; -import { Provider } from 'reactabular-table'; +import { Provider } from './base'; import { DropdownPosition, DropdownDirection } from '@patternfly/react-core'; import { css, getModifier } from '@patternfly/react-styles'; import PropTypes from 'prop-types'; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/body-row.js b/packages/patternfly-4/react-table/src/components/Table/base/body-row.js new file mode 100644 index 00000000000..c3c63aeaa83 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/body-row.js @@ -0,0 +1,85 @@ +/** + * body-row.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ + +import { isEqual, isFunction } from 'lodash-es'; +import React from 'react'; +import columnsAreEqual from './columns-are-equal'; +import evaluateFormatters from './evaluate-formatters'; +import evaluateTransforms from './evaluate-transforms'; +import mergeProps from './merge-props'; +import { tableBodyRowDefaults, tableBodyRowTypes } from './types'; + +class BodyRow extends React.Component { + shouldComponentUpdate(nextProps) { // eslint-disable-line no-unused-vars + const previousProps = this.props; + + // Check for row based override. + const { renderers } = nextProps; + + if (renderers && renderers.row && renderers.row.shouldComponentUpdate) { + if (isFunction(renderers.row.shouldComponentUpdate)) { + return renderers.row.shouldComponentUpdate.call(this, nextProps); + } + + return true; + } + + return !( + columnsAreEqual(previousProps.columns, nextProps.columns) && + isEqual(previousProps.rowData, nextProps.rowData) + ); + } + render() { + const { + columns, renderers, onRow, rowKey, rowIndex, rowData + } = this.props; + + return React.createElement( + renderers.row, + onRow(rowData, { rowIndex, rowKey }), + columns.map((column, columnIndex) => { + const { property, cell, props } = column; + const evaluatedProperty = property || (cell && cell.property); + const { + transforms = [], + formatters = [] + } = cell || {}; // TODO: test against this case + const extraParameters = { + columnIndex, + property: evaluatedProperty, + column, + rowData, + rowIndex, + rowKey + }; + const transformed = evaluateTransforms(transforms, rowData[evaluatedProperty], extraParameters); + + if (!transformed) { + console.warn('Table.Body - Failed to receive a transformed result'); // eslint-disable-line max-len, no-console + } + + return React.createElement( + renderers.cell, + { + key: `${columnIndex}-cell`, + ...mergeProps( + props, + cell && cell.props, + transformed + ) + }, + transformed.children || evaluateFormatters(formatters)(rowData[`_${evaluatedProperty}`] || + rowData[evaluatedProperty], extraParameters) + ); + }) + ); + } +} +BodyRow.defaultProps = tableBodyRowDefaults; +BodyRow.propTypes = tableBodyRowTypes; + +export default BodyRow; \ No newline at end of file diff --git a/packages/patternfly-4/react-table/src/components/Table/base/body.js b/packages/patternfly-4/react-table/src/components/Table/base/body.js new file mode 100644 index 00000000000..7c3b2e8e373 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/body.js @@ -0,0 +1,67 @@ +/** + * body.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import { isEqual, isFunction } from 'lodash-es'; +import React from 'react'; +import { tableBodyTypes, tableBodyDefaults, tableBodyContextTypes } from './types'; +import BodyRow from './body-row'; +import resolveRowKey from './resolve-row-key'; + +class Body extends React.Component { + shouldComponentUpdate(nextProps, nextState, nextContext) { + // eslint-disable-line no-unused-vars + // Skip checking props against `onRow` since that can be bound at render(). + // That's not particularly good practice but you never know how the users + // prefer to define the handler. + + // Check for wrapper based override. + const { renderers } = nextContext; + + if (renderers && renderers.body && renderers.body.wrapper.shouldComponentUpdate) { + if (isFunction(renderers.body.wrapper.shouldComponentUpdate)) { + return renderers.body.wrapper.shouldComponentUpdate.call(this, nextProps, nextState, nextContext); + } + + return true; + } + + return !(isEqual(omitOnRow(this.props), omitOnRow(nextProps)) && isEqual(this.context, nextContext)); + } + render() { + const { onRow, rows, rowKey, ...props } = this.props; + const { columns, renderers } = this.context; + + return React.createElement( + renderers.body.wrapper, + props, + rows.map((rowData, index) => { + const rowIndex = rowData._index || index; + const key = resolveRowKey({ rowData, rowIndex, rowKey }); + + return React.createElement(BodyRow, { + key, + renderers: renderers.body, + onRow, + rowKey: key, + rowIndex, + rowData, + columns + }); + }) + ); + } +} +Body.propTypes = tableBodyTypes; +Body.defaultProps = tableBodyDefaults; +Body.contextTypes = tableBodyContextTypes; + +function omitOnRow(props) { + const { onRow, ...ret } = props; // eslint-disable-line no-unused-vars + + return ret; +} + +export default Body; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/columns-are-equal.js b/packages/patternfly-4/react-table/src/components/Table/base/columns-are-equal.js new file mode 100644 index 00000000000..9541dad5bea --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/columns-are-equal.js @@ -0,0 +1,19 @@ +/** + * columns-are-equal.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import { isFunction, isEqualWith } from 'lodash-es'; + +function columnsAreEqual(oldColumns, newColumns) { + return isEqualWith(oldColumns, newColumns, (a, b) => { + if (isFunction(a) && isFunction(b)) { + return true; + } + + return undefined; + }); +} + +export default columnsAreEqual; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/evaluate-formatters.js b/packages/patternfly-4/react-table/src/components/Table/base/evaluate-formatters.js new file mode 100644 index 00000000000..cf0cfe47be1 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/evaluate-formatters.js @@ -0,0 +1,18 @@ +/** + * evaluate-formatters.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +function evaluateFormatters(formatters) { + return (value, extra) => + formatters.reduce( + (parameters, formatter) => ({ + value: formatter(parameters.value, parameters.extra), + extra + }), + { value, extra } + ).value; +} + +export default evaluateFormatters; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/evaluate-transforms.js b/packages/patternfly-4/react-table/src/components/Table/base/evaluate-transforms.js new file mode 100644 index 00000000000..b0e7deed564 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/evaluate-transforms.js @@ -0,0 +1,24 @@ +/** + * evaluate-transforms.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import { isFunction } from 'lodash-es'; +import mergeProps from './merge-props'; + +function evaluateTransforms(transforms = [], value, extraParameters = {}) { + if (process.env.NODE_ENV !== 'production') { + if (!transforms.every(isFunction)) { + throw new Error("All transforms weren't functions!", transforms); + } + } + + if (transforms.length === 0) { + return {}; + } + + return mergeProps(...transforms.map(transform => transform(value, extraParameters))); +} + +export default evaluateTransforms; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/header-row.js b/packages/patternfly-4/react-table/src/components/Table/base/header-row.js new file mode 100644 index 00000000000..f4925b14477 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/header-row.js @@ -0,0 +1,45 @@ +/** + * header-row.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import React from 'react'; +import evaluateFormatters from './evaluate-formatters'; +import evaluateTransforms from './evaluate-transforms'; +import mergeProps from './merge-props'; +import { tableHeaderRowTypes, tableHeaderRowDefaults } from './types'; + +const HeaderRow = ({ rowData, rowIndex, renderers, onRow }) => + React.createElement( + renderers.row, + onRow(rowData, { rowIndex }), + rowData.map((column, columnIndex) => { + const { property, header = {}, props = {} } = column; + const evaluatedProperty = property || (header && header.property); + const { label, transforms = [], formatters = [] } = header; + const extraParameters = { + columnIndex, + property: evaluatedProperty, + column + }; + const transformedProps = evaluateTransforms(transforms, label, extraParameters); + + if (!transformedProps) { + console.warn('Table.Header - Failed to receive a transformed result'); // eslint-disable-line max-len, no-console + } + + return React.createElement( + renderers.cell, + { + key: `${columnIndex}-header`, + ...mergeProps(props, header && header.props, transformedProps) + }, + transformedProps.children || evaluateFormatters(formatters)(label, extraParameters) + ); + }) + ); +HeaderRow.defaultProps = tableHeaderRowDefaults; +HeaderRow.propTypes = tableHeaderRowTypes; + +export default HeaderRow; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/header.js b/packages/patternfly-4/react-table/src/components/Table/base/header.js new file mode 100644 index 00000000000..49f5c06d01b --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/header.js @@ -0,0 +1,38 @@ +/** + * header.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import React from 'react'; +import { tableHeaderTypes, tableHeaderContextTypes } from './types'; +import HeaderRow from './header-row'; + +// eslint-disable-next-line react/prefer-stateless-function +class Header extends React.Component { + render() { + const { children, headerRows, onRow, ...props } = this.props; + const { renderers, columns } = this.context; + + // If headerRows aren't passed, default to bodyColumns as header rows + return React.createElement( + renderers.header.wrapper, + props, + [ + (headerRows || [columns]).map((rowData, rowIndex) => + React.createElement(HeaderRow, { + key: `${rowIndex}-header-row`, + renderers: renderers.header, + onRow, + rowData, + rowIndex + }) + ) + ].concat(children) + ); + } +} +Header.propTypes = tableHeaderTypes; +Header.contextTypes = tableHeaderContextTypes; + +export default Header; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/index.js b/packages/patternfly-4/react-table/src/components/Table/base/index.js new file mode 100644 index 00000000000..31f9e2760e6 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/index.js @@ -0,0 +1,9 @@ +export { default as Provider } from './provider'; +export { default as Header } from './header'; +export { default as Body } from './body'; +export { default as BodyRow } from './body-row'; +export { default as evaluateFormatters } from './evaluate-formatters'; +export { default as evaluateTransforms } from './evaluate-transforms'; +export { default as mergeProps } from './merge-props'; +export { default as columnsAreEqual } from './columns-are-equal'; +export { default as resolveRowKey } from './resolve-row-key'; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/merge-props.js b/packages/patternfly-4/react-table/src/components/Table/base/merge-props.js new file mode 100644 index 00000000000..e7a58581138 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/merge-props.js @@ -0,0 +1,36 @@ +/** + * merge-props.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import { mergeWith } from 'lodash-es'; +import classNames from 'classnames'; + +function mergePropPair(...props) { + const firstProps = props[0]; + const restProps = props.slice(1); + + if (!restProps.length) { + return mergeWith({}, firstProps); + } + + // Avoid mutating the first prop collection + return mergeWith(mergeWith({}, firstProps), ...restProps, (a, b, key) => { + if (key === 'children') { + // Children have to be merged in reverse order for Reactabular + // logic to work. + return { ...b, ...a }; + } + + if (key === 'className') { + // Process class names through classNames to merge properly + // as a string. + return classNames(a, b); + } + + return undefined; + }); +} + +export default mergePropPair; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/provider.js b/packages/patternfly-4/react-table/src/components/Table/base/provider.js new file mode 100644 index 00000000000..499bba82518 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/provider.js @@ -0,0 +1,56 @@ +/** + * provider.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { tableTypes, tableDefaults, tableContextTypes } from './types'; + +const componentDefaults = tableDefaults.renderers; + +export default class Provider extends React.Component { + getChildContext() { + const { columns, components, renderers } = this.props; + let finalRenderers = renderers; + + // XXXXX: Drop in the next major version + if (components) { + // eslint-disable-next-line no-console + console.warn( + '`components` have been deprecated in favor of `renderers` and will be removed in the next major version, please rename!' + ); + + finalRenderers = components; + } + + return { + columns, + renderers: { + table: finalRenderers.table || componentDefaults.table, + header: { ...componentDefaults.header, ...finalRenderers.header }, + body: { ...componentDefaults.body, ...finalRenderers.body } + } + }; + } + render() { + const { + columns, // eslint-disable-line no-unused-vars + renderers, + components, // XXXXX: Drop in the next major version + children, + ...props + } = this.props; + + return React.createElement(renderers.table || tableDefaults.renderers.table, props, children); + } +} +Provider.propTypes = { + ...tableTypes, + children: PropTypes.any +}; +Provider.defaultProps = { + ...tableDefaults +}; +Provider.childContextTypes = tableContextTypes; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/resolve-row-key.js b/packages/patternfly-4/react-table/src/components/Table/base/resolve-row-key.js new file mode 100644 index 00000000000..ad2d9d74c4a --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/resolve-row-key.js @@ -0,0 +1,31 @@ +/** + * resolve-row-key.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import { isArray } from 'lodash-es'; + +function resolveRowKey({ rowData, rowIndex, rowKey }) { + if (typeof rowKey === 'function') { + return `${rowKey({ rowData, rowIndex })}-row`; + } else if (process.env.NODE_ENV !== 'production') { + // Arrays cannot have rowKeys by definition so we have to go by index there. + if (!isArray(rowData) && rowData[rowKey] === undefined) { + console.warn( + // eslint-disable-line no-console + 'Table.Body - Missing valid rowKey!', + rowData, + rowKey + ); + } + } + + if (rowData[rowKey] === 0) { + return `${rowData[rowKey]}-row`; + } + + return `${rowData[rowKey] || rowIndex}-row`; +} + +export default resolveRowKey; diff --git a/packages/patternfly-4/react-table/src/components/Table/base/types.js b/packages/patternfly-4/react-table/src/components/Table/base/types.js new file mode 100644 index 00000000000..20293559f6c --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/base/types.js @@ -0,0 +1,107 @@ +/** + * types.js + * + * Forked from reactabular-table version 8.14.0 + * https://github.com/reactabular/reactabular/tree/v8.14.0/packages/reactabular-table/src + * */ +import PropTypes from 'prop-types'; + +const arrayOfObjectColumns = PropTypes.arrayOf( + PropTypes.shape({ + header: PropTypes.shape({ + label: PropTypes.string, + transforms: PropTypes.arrayOf(PropTypes.func), + formatters: PropTypes.arrayOf(PropTypes.func), + props: PropTypes.object + }), + cell: PropTypes.shape({ + property: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + transforms: PropTypes.arrayOf(PropTypes.func), + formatters: PropTypes.arrayOf(PropTypes.func), + props: PropTypes.object + }) + }) +); +const arrayOfArrayColumns = PropTypes.arrayOf(PropTypes.array); +const rowsType = PropTypes.oneOfType([arrayOfObjectColumns, arrayOfArrayColumns]); +const rowKeyType = PropTypes.oneOfType([PropTypes.func, PropTypes.string]); +const rowDataType = PropTypes.oneOfType([PropTypes.array, PropTypes.object]); +const tableTypes = { + columns: PropTypes.array.isRequired, + renderers: PropTypes.object, + components: PropTypes.object // XXXXX: Deprecated in favor of renderers, remove in the next major! +}; +const tableContextTypes = { + columns: PropTypes.array.isRequired, + renderers: PropTypes.object +}; +const tableBodyDefaults = { + onRow: () => {} +}; +const tableBodyTypes = { + onRow: PropTypes.func, + rows: rowsType.isRequired, + rowKey: rowKeyType +}; +const tableBodyContextTypes = { + columns: PropTypes.array.isRequired, + renderers: PropTypes.object +}; +const tableBodyRowDefaults = { + onRow: () => ({}) +}; +const tableBodyRowTypes = { + columns: PropTypes.array.isRequired, + renderers: PropTypes.object, + onRow: PropTypes.func, + rowIndex: PropTypes.number.isRequired, + rowData: rowDataType.isRequired, + rowKey: PropTypes.string.isRequired +}; +const tableHeaderTypes = { + headerRows: PropTypes.arrayOf(arrayOfObjectColumns), + children: PropTypes.any +}; +const tableHeaderContextTypes = { + columns: PropTypes.array.isRequired, + renderers: PropTypes.object +}; +const tableHeaderRowDefaults = { + onRow: () => ({}) +}; +const tableHeaderRowTypes = { + renderers: PropTypes.object, + onRow: PropTypes.func, + rowIndex: PropTypes.number.isRequired, + rowData: rowDataType.isRequired +}; +const tableDefaults = { + renderers: { + table: 'table', + header: { + wrapper: 'thead', + row: 'tr', + cell: 'th' + }, + body: { + wrapper: 'tbody', + row: 'tr', + cell: 'td' + } + } +}; + +export { + tableTypes, + tableContextTypes, + tableBodyTypes, + tableBodyDefaults, + tableBodyContextTypes, + tableBodyRowTypes, + tableBodyRowDefaults, + tableHeaderTypes, + tableHeaderContextTypes, + tableHeaderRowTypes, + tableHeaderRowDefaults, + tableDefaults +}; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js index 31d438a8f39..77814d17152 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js @@ -6,7 +6,7 @@ import calculateRows from './utils/calculateRows'; import createDetectElementResize from './utils/detectElementResize'; const initialContext = { - amountOfRowsToRender: 3, // First few rows for initial measurement + amountOfRowsToRender: 0, startIndex: 0, // Index where to start rendering startHeight: 0, // Heights for extra rows to mimic scrolling endHeight: 0, @@ -201,11 +201,7 @@ class Body extends React.Component { } const style = { height }; - if (!container) { - // if we do not have a parent container to scroll, set the body to scroll - style.display = 'block'; - style.overflow = 'auto'; - } + const tableBodyProps = { ...props, height, @@ -219,9 +215,12 @@ class Body extends React.Component { rowsToRender }; - // do not listen to tbody onScroll if we are using window scroller if (!container) { + // do not listen to tbody onScroll if we are using window scroller tableBodyProps.onScroll = this.onScroll; + // if we do not have a parent container to scroll, set the body to scroll + tableBodyProps.style.display = 'block'; + tableBodyProps.style.overflow = 'auto'; } return ( @@ -253,11 +252,12 @@ Body.defaultProps = { container: undefined }; -const VirtualizedBody = ({ tableBody, ...props }) => ( +// eslint-disable-next-line react/no-multi-comp +const VirtualizedBody = React.forwardRef((props, ref) => ( - {({ headerData, rows }) => } + {({ headerData, rows }) => } -); +)); VirtualizedBody.defaultProps = TableBody.defaultProps; VirtualizedBody.propTypes = { diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md index 8269ae45b24..7bb243c97f0 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md @@ -141,7 +141,7 @@ class SortableExample extends React.Component { aria-rowcount={rows.length} > - + ); } From 17a8815f28b5aafcef989fc106f39a4a648b6afd Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Mon, 6 May 2019 14:32:48 -0400 Subject: [PATCH 3/6] isolate rowsToRender to extension --- .../react-table/src/components/Table/Body.js | 11 ++++------- .../src/components/Virtualized/Body.js | 11 ++++------- .../src/components/Virtualized/Virtualized.md | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/patternfly-4/react-table/src/components/Table/Body.js b/packages/patternfly-4/react-table/src/components/Table/Body.js index 69b46cfdec8..0783977e36d 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Body.js +++ b/packages/patternfly-4/react-table/src/components/Table/Body.js @@ -10,16 +10,13 @@ const propTypes = { /** Specify key which should be used for labeling each row. */ rowKey: PropTypes.string, /** Function that is fired when user clicks on row. */ - onRowClick: PropTypes.func, - /** Virtualized rows (optional provided in place of rows) */ - rowsToRender: PropTypes.array + onRowClick: PropTypes.func }; const defaultProps = { rowKey: 'id', className: '', - onRowClick: () => undefined, - rowsToRender: undefined + onRowClick: () => undefined }; const flagVisibility = rows => { @@ -85,7 +82,7 @@ class ContextBody extends React.Component { }; render() { - const { className, headerData, rows, rowKey, rowsToRender, children, onRowClick, ...props } = this.props; + const { className, headerData, rows, rowKey, children, onRowClick, ...props } = this.props; let mappedRows; if (headerData.length > 0) { @@ -120,7 +117,7 @@ class ContextBody extends React.Component { const TableBody = props => ( - {({ headerData, rows }) => } + {({ headerData, rows }) => } ); diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js index 77814d17152..90debce6134 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js @@ -212,7 +212,7 @@ class Body extends React.Component { 'aria-rowindex': row['aria-rowindex'], ...(onRow ? onRow(row, extra) : {}) }), - rowsToRender + rows: rowsToRender }; if (!container) { @@ -252,12 +252,11 @@ Body.defaultProps = { container: undefined }; -// eslint-disable-next-line react/no-multi-comp -const VirtualizedBody = React.forwardRef((props, ref) => ( +const VirtualizedBody = ({ tableBody, ...props }) => ( - {({ headerData, rows }) => } + {({ headerData, rows }) => } -)); +); VirtualizedBody.defaultProps = TableBody.defaultProps; VirtualizedBody.propTypes = { @@ -267,8 +266,6 @@ VirtualizedBody.propTypes = { rowKey: PropTypes.string, /** Function that is fired when user clicks on row. */ onRowClick: PropTypes.func, - /** Virtualized rows (optional provided in place of rows) */ - rowsToRender: PropTypes.array, /** the height of the body or window container */ height: heightPropCheck, /** a callback return the container ref */ diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md index 7bb243c97f0..8269ae45b24 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md @@ -141,7 +141,7 @@ class SortableExample extends React.Component { aria-rowcount={rows.length} > - + ); } From e728dddea10b75c7dc9d149ecb14e6a304743dc6 Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Mon, 6 May 2019 14:56:25 -0400 Subject: [PATCH 4/6] remove row wrapper shouldComponentUpdate --- .../src/components/Virtualized/RowWrapper.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js index 887ec5bfcb2..c0ebcc346ff 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js @@ -26,16 +26,6 @@ class VirtualizedRowWrapper extends React.Component { return Math.ceil(el.offsetHeight + margin); }; - static shouldComponentUpdate(nextProps) { - const { columns, rowData } = this.props; - // Update only if a row has not been measured and either - // columns or rowData hasn't changed - if (nextProps.rowData._measured) { - return !(columnsAreEqual(columns, nextProps.columns) && isEqual(rowData, nextProps.rowData)); - } - return true; - } - componentDidMount() { this.updateRowHeight(); } From 5198a07aa615a5d3a5f519b246195e560b00326a Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Mon, 6 May 2019 15:16:59 -0400 Subject: [PATCH 5/6] remove reactabular-table from extension --- packages/patternfly-4/react-virtualized-extension/package.json | 3 +-- .../src/components/Virtualized/RowWrapper.js | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/patternfly-4/react-virtualized-extension/package.json b/packages/patternfly-4/react-virtualized-extension/package.json index 2fa04ad50ff..537e163e0aa 100644 --- a/packages/patternfly-4/react-virtualized-extension/package.json +++ b/packages/patternfly-4/react-virtualized-extension/package.json @@ -31,8 +31,7 @@ "@patternfly/react-core": "^3.16.10", "@patternfly/react-icons": "^3.8.1", "@patternfly/react-styles": "^3.2.0", - "exenv": "^1.2.2", - "reactabular-table": "^8.14.0" + "exenv": "^1.2.2" }, "peerDependencies": { "@patternfly/react-table": "^2.5.11", diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js index c0ebcc346ff..3830525fd1a 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/RowWrapper.js @@ -1,7 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { isEqual } from 'lodash-es'; -import { columnsAreEqual } from 'reactabular-table'; import { RowWrapper } from '@patternfly/react-table'; import { VirtualizedBodyContext } from './Body'; From 18734fece06df29a60025fb70eba2db3afaaab73 Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Fri, 10 May 2019 09:50:58 -0400 Subject: [PATCH 6/6] resize listener --- .../patternfly-4/react-docs/gatsby-node.js | 28 ++++++----- .../react-virtualized-extension/package.json | 8 +-- .../src/components/Virtualized/Body.js | 22 +++++++-- .../src/components/Virtualized/Virtualized.md | 2 +- .../components/Virtualized/WindowScroller.md | 2 +- .../utils/calculateAverageHeight.js | 4 +- .../Virtualized/utils/calculateRows.js | 49 ++++++++++--------- 7 files changed, 66 insertions(+), 49 deletions(-) diff --git a/packages/patternfly-4/react-docs/gatsby-node.js b/packages/patternfly-4/react-docs/gatsby-node.js index 02ac6a3820d..24b7f06fe38 100644 --- a/packages/patternfly-4/react-docs/gatsby-node.js +++ b/packages/patternfly-4/react-docs/gatsby-node.js @@ -1,5 +1,5 @@ -const navHelpers = require('./src/helpers/navHelpers'); -const path = require('path'); +const navHelpers = require("./src/helpers/navHelpers"); +const path = require("path"); // Add map PR-related environment variables to gatsby nodes exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { @@ -8,17 +8,17 @@ exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { // Docs https://www.gatsbyjs.org/docs/actions/#createNode actions.createNode({ name: 'PR_INFO', - num: num || '', - url: url || '', + num: num ? num : '', + url: url ? url : '', id: createNodeId(`PR_INFO`), parent: null, children: [], internal: { contentDigest: createContentDigest({ a: 'PR_INFO' }), - type: `EnvVars` - } + type: `EnvVars`, + }, }); -} +}; // Create pages for markdown files exports.createPages = ({ graphql, actions }) => { @@ -35,6 +35,7 @@ exports.createPages = ({ graphql, actions }) => { } } } + } `); return mdx.then(({ data }) => { @@ -54,7 +55,7 @@ exports.createPages = ({ graphql, actions }) => { component: path.resolve('./src/templates/mdxFullscreenTemplate.js'), context: { title: node.frontmatter.title, - fileAbsolutePath: node.fileAbsolutePath // Helps us get the markdown + fileAbsolutePath: node.fileAbsolutePath, // Helps us get the markdown } }); } else { @@ -73,8 +74,9 @@ exports.createPages = ({ graphql, actions }) => { }); } }); - }) -} + }); +}; + exports.onCreateWebpackConfig = ({ stage, actions }) => { actions.setWebpackConfig({ @@ -98,6 +100,6 @@ exports.onCreateWebpackConfig = ({ stage, actions }) => { // Hack to work downstream in https://github.com/patternfly/patternfly-org '@content': path.resolve(__dirname, 'src/components/componentDocs'), } - } - }); -} + }, + }) +}; \ No newline at end of file diff --git a/packages/patternfly-4/react-virtualized-extension/package.json b/packages/patternfly-4/react-virtualized-extension/package.json index 537e163e0aa..8cc8de4868c 100644 --- a/packages/patternfly-4/react-virtualized-extension/package.json +++ b/packages/patternfly-4/react-virtualized-extension/package.json @@ -28,13 +28,13 @@ "homepage": "https://github.com/patternfly/patternfly-react/tree/master/packages/patternfly-4/", "dependencies": { "@patternfly/patternfly": "2.6.5", - "@patternfly/react-core": "^3.16.10", - "@patternfly/react-icons": "^3.8.1", - "@patternfly/react-styles": "^3.2.0", + "@patternfly/react-core": "^3.18.1", + "@patternfly/react-icons": "^3.9.2", + "@patternfly/react-styles": "^3.2.2", "exenv": "^1.2.2" }, "peerDependencies": { - "@patternfly/react-table": "^2.5.11", + "@patternfly/react-table": "^2.6.1", "lodash-es": "4.x", "prop-types": "^15.6.1", "react": "^16.4.0", diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js index 90debce6134..0942fd8dead 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Body.js @@ -106,13 +106,22 @@ class Body extends React.Component { registerContainer = () => { setTimeout(() => { const element = this.props.container(); - element && element.addEventListener('scroll', this.onScroll); - this._detectElementResize = createDetectElementResize(); - this._detectElementResize.addResizeListener(element, this.onResize); - this.setContainerOffset(); + if (element) { + element && element.addEventListener('scroll', this.onScroll); + this._detectElementResize = createDetectElementResize(); + this._detectElementResize.addResizeListener(element, this.onResize); + this.setContainerOffset(); + } }, 0); }; + unregisterContainer = () => { + const element = this.props.container(); + if (element && element.__resizeListeners__) { + element.removeEventListener('scroll', this.onScroll); + } + }; + setContainerOffset = () => { const element = this.props.container && this.props.container(); if (element) { @@ -147,7 +156,10 @@ class Body extends React.Component { componentWillUnmount() { if (this.tbodyRef && this.tbodyRef.__resizeListeners__) { - this.tbodyRef && this._detectElementResize.removeResizeListener(this.tbodyRef, this.onResize); + this._detectElementResize.removeResizeListener(this.tbodyRef, this.onResize); + } + if (this.props.container) { + this.unregisterContainer(); } clearTimeout(this.timeoutId); } diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md index 8269ae45b24..d397d5ea3a4 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/Virtualized.md @@ -244,7 +244,7 @@ class DynamicHeightExample extends React.Component { constructor(props) { super(props); const rows = []; - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 100000; i++) { const cells = []; const num = Math.floor(Math.random() * Math.floor(9)) + 1; for (let j = 0; j < 5; j++) { diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md index 4682d4ea4ac..56543f169b6 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/WindowScroller.md @@ -33,7 +33,7 @@ class WindowScrollerExample extends React.Component { this.container = element; }; const rows = []; - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 100000; i++) { rows.push({ id: UUID(), cells: [`one-${i}`, `two-${i}`, `three-${i}`, `four-${i}`, `five-${i}`] diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js index 0d8809a9df3..cf3625b0ac6 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateAverageHeight.js @@ -8,11 +8,11 @@ * - Use arrow-rowindex based measured amounts for simplicity * - prevent divide by zero exception * */ -const calculateAverageHeight = (measuredRows) => { +const calculateAverageHeight = measuredRows => { const measuredAmounts = Object.keys(measuredRows).map(key => measuredRows[key]); const amountOfMeasuredRows = measuredAmounts.length; // prevent divide by zero exception - return Math.max(measuredAmounts.reduce((a, b) => a + b / amountOfMeasuredRows, 0), 1); + return Math.max(measuredAmounts.reduce((a, b) => a + b, 0) / amountOfMeasuredRows, 1); }; export default calculateAverageHeight; diff --git a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js index 7af90b722ac..92c3d9cb301 100644 --- a/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js +++ b/packages/patternfly-4/react-virtualized-extension/src/components/Virtualized/utils/calculateRows.js @@ -5,44 +5,47 @@ * Forked from version 8.17.0; includes the following modifications: * 1) Calculate actual row heights in determining startIndex. This allows dynamic row heights. * */ -import calculateAverageHeight from './calculateAverageHeight'; +// import calculateAverageHeight from './calculateAverageHeight'; const calculateRows = ({ measuredRows, height, rowKey, rows, scrollTop = 0 }) => { - // used exact measured row heights for determining `startIndex` for smooth scroll - // average heights are not accurate when there is lots of variation in row heights + // default overscan to 10 for now, could be accepted as a prop in the future + const overscan = 20; + + // averageHeight of measuredRows can still be used to closely approximate amount of rows to render + const measuredAmounts = Object.keys(measuredRows).map(key => measuredRows[key]); + const amountOfMeasuredRows = measuredAmounts.length; + const totalMeasuredHeight = measuredAmounts.reduce((a, b) => a + b, 0); + + // if we have no rows, use a small row height so we get a good sample in future loops + const averageHeight = amountOfMeasuredRows > 0 ? totalMeasuredHeight / amountOfMeasuredRows : 50; + let startIndex = 0; let startHeight = 0; let accruedHeight = 0; + let i = 0; - //default overscan to 10 for now, could be accepted as a prop in the future - const overscan = 10; - - for (let i = 0; i < Object.keys(measuredRows).length; i++) { + while (accruedHeight < scrollTop) { // measuredRows use aria-rowindex as identifiers which is 1 based - accruedHeight += measuredRows[i + 1]; - - if (scrollTop < accruedHeight) { + if (measuredRows.hasOwnProperty(i + 1)) { + accruedHeight += measuredRows[i + 1]; + } else { + accruedHeight += averageHeight; + } + if (scrollTop <= accruedHeight) { startIndex = i; break; - } - else if(i + overscan > Object.keys(measuredRows).length){ - // stop accruing after we reach i + overscan + } else if (i + overscan > rows.length) { + // stop accruing after we reach i + overscan (the end) startHeight = accruedHeight; startIndex = i; break; } - else { - // accrue and continue - startHeight = accruedHeight; - } + // accrue and continue + startHeight = accruedHeight; + i += 1; } - // averageHeight of measuredRows can still be used to closely approximate amount of rows to render - // if this causes issues w/ row visibility, exact heights can still be used - const averageHeight = calculateAverageHeight(measuredRows); const amountOfRowsToRender = Math.ceil(height / averageHeight) + overscan; - - // const zeroedIndex = startIndex; const rowsToRender = rows.slice(startIndex, Math.max(startIndex + amountOfRowsToRender, 0)); if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined' && window.LOG_VIRTUALIZED) { @@ -90,7 +93,7 @@ const calculateRows = ({ measuredRows, height, rowKey, rows, scrollTop = 0 }) => return { amountOfRowsToRender, - startIndex: startIndex, + startIndex, showExtraRow: !(startIndex % 2), startHeight, endHeight