@patternfly/react-data-view 6.4.0 → 6.5.0-prerelease.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.d.ts +2 -0
  2. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.js +3 -2
  3. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.test.d.ts +1 -1
  4. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.test.js +8 -2
  5. package/dist/cjs/DataViewTable/DataViewTable.d.ts +2 -1
  6. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.d.ts +11 -0
  7. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.js +46 -6
  8. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.test.js +47 -9
  9. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.d.ts +4 -0
  10. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.js +31 -2
  11. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.test.d.ts +1 -1
  12. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.test.js +88 -0
  13. package/dist/cjs/DataViewTh/DataViewTh.d.ts +4 -4
  14. package/dist/cjs/DataViewTh/DataViewTh.js +8 -1
  15. package/dist/cjs/DataViewTh/index.d.ts +2 -0
  16. package/dist/cjs/DataViewTh/index.js +23 -0
  17. package/dist/cjs/DataViewToolbar/DataViewToolbar.js +13 -1
  18. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.d.ts +28 -0
  19. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.js +230 -0
  20. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.test.d.ts +1 -0
  21. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.test.js +176 -0
  22. package/dist/cjs/DataViewTreeFilter/index.d.ts +2 -0
  23. package/dist/cjs/DataViewTreeFilter/index.js +23 -0
  24. package/dist/cjs/Hooks/selection.d.ts +8 -8
  25. package/dist/cjs/index.d.ts +6 -0
  26. package/dist/cjs/index.js +10 -1
  27. package/dist/dynamic/DataViewTh/package.json +1 -0
  28. package/dist/dynamic/DataViewTreeFilter/package.json +1 -0
  29. package/dist/dynamic-modules.json +62 -0
  30. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.d.ts +2 -0
  31. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.js +3 -2
  32. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.d.ts +1 -1
  33. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.js +9 -3
  34. package/dist/esm/DataViewTable/DataViewTable.d.ts +2 -1
  35. package/dist/esm/DataViewTableBasic/DataViewTableBasic.d.ts +11 -0
  36. package/dist/esm/DataViewTableBasic/DataViewTableBasic.js +48 -8
  37. package/dist/esm/DataViewTableBasic/DataViewTableBasic.test.js +45 -10
  38. package/dist/esm/DataViewTextFilter/DataViewTextFilter.d.ts +4 -0
  39. package/dist/esm/DataViewTextFilter/DataViewTextFilter.js +31 -2
  40. package/dist/esm/DataViewTextFilter/DataViewTextFilter.test.d.ts +1 -1
  41. package/dist/esm/DataViewTextFilter/DataViewTextFilter.test.js +90 -2
  42. package/dist/esm/DataViewTh/DataViewTh.d.ts +4 -4
  43. package/dist/esm/DataViewTh/DataViewTh.js +8 -1
  44. package/dist/esm/DataViewTh/index.d.ts +2 -0
  45. package/dist/esm/DataViewTh/index.js +2 -0
  46. package/dist/esm/DataViewToolbar/DataViewToolbar.js +13 -1
  47. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.d.ts +28 -0
  48. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.js +226 -0
  49. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.test.d.ts +1 -0
  50. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.test.js +171 -0
  51. package/dist/esm/DataViewTreeFilter/index.d.ts +2 -0
  52. package/dist/esm/DataViewTreeFilter/index.js +2 -0
  53. package/dist/esm/Hooks/selection.d.ts +8 -8
  54. package/dist/esm/index.d.ts +6 -0
  55. package/dist/esm/index.js +6 -0
  56. package/dist/tsconfig.tsbuildinfo +1 -1
  57. package/generate-fed-package-json.js +18 -0
  58. package/generate-index.js +2 -2
  59. package/package.json +6 -6
  60. package/patternfly-docs/content/extensions/data-view/examples/DataView/DataView.md +10 -4
  61. package/patternfly-docs/content/extensions/data-view/examples/DataView/PredefinedLayoutFullExample.tsx +2 -1
  62. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExpandableExample.tsx +108 -0
  63. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableInteractiveExample.tsx +148 -0
  64. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickyExample.tsx +90 -0
  65. package/patternfly-docs/content/extensions/data-view/examples/Table/Table.md +63 -2
  66. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/FiltersExample.tsx +3 -2
  67. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/PaginationExample.tsx +1 -1
  68. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md +9 -2
  69. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/TreeFilterExample.tsx +248 -0
  70. package/patternfly-docs/patternfly-docs.config.js +4 -1
  71. package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.test.tsx +16 -7
  72. package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.tsx +5 -1
  73. package/src/DataViewTable/DataViewTable.tsx +3 -1
  74. package/src/DataViewTable/__snapshots__/DataViewTable.test.tsx.snap +7 -7
  75. package/src/DataViewTableBasic/DataViewTableBasic.test.tsx +54 -12
  76. package/src/DataViewTableBasic/DataViewTableBasic.tsx +101 -10
  77. package/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap +10 -10
  78. package/src/DataViewTextFilter/DataViewTextFilter.test.tsx +140 -1
  79. package/src/DataViewTextFilter/DataViewTextFilter.tsx +63 -22
  80. package/src/DataViewTh/DataViewTh.tsx +15 -7
  81. package/src/DataViewTh/index.ts +2 -0
  82. package/src/DataViewToolbar/DataViewToolbar.tsx +17 -2
  83. package/src/DataViewToolbar/__snapshots__/DataViewToolbar.test.tsx.snap +288 -280
  84. package/src/DataViewTreeFilter/DataViewTreeFilter.test.tsx +233 -0
  85. package/src/DataViewTreeFilter/DataViewTreeFilter.tsx +365 -0
  86. package/src/DataViewTreeFilter/__snapshots__/DataViewTreeFilter.test.tsx.snap +199 -0
  87. package/src/DataViewTreeFilter/index.ts +2 -0
  88. package/src/Hooks/selection.ts +8 -8
  89. package/src/index.ts +9 -0
@@ -0,0 +1,233 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
3
+ import userEvent from '@testing-library/user-event';
4
+ import DataViewTreeFilter, { DataViewTreeFilterProps } from './DataViewTreeFilter';
5
+ import DataViewToolbar from '../DataViewToolbar';
6
+ import { TreeViewDataItem } from '@patternfly/react-core';
7
+
8
+ describe('DataViewTreeFilter component', () => {
9
+ const treeItems: TreeViewDataItem[] = [
10
+ {
11
+ name: 'Linux',
12
+ id: 'os-linux',
13
+ checkProps: { 'aria-label': 'linux-check', checked: false },
14
+ children: [
15
+ {
16
+ name: 'Ubuntu 22.04',
17
+ id: 'os-ubuntu',
18
+ checkProps: { checked: false }
19
+ },
20
+ {
21
+ name: 'RHEL 9',
22
+ id: 'os-rhel',
23
+ checkProps: { checked: false }
24
+ },
25
+ {
26
+ name: 'Debian 12',
27
+ id: 'os-debian',
28
+ checkProps: { checked: false }
29
+ },
30
+ {
31
+ name: 'CentOS 8',
32
+ id: 'os-centos',
33
+ checkProps: { checked: false }
34
+ },
35
+ {
36
+ name: 'Fedora 38',
37
+ id: 'os-fedora',
38
+ checkProps: { checked: false }
39
+ }
40
+ ],
41
+ defaultExpanded: true
42
+ },
43
+ {
44
+ name: 'Windows',
45
+ id: 'os-windows',
46
+ checkProps: { 'aria-label': 'windows-check', checked: false },
47
+ children: [
48
+ {
49
+ name: 'Windows Server 2022',
50
+ id: 'os-windows-2022',
51
+ checkProps: { checked: false }
52
+ }
53
+ ]
54
+ },
55
+ {
56
+ name: 'macOS',
57
+ id: 'os-macos',
58
+ checkProps: { 'aria-label': 'macos-check', checked: false },
59
+ children: [
60
+ {
61
+ name: 'macOS Ventura',
62
+ id: 'os-macos-ventura',
63
+ checkProps: { checked: false }
64
+ },
65
+ {
66
+ name: 'macOS Sonoma',
67
+ id: 'os-macos-sonoma',
68
+ checkProps: { checked: false }
69
+ }
70
+ ]
71
+ }
72
+ ];
73
+
74
+ const defaultProps: DataViewTreeFilterProps = {
75
+ filterId: 'test-tree-filter',
76
+ title: 'Test Tree Filter',
77
+ value: ['Linux'],
78
+ items: treeItems
79
+ };
80
+ beforeEach(() => {
81
+ jest.clearAllMocks();
82
+ });
83
+
84
+ it('should render correctly', () => {
85
+ const { container } = render(
86
+ <DataViewToolbar filters={<DataViewTreeFilter {...defaultProps} />} />
87
+ );
88
+ expect(container).toMatchSnapshot();
89
+ });
90
+
91
+ it('should use chipTitle for the filter chip category when provided', () => {
92
+ render(
93
+ <DataViewToolbar
94
+ filters={<DataViewTreeFilter {...defaultProps} chipTitle="Short name" />}
95
+ />
96
+ );
97
+ expect(screen.getByText('Short name')).toBeInTheDocument();
98
+ expect(screen.getByText('Test Tree Filter')).toBeInTheDocument();
99
+ });
100
+
101
+ describe('defaultExpanded', () => {
102
+ it('should have expanded items by default', async () => {
103
+ render(
104
+ <DataViewToolbar
105
+ filters={
106
+ <DataViewTreeFilter
107
+ filterId="os"
108
+ title="Operating System"
109
+ items={treeItems}
110
+ defaultExpanded={true}
111
+ />
112
+ }
113
+ />
114
+ );
115
+
116
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
117
+ await userEvent.click(openMenu);
118
+ await waitFor(() => {
119
+ const node = screen.getByText('Ubuntu 22.04');
120
+ expect(node).toHaveClass('pf-v6-c-tree-view__node-text');
121
+ expect(node).toBeInTheDocument();
122
+ });
123
+ });
124
+ });
125
+ describe('onChange callback', () => {
126
+ it('onChange should be called on toggle of node', async () => {
127
+ const mockOnChange = jest.fn();
128
+ render(
129
+ <DataViewToolbar
130
+ filters={
131
+ <DataViewTreeFilter
132
+ filterId="os"
133
+ title="Operating System"
134
+ items={treeItems}
135
+ defaultExpanded={true}
136
+ onChange={mockOnChange}
137
+ />
138
+ }
139
+ />
140
+ );
141
+
142
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
143
+ await userEvent.click(openMenu);
144
+
145
+ await waitFor(() => {
146
+ const node = screen.getByText('Ubuntu 22.04');
147
+ expect(node).toBeInTheDocument();
148
+ });
149
+
150
+ const node = screen.getByText('Ubuntu 22.04');
151
+ await userEvent.click(node);
152
+
153
+ await waitFor(() => {
154
+ expect(mockOnChange).toHaveBeenCalled();
155
+ });
156
+ });
157
+ });
158
+ describe('onSelect callback', () => {
159
+ it('onSelect should return list of selected items when item is selected', async () => {
160
+ const mockOnSelect = jest.fn();
161
+ render(
162
+ <DataViewToolbar
163
+ filters={
164
+ <DataViewTreeFilter
165
+ filterId="os"
166
+ title="Operating System"
167
+ items={treeItems}
168
+ defaultExpanded={true}
169
+ onSelect={mockOnSelect}
170
+ />
171
+ }
172
+ />
173
+ );
174
+
175
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
176
+ await userEvent.click(openMenu);
177
+
178
+ await waitFor(() => {
179
+ const node = screen.getByText('Ubuntu 22.04');
180
+ expect(node).toBeInTheDocument();
181
+ });
182
+
183
+ const node = screen.getByText('Ubuntu 22.04');
184
+ await userEvent.click(node);
185
+
186
+ await waitFor(() => {
187
+ expect(mockOnSelect).toHaveBeenCalled();
188
+ expect(mockOnSelect).toHaveBeenCalledWith(
189
+ expect.arrayContaining([
190
+ expect.objectContaining({
191
+ name: 'Ubuntu 22.04',
192
+ id: 'os-ubuntu'
193
+ })
194
+ ])
195
+ );
196
+ });
197
+ });
198
+ });
199
+
200
+ describe('rendering all items', () => {
201
+ it('all tree items should be rendered', async () => {
202
+ render(
203
+ <DataViewToolbar
204
+ filters={
205
+ <DataViewTreeFilter
206
+ filterId="os"
207
+ title="Operating System"
208
+ items={treeItems}
209
+ defaultExpanded={true}
210
+ />
211
+ }
212
+ />
213
+ );
214
+
215
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
216
+ await userEvent.click(openMenu);
217
+
218
+ await waitFor(() => {
219
+ expect(screen.getByText('Linux')).toBeInTheDocument();
220
+ expect(screen.getByText('Windows')).toBeInTheDocument();
221
+ expect(screen.getByText('macOS')).toBeInTheDocument();
222
+ expect(screen.getByText('Ubuntu 22.04')).toBeInTheDocument();
223
+ expect(screen.getByText('RHEL 9')).toBeInTheDocument();
224
+ expect(screen.getByText('Debian 12')).toBeInTheDocument();
225
+ expect(screen.getByText('CentOS 8')).toBeInTheDocument();
226
+ expect(screen.getByText('Fedora 38')).toBeInTheDocument();
227
+ expect(screen.getByText('Windows Server 2022')).toBeInTheDocument();
228
+ expect(screen.getByText('macOS Ventura')).toBeInTheDocument();
229
+ expect(screen.getByText('macOS Sonoma')).toBeInTheDocument();
230
+ });
231
+ });
232
+ });
233
+ });
@@ -0,0 +1,365 @@
1
+ import { Dropdown, MenuToggle, MenuToggleElement, ToolbarFilter, ToolbarFilterProps, TreeView, TreeViewDataItem } from '@patternfly/react-core'
2
+ import React, { FC, useState, useRef, useEffect } from 'react'
3
+ import { createUseStyles } from 'react-jss'
4
+
5
+ /** This style is needed so the tree filter dropdown looks like the basic filter dropdow */
6
+ const useStyles = createUseStyles({
7
+ dataViewTreeFilterTreeView: {
8
+ '& .pf-v6-c-tree-view__node::after': {
9
+ borderRadius: 0,
10
+ borderRightStyle: 'none',
11
+ borderLeftStyle: 'none'
12
+ },
13
+ '& .pf-v6-c-tree-view__content': {
14
+ borderRadius: 0
15
+ }
16
+ }
17
+ })
18
+
19
+ // Generic helper to collect items from tree based on predicate
20
+ const collectTreeItems = (
21
+ items: TreeViewDataItem[],
22
+ predicate: (item: TreeViewDataItem) => boolean,
23
+ leafOnly = false
24
+ ): TreeViewDataItem[] => {
25
+ const collected: TreeViewDataItem[] = [];
26
+
27
+ const collect = (item: TreeViewDataItem) => {
28
+ const isLeaf = !item.children || item.children.length === 0;
29
+
30
+ if (predicate(item) && (!leafOnly || isLeaf)) {
31
+ collected.push(item);
32
+ }
33
+
34
+ item.children?.forEach(child => collect(child));
35
+ };
36
+
37
+ items.forEach(item => collect(item));
38
+ return collected;
39
+ };
40
+
41
+ // Helper function to get all checked items (not just leaf nodes)
42
+ const getAllCheckedItems = (items: TreeViewDataItem[]): TreeViewDataItem[] =>
43
+ collectTreeItems(items, item => item.checkProps?.checked === true, false);
44
+
45
+ // Get all checked leaf items (returns array of names)
46
+ const getAllCheckedLeafItems = (items: TreeViewDataItem[]): string[] =>
47
+ collectTreeItems(
48
+ items,
49
+ item => item.checkProps?.checked === true,
50
+ true
51
+ ).map(item => String(item.name));
52
+
53
+ // Helper function to expand all nodes in the tree
54
+ const expandAllNodes = (items: TreeViewDataItem[]): TreeViewDataItem[] =>
55
+ items.map(item => ({
56
+ ...item,
57
+ defaultExpanded: true,
58
+ children: item.children ? expandAllNodes(item.children) : undefined
59
+ }));
60
+
61
+ // Helper function to set pre-selected items
62
+ const setPreSelectedItems = (items: TreeViewDataItem[], selectedIds: string[]): TreeViewDataItem[] =>
63
+ items.map(item => {
64
+ const isSelected = selectedIds.includes(String(item.id));
65
+ const hasSelectedChildren = item.children?.some(child => selectedIds.includes(String(child.id))) ?? false;
66
+
67
+ return {
68
+ ...item,
69
+ checkProps: item.checkProps ? {
70
+ ...item.checkProps,
71
+ checked: isSelected || hasSelectedChildren
72
+ } : undefined,
73
+ children: item.children ? setPreSelectedItems(item.children, selectedIds) : undefined
74
+ };
75
+ });
76
+
77
+ // Helper function to uncheck all items recursively
78
+ const uncheckRecursive = (items: TreeViewDataItem[]): TreeViewDataItem[] =>
79
+ items.map(item => ({
80
+ ...item,
81
+ checkProps: item.checkProps ? { ...item.checkProps, checked: false } : undefined,
82
+ children: item.children ? uncheckRecursive(item.children) : undefined
83
+ }));
84
+
85
+ export interface DataViewTreeFilterProps {
86
+ /** Unique key for the filter attribute */
87
+ filterId: string;
88
+ /** Array of current filter values */
89
+ value?: string[];
90
+ /** Filter title displayed in the toolbar */
91
+ title: string;
92
+ /** Label for the applied filter chip / category; defaults to title */
93
+ chipTitle?: string;
94
+ /** Callback for when the selection changes */
95
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
96
+ /** Controls visibility of the filter in the toolbar */
97
+ showToolbarItem?: ToolbarFilterProps['showToolbarItem'];
98
+ /** Custom OUIA ID */
99
+ ouiaId?: string;
100
+ /** Hierarchical data items for the tree structure */
101
+ items?: TreeViewDataItem[];
102
+ /** When true, expands all tree nodes by default */
103
+ defaultExpanded?: boolean;
104
+ /** Callback for when tree items are selected/deselected, provides all currently selected nodes */
105
+ onSelect?: (selectedItems: TreeViewDataItem[]) => void;
106
+ /** Array of pre-selected item id's to be checked on initial render */
107
+ defaultSelected?: string[];
108
+ }
109
+
110
+ export const DataViewTreeFilter: FC<DataViewTreeFilterProps> = ({
111
+ filterId,
112
+ title,
113
+ chipTitle,
114
+ value = [],
115
+ onChange,
116
+ showToolbarItem,
117
+ ouiaId = 'DataViewTreeFilter',
118
+ items,
119
+ defaultExpanded = false,
120
+ onSelect,
121
+ defaultSelected = []
122
+ }: DataViewTreeFilterProps) => {
123
+ const categoryName = chipTitle ?? title;
124
+ const classes = useStyles();
125
+ const [isOpen, setIsOpen] = useState(false);
126
+ const [treeData, setTreeData] = useState<TreeViewDataItem[]>(items || []);
127
+ const menuRef = useRef<HTMLDivElement>(null);
128
+ const isInitialMount = useRef(true);
129
+ const hasCalledInitialOnChange = useRef(false);
130
+
131
+ // Initialize tree data with defaultExpanded and defaultSelected (only on first mount)
132
+ useEffect(() => {
133
+ if (!items) {
134
+ return;
135
+ }
136
+
137
+ let initializedData = [...items];
138
+
139
+ // Apply default expansion
140
+ if (defaultExpanded) {
141
+ initializedData = expandAllNodes(initializedData);
142
+ }
143
+
144
+ // Apply pre-selected items only on initial mount
145
+ if (isInitialMount.current && defaultSelected.length > 0) {
146
+ initializedData = setPreSelectedItems(initializedData, defaultSelected);
147
+ }
148
+
149
+ setTreeData(initializedData);
150
+
151
+ if (isInitialMount.current) {
152
+ isInitialMount.current = false;
153
+ }
154
+ }, [items, defaultExpanded]);
155
+
156
+ // Call onChange and onSelect after tree data is initialized with default selections
157
+ useEffect(() => {
158
+ if (!hasCalledInitialOnChange.current && defaultSelected.length > 0 && treeData.length > 0) {
159
+ const selectedValues = getAllCheckedLeafItems(treeData);
160
+
161
+ // Only call if there are actually selected values
162
+ if (selectedValues.length > 0) {
163
+ // Calculate both values synchronously before calling callbacks
164
+ const selectedItems = getAllCheckedItems(treeData);
165
+
166
+ // useEffect already runs after render, so this is safe
167
+ if (onChange) {
168
+ onChange(undefined, selectedValues);
169
+ }
170
+
171
+ if (onSelect) {
172
+ onSelect(selectedItems);
173
+ }
174
+
175
+ hasCalledInitialOnChange.current = true;
176
+ }
177
+ }
178
+ }, [treeData, onChange, onSelect, defaultSelected.length]);
179
+
180
+
181
+ // Sync tree checkboxes when value prop changes (when clearAllFilters is called)
182
+ useEffect(() => {
183
+ if (value.length === 0) {
184
+ setTreeData(currentTreeData => {
185
+ if (currentTreeData.length === 0) {
186
+ return currentTreeData;
187
+ }
188
+
189
+ const currentCheckedItems = getAllCheckedLeafItems(currentTreeData);
190
+
191
+ // Only update if there are checked items that need to be unchecked
192
+ if (currentCheckedItems.length > 0) {
193
+ return uncheckRecursive(currentTreeData);
194
+ }
195
+
196
+ return currentTreeData;
197
+ });
198
+ }
199
+ }, [value]);
200
+
201
+ // Check if all children are checked (recursive)
202
+ const areAllChildrenChecked = (item: TreeViewDataItem): boolean => {
203
+ if (!item.children?.length) {
204
+ return item.checkProps?.checked === true;
205
+ }
206
+ return item.children.every(child => areAllChildrenChecked(child));
207
+ };
208
+
209
+ // Check if some children are checked (recursive)
210
+ const areSomeChildrenChecked = (item: TreeViewDataItem): boolean => {
211
+ if (!item.children?.length) {
212
+ return item.checkProps?.checked === true;
213
+ }
214
+ return item.children.some(child => areSomeChildrenChecked(child));
215
+ };
216
+
217
+ // Find tree item by name
218
+ const findItemByName = (items: TreeViewDataItem[], name: string): TreeViewDataItem | null => {
219
+ for (const item of items) {
220
+ if (item.name === name) {
221
+ return item;
222
+ }
223
+ if (item.children) {
224
+ const found = findItemByName(item.children, name);
225
+ if (found) {
226
+ return found;
227
+ }
228
+ }
229
+ }
230
+ return null;
231
+ };
232
+
233
+ // Find parent item by child ID
234
+ const findParentById = (items: TreeViewDataItem[], childId: string): TreeViewDataItem | null => {
235
+ for (const item of items) {
236
+ if (item.children?.some(child => child.id === childId)) {
237
+ return item;
238
+ }
239
+ if (item.children) {
240
+ const found = findParentById(item.children, childId);
241
+ if (found) {
242
+ return found;
243
+ }
244
+ }
245
+ }
246
+ return null;
247
+ };
248
+
249
+ // Update parent checkbox states based on children (recursive)
250
+ const onCheckParentHandle = (childId: string): void => {
251
+ const parent = findParentById(treeData, childId);
252
+ if (!parent) {
253
+ return;
254
+ }
255
+
256
+ if (parent.checkProps) {
257
+ const allChildrenChecked = areAllChildrenChecked(parent);
258
+ const someChildrenChecked = areSomeChildrenChecked(parent);
259
+
260
+ if (allChildrenChecked) {
261
+ parent.checkProps.checked = true;
262
+ } else if (someChildrenChecked) {
263
+ parent.checkProps.checked = null;
264
+ } else {
265
+ parent.checkProps.checked = false;
266
+ }
267
+ }
268
+
269
+ if (parent.id) {
270
+ onCheckParentHandle(parent.id);
271
+ }
272
+ };
273
+
274
+ // Check/uncheck item and all its children (recursive)
275
+ const onCheckHandle = (treeViewItem: TreeViewDataItem, checked: boolean): void => {
276
+ if (treeViewItem.checkProps) {
277
+ treeViewItem.checkProps.checked = checked;
278
+ }
279
+
280
+ treeViewItem.children?.forEach(child => onCheckHandle(child, checked));
281
+ };
282
+
283
+ // Handle checkbox change event
284
+ const onCheck = (event: React.ChangeEvent, treeViewItem: TreeViewDataItem) => {
285
+ const checked = (event.target as HTMLInputElement).checked;
286
+
287
+ onCheckHandle(treeViewItem, checked);
288
+
289
+ if (treeViewItem.id) {
290
+ onCheckParentHandle(treeViewItem.id);
291
+ }
292
+
293
+ setTreeData(prev => [...prev]);
294
+
295
+ const selectedValues = getAllCheckedLeafItems(treeData);
296
+ onChange?.(event as any, selectedValues);
297
+
298
+ if (onSelect) {
299
+ const selectedItems = getAllCheckedItems(treeData);
300
+ onSelect(selectedItems);
301
+ }
302
+ };
303
+
304
+ // Clear a specific filter by name (when label chip is removed)
305
+ const onFilterSelectorClear = (itemName: string) => {
306
+ const treeViewItem = findItemByName(treeData, itemName);
307
+ if (!treeViewItem) {
308
+ return;
309
+ }
310
+
311
+ onCheckHandle(treeViewItem, false);
312
+ if (treeViewItem.id) {
313
+ onCheckParentHandle(treeViewItem.id);
314
+ }
315
+ };
316
+
317
+ // Uncheck all items in the tree
318
+ const uncheckAllItems = () => {
319
+ const updatedTreeData = uncheckRecursive(treeData);
320
+ setTreeData(updatedTreeData);
321
+ onChange?.(undefined, []);
322
+ };
323
+
324
+ const dropdown = (
325
+ <Dropdown
326
+ ref={menuRef}
327
+ isOpen={isOpen}
328
+ onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
329
+ toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
330
+ <MenuToggle ref={toggleRef} onClick={() => setIsOpen(!isOpen)} isExpanded={isOpen}>
331
+ {title}
332
+ </MenuToggle>
333
+ )}
334
+ ouiaId={ouiaId}
335
+ shouldFocusToggleOnSelect
336
+ >
337
+ <TreeView
338
+ hasAnimations
339
+ data={treeData}
340
+ onCheck={onCheck}
341
+ hasCheckboxes
342
+ className={classes.dataViewTreeFilterTreeView}
343
+ />
344
+ </Dropdown>
345
+ );
346
+
347
+ return (
348
+ <ToolbarFilter
349
+ key={filterId}
350
+ data-ouia-component-id={ouiaId}
351
+ labels={value.map(item => ({ key: item, node: item }))}
352
+ deleteLabel={(_, label) => {
353
+ const labelKey = typeof label === 'string' ? label : label.key;
354
+ onChange?.(undefined, value.filter(item => item !== labelKey));
355
+ onFilterSelectorClear(labelKey);
356
+ }}
357
+ deleteLabelGroup={uncheckAllItems}
358
+ categoryName={categoryName}
359
+ showToolbarItem={showToolbarItem}>
360
+ {dropdown}
361
+ </ToolbarFilter>
362
+ )
363
+ }
364
+
365
+ export default DataViewTreeFilter;