diff --git a/package-lock.json b/package-lock.json
index 277d1e5f..a9f4eea8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3700,9 +3700,9 @@
}
},
"node_modules/@patternfly/ast-helpers": {
- "version": "1.4.0-alpha.146",
- "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.146.tgz",
- "integrity": "sha512-gW5yMl/ZFsFDpN7gYvegw3crpbBUkcuO1GK2gZCKMGJEyGytAVQncxZxfwZxZt9Y7DFdwPBG3oyu432cL9jLmA==",
+ "version": "1.4.0-alpha.149",
+ "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.149.tgz",
+ "integrity": "sha512-X/NovaiGapE9zOZuBJscuW8oLFYPwO7bG/apmIGAfXpEYkC+qwcRnXsqQB/mngmn+B1eLrdWxKR/xDWsi381GA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3714,9 +3714,9 @@
}
},
"node_modules/@patternfly/documentation-framework": {
- "version": "6.5.5",
- "resolved": "https://registry.npmjs.org/@patternfly/documentation-framework/-/documentation-framework-6.5.5.tgz",
- "integrity": "sha512-DxdcmM+XApOoPf/K0ZD68iOZq2Be+xvB3ohgDskUIav+3UuJrdk4qo2nHY1FjG++zLGwZA/6LS7tbJwEWWdG8w==",
+ "version": "6.5.8",
+ "resolved": "https://registry.npmjs.org/@patternfly/documentation-framework/-/documentation-framework-6.5.8.tgz",
+ "integrity": "sha512-cZC+9vXGDXVZnjLQfdlixikYKHGXWLgrsn+43a/VDbI2yne+oUTdzDCNA+R1H3hovjCO11gMM8DpGhoty4DHwA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3724,7 +3724,7 @@
"@babel/preset-env": "^7.24.3",
"@babel/preset-react": "^7.24.1",
"@mdx-js/util": "1.6.16",
- "@patternfly/ast-helpers": "^1.4.0-alpha.146",
+ "@patternfly/ast-helpers": "^1.4.0-alpha.149",
"@reach/router": "npm:@gatsbyjs/reach-router@1.3.9",
"autoprefixer": "9.8.6",
"babel-loader": "^9.1.3",
@@ -28573,14 +28573,14 @@
"license": "MIT",
"dependencies": {
"@patternfly/react-component-groups": "^6.1.0",
- "@patternfly/react-core": "^6.0.0",
- "@patternfly/react-icons": "^6.0.0",
- "@patternfly/react-table": "^6.0.0",
+ "@patternfly/react-core": "^6.1.0",
+ "@patternfly/react-icons": "^6.1.0",
+ "@patternfly/react-table": "^6.1.0",
"clsx": "^2.1.1",
"react-jss": "^10.10.0"
},
"devDependencies": {
- "@patternfly/documentation-framework": "^6.5.4",
+ "@patternfly/documentation-framework": "^6.5.8",
"@patternfly/patternfly": "^6.0.0",
"@patternfly/patternfly-a11y": "^5.0.0",
"@patternfly/react-code-editor": "^6.0.0",
@@ -30994,9 +30994,9 @@
"optional": true
},
"@patternfly/ast-helpers": {
- "version": "1.4.0-alpha.146",
- "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.146.tgz",
- "integrity": "sha512-gW5yMl/ZFsFDpN7gYvegw3crpbBUkcuO1GK2gZCKMGJEyGytAVQncxZxfwZxZt9Y7DFdwPBG3oyu432cL9jLmA==",
+ "version": "1.4.0-alpha.149",
+ "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.149.tgz",
+ "integrity": "sha512-X/NovaiGapE9zOZuBJscuW8oLFYPwO7bG/apmIGAfXpEYkC+qwcRnXsqQB/mngmn+B1eLrdWxKR/xDWsi381GA==",
"dev": true,
"requires": {
"acorn": "^8.4.1",
@@ -31007,16 +31007,16 @@
}
},
"@patternfly/documentation-framework": {
- "version": "6.5.5",
- "resolved": "https://registry.npmjs.org/@patternfly/documentation-framework/-/documentation-framework-6.5.5.tgz",
- "integrity": "sha512-DxdcmM+XApOoPf/K0ZD68iOZq2Be+xvB3ohgDskUIav+3UuJrdk4qo2nHY1FjG++zLGwZA/6LS7tbJwEWWdG8w==",
+ "version": "6.5.8",
+ "resolved": "https://registry.npmjs.org/@patternfly/documentation-framework/-/documentation-framework-6.5.8.tgz",
+ "integrity": "sha512-cZC+9vXGDXVZnjLQfdlixikYKHGXWLgrsn+43a/VDbI2yne+oUTdzDCNA+R1H3hovjCO11gMM8DpGhoty4DHwA==",
"dev": true,
"requires": {
"@babel/core": "^7.24.3",
"@babel/preset-env": "^7.24.3",
"@babel/preset-react": "^7.24.1",
"@mdx-js/util": "1.6.16",
- "@patternfly/ast-helpers": "^1.4.0-alpha.146",
+ "@patternfly/ast-helpers": "^1.4.0-alpha.149",
"@reach/router": "npm:@gatsbyjs/reach-router@1.3.9",
"autoprefixer": "9.8.6",
"babel-loader": "^9.1.3",
@@ -31449,14 +31449,14 @@
"@patternfly/react-data-view": {
"version": "file:packages/module",
"requires": {
- "@patternfly/documentation-framework": "^6.5.4",
+ "@patternfly/documentation-framework": "^6.5.8",
"@patternfly/patternfly": "^6.0.0",
"@patternfly/patternfly-a11y": "^5.0.0",
"@patternfly/react-code-editor": "^6.0.0",
"@patternfly/react-component-groups": "^6.1.0",
- "@patternfly/react-core": "^6.0.0",
- "@patternfly/react-icons": "^6.0.0",
- "@patternfly/react-table": "^6.0.0",
+ "@patternfly/react-core": "^6.1.0",
+ "@patternfly/react-icons": "^6.1.0",
+ "@patternfly/react-table": "^6.1.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-router-dom": "^5.3.3",
diff --git a/packages/module/package.json b/packages/module/package.json
index 9539eaa6..cee5672b 100644
--- a/packages/module/package.json
+++ b/packages/module/package.json
@@ -32,9 +32,9 @@
},
"dependencies": {
"@patternfly/react-component-groups": "^6.1.0",
- "@patternfly/react-core": "^6.0.0",
- "@patternfly/react-icons": "^6.0.0",
- "@patternfly/react-table": "^6.0.0",
+ "@patternfly/react-core": "^6.1.0",
+ "@patternfly/react-icons": "^6.1.0",
+ "@patternfly/react-table": "^6.1.0",
"clsx": "^2.1.1",
"react-jss": "^10.10.0"
},
@@ -43,7 +43,7 @@
"react-dom": "^17 || ^18"
},
"devDependencies": {
- "@patternfly/documentation-framework": "^6.5.4",
+ "@patternfly/documentation-framework": "^6.5.8",
"@patternfly/patternfly": "^6.0.0",
"@patternfly/react-code-editor": "^6.0.0",
"@patternfly/patternfly-a11y": "^5.0.0",
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/FiltersExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/FiltersExample.tsx
index afdbcac0..e841ec78 100644
--- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/FiltersExample.tsx
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/FiltersExample.tsx
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
-import { Pagination } from '@patternfly/react-core';
+import { Pagination, TreeViewDataItem } from '@patternfly/react-core';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
import { useDataViewFilters, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
@@ -8,6 +8,7 @@ import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataVi
import { DataViewFilterOption, DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter';
+import { DataViewTreeFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTreeFilter';
const perPageOptions = [
{ title: '5', value: 5 },
@@ -43,6 +44,61 @@ const filterOptions: DataViewFilterOption[] = [
{ label: 'Workspace three', value: 'workspace-three' }
];
+const statusOptions: TreeViewDataItem[] = [
+ {
+ name: 'Ready',
+ id: 'ready',
+ checkProps: { checked: false },
+ customBadgeContent: 1,
+ children: [
+ {
+ name: 'Updated',
+ id: 'updated',
+ checkProps: { checked: false },
+ customBadgeContent: 0
+ },
+ {
+ name: 'Waiting to update',
+ id: 'waiting',
+ checkProps: { checked: false },
+ customBadgeContent: 0
+ },
+ {
+ name: 'Conditions degraded',
+ id: 'degraded',
+ checkProps: { checked: false },
+ customBadgeContent: 1
+ },
+ {
+ name: 'Approval required',
+ id: 'approval',
+ checkProps: { checked: false },
+ customBadgeContent: 0
+ }
+ ]
+ },
+ {
+ name: 'Not ready',
+ id: 'nr',
+ checkProps: { checked: false },
+ customBadgeContent: 1,
+ children: [
+ {
+ name: 'Conditions degraded',
+ id: 'nr-degraded',
+ checkProps: { checked: false },
+ customBadgeContent: 1
+ }
+ ]
+ },
+ {
+ name: 'Updating',
+ id: 'updating',
+ checkProps: { checked: false },
+ customBadgeContent: 0
+ }
+];
+
const columns = [ 'Name', 'Branch', 'Pull requests', 'Workspace', 'Last commit' ];
const ouiaId = 'LayoutExample';
@@ -81,6 +137,7 @@ const MyTable: React.FunctionComponent = () => {
+
}
/>
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md
index 1901f424..91a77117 100644
--- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md
@@ -25,6 +25,7 @@ import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataView
import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter';
+import { DataViewTreeFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTreeFilter';
The **data view toolbar** component renders a default opinionated data view toolbar above or below the data section.
diff --git a/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.tsx b/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.tsx
new file mode 100644
index 00000000..d0db0588
--- /dev/null
+++ b/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.tsx
@@ -0,0 +1,276 @@
+import React from 'react';
+import {
+ MenuProps,
+ ToolbarLabel,
+ ToolbarFilter,
+ TreeViewDataItem,
+ MenuContainer,
+ MenuToggle,
+ Panel,
+ PanelMain,
+ PanelMainBody,
+ Title,
+ TreeView,
+} from '@patternfly/react-core';
+
+const isToolbarLabel = (label: string | ToolbarLabel): label is ToolbarLabel =>
+ typeof label === 'object' && 'key' in label;
+
+/** extends MenuProps */
+export interface DataViewTreeFilterProps extends Omit {
+ /** Unique key for the filter attribute */
+ filterId: string;
+ /** Array of current filter values */
+ value?: string[];
+ /** Filter title displayed in the toolbar */
+ title: string;
+ /** Placeholder text of the menu */
+ placeholder?: string;
+ /** Filter options displayed */
+ options: TreeViewDataItem[];
+ /** Callback for updating when item selection changes. */
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
+ /** Controls visibility of the filter in the toolbar */
+ showToolbarItem?: boolean;
+ /** Controls visibility of the filter icon */
+ showIcon?: boolean;
+ /** Controls visibility of the selected items badge */
+ showBadge?: boolean;
+ /** Custom OUIA ID */
+ ouiaId?: string;
+}
+
+export const DataViewTreeFilter: React.FC = ({
+ filterId,
+ title,
+ value = [],
+ onChange,
+ placeholder,
+ options = [],
+ showToolbarItem,
+ ouiaId = 'DataViewTreeFilter',
+ ...props
+}: DataViewTreeFilterProps) => {
+ const [ isOpen, setIsOpen ] = React.useState(false);
+ const toggleRef = React.useRef(null);
+ const menuRef = React.useRef(null);
+ const [ checkedItems, setCheckedItems ] = React.useState([]);
+
+
+ // const containerRef = React.useRef(null);
+
+ // const handleToggleClick = (event: React.MouseEvent) => {
+ // event.stopPropagation();
+ // setTimeout(() => {
+ // const firstElement = menuRef.current?.querySelector('li > button:not(:disabled)') as HTMLElement;
+ // firstElement?.focus();
+ // }, 0);
+ // setIsOpen(prev => !prev);
+ // };
+
+ // const handleSelect = (event?: React.MouseEvent, itemId?: string | number) => {
+ // const activeItem = String(itemId);
+ // const isSelected = value.includes(activeItem);
+
+ // onChange?.(
+ // event,
+ // isSelected ? value.filter(item => item !== activeItem) : [ activeItem, ...value ]
+ // );
+ // };
+
+ // const handleClickOutside = (event: MouseEvent) =>
+ // isOpen &&
+ // menuRef.current && toggleRef.current &&
+ // !menuRef.current.contains(event.target as Node) && !toggleRef.current.contains(event.target as Node)
+ // && setIsOpen(false);
+
+
+ // React.useEffect(() => {
+ // window.addEventListener('click', handleClickOutside);
+ // return () => {
+ // window.removeEventListener('click', handleClickOutside);
+ // };
+ // }, [ isOpen ]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const isChecked = (dataItem: TreeViewDataItem) => checkedItems.some((item) => item.id === dataItem.id);
+ const areAllDescendantsChecked = (dataItem: TreeViewDataItem) =>
+ dataItem.children ? dataItem.children.every((child) => areAllDescendantsChecked(child)) : isChecked(dataItem);
+ const areSomeDescendantsChecked = (dataItem: TreeViewDataItem) =>
+ dataItem.children ? dataItem.children.some((child) => areSomeDescendantsChecked(child)) : isChecked(dataItem);
+ const flattenTree = (tree: TreeViewDataItem[]) => {
+ let result: TreeViewDataItem[] = [];
+ tree.forEach((item) => {
+ result.push(item);
+ if (item.children) {
+ result = result.concat(flattenTree(item.children));
+ }
+ });
+ return result;
+ };
+
+ const mapTree = (item: TreeViewDataItem) => {
+ const hasCheck = areAllDescendantsChecked(item);
+ item.checkProps = item.checkProps || {};
+ // Reset checked properties to be updated
+ item.checkProps.checked = false;
+
+ if (hasCheck) {
+ item.checkProps.checked = true;
+ } else {
+ const hasPartialCheck = areSomeDescendantsChecked(item);
+ if (hasPartialCheck) {
+ item.checkProps.checked = null;
+ }
+ }
+
+ if (item.children) {
+ return {
+ ...item,
+ children: item.children.map(mapTree)
+ };
+ }
+ return item;
+ };
+
+ const filterItems = (item: TreeViewDataItem, checkedItem: TreeViewDataItem) => {
+ if (item.id === checkedItem.id) {
+ return true;
+ }
+
+ if (item.children) {
+ return (
+ (item.children = item.children
+ .map((opt) => Object.assign({}, opt))
+ .filter((child) => filterItems(child, checkedItem))).length > 0
+ );
+ }
+ };
+
+ const onCheck = (evt: React.ChangeEvent, treeViewItem: TreeViewDataItem, treeType: string) => {
+ const checked = (evt.target as HTMLInputElement).checked;
+
+
+ const checkedItemTree = options
+ .map((opt) => Object.assign({}, opt))
+ .filter((item) => filterItems(item, treeViewItem));
+ const flatCheckedItems = flattenTree(checkedItemTree);
+ setCheckedItems((prevCheckedItems) =>
+ checked
+ ? prevCheckedItems.concat(flatCheckedItems.filter((item) => !prevCheckedItems.some((i) => i.id === item.id)))
+ : prevCheckedItems.filter((item) => !flatCheckedItems.some((i) => i.id === item.id))
+ );
+ };
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const toggle = (
+
+ {isOpen ? 'Expanded' : 'Collapsed'}
+
+ );
+ const optionsMapped = options.map(mapTree);
+
+ const menu = (
+
+
+
+
+
+ {title}
+
+
+
+ onCheck(event, item, 'status')}
+ />
+
+
+
+
+ );
+
+
+ return (
+ {
+ const activeOption = options.find(option => option.id === item);
+ return ({ key: activeOption?.id as string, node: activeOption?.id })
+ })}
+ deleteLabel={(_, label) =>
+ onChange?.(undefined, value.filter(item => item !== (isToolbarLabel(label) ? label.key : label)))
+ }
+ categoryName={title}
+ showToolbarItem={showToolbarItem}
+ >
+ {/* : undefined}
+ badge={value.length > 0 && showBadge ? {value.length} : undefined}
+ style={{ width: '200px' }}
+ >
+ {placeholder ?? title}
+
+ }
+ triggerRef={toggleRef}
+ popper={
+
+ }
+ popperRef={menuRef}
+ appendTo={containerRef.current || undefined}
+ aria-label={`${title ?? filterId} filter`}
+ isVisible={isOpen}
+ /> */}
+ setIsOpen(isOpen)}
+ onOpenChangeKeys={[ 'Escape' ]}
+ menu={menu}
+ menuRef={menuRef}
+ toggle={toggle}
+ toggleRef={toggleRef}
+ />
+
+ );
+};
+
+export default DataViewTreeFilter;
diff --git a/packages/module/src/DataViewTreeFilter/index.ts b/packages/module/src/DataViewTreeFilter/index.ts
new file mode 100644
index 00000000..d292c7a6
--- /dev/null
+++ b/packages/module/src/DataViewTreeFilter/index.ts
@@ -0,0 +1,2 @@
+export { default } from './DataViewTreeFilter';
+export * from './DataViewTreeFilter';
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts
index aca7f761..d09fbc5c 100644
--- a/packages/module/src/index.ts
+++ b/packages/module/src/index.ts
@@ -4,6 +4,9 @@ export { default as InternalContext } from './InternalContext';
export * from './InternalContext';
export * from './Hooks';
+export { default as DataViewTreeFilter } from './DataViewTreeFilter';
+export * from './DataViewTreeFilter';
+
export { default as DataViewToolbar } from './DataViewToolbar';
export * from './DataViewToolbar';