diff --git a/packages/react-virtualized-extension/src/components/Virtualized/VirtualGrid.ts b/packages/react-virtualized-extension/src/components/Virtualized/VirtualGrid.ts index 715dd39ec8f..0a0cc84ac4c 100644 --- a/packages/react-virtualized-extension/src/components/Virtualized/VirtualGrid.ts +++ b/packages/react-virtualized-extension/src/components/Virtualized/VirtualGrid.ts @@ -36,7 +36,7 @@ export const DEFAULT_SCROLLING_RESET_TIME_INTERVAL = 150; let size = 0; function scrollbarSize(recalc: boolean) { - if ((!size && size !== 0 || recalc) && canUseDOM) { + if (((!size && size !== 0) || recalc) && canUseDOM) { const scrollDiv = document.createElement('div'); scrollDiv.style.position = 'absolute'; scrollDiv.style.top = '-9999px'; @@ -51,7 +51,6 @@ function scrollbarSize(recalc: boolean) { return size; } - /** * Controls whether the VirtualGrid updates the DOM element's scrollLeft/scrollTop based on the current state or just observes it. * This prevents VirtualGrid from interrupting mouse-wheel animations (see issue #2). @@ -249,8 +248,6 @@ interface InstanceProps { prevIsScrolling: boolean; prevScrollToColumn: number; prevScrollToRow: number; - prevScrollLeft?: number; - prevScrollTop?: number; columnSizeAndPositionManager: ScalingCellSizeAndPositionManager; rowSizeAndPositionManager: ScalingCellSizeAndPositionManager; @@ -364,8 +361,6 @@ export class VirtualGrid extends React.Component
-import * as React from 'react'; -import { debounce } from '@patternfly/react-core'; -import { ActionsColumn, Table, TableHeader, TableGridBreakpoint, headerCol, sortable, SortByDirection } from '@patternfly/react-table'; +import { debounce, +Button, +ButtonVariant, +Bullseye, +Toolbar, +ToolbarItem, +ToolbarContent, +ToolbarFilter, +ToolbarToggleGroup, +ToolbarGroup, +Dropdown, +DropdownItem, +DropdownPosition, +DropdownToggle, +InputGroup, +Title, +Select, +SelectOption, +SelectVariant, +EmptyState, +EmptyStateIcon, +EmptyStateBody, +EmptyStateSecondaryActions +} from '@patternfly/react-core'; +import { ActionsColumn, Table, TableHeader, TableGridBreakpoint, headerCol, sortable, SortByDirection, TextInput } from '@patternfly/react-table'; +import { SearchIcon, FilterIcon } from '@patternfly/react-icons'; import { CellMeasurerCache, CellMeasurer} from 'react-virtualized'; -import { AutoSizer, VirtualTableBody } from '@patternfly/react-virtualized-extension'; +import { AutoSizer, VirtualTableBody, WindowScroller } from '@patternfly/react-virtualized-extension'; import virtualGridStyles from './VirtualGrid.example.css'; - ## Examples + ```js title=Basic import * as React from 'react'; import { debounce } from '@patternfly/react-core'; import { Table, TableHeader, TableGridBreakpoint } from '@patternfly/react-table'; -import { CellMeasurerCache, CellMeasurer} from 'react-virtualized'; +import { CellMeasurerCache, CellMeasurer } from 'react-virtualized'; import { AutoSizer, VirtualTableBody } from '@patternfly/react-virtualized-extension'; import virtualGridStyles from './VirtualGrid.example.css'; class VirtualizedExample extends React.Component { - constructor(props){ - super(props); - const rows = []; + constructor(props) { + super(props); + const rows = []; for (let i = 0; i < 100; i++) { rows.push({ id: `basic-row-${i}`, @@ -39,23 +62,35 @@ class VirtualizedExample extends React.Component { this.state = { columns: [ - { title: 'Repositories', props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl'} }, - { title: 'Branches', props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl'} }, - { title: 'Pull requests', props: { className: 'pf-m-4-col-on-md pf-m-4-col-on-lg pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-md'} }, - { title: 'Workspaces', props: { className: 'pf-m-2-col-on-lg pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-lg'} }, - { title: 'Last Commit', props: { className: 'pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-xl'} } + { + title: 'Repositories', + props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl' } + }, + { + title: 'Branches', + props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl' } + }, + { + title: 'Pull requests', + props: { className: 'pf-m-4-col-on-md pf-m-4-col-on-lg pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-md' } + }, + { + title: 'Workspaces', + props: { className: 'pf-m-2-col-on-lg pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-lg' } + }, + { title: 'Last Commit', props: { className: 'pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-xl' } } ], rows }; this._handleResize = debounce(this._handleResize.bind(this), 100); } - componentDidMount(){ + componentDidMount() { // re-render after resize window.addEventListener('resize', this._handleResize); } - - componentWillUnmount(){ + + componentWillUnmount() { window.removeEventListener('resize', this._handleResize); } @@ -64,7 +99,7 @@ class VirtualizedExample extends React.Component { } render() { - const {columns, rows} = this.state; + const { columns, rows } = this.state; const measurementCache = new CellMeasurerCache({ fixedWidth: true, @@ -72,32 +107,35 @@ class VirtualizedExample extends React.Component { keyMapper: rowIndex => rowIndex }); - const rowRenderer = ({index, isScrolling, key, style, parent}) => { - const {rows, columns} = this.state; + const rowRenderer = ({ index, isScrolling, key, style, parent }) => { + const { rows, columns } = this.state; const text = rows[index].cells[0]; - return - - {text} - {text} - {text} - {text} - {text} - - ; - } + return ( + + + + {text} + + + {text} + + + {text} + + + {text} + + + {text} + + + + ); + }; return ( -
+
- {({width}) => ( + {({ width }) => ( rowIndex }); - const rowRenderer = ({index, isScrolling, key, style, parent}) => { - const {rows, columns} = this.state; + const rowRenderer = ({ index, isScrolling, key, style, parent }) => { + const { rows, columns } = this.state; const text = rows[index].cells[0]; - return - - {text} - {text} - {text} - {text} - {text} - - ; - } + return ( + + + + {text} + + + {text} + + + {text} + + + {text} + + + {text} + + + + ); + }; return ( -
+
- {({width}) => ( + {({ width }) => ( this.sortableVirtualBody = ref} + ref={ref => (this.sortableVirtualBody = ref)} className="pf-c-table pf-c-virtualized pf-c-window-scroller" deferredMeasurementCache={measurementCache} rowHeight={measurementCache.rowHeight} @@ -267,14 +322,14 @@ class SortableExample extends React.Component { import * as React from 'react'; import { debounce } from 'lodash'; import { Table, TableHeader, headerCol, TableGridBreakpoint } from '@patternfly/react-table'; -import { CellMeasurerCache, CellMeasurer} from 'react-virtualized'; +import { CellMeasurerCache, CellMeasurer } from 'react-virtualized'; import { AutoSizer, VirtualTableBody } from '@patternfly/react-virtualized-extension'; import virtualGridStyles from './VirtualGrid.example.css'; class SelectableExample extends React.Component { - constructor(props){ - super(props); - const rows = []; + constructor(props) { + super(props); + const rows = []; for (let i = 0; i < 100; i++) { rows.push({ selected: false, @@ -288,10 +343,20 @@ class SelectableExample extends React.Component { this.state = { columns: [ // headerCol transform adds checkbox column with pf-m-2-sm, pf-m-1-md+ column space - { title: 'Repositories', cellTransforms: [headerCol()], props: { className: 'pf-m-5-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl'} }, - { title: 'Pull requests', props: { className: 'pf-m-5-col-on-sm pf-m-4-col-on-md pf-m-4-col-on-lg pf-m-3-col-on-xl'} }, - { title: 'Workspaces', props: { className: 'pf-m-2-col-on-lg pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-lg'} }, - { title: 'Last Commit', props: { className: 'pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-xl'} } + { + title: 'Repositories', + cellTransforms: [headerCol()], + props: { className: 'pf-m-5-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl' } + }, + { + title: 'Pull requests', + props: { className: 'pf-m-5-col-on-sm pf-m-4-col-on-md pf-m-4-col-on-lg pf-m-3-col-on-xl' } + }, + { + title: 'Workspaces', + props: { className: 'pf-m-2-col-on-lg pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-lg' } + }, + { title: 'Last Commit', props: { className: 'pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-xl' } } ], rows }; @@ -300,12 +365,12 @@ class SelectableExample extends React.Component { this._handleResize = debounce(this._handleResize.bind(this), 100); } - componentDidMount(){ + componentDidMount() { // re-render after resize window.addEventListener('resize', this._handleResize); } - - componentWillUnmount(){ + + componentWillUnmount() { window.removeEventListener('resize', this._handleResize); } @@ -332,7 +397,7 @@ class SelectableExample extends React.Component { } render() { - const {columns, rows} = this.state; + const { columns, rows } = this.state; const measurementCache = new CellMeasurerCache({ fixedWidth: true, @@ -340,37 +405,41 @@ class SelectableExample extends React.Component { keyMapper: rowIndex => rowIndex }); - const rowRenderer = ({index, isScrolling, key, style, parent}) => { - const {rows, columns} = this.state; + const rowRenderer = ({ index, isScrolling, key, style, parent }) => { + const { rows, columns } = this.state; const text = rows[index].cells[0]; - return - - - - { this.onSelect(e, e.target.checked, 0, {id: rows[index].id})}} + return ( + + + + { + this.onSelect(e, e.target.checked, 0, { id: rows[index].id }); + }} /> - - {text} - {text} - {text} - {text} - - ; - } + + + {text} + + + {text} + + + {text} + + + {text} + + + + ); + }; return ( -
+
- {({width}) => ( + {({ width }) => ( this.selectableVirtualBody = ref} + ref={ref => (this.selectableVirtualBody = ref)} className="pf-c-table pf-c-virtualized pf-c-window-scroller" deferredMeasurementCache={measurementCache} rowHeight={measurementCache.rowHeight} @@ -409,14 +478,14 @@ class SelectableExample extends React.Component { import * as React from 'react'; import { debounce } from 'lodash'; import { ActionsColumn, Table, TableHeader, TableGridBreakpoint } from '@patternfly/react-table'; -import { CellMeasurerCache, CellMeasurer} from 'react-virtualized'; +import { CellMeasurerCache, CellMeasurer } from 'react-virtualized'; import { AutoSizer, VirtualTableBody } from '@patternfly/react-virtualized-extension'; import virtualGridStyles from './VirtualGrid.example.css'; class ActionsExample extends React.Component { - constructor(props){ - super(props); - const rows = []; + constructor(props) { + super(props); + const rows = []; for (let i = 0; i < 100; i++) { rows.push({ disableActions: i % 3 === 2, @@ -429,12 +498,18 @@ class ActionsExample extends React.Component { this.state = { columns: [ - { title: 'Name', props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl'} }, - { title: 'Namespace', props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl'} }, - { title: 'Labels', props: { className: 'pf-m-4-col-on-md pf-m-4-col-on-lg pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-md'} }, - { title: 'Status', props: { className: 'pf-m-2-col-on-lg pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-lg'} }, - { title: 'Pod Selector', props: { className: 'pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-xl'} }, - { title: '', props: { className: 'pf-c-table__action'}}, + { title: 'Name', props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl' } }, + { + title: 'Namespace', + props: { className: 'pf-m-6-col-on-sm pf-m-4-col-on-md pf-m-3-col-on-lg pf-m-2-col-on-xl' } + }, + { + title: 'Labels', + props: { className: 'pf-m-4-col-on-md pf-m-4-col-on-lg pf-m-3-col-on-xl pf-m-hidden pf-m-visible-on-md' } + }, + { title: 'Status', props: { className: 'pf-m-2-col-on-lg pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-lg' } }, + { title: 'Pod Selector', props: { className: 'pf-m-2-col-on-xl pf-m-hidden pf-m-visible-on-xl' } }, + { title: '', props: { className: 'pf-c-table__action' } } ], rows, actions: [ @@ -459,12 +534,12 @@ class ActionsExample extends React.Component { this._handleResize = debounce(this._handleResize.bind(this), 100); } - componentDidMount(){ + componentDidMount() { // re-render after resize window.addEventListener('resize', this._handleResize); } - componentWillUnmount(){ + componentWillUnmount() { window.removeEventListener('resize', this._handleResize); } @@ -473,7 +548,7 @@ class ActionsExample extends React.Component { } render() { - const {columns, rows} = this.state; + const { columns, rows } = this.state; const measurementCache = new CellMeasurerCache({ fixedWidth: true, @@ -481,39 +556,43 @@ class ActionsExample extends React.Component { keyMapper: rowIndex => rowIndex }); - const rowRenderer = ({index, isScrolling, key, style, parent}) => { - const {rows, columns, actions} = this.state; + const rowRenderer = ({ index, isScrolling, key, style, parent }) => { + const { rows, columns, actions } = this.state; const text = rows[index].cells[0]; - return - - {text} - {text} - {text} - {text} - {text} - - - - - ; - } + return ( + + + + {text} + + + {text} + + + {text} + + + {text} + + + {text} + + + + + + + ); + }; return ( -
+
- {({width}) => ( + {({ width }) => ( this.actionsVirtualBody = ref} + ref={ref => (this.actionsVirtualBody = ref)} className="pf-c-table pf-c-virtualized pf-c-window-scroller" deferredMeasurementCache={measurementCache} rowHeight={measurementCache.rowHeight} @@ -545,3 +624,488 @@ class ActionsExample extends React.Component { } } ``` + +```js title=Filterable-with-WindowScroller +import * as React from 'react'; +import { + Button, + ButtonVariant, + Bullseye, + Toolbar, + ToolbarItem, + ToolbarContent, + ToolbarFilter, + ToolbarToggleGroup, + ToolbarGroup, + Dropdown, + DropdownItem, + DropdownPosition, + DropdownToggle, + InputGroup, + Title, + Select, + SelectOption, + SelectVariant, + EmptyState, + EmptyStateIcon, + EmptyStateBody, + EmptyStateSecondaryActions +} from '@patternfly/react-core'; +import { debounce } from 'lodash'; +import { SearchIcon, FilterIcon } from '@patternfly/react-icons'; +import { ActionsColumn, Table, TableHeader, TableGridBreakpoint, TextInput } from '@patternfly/react-table'; +import { CellMeasurerCache, CellMeasurer } from 'react-virtualized'; +import { AutoSizer, VirtualTableBody, WindowScroller } from '@patternfly/react-virtualized-extension'; +import virtualGridStyles from './VirtualGrid.example.css'; + +class FilterExample extends React.Component { + constructor(props) { + super(props); + + this.actionsVirtualBody = null; + + const rows = []; + for (let i = 0; i < 100; i++) { + const data = {}; + if (i % 2 === 0) { + data.cells = [`US-Node ${i}`, i, i, 'Down', 'Brno']; + } else if (i % 3 === 0) { + data.cells = [`CN-Node ${i}`, i, i, 'Running', 'Westford']; + } else { + data.cells = [`US-Node ${i}`, i, i, 'Stopped', 'Raleigh']; + } + rows.push(data); + } + this.scrollableElement = React.createRef(); + + this.state = { + scrollableElement: null, + + filters: { + location: [], + name: [], + status: [] + }, + currentCategory: 'Name', + isFilterDropdownOpen: false, + isCategoryDropdownOpen: false, + nameInput: '', + columns: [ + { title: 'Servers' }, + { title: 'Threads' }, + { title: 'Applications' }, + { title: 'Status' }, + { title: 'Location' } + ], + rows, + inputValue: '', + actions: [ + { + title: 'Some action', + onClick: (event, rowId, rowData, extra) => console.log('clicked on Some action, on row: ', rowId) + }, + { + title:
Another action
, + onClick: (event, rowId, rowData, extra) => console.log('clicked on Another action, on row: ', rowId) + }, + { + isSeparator: true + }, + { + title: 'Third action', + onClick: (event, rowId, rowData, extra) => console.log('clicked on Third action, on row: ', rowId) + } + ] + }; + + this._handleResize = debounce(this._handleResize.bind(this), 100); + + this.onDelete = (type = '', id = '') => { + if (type) { + this.setState(prevState => { + prevState.filters[type.toLowerCase()] = prevState.filters[type.toLowerCase()].filter(s => s !== id); + return { + filters: prevState.filters + }; + }); + } else { + this.setState({ + filters: { + location: [], + name: [], + status: [] + }, + inputValue: '' + }); + } + }; + + this.onCategoryToggle = isOpen => { + this.setState({ + isCategoryDropdownOpen: isOpen + }); + }; + + this.onCategorySelect = event => { + this.setState({ + currentCategory: event.target.innerText, + isCategoryDropdownOpen: !this.state.isCategoryDropdownOpen + }); + }; + + this.onFilterToggle = isOpen => { + this.setState({ + isFilterDropdownOpen: isOpen + }); + }; + + this.onFilterSelect = event => { + this.setState({ + isFilterDropdownOpen: !this.state.isFilterDropdownOpen + }); + }; + + this.onInputChange = newValue => { + // this.setState({ inputValue: newValue }); + if (newValue === '') { + this.onDelete(); + this.setState({ + inputValue: newValue + }); + } else { + this.setState(prevState => { + return { + filters: { + ...prevState.filters, + ['name']: [newValue] + }, + inputValue: newValue + }; + }); + } + }; + + this.onRowSelect = (event, isSelected, rowId) => { + let rows; + if (rowId === -1) { + rows = this.state.rows.map(oneRow => { + oneRow.selected = isSelected; + return oneRow; + }); + } else { + rows = [...this.state.rows]; + rows[rowId].selected = isSelected; + } + this.setState({ + rows + }); + }; + + this.onStatusSelect = (event, selection) => { + const checked = event.target.checked; + this.setState(prevState => { + const prevSelections = prevState.filters['status']; + return { + filters: { + ...prevState.filters, + status: checked ? [...prevSelections, selection] : prevSelections.filter(value => value !== selection) + } + }; + }); + }; + + this.onNameInput = event => { + if (event.key && event.key !== 'Enter') { + return; + } + + const { inputValue } = this.state; + this.setState(prevState => { + const prevFilters = prevState.filters['name']; + return { + filters: { + ...prevState.filters, + ['name']: prevFilters.includes(inputValue) ? prevFilters : [...prevFilters, inputValue] + }, + inputValue: '' + }; + }); + }; + + this.onLocationSelect = (event, selection) => { + this.setState(prevState => { + return { + filters: { + ...prevState.filters, + ['location']: [selection] + } + }; + }); + this.onFilterSelect(); + }; + + this._handleResize = debounce(this._handleResize.bind(this), 100); + this._bindBodyRef = this._bindBodyRef.bind(this); + } + + componentDidMount() { + // re-render after resize + window.addEventListener('resize', this._handleResize); + + setTimeout(() => { + const scollableElement = document.getElementById('content-scrollable-1'); + this.setState({ scollableElement }); + }); + + // re-render after resize + window.addEventListener('resize', this._handleResize); + } + + componentWillUnmount() { + window.removeEventListener('resize', this._handleResize); + } + + _handleResize() { + this._cellMeasurementCache.clearAll(); + this._bodyRef.recomputeVirtualGridSize(); + } + + _bindBodyRef(ref) { + this._bodyRef = ref; + } + + buildCategoryDropdown() { + const { isCategoryDropdownOpen, currentCategory } = this.state; + + return ( + + + {currentCategory} + + } + isOpen={isCategoryDropdownOpen} + dropdownItems={[ + Location, + Name, + Status + ]} + style={{ width: '100%' }} + > + + ); + } + + buildFilterDropdown() { + const { currentCategory, isFilterDropdownOpen, inputValue, filters } = this.state; + + const locationMenuItems = [ + , + , + , + , + + ]; + + const statusMenuItems = [ + , + , + , + , + + ]; + + return ( + + + + + + + + + + + + + + + ); + } + + renderToolbar() { + const { filters } = this.state; + return ( + + + } breakpoint="xl"> + + {this.buildCategoryDropdown()} + {this.buildFilterDropdown()} + + + + + ); + } + + render() { + const { loading, rows, columns, actions, filters, scollableElement } = this.state; + + const filteredRows = + filters.name.length > 0 || filters.location.length > 0 || filters.status.length > 0 + ? rows.filter(row => { + return ( + (filters.name.length === 0 || + filters.name.some(name => row.cells[0].toLowerCase().includes(name.toLowerCase()))) && + (filters.location.length === 0 || filters.location.includes(row.cells[4])) && + (filters.status.length === 0 || filters.status.includes(row.cells[3])) + ); + }) + : rows; + const measurementCache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 44, + keyMapper: rowIndex => rowIndex + }); + + const rowRenderer = ({ index, isScrolling, key, style, parent }) => { + const { columns, actions } = this.state; + + return ( + + + {filteredRows[index].cells[0]} + {filteredRows[index].cells[1]} + {filteredRows[index].cells[2]} + {filteredRows[index].cells[3]} + {filteredRows[index].cells[4]} + + + + + + ); + }; + + return ( + + {this.renderToolbar()} + +
+
+ {!loading && filteredRows.length > 0 && ( +
+ + +
+ + {({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => ( + + {({ width }) => ( +
+ (this.actionsVirtualBody = ref)} + autoHeight + className="pf-c-table pf-c-virtualized pf-c-window-scroller" + deferredMeasurementCache={measurementCache} + rowHeight={measurementCache.rowHeight} + height={height || 0} + overscanRowCount={10} + columnCount={6} + rows={filteredRows} + rowCount={filteredRows.length} + rowRenderer={rowRenderer} + scrollTop={scrollTop} + width={width} + /> +
+ )} +
+ )} +
+
+ )} +
+
+
+ ); + } +} +```