diff --git a/jest.config.js b/jest.config.js index cd73600..56fc911 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,8 @@ module.exports = { ], roots: ['/packages'], transform: { - '^.+\\.[jt]sx?$': 'babel-jest' + '^.+\\.[jt]sx?$': 'babel-jest', + '^.+\\.svg$': 'jest-transform-stub' }, transformIgnorePatterns: ['node_modules/(?!@patternfly)'], moduleNameMapper: { diff --git a/package.json b/package.json index a37eb58..ed2286b 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@testing-library/jest-dom": "5.16.5", "@testing-library/dom": "9.3.0", "jest-environment-jsdom": "^29.5.0", + "jest-transform-stub": "^2.0.0", "serve": "^14.2.0" } } diff --git a/packages/module/patternfly-docs/content/examples/FilterableWithWindowScroller.tsx b/packages/module/patternfly-docs/content/examples/FilterableWithWindowScroller.tsx index 5c08f14..97a029e 100644 --- a/packages/module/patternfly-docs/content/examples/FilterableWithWindowScroller.tsx +++ b/packages/module/patternfly-docs/content/examples/FilterableWithWindowScroller.tsx @@ -1,479 +1,485 @@ -/* eslint-disable no-console */ import React from 'react'; +import { CellMeasurerCache, CellMeasurer } from 'react-virtualized'; +import { AutoSizer, VirtualTableBody, WindowScroller } from '@patternfly/react-virtualized-extension'; +import { Table, Thead, Tr, Th, Td, TableGridBreakpoint, ActionsColumn, Tbody } from '@patternfly/react-table'; import { - Button, - ButtonVariant, - Toolbar, + SelectOption, ToolbarItem, - ToolbarContent, + Select, + MenuToggleElement, + MenuToggle, ToolbarFilter, + SearchInput, + Badge, + Toolbar, + ToolbarContent, ToolbarToggleGroup, ToolbarGroup, - InputGroup, - InputGroupItem, - TextInput + ToolbarChipGroup, + Button, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateHeader, + EmptyStateIcon, + Bullseye } from '@patternfly/react-core'; -import { debounce } from '@patternfly/react-core'; -import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; -import { ActionsColumn } from '@patternfly/react-table'; -import { Table as TableDeprecated, TableHeader as TableHeaderDeprecated } from '@patternfly/react-table/deprecated'; -import { - Dropdown as DropdownDeprecated, - DropdownItem as DropdownItemDeprecated, - DropdownPosition as DropdownPositionDeprecated, - DropdownToggle as DropdownToggleDeprecated, - Select as SelectDeprecated, - SelectOption as SelectOptionDeprecated, - SelectVariant as SelectVariantDeprecated -} from '@patternfly/react-core/deprecated'; -import { CellMeasurerCache, CellMeasurer } from 'react-virtualized'; -import { AutoSizer, VirtualTableBody, WindowScroller } from '@patternfly/react-virtualized-extension'; - -export 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: '' - }); - } - }; +import { FilterIcon, SearchIcon } from '@patternfly/react-icons'; + +export const ComposableTableWindowScroller = () => { + const [scrollableElement, setScrollableElement] = React.useState(); + React.useEffect(() => { + const scrollableElement = document.getElementById('content-scrollable-2') as HTMLElement; + setScrollableElement(scrollableElement); + }, []); + + interface DataType { + cells: (string | number)[]; + id: string; + disableActions: boolean; + } - this.onCategoryToggle = (_event, isOpen) => { - this.setState({ - isCategoryDropdownOpen: isOpen + const rows: DataType[] = []; + for (let i = 0; i < 100; i++) { + if (i % 2 === 0) { + rows.push({ + disableActions: false, + id: `actions-row-${i}`, + cells: [`US-Node ${i}`, i, i, 'Down', 'Brno'] }); - }; - - this.onCategorySelect = (event) => { - this.setState({ - currentCategory: event.target.innerText, - isCategoryDropdownOpen: !this.state.isCategoryDropdownOpen + } else if (i % 3 === 0) { + rows.push({ + disableActions: false, + id: `actions-row-${i}`, + cells: [`CN-Node ${i}`, i, i, 'Running', 'Westford'] }); - }; - - this.onFilterToggle = (_event, isOpen) => { - this.setState({ - isFilterDropdownOpen: isOpen + } else { + rows.push({ + disableActions: true, + id: `actions-row-${i}`, + cells: [`US-Node ${i}`, i, i, 'Stopped', 'Raleigh'] }); - }; - - this.onFilterSelect = (_event) => { - this.setState({ - isFilterDropdownOpen: !this.state.isFilterDropdownOpen - }); - }; - - this.onInputChange = (_event, newValue) => { - // this.setState({ inputValue: newValue }); - if (newValue === '') { - this.onDelete(); - this.setState({ - inputValue: newValue - }); - } else { - this.setState((prevState) => ({ - 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 + const actions = [ + { + title: 'Some action', + // eslint-disable-next-line no-console + onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Some action, on row: ', rowId) + }, + { + title:
Another action
, + // eslint-disable-next-line no-console + onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Another action, on row: ', rowId) + }, + { + isSeparator: true + }, + { + title: 'Third action', + // eslint-disable-next-line no-console + onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Third action, on row: ', rowId) + } + ]; + + const columns = ['Servers', 'Threads', 'Applications', 'Status', 'Location']; + const scrollToIndex = -1; // can be used to programmatically set current index + + const [isCategoryDropdownOpen, setIsCategoryDropdownOpen] = React.useState(false); + const [isFilterDropdownOpen, setIsFilterDropdownOpen] = React.useState(false); + const [currentCategory, setCurrentCategory] = React.useState('Name'); + const [filters, setFilters] = React.useState>({ location: [], name: [], status: [] }); + const [inputValue, setInputValue] = React.useState(''); + + const onDelete = (type: string | ToolbarChipGroup, id: string) => { + if (type === 'Location') { + setFilters({ + ...filters, + location: filters.location.filter((fil: string) => fil !== id) }); - }; - - 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) - } - }; + } else if (type === 'Name') { + setFilters({ + ...filters, + name: filters.name.filter((fil: string) => fil !== id) }); - }; - - 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: '' - }; + } else if (type === 'Status') { + setFilters({ + ...filters, + status: filters.status.filter((fil: string) => fil !== id) }); - }; - - this.onLocationSelect = (event, selection) => { - this.setState((prevState) => ({ - 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 }); + } else { + setFilters({ location: [], name: [], status: [] }); + } + }; + + const onCategoryToggle = () => { + setIsCategoryDropdownOpen(!isCategoryDropdownOpen); + }; + + const onCategorySelect = (event) => { + setCurrentCategory(event.target.innerText); + setIsCategoryDropdownOpen(!isCategoryDropdownOpen); + }; + + const onFilterToggle = () => { + setIsFilterDropdownOpen(!isFilterDropdownOpen); + }; + + const onInputChange = (newValue: string) => { + setInputValue(newValue); + }; + + const onStatusSelect = (event: React.MouseEvent | undefined, selection: string | number | undefined) => { + const checked = (event?.target as HTMLInputElement).checked; + setFilters({ + ...filters, + status: (checked && selection) ? [...filters.status, `${selection}`] : filters.status.filter((value) => value !== selection) }); + setIsFilterDropdownOpen(false); + }; + + const onNameInput = (event: React.SyntheticEvent | React.KeyboardEvent) => { + setIsCategoryDropdownOpen(false); + const pressedKey = (event as React.KeyboardEvent).key; + if (pressedKey && pressedKey !== 'Enter') { + return; + } - // 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; + const prevFilters = filters.name; + setFilters({ ...filters, name: prevFilters.includes(inputValue) ? prevFilters : [...prevFilters, inputValue] }); + }; + + const onFilterSelect = () => { + setIsFilterDropdownOpen(!isFilterDropdownOpen); + setIsCategoryDropdownOpen(false); + }; + + const onLocationSelect = (_event: React.MouseEvent | undefined, selection: string | number | undefined) => { + setFilters({ ...filters, location: [`${selection}`] }); + + setIsFilterDropdownOpen(false); + onFilterSelect(); + }; + + const buildCategoryDropdown = () => { + const categoryMenuItems = [ + + Location + , + + Name + , + + Status + + ]; return ( - - {currentCategory} - - } + ); - } - - buildFilterDropdown() { - const { currentCategory, isFilterDropdownOpen, inputValue, filters } = this.state; + }; + const buildFilterDropdown = () => { const locationMenuItems = [ - , - , - , - , - + + Raleigh + , + + Westford + , + + Boston + , + + Brno + , + + Bangalore + ]; const statusMenuItems = [ - , - , - , - , - + + Running + , + + Stopped + , + + Down + , + + Degraded + , + + Needs maintenance + ]; return ( onDelete(category, chip as string)} categoryName="Location" showToolbarItem={currentCategory === 'Location'} > - ) => ( + + {filters.location[0] || `Any`} + + )} > {locationMenuItems} - + onDelete(category, chip as string)} categoryName="Name" showToolbarItem={currentCategory === 'Name'} > - - - - - - - - + onInputChange(value)} + value={inputValue} + onClear={() => { + onInputChange(''); + }} + onSearch={onNameInput} // any typing is needed because of what I think is a bug in the SearchInput typing + /> onDelete(category, chip as string)} categoryName="Status" showToolbarItem={currentCategory === 'Status'} > - ) => ( + + Filter by status + {filters.status.length > 0 && {filters.status.length}} + + )} > {statusMenuItems} - + ); + }; + + const renderToolbar = () => ( + setFilters({ location: [], name: [], status: [] })} + collapseListedFiltersBreakpoint="xl" + > + + } breakpoint="xl"> + + {buildCategoryDropdown()} + {buildFilterDropdown()} + + + + + ); + + const measurementCache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 44, + keyMapper: (rowIndex) => rowIndex + }); + + const filteredRows = + filters.name.length > 0 || filters.location.length > 0 || filters.status.length > 0 + ? rows.filter( + (row) => + (filters.name.length === 0 || + filters.name.some((name) => (row.cells[0] as string).toLowerCase().includes(name.toLowerCase()))) && + (filters.location.length === 0 || filters.location.includes(row.cells[4] as string)) && + (filters.status.length === 0 || filters.status.includes(row.cells[3] as string)) + ) + : rows; + + const emptyState = ( + + } + /> + No results match the filter criteria. Clear all filters and try again. + + + + + + + ); + + const rowRenderer = ({ index: rowIndex, _isScrolling, key, style, parent }) => ( + + + {columns.map((col, index) => ( + {filteredRows[rowIndex].cells[index]} + ))} + + + + + + ); + + interface ScrollableContainerStyle { + height: number; + overflowX: 'auto'; + overflowY: 'scroll'; + scrollBehavior: 'smooth'; + WebkitOverflowScrolling: 'touch'; + position: 'relative'; } - renderToolbar() { - 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) => ( - (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 { 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 && ( -
- + {renderToolbar()} + + + + {columns.map((col, index) => ( + + ))} + + + + {filteredRows.length === 0 && ( + + + + + + )} +
{col}
+ {emptyState} +
+ + {({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => ( + + {({ width }) => ( +
void}> + - - - - {({ height, _isScrolling, registerChild, _onChildScroll, scrollTop }) => ( - - {({ width }) => ( -
- (this.actionsVirtualBody = ref)} - autoHeight - className="pf-v5-c-table pf-v5-c-virtualized pf-v5-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} - role="grid" - /> -
- )} -
- )} -
+ rowCount={filteredRows.length} + rowRenderer={rowRenderer} + scrollToIndex={scrollToIndex} + scrollTop={scrollTop} + width={width} + role="grid" + />
)} -
-
- - ); - } -} + + )} + +
+ ); +}; diff --git a/packages/module/patternfly-docs/generated/extensions/virtual-scroll-table/react.js b/packages/module/patternfly-docs/generated/extensions/virtual-scroll-table/react.js index 29ac790..4011646 100644 --- a/packages/module/patternfly-docs/generated/extensions/virtual-scroll-table/react.js +++ b/packages/module/patternfly-docs/generated/extensions/virtual-scroll-table/react.js @@ -224,7 +224,7 @@ pageData.examples = { , 'Filterable with WindowScroller': props => - console.log('clicked on Some action, on row: ', rowId)\n },\n {\n title:
Another action
,\n onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Another action, on row: ', rowId)\n },\n {\n isSeparator: true\n },\n {\n title: 'Third action',\n onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Third action, on row: ', rowId)\n }\n ]\n };\n\n this._handleResize = debounce(this._handleResize.bind(this), 100);\n\n this.onDelete = (type = '', id = '') => {\n if (type) {\n this.setState((prevState) => {\n prevState.filters[type.toLowerCase()] = prevState.filters[type.toLowerCase()].filter((s) => s !== id);\n return {\n filters: prevState.filters\n };\n });\n } else {\n this.setState({\n filters: {\n location: [],\n name: [],\n status: []\n },\n inputValue: ''\n });\n }\n };\n\n this.onCategoryToggle = (_event, isOpen) => {\n this.setState({\n isCategoryDropdownOpen: isOpen\n });\n };\n\n this.onCategorySelect = (event) => {\n this.setState({\n currentCategory: event.target.innerText,\n isCategoryDropdownOpen: !this.state.isCategoryDropdownOpen\n });\n };\n\n this.onFilterToggle = (_event, isOpen) => {\n this.setState({\n isFilterDropdownOpen: isOpen\n });\n };\n\n this.onFilterSelect = (_event) => {\n this.setState({\n isFilterDropdownOpen: !this.state.isFilterDropdownOpen\n });\n };\n\n this.onInputChange = (_event, newValue) => {\n // this.setState({ inputValue: newValue });\n if (newValue === '') {\n this.onDelete();\n this.setState({\n inputValue: newValue\n });\n } else {\n this.setState((prevState) => ({\n filters: {\n ...prevState.filters,\n ['name']: [newValue]\n },\n inputValue: newValue\n }));\n }\n };\n\n this.onRowSelect = (event, isSelected, rowId) => {\n let rows;\n if (rowId === -1) {\n rows = this.state.rows.map((oneRow) => {\n oneRow.selected = isSelected;\n return oneRow;\n });\n } else {\n rows = [...this.state.rows];\n rows[rowId].selected = isSelected;\n }\n this.setState({\n rows\n });\n };\n\n this.onStatusSelect = (event, selection) => {\n const checked = event.target.checked;\n this.setState((prevState) => {\n const prevSelections = prevState.filters.status;\n return {\n filters: {\n ...prevState.filters,\n status: checked ? [...prevSelections, selection] : prevSelections.filter((value) => value !== selection)\n }\n };\n });\n };\n\n this.onNameInput = (event) => {\n if (event.key && event.key !== 'Enter') {\n return;\n }\n\n const { inputValue } = this.state;\n this.setState((prevState) => {\n const prevFilters = prevState.filters.name;\n return {\n filters: {\n ...prevState.filters,\n ['name']: prevFilters.includes(inputValue) ? prevFilters : [...prevFilters, inputValue]\n },\n inputValue: ''\n };\n });\n };\n\n this.onLocationSelect = (event, selection) => {\n this.setState((prevState) => ({\n filters: {\n ...prevState.filters,\n ['location']: [selection]\n }\n }));\n this.onFilterSelect();\n };\n\n this._handleResize = debounce(this._handleResize.bind(this), 100);\n this._bindBodyRef = this._bindBodyRef.bind(this);\n }\n\n componentDidMount() {\n // re-render after resize\n window.addEventListener('resize', this._handleResize);\n\n setTimeout(() => {\n const scollableElement = document.getElementById('content-scrollable-1');\n this.setState({ scollableElement });\n });\n\n // re-render after resize\n window.addEventListener('resize', this._handleResize);\n }\n\n componentWillUnmount() {\n window.removeEventListener('resize', this._handleResize);\n }\n\n _handleResize() {\n this._cellMeasurementCache.clearAll();\n this._bodyRef.recomputeVirtualGridSize();\n }\n\n _bindBodyRef(ref) {\n this._bodyRef = ref;\n }\n\n buildCategoryDropdown() {\n const { isCategoryDropdownOpen, currentCategory } = this.state;\n\n return (\n \n \n {currentCategory}\n \n }\n isOpen={isCategoryDropdownOpen}\n dropdownItems={[\n Location,\n Name,\n Status\n ]}\n style={{ width: '100%' }}\n >\n \n );\n }\n\n buildFilterDropdown() {\n const { currentCategory, isFilterDropdownOpen, inputValue, filters } = this.state;\n\n const locationMenuItems = [\n ,\n ,\n ,\n ,\n \n ];\n\n const statusMenuItems = [\n ,\n ,\n ,\n ,\n \n ];\n\n return (\n \n \n \n {locationMenuItems}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {statusMenuItems}\n \n \n \n );\n }\n\n renderToolbar() {\n return (\n \n \n } breakpoint=\"xl\">\n \n {this.buildCategoryDropdown()}\n {this.buildFilterDropdown()}\n \n \n \n \n );\n }\n\n render() {\n const { loading, rows, columns, actions, filters, scollableElement } = this.state;\n\n const filteredRows =\n filters.name.length > 0 || filters.location.length > 0 || filters.status.length > 0\n ? rows.filter((row) => (\n (filters.name.length === 0 ||\n filters.name.some((name) => row.cells[0].toLowerCase().includes(name.toLowerCase()))) &&\n (filters.location.length === 0 || filters.location.includes(row.cells[4])) &&\n (filters.status.length === 0 || filters.status.includes(row.cells[3]))\n ))\n : rows;\n const measurementCache = new CellMeasurerCache({\n fixedWidth: true,\n minHeight: 44,\n keyMapper: (rowIndex) => rowIndex\n });\n\n const rowRenderer = ({ index, _isScrolling, key, style, parent }) => {\n const { actions } = this.state;\n\n return (\n \n \n {filteredRows[index].cells[0]}\n {filteredRows[index].cells[1]}\n {filteredRows[index].cells[2]}\n {filteredRows[index].cells[3]}\n {filteredRows[index].cells[4]}\n \n \n \n \n \n );\n };\n\n return (\n \n {this.renderToolbar()}\n\n \n
\n {!loading && filteredRows.length > 0 && (\n
\n \n \n \n \n {({ height, _isScrolling, registerChild, _onChildScroll, scrollTop }) => (\n \n {({ width }) => (\n
\n (this.actionsVirtualBody = ref)}\n autoHeight\n className=\"pf-v5-c-table pf-v5-c-virtualized pf-v5-c-window-scroller\"\n deferredMeasurementCache={measurementCache}\n rowHeight={measurementCache.rowHeight}\n height={height || 0}\n overscanRowCount={10}\n columnCount={6}\n rows={filteredRows}\n rowCount={filteredRows.length}\n rowRenderer={rowRenderer}\n scrollTop={scrollTop}\n width={width}\n role=\"grid\"\n />\n
\n )}\n
\n )}\n
\n
\n )}\n
\n \n
\n );\n }\n}\n","title":"Filterable with WindowScroller","lang":"js"}}> + {\n const [scrollableElement, setScrollableElement] = React.useState();\n React.useEffect(() => {\n const scrollableElement = document.getElementById('content-scrollable-2') as HTMLElement;\n setScrollableElement(scrollableElement);\n }, []);\n\n interface DataType {\n cells: (string | number)[];\n id: string;\n disableActions: boolean;\n }\n\n const rows: DataType[] = [];\n for (let i = 0; i < 100; i++) {\n if (i % 2 === 0) {\n rows.push({\n disableActions: false,\n id: `actions-row-${i}`,\n cells: [`US-Node ${i}`, i, i, 'Down', 'Brno']\n });\n } else if (i % 3 === 0) {\n rows.push({\n disableActions: false,\n id: `actions-row-${i}`,\n cells: [`CN-Node ${i}`, i, i, 'Running', 'Westford']\n });\n } else {\n rows.push({\n disableActions: true,\n id: `actions-row-${i}`,\n cells: [`US-Node ${i}`, i, i, 'Stopped', 'Raleigh']\n });\n }\n }\n\n const actions = [\n {\n title: 'Some action',\n // eslint-disable-next-line no-console\n onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Some action, on row: ', rowId)\n },\n {\n title:
Another action
,\n // eslint-disable-next-line no-console\n onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Another action, on row: ', rowId)\n },\n {\n isSeparator: true\n },\n {\n title: 'Third action',\n // eslint-disable-next-line no-console\n onClick: (_event, rowId, _rowData, _extra) => console.log('clicked on Third action, on row: ', rowId)\n }\n ];\n\n const columns = ['Servers', 'Threads', 'Applications', 'Status', 'Location'];\n const scrollToIndex = -1; // can be used to programmatically set current index\n\n const [isCategoryDropdownOpen, setIsCategoryDropdownOpen] = React.useState(false);\n const [isFilterDropdownOpen, setIsFilterDropdownOpen] = React.useState(false);\n const [currentCategory, setCurrentCategory] = React.useState('Name');\n const [filters, setFilters] = React.useState>({ location: [], name: [], status: [] });\n const [inputValue, setInputValue] = React.useState('');\n\n const onDelete = (type: string | ToolbarChipGroup, id: string) => {\n if (type === 'Location') {\n setFilters({\n ...filters,\n location: filters.location.filter((fil: string) => fil !== id)\n });\n } else if (type === 'Name') {\n setFilters({\n ...filters,\n name: filters.name.filter((fil: string) => fil !== id)\n });\n } else if (type === 'Status') {\n setFilters({\n ...filters,\n status: filters.status.filter((fil: string) => fil !== id)\n });\n } else {\n setFilters({ location: [], name: [], status: [] });\n }\n };\n\n const onCategoryToggle = () => {\n setIsCategoryDropdownOpen(!isCategoryDropdownOpen);\n };\n\n const onCategorySelect = (event) => {\n setCurrentCategory(event.target.innerText);\n setIsCategoryDropdownOpen(!isCategoryDropdownOpen);\n };\n\n const onFilterToggle = () => {\n setIsFilterDropdownOpen(!isFilterDropdownOpen);\n };\n\n const onInputChange = (newValue: string) => {\n setInputValue(newValue);\n };\n\n const onStatusSelect = (event: React.MouseEvent | undefined, selection: string | number | undefined) => {\n const checked = (event?.target as HTMLInputElement).checked;\n setFilters({\n ...filters,\n status: (checked && selection) ? [...filters.status, `${selection}`] : filters.status.filter((value) => value !== selection)\n });\n setIsFilterDropdownOpen(false);\n };\n\n const onNameInput = (event: React.SyntheticEvent | React.KeyboardEvent) => {\n setIsCategoryDropdownOpen(false);\n const pressedKey = (event as React.KeyboardEvent).key;\n if (pressedKey && pressedKey !== 'Enter') {\n return;\n }\n\n const prevFilters = filters.name;\n setFilters({ ...filters, name: prevFilters.includes(inputValue) ? prevFilters : [...prevFilters, inputValue] });\n };\n\n const onFilterSelect = () => {\n setIsFilterDropdownOpen(!isFilterDropdownOpen);\n setIsCategoryDropdownOpen(false);\n };\n\n const onLocationSelect = (_event: React.MouseEvent | undefined, selection: string | number | undefined) => {\n setFilters({ ...filters, location: [`${selection}`] });\n\n setIsFilterDropdownOpen(false);\n onFilterSelect();\n };\n\n const buildCategoryDropdown = () => {\n const categoryMenuItems = [\n \n Location\n ,\n \n Name\n ,\n \n Status\n \n ];\n\n return (\n \n ) => (\n }\n style={\n {\n width: '100%',\n verticalAlign: 'text-bottom'\n } as React.CSSProperties\n }\n >\n {currentCategory}\n \n )}\n isOpen={isCategoryDropdownOpen}\n >\n {categoryMenuItems}\n \n \n );\n };\n\n const buildFilterDropdown = () => {\n const locationMenuItems = [\n \n Raleigh\n ,\n \n Westford\n ,\n \n Boston\n ,\n \n Brno\n ,\n \n Bangalore\n \n ];\n\n const statusMenuItems = [\n \n Running\n ,\n \n Stopped\n ,\n \n Down\n ,\n \n Degraded\n ,\n \n Needs maintenance\n \n ];\n\n return (\n \n onDelete(category, chip as string)}\n categoryName=\"Location\"\n showToolbarItem={currentCategory === 'Location'}\n >\n ) => (\n \n {filters.location[0] || `Any`}\n \n )}\n >\n {locationMenuItems}\n \n \n onDelete(category, chip as string)}\n categoryName=\"Name\"\n showToolbarItem={currentCategory === 'Name'}\n >\n onInputChange(value)}\n value={inputValue}\n onClear={() => {\n onInputChange('');\n }}\n onSearch={onNameInput} // any typing is needed because of what I think is a bug in the SearchInput typing\n />\n \n onDelete(category, chip as string)}\n categoryName=\"Status\"\n showToolbarItem={currentCategory === 'Status'}\n >\n ) => (\n \n Filter by status\n {filters.status.length > 0 && {filters.status.length}}\n \n )}\n >\n {statusMenuItems}\n \n \n \n );\n };\n\n const renderToolbar = () => (\n setFilters({ location: [], name: [], status: [] })}\n collapseListedFiltersBreakpoint=\"xl\"\n >\n \n } breakpoint=\"xl\">\n \n {buildCategoryDropdown()}\n {buildFilterDropdown()}\n \n \n \n \n );\n\n const measurementCache = new CellMeasurerCache({\n fixedWidth: true,\n minHeight: 44,\n keyMapper: (rowIndex) => rowIndex\n });\n\n const filteredRows =\n filters.name.length > 0 || filters.location.length > 0 || filters.status.length > 0\n ? rows.filter(\n (row) =>\n (filters.name.length === 0 ||\n filters.name.some((name) => (row.cells[0] as string).toLowerCase().includes(name.toLowerCase()))) &&\n (filters.location.length === 0 || filters.location.includes(row.cells[4] as string)) &&\n (filters.status.length === 0 || filters.status.includes(row.cells[3] as string))\n )\n : rows;\n\n const emptyState = (\n \n }\n />\n No results match the filter criteria. Clear all filters and try again.\n \n \n {\n setFilters({ location: [], name: [], status: [] });\n }}\n >\n Clear all filters\n \n \n \n \n );\n\n const rowRenderer = ({ index: rowIndex, _isScrolling, key, style, parent }) => (\n \n \n {columns.map((col, index) => (\n {filteredRows[rowIndex].cells[index]}\n ))}\n \n \n \n \n \n );\n\n interface ScrollableContainerStyle {\n height: number;\n overflowX: 'auto';\n overflowY: 'scroll';\n scrollBehavior: 'smooth';\n WebkitOverflowScrolling: 'touch';\n position: 'relative';\n }\n\n const scrollableContainerStyle: ScrollableContainerStyle = {\n height: 500 /* important note: the scrollable container should have some sort of fixed height, or it should be wrapped in container that is smaller than ReactVirtualized__VirtualGrid container and has overflow visible if using the Window Scroller. See WindowScroller.example.css */,\n overflowX: 'auto',\n overflowY: 'scroll',\n scrollBehavior: 'smooth',\n WebkitOverflowScrolling: 'touch',\n position: 'relative'\n };\n\n return (\n \n {renderToolbar()}\n \n \n \n {columns.map((col, index) => (\n \n ))}\n \n \n \n {filteredRows.length === 0 && (\n \n \n \n \n \n )}\n
{col}
\n {emptyState}\n
\n \n {({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => (\n \n {({ width }) => (\n
void}>\n \n
\n )}\n
\n )}\n
\n \n );\n};\n","title":"Filterable with WindowScroller","lang":"js"}}>
}; diff --git a/yarn.lock b/yarn.lock index 44e93fe..5fe5b35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2070,10 +2070,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@patternfly/ast-helpers@^0.4.57": - version "0.4.79" - resolved "https://registry.yarnpkg.com/@patternfly/ast-helpers/-/ast-helpers-0.4.79.tgz#7d738323681d5a1138d973b1daf534f1a05080b2" - integrity sha512-Nn2Uut7Z1nQwDr5LAf1bns6yCFQFQWwXE2I1Ucvbk6s2Gl2X/85VGvww8FVO+YiW/plLZMLBFRPJMP4fs2N8Cg== +"@patternfly/ast-helpers@^1.3.8": + version "1.3.8" + resolved "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.3.8.tgz#2575b67f3a051e21fff05d38236771de5939622c" + integrity sha512-68cpY/Q/ZIjWzs/pDiG7Jg2BhuPiA36vAlF9wi7qdGaFJTrZGmaosm7whbzpL0TFJZFKDmFYID50EnYzHZOI8w== dependencies: acorn "^8.4.1" acorn-class-fields "^1.0.0" @@ -2081,10 +2081,10 @@ acorn-static-class-features "^1.0.0" astring "^1.7.5" -"@patternfly/documentation-framework@^5.0.15": - version "5.0.15" - resolved "https://registry.yarnpkg.com/@patternfly/documentation-framework/-/documentation-framework-5.0.15.tgz#56e6608de10a95a92787686e5db1eb3ff7c09b89" - integrity sha512-3r9fqPeAKY8GeJM6m7VPyPsiggxhyDbbWTo2/4T4NoNisVPpFXi0AzUaTpvJU2KcBx5pSot1mb84V3f8DHzWXg== +"@patternfly/documentation-framework@^5.2.22": + version "5.3.8" + resolved "https://registry.npmjs.org/@patternfly/documentation-framework/-/documentation-framework-5.3.8.tgz#4eb7dcd8b6e7c3a8b1c4680b1d43d45db114201f" + integrity sha512-RiRaC183RdzbUDcpgkT9ekMcBFLhiBfHnByJMgwyveqR10qcGDezT59Pe3bbHSWP+9ly3eCRIkgGHh8rVkfBQg== dependencies: "@babel/core" "7.18.2" "@babel/plugin-proposal-class-properties" "7.17.12" @@ -2094,7 +2094,7 @@ "@babel/plugin-transform-react-jsx" "7.17.12" "@babel/preset-env" "7.18.2" "@mdx-js/util" "1.6.16" - "@patternfly/ast-helpers" "^0.4.57" + "@patternfly/ast-helpers" "^1.3.8" "@reach/router" "npm:@gatsbyjs/reach-router@1.3.9" autoprefixer "9.8.6" babel-loader "9.1.2" @@ -2170,60 +2170,60 @@ puppeteer-cluster "^0.23.0" xmldoc "^1.1.2" -"@patternfly/patternfly@^5.0.0": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-5.0.2.tgz#f5daf2c98ccb85e6466d42fd61d39ba3c10ed532" - integrity sha512-PB8+MLdYVgF1hIOxGmnVsZG+YHUX3RePe5W1oMS4gS00EmSgw1cobr1Qbpy/BqqS8/R9DRN4hZ2FKDT0d5tkFQ== +"@patternfly/patternfly@^6.0.0-alpha.9": + version "6.0.0-alpha.23" + resolved "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.0.0-alpha.23.tgz#75d96c445c08200ffcd74aebdc221334f1d0cc13" + integrity sha512-h1BRJkZkdTRzg0w1Q66axH1R3hEQM/f2UfKg4H/r8JmP8sRgpshOx2eSMfAask+dqWMJIM/r82fCsNxaDLW/9Q== -"@patternfly/react-code-editor@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-code-editor/-/react-code-editor-5.0.0.tgz#2ba9d49a84023907b94fcbec13ec62b2d463d33e" - integrity sha512-Ya1nuw2Zcor/MET+s0+VuYq2VsRb+VzKpZQ8Y1MbrXJdlWV6QS5Wf1M7jDl9lYkiJaS3pjG7eXNeVX2YJ+mQiw== +"@patternfly/react-code-editor@^6.0.0-alpha.1": + version "6.0.0-alpha.2" + resolved "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.0.0-alpha.2.tgz#96a03ab9218a20fcf63f96f2a1c981f722fe5500" + integrity sha512-YN10UNzVldVk/k93ndX+lXhPQNsh5Psgn+FGTYLMLN8ZlgjtwOPCxWmwj6eseYEvGCdA5QmaN8UjDCY089ieOg== dependencies: - "@patternfly/react-core" "^5.0.0" - "@patternfly/react-icons" "^5.0.0" - "@patternfly/react-styles" "^5.0.0" + "@patternfly/react-core" "^6.0.0-alpha.2" + "@patternfly/react-icons" "^6.0.0-alpha.2" + "@patternfly/react-styles" "^6.0.0-alpha.2" react-dropzone "14.2.3" tslib "^2.5.0" -"@patternfly/react-core@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-5.0.0.tgz#96c9e2315047eec94d28f5621c02fa147182dd6f" - integrity sha512-kewRVFhLw0Dvt8250pqrO47sVRx8E93sMGZbHQomJnZdachYeQ9STnQTP2gvOBq/GPnMei0LZLv0T99g8mPE4w== +"@patternfly/react-core@^6.0.0-alpha.1", "@patternfly/react-core@^6.0.0-alpha.2": + version "6.0.0-alpha.2" + resolved "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.0.0-alpha.2.tgz#038f5d9f32536bba057c4a0c3f80b31eb7e0bd9b" + integrity sha512-1AnOI3ZmjBtAh97+aDNCmEgE/7LvUGu3MB3BYJkYFWXz8CIKCrurc3k3e3gCAVdg0oNXhcoSpUsjXnS4/e7qkA== dependencies: - "@patternfly/react-icons" "^5.0.0" - "@patternfly/react-styles" "^5.0.0" - "@patternfly/react-tokens" "^5.0.0" - focus-trap "7.4.3" + "@patternfly/react-icons" "^6.0.0-alpha.2" + "@patternfly/react-styles" "^6.0.0-alpha.2" + "@patternfly/react-tokens" "^6.0.0-alpha.2" + focus-trap "7.5.2" react-dropzone "^14.2.3" tslib "^2.5.0" -"@patternfly/react-icons@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-5.0.0.tgz#bb56ead97425f1b3ab886ee291ba6b6af4088e9d" - integrity sha512-GG5Y/UYl0h346MyDU9U650Csaq4Mxk8S6U8XC7ERk/xIrRr2RF67O2uY7zKBDMTNLYdBvPzgc2s3OMV1+d2/mg== - -"@patternfly/react-styles@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-5.0.0.tgz#63705ad498ff271fd056e92bd07b2c720ef3491a" - integrity sha512-xbSCgjx+fPrXbIzUznwTFWtJEbzVS0Wn4zrejdKJYQTY+4YcuPlFkeq2tl3syzwGsaYMpHiFwQiTaKyTvlwtuw== - -"@patternfly/react-table@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-5.0.0.tgz#2808f22d01818c31e6ddc69cc3a07c9381dc6d84" - integrity sha512-Q3MBo9+ZmBvLJzVHxmV9f/4qQAz5Si743zVLHRwjh+tjbn/DrcbxJdT8Uxa3NGKkpvszzgi/LPeXipJOHOELug== - dependencies: - "@patternfly/react-core" "^5.0.0" - "@patternfly/react-icons" "^5.0.0" - "@patternfly/react-styles" "^5.0.0" - "@patternfly/react-tokens" "^5.0.0" +"@patternfly/react-icons@^6.0.0-alpha.1", "@patternfly/react-icons@^6.0.0-alpha.2": + version "6.0.0-alpha.2" + resolved "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.0.0-alpha.2.tgz#7555a1410a6038c7c59f2f8d515adec572392f86" + integrity sha512-OGBen2niZ5S88rY2DVeO+P06oaZP5cO5MmdgW7iOzhVuyHkUS+IB15bN7+RxO9PJkEpfMQz0Ui/rQaXFGxLaSw== + +"@patternfly/react-styles@^6.0.0-alpha.1", "@patternfly/react-styles@^6.0.0-alpha.2": + version "6.0.0-alpha.2" + resolved "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.0.0-alpha.2.tgz#0d2973b1c8e42e2f35f208874f12c754f0c2a8b0" + integrity sha512-39VT5sKQdxck48mjSgbiL9gn6SaaVuYrlqQcrkpRB4F7qk0rJdVAdjwS/PliRjsb/Ywz7xG9UWK/D/1OoQEf2A== + +"@patternfly/react-table@^6.0.0-alpha.1": + version "6.0.0-alpha.2" + resolved "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.0.0-alpha.2.tgz#c454750020518ad8f234d8f630c26f4114c0e249" + integrity sha512-jbyC/JS4F8NifKhdQ7N08ncD2XUoswjDrORoi354Vw+luvS1e+OlcPTCniJZkJP4YlkdeKROTHHe1aJle80BWw== + dependencies: + "@patternfly/react-core" "^6.0.0-alpha.2" + "@patternfly/react-icons" "^6.0.0-alpha.2" + "@patternfly/react-styles" "^6.0.0-alpha.2" + "@patternfly/react-tokens" "^6.0.0-alpha.2" lodash "^4.17.19" tslib "^2.5.0" -"@patternfly/react-tokens@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-5.0.0.tgz#8e2698d32d5353359e713312687a6b08ead0080b" - integrity sha512-to2CXIZ6WTuzBcjLZ+nXi5LhnYkSIDu3RBMRZwrplmECOoUWv87CC+2T0EVxtASRtpQfikjD2PDKMsif5i0BxQ== +"@patternfly/react-tokens@^6.0.0-alpha.2": + version "6.0.0-alpha.2" + resolved "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.0.0-alpha.2.tgz#d0509f67638fa244ae91175b0e308f7935b4ec91" + integrity sha512-A4CN2WLUzjpHD2WAOexYG9X+yy5fowZXmUpLX87+Gk+sl5ziYOFY661l6b+ZgWmvNZ+g58K75JKb8Myu3bx7Ow== "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -5744,12 +5744,12 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -focus-trap@7.4.3: - version "7.4.3" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.4.3.tgz#a3dae73d44df359eb92bbf37b18e173e813b16c5" - integrity sha512-BgSSbK4GPnS2VbtZ50VtOv1Sti6DIkj3+LkVjiWMNjLeAp1SH1UlLx3ULu/DCu4vq5R4/uvTm+zrvsMsuYmGLg== +focus-trap@7.5.2: + version "7.5.2" + resolved "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz#e5ee678d10a18651f2591ffb66c949fb098d57cf" + integrity sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw== dependencies: - tabbable "^6.1.2" + tabbable "^6.2.0" follow-redirects@1.5.10: version "1.5.10" @@ -7466,6 +7466,11 @@ jest-snapshot@^29.5.0: pretty-format "^29.5.0" semver "^7.3.5" +jest-transform-stub@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d" + integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg== + jest-util@^29.2.1: version "29.2.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.2.1.tgz#f26872ba0dc8cbefaba32c34f98935f6cf5fc747" @@ -10621,10 +10626,10 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tabbable@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.1.2.tgz#b0d3ca81d582d48a80f71b267d1434b1469a3703" - integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ== +tabbable@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: version "2.2.1"