@patternfly/react-data-view 6.3.0-prerelease.3 → 6.4.0-prerelease.10

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 (95) hide show
  1. package/dist/cjs/DataViewTable/DataViewTable.d.ts +6 -1
  2. package/dist/cjs/DataViewTable/DataViewTable.js +21 -1
  3. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.d.ts +13 -0
  4. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.js +46 -6
  5. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.test.js +47 -9
  6. package/dist/cjs/DataViewTableHead/DataViewTableHead.d.ts +2 -0
  7. package/dist/cjs/DataViewTableHead/DataViewTableHead.js +5 -4
  8. package/dist/cjs/DataViewTableTree/DataViewTableTree.d.ts +2 -0
  9. package/dist/cjs/DataViewTableTree/DataViewTableTree.js +28 -1
  10. package/dist/cjs/DataViewTableTree/DataViewTableTree.test.js +4 -0
  11. package/dist/cjs/DataViewTh/DataViewTh.d.ts +32 -0
  12. package/dist/cjs/DataViewTh/DataViewTh.js +222 -0
  13. package/dist/cjs/DataViewTh/index.d.ts +2 -0
  14. package/dist/cjs/DataViewTh/index.js +23 -0
  15. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.d.ts +26 -0
  16. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.js +229 -0
  17. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.test.d.ts +1 -0
  18. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.test.js +171 -0
  19. package/dist/cjs/DataViewTreeFilter/index.d.ts +2 -0
  20. package/dist/cjs/DataViewTreeFilter/index.js +23 -0
  21. package/dist/cjs/Hooks/selection.d.ts +1 -0
  22. package/dist/cjs/Hooks/selection.js +5 -1
  23. package/dist/cjs/Hooks/selection.test.js +48 -0
  24. package/dist/cjs/InternalContext/InternalContext.d.ts +2 -0
  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/DataViewTable/DataViewTable.d.ts +6 -1
  31. package/dist/esm/DataViewTable/DataViewTable.js +21 -1
  32. package/dist/esm/DataViewTableBasic/DataViewTableBasic.d.ts +13 -0
  33. package/dist/esm/DataViewTableBasic/DataViewTableBasic.js +48 -8
  34. package/dist/esm/DataViewTableBasic/DataViewTableBasic.test.js +45 -10
  35. package/dist/esm/DataViewTableHead/DataViewTableHead.d.ts +2 -0
  36. package/dist/esm/DataViewTableHead/DataViewTableHead.js +5 -4
  37. package/dist/esm/DataViewTableTree/DataViewTableTree.d.ts +2 -0
  38. package/dist/esm/DataViewTableTree/DataViewTableTree.js +29 -2
  39. package/dist/esm/DataViewTableTree/DataViewTableTree.test.js +4 -0
  40. package/dist/esm/DataViewTh/DataViewTh.d.ts +32 -0
  41. package/dist/esm/DataViewTh/DataViewTh.js +215 -0
  42. package/dist/esm/DataViewTh/index.d.ts +2 -0
  43. package/dist/esm/DataViewTh/index.js +2 -0
  44. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.d.ts +26 -0
  45. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.js +225 -0
  46. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.test.d.ts +1 -0
  47. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.test.js +166 -0
  48. package/dist/esm/DataViewTreeFilter/index.d.ts +2 -0
  49. package/dist/esm/DataViewTreeFilter/index.js +2 -0
  50. package/dist/esm/Hooks/selection.d.ts +1 -0
  51. package/dist/esm/Hooks/selection.js +5 -1
  52. package/dist/esm/Hooks/selection.test.js +48 -0
  53. package/dist/esm/InternalContext/InternalContext.d.ts +2 -0
  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 +12 -12
  60. package/patternfly-docs/content/extensions/data-view/examples/DataView/DataView.md +10 -4
  61. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExpandableExample.tsx +108 -0
  62. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableInteractiveExample.tsx +148 -0
  63. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableResizableColumnsExample.tsx +155 -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/DataViewTableTreeExample.tsx +1 -0
  66. package/patternfly-docs/content/extensions/data-view/examples/Table/Table.md +113 -14
  67. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/SelectionExample.tsx +14 -3
  68. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md +10 -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/release.config.js +1 -1
  72. package/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap +0 -2
  73. package/src/DataViewFilters/__snapshots__/DataViewFilters.test.tsx.snap +0 -2
  74. package/src/DataViewTable/DataViewTable.tsx +51 -28
  75. package/src/DataViewTable/__snapshots__/DataViewTable.test.tsx.snap +17 -25
  76. package/src/DataViewTableBasic/DataViewTableBasic.test.tsx +54 -12
  77. package/src/DataViewTableBasic/DataViewTableBasic.tsx +104 -10
  78. package/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap +30 -30
  79. package/src/DataViewTableHead/DataViewTableHead.tsx +24 -23
  80. package/src/DataViewTableHead/__snapshots__/DataViewTableHead.test.tsx.snap +15 -15
  81. package/src/DataViewTableTree/DataViewTableTree.test.tsx +9 -0
  82. package/src/DataViewTableTree/DataViewTableTree.tsx +35 -1
  83. package/src/DataViewTableTree/__snapshots__/DataViewTableTree.test.tsx.snap +977 -28
  84. package/src/DataViewTextFilter/__snapshots__/DataViewTextFilter.test.tsx.snap +0 -3
  85. package/src/DataViewTh/DataViewTh.tsx +342 -0
  86. package/src/DataViewTh/index.ts +2 -0
  87. package/src/DataViewToolbar/__snapshots__/DataViewToolbar.test.tsx.snap +0 -10
  88. package/src/DataViewTreeFilter/DataViewTreeFilter.test.tsx +222 -0
  89. package/src/DataViewTreeFilter/DataViewTreeFilter.tsx +361 -0
  90. package/src/DataViewTreeFilter/__snapshots__/DataViewTreeFilter.test.tsx.snap +199 -0
  91. package/src/DataViewTreeFilter/index.ts +2 -0
  92. package/src/Hooks/selection.test.tsx +65 -1
  93. package/src/Hooks/selection.ts +6 -1
  94. package/src/InternalContext/InternalContext.tsx +2 -0
  95. package/src/index.ts +9 -0
@@ -0,0 +1,215 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { useState, useCallback, useRef, useEffect, Fragment } from 'react';
14
+ import { Th } from '@patternfly/react-table';
15
+ import { Button, getLanguageDirection } from '@patternfly/react-core';
16
+ import { createUseStyles } from 'react-jss';
17
+ import tableCellPaddingBlockEnd from '@patternfly/react-tokens/dist/esm/c_table_cell_PaddingBlockEnd';
18
+ import tableCellPaddingInlineEnd from '@patternfly/react-tokens/dist/esm/c_table_cell_PaddingInlineEnd';
19
+ import globalFontSizeBodyDefault from '@patternfly/react-tokens/dist/esm/t_global_font_size_body_default';
20
+ const ResizeIcon = () => (_jsx("svg", { className: "pf-v6-svg", viewBox: "0 0 1024 1024", fill: "currentColor", "aria-hidden": "true", role: "img", width: "1em", height: "1em", children: _jsx("path", { fillRule: "evenodd", d: "M52.7,529.8l190.5,161.9c18.6,15.9,50.5,4.7,50.5-17.8v-324c0-22.4-31.8-33.6-50.5-17.8L52.7,494.2c-11.5,9.8-11.5,25.8,0,35.6ZM480.8,12.9h62.4v998.3h-62.4V12.9ZM971.3,529.8l-190.5,161.9c-18.6,15.9-50.5,4.7-50.5-17.8v-324c0-22.4,31.8-33.6,50.5-17.8l190.5,161.9c11.5,9.8,11.5,25.8,0,35.6Z" }) }));
21
+ const useStyles = createUseStyles({
22
+ dataViewResizeableTh: {
23
+ [tableCellPaddingInlineEnd.name]: `calc(${globalFontSizeBodyDefault.var} * 2)`
24
+ },
25
+ dataViewResizableButton: {
26
+ position: 'absolute',
27
+ insetInlineEnd: `calc(${globalFontSizeBodyDefault.var} / 2)`,
28
+ insetBlockEnd: tableCellPaddingBlockEnd.var,
29
+ cursor: 'grab',
30
+ '&:active': {
31
+ cursor: 'grabbing'
32
+ }
33
+ }
34
+ });
35
+ export const DataViewTh = (_a) => {
36
+ var { content, resizableProps = {}, hasResizableColumns = false, thProps } = _a, props = __rest(_a, ["content", "resizableProps", "hasResizableColumns", "thProps"]);
37
+ const thRef = useRef(null);
38
+ const [width, setWidth] = useState((resizableProps === null || resizableProps === void 0 ? void 0 : resizableProps.width) ? resizableProps.width : 0);
39
+ // Tracks the current column width for the onResize callback, because the width state is not updated until after the resize is complete
40
+ const trackedWidth = useRef(0);
41
+ const classes = useStyles();
42
+ const isResizable = (resizableProps === null || resizableProps === void 0 ? void 0 : resizableProps.isResizable) || false;
43
+ const increment = (resizableProps === null || resizableProps === void 0 ? void 0 : resizableProps.increment) || 5;
44
+ const shiftIncrement = (resizableProps === null || resizableProps === void 0 ? void 0 : resizableProps.shiftIncrement) || 25;
45
+ const resizeButtonAriaLabel = resizableProps === null || resizableProps === void 0 ? void 0 : resizableProps.resizeButtonAriaLabel;
46
+ const onResize = (resizableProps === null || resizableProps === void 0 ? void 0 : resizableProps.onResize) || undefined;
47
+ const screenReaderText = (resizableProps === null || resizableProps === void 0 ? void 0 : resizableProps.screenReaderText) || `Column ${width.toFixed(0)} pixels`;
48
+ const dataViewThClassName = isResizable ? classes.dataViewResizeableTh : undefined;
49
+ const resizeButtonRef = useRef(null);
50
+ const setInitialVals = useRef(true);
51
+ const dragOffset = useRef(0);
52
+ const isResizing = useRef(false);
53
+ const isInView = useRef(true);
54
+ if (isResizable && !resizeButtonAriaLabel) {
55
+ // eslint-disable-next-line no-console
56
+ console.warn('DataViewTh: Missing resizeButtonAriaLabel. An aria label must be passed to each resizable column to provide a context specific label for its resize button.');
57
+ }
58
+ useEffect(() => {
59
+ if (!isResizable) {
60
+ return;
61
+ }
62
+ const observed = resizeButtonRef.current;
63
+ const observer = new IntersectionObserver(([entry]) => {
64
+ isInView.current = entry.isIntersecting;
65
+ }, { threshold: 0.3 });
66
+ if (observed) {
67
+ observer.observe(observed);
68
+ }
69
+ return () => {
70
+ if (observed) {
71
+ observer.unobserve(observed);
72
+ }
73
+ };
74
+ }, []);
75
+ useEffect(() => {
76
+ var _a;
77
+ if ((isResizable || hasResizableColumns) && setInitialVals.current && width === 0) {
78
+ setWidth(((_a = thRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width) || 0);
79
+ setInitialVals.current = false;
80
+ }
81
+ }, [isResizable, hasResizableColumns, setInitialVals]);
82
+ const setDragOffset = (e) => {
83
+ var _a, _b;
84
+ const isRTL = getLanguageDirection(thRef.current) === 'rtl';
85
+ const startingMousePos = 'clientX' in e ? e.clientX : e.touches[0].clientX;
86
+ const startingColumnPos = (isRTL ? (_a = thRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().left : (_b = thRef.current) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect().right) || 0;
87
+ dragOffset.current = startingColumnPos - startingMousePos;
88
+ };
89
+ const handleMousedown = (e) => {
90
+ e.stopPropagation();
91
+ e.preventDefault();
92
+ document.addEventListener('mousemove', callbackMouseMove);
93
+ document.addEventListener('mouseup', callbackMouseUp);
94
+ // When a user begins resizing a column, set the drag offset so the drag motion aligns with the column edge
95
+ if (dragOffset.current === 0) {
96
+ setDragOffset(e);
97
+ }
98
+ isResizing.current = true;
99
+ };
100
+ const handleMouseMove = (e) => {
101
+ const mousePos = e.clientX;
102
+ handleControlMove(e, dragOffset.current + mousePos);
103
+ };
104
+ const handleTouchStart = (e) => {
105
+ e.stopPropagation();
106
+ document.addEventListener('touchmove', callbackTouchMove, { passive: false });
107
+ document.addEventListener('touchend', callbackTouchEnd);
108
+ // When a user begins resizing a column, set the drag offset so the drag motion aligns with the column edge
109
+ if (dragOffset.current === 0) {
110
+ setDragOffset(e);
111
+ }
112
+ isResizing.current = true;
113
+ };
114
+ const handleTouchMove = (e) => {
115
+ e.preventDefault();
116
+ e.stopImmediatePropagation();
117
+ const touchPos = e.touches[0].clientX;
118
+ handleControlMove(e, touchPos);
119
+ };
120
+ const handleControlMove = (e, controlPosition) => {
121
+ var _a, _b;
122
+ e.stopPropagation();
123
+ if (!isResizing.current) {
124
+ return;
125
+ }
126
+ const columnRect = (_a = thRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
127
+ if (columnRect === undefined) {
128
+ return;
129
+ }
130
+ const mousePos = controlPosition;
131
+ const isRTL = getLanguageDirection(thRef.current) === 'rtl';
132
+ let newSize = isRTL ? (columnRect === null || columnRect === void 0 ? void 0 : columnRect.right) - mousePos : mousePos - (columnRect === null || columnRect === void 0 ? void 0 : columnRect.left);
133
+ // Prevent the column from shrinking below a specified minimum width
134
+ if (resizableProps.minWidth && newSize < resizableProps.minWidth) {
135
+ newSize = resizableProps.minWidth;
136
+ }
137
+ (_b = thRef.current) === null || _b === void 0 ? void 0 : _b.style.setProperty('min-width', newSize + 'px');
138
+ trackedWidth.current = newSize;
139
+ setWidth(newSize);
140
+ };
141
+ const handleMouseup = (e) => {
142
+ var _a;
143
+ if (!isResizing.current) {
144
+ return;
145
+ }
146
+ // Reset the isResizing and dragOffset values to their initial states
147
+ isResizing.current = false;
148
+ dragOffset.current = 0;
149
+ // Call the onResize callback with the new width
150
+ onResize && onResize(e, thProps === null || thProps === void 0 ? void 0 : thProps.id, trackedWidth.current);
151
+ // Handle scroll into view when column drag button is moved off screen
152
+ if (resizeButtonRef.current && !isInView.current) {
153
+ (_a = resizeButtonRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth', block: 'center' });
154
+ }
155
+ document.removeEventListener('mousemove', callbackMouseMove);
156
+ document.removeEventListener('mouseup', callbackMouseUp);
157
+ };
158
+ const handleTouchEnd = (e) => {
159
+ e.stopPropagation();
160
+ if (!isResizing) {
161
+ return;
162
+ }
163
+ // Reset the isResizing and dragOffset values to their initial states
164
+ isResizing.current = false;
165
+ dragOffset.current = 0;
166
+ // Call the onResize callback with the new width
167
+ onResize && onResize(e, thProps === null || thProps === void 0 ? void 0 : thProps.id, trackedWidth.current);
168
+ document.removeEventListener('touchmove', callbackTouchMove);
169
+ document.removeEventListener('touchend', callbackTouchEnd);
170
+ };
171
+ const callbackMouseMove = useCallback(handleMouseMove, []);
172
+ const callbackTouchEnd = useCallback(handleTouchEnd, []);
173
+ const callbackTouchMove = useCallback(handleTouchMove, []);
174
+ const callbackMouseUp = useCallback(handleMouseup, []);
175
+ const handleKeys = (e) => {
176
+ var _a, _b;
177
+ const key = e.key;
178
+ if (key === 'Tab') {
179
+ isResizing.current = false;
180
+ }
181
+ if (key !== 'ArrowLeft' && key !== 'ArrowRight') {
182
+ return;
183
+ }
184
+ e.preventDefault();
185
+ isResizing.current = true;
186
+ const isRTL = getLanguageDirection(thRef.current) === 'rtl';
187
+ const columnRect = (_a = thRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
188
+ let newSize = (columnRect === null || columnRect === void 0 ? void 0 : columnRect.width) || 0;
189
+ let delta = 0;
190
+ const _increment = e.shiftKey ? shiftIncrement : increment;
191
+ if (key === 'ArrowRight') {
192
+ if (isRTL) {
193
+ delta = -_increment;
194
+ }
195
+ else {
196
+ delta = _increment;
197
+ }
198
+ }
199
+ else if (key === 'ArrowLeft') {
200
+ if (isRTL) {
201
+ delta = _increment;
202
+ }
203
+ else {
204
+ delta = -_increment;
205
+ }
206
+ }
207
+ newSize = newSize + delta;
208
+ (_b = thRef.current) === null || _b === void 0 ? void 0 : _b.style.setProperty('min-width', newSize + 'px');
209
+ setWidth(newSize);
210
+ onResize && onResize(e, thProps === null || thProps === void 0 ? void 0 : thProps.id, newSize);
211
+ };
212
+ const resizableContent = (_jsxs(Fragment, { children: [_jsx("div", { "aria-live": "polite", className: "pf-v6-screen-reader", children: screenReaderText }), _jsx(Button, { ref: resizeButtonRef, variant: "plain", hasNoPadding: true, icon: _jsx(ResizeIcon, {}), onMouseDown: handleMousedown, onKeyDown: handleKeys, onTouchStart: handleTouchStart, "aria-label": resizeButtonAriaLabel, className: classes.dataViewResizableButton })] }));
213
+ return (_jsx(Th, Object.assign({}, thProps, props, { style: width > 0 ? { minWidth: width } : undefined, ref: thRef, modifier: "truncate", className: dataViewThClassName }, (isResizable && { additionalContent: resizableContent }), { children: content })));
214
+ };
215
+ export default DataViewTh;
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewTh';
2
+ export * from './DataViewTh';
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewTh';
2
+ export * from './DataViewTh';
@@ -0,0 +1,26 @@
1
+ import { ToolbarFilterProps, TreeViewDataItem } from '@patternfly/react-core';
2
+ import React, { FC } from 'react';
3
+ export interface DataViewTreeFilterProps {
4
+ /** Unique key for the filter attribute */
5
+ filterId: string;
6
+ /** Array of current filter values */
7
+ value?: string[];
8
+ /** Filter title displayed in the toolbar */
9
+ title: string;
10
+ /** Callback for when the selection changes */
11
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
12
+ /** Controls visibility of the filter in the toolbar */
13
+ showToolbarItem?: ToolbarFilterProps['showToolbarItem'];
14
+ /** Custom OUIA ID */
15
+ ouiaId?: string;
16
+ /** Hierarchical data items for the tree structure */
17
+ items?: TreeViewDataItem[];
18
+ /** When true, expands all tree nodes by default */
19
+ defaultExpanded?: boolean;
20
+ /** Callback for when tree items are selected/deselected, provides all currently selected nodes */
21
+ onSelect?: (selectedItems: TreeViewDataItem[]) => void;
22
+ /** Array of pre-selected item id's to be checked on initial render */
23
+ defaultSelected?: string[];
24
+ }
25
+ export declare const DataViewTreeFilter: FC<DataViewTreeFilterProps>;
26
+ export default DataViewTreeFilter;
@@ -0,0 +1,225 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Dropdown, MenuToggle, ToolbarFilter, TreeView } from '@patternfly/react-core';
3
+ import { useState, useRef, useEffect } from 'react';
4
+ import { createUseStyles } from 'react-jss';
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
+ // Generic helper to collect items from tree based on predicate
19
+ const collectTreeItems = (items, predicate, leafOnly = false) => {
20
+ const collected = [];
21
+ const collect = (item) => {
22
+ var _a;
23
+ const isLeaf = !item.children || item.children.length === 0;
24
+ if (predicate(item) && (!leafOnly || isLeaf)) {
25
+ collected.push(item);
26
+ }
27
+ (_a = item.children) === null || _a === void 0 ? void 0 : _a.forEach(child => collect(child));
28
+ };
29
+ items.forEach(item => collect(item));
30
+ return collected;
31
+ };
32
+ // Helper function to get all checked items (not just leaf nodes)
33
+ const getAllCheckedItems = (items) => collectTreeItems(items, item => { var _a; return ((_a = item.checkProps) === null || _a === void 0 ? void 0 : _a.checked) === true; }, false);
34
+ // Get all checked leaf items (returns array of names)
35
+ const getAllCheckedLeafItems = (items) => collectTreeItems(items, item => { var _a; return ((_a = item.checkProps) === null || _a === void 0 ? void 0 : _a.checked) === true; }, true).map(item => String(item.name));
36
+ // Helper function to expand all nodes in the tree
37
+ const expandAllNodes = (items) => items.map(item => (Object.assign(Object.assign({}, item), { defaultExpanded: true, children: item.children ? expandAllNodes(item.children) : undefined })));
38
+ // Helper function to set pre-selected items
39
+ const setPreSelectedItems = (items, selectedIds) => items.map(item => {
40
+ var _a, _b;
41
+ const isSelected = selectedIds.includes(String(item.id));
42
+ const hasSelectedChildren = (_b = (_a = item.children) === null || _a === void 0 ? void 0 : _a.some(child => selectedIds.includes(String(child.id)))) !== null && _b !== void 0 ? _b : false;
43
+ return Object.assign(Object.assign({}, item), { checkProps: item.checkProps ? Object.assign(Object.assign({}, item.checkProps), { checked: isSelected || hasSelectedChildren }) : undefined, children: item.children ? setPreSelectedItems(item.children, selectedIds) : undefined });
44
+ });
45
+ // Helper function to uncheck all items recursively
46
+ const uncheckRecursive = (items) => items.map(item => (Object.assign(Object.assign({}, item), { checkProps: item.checkProps ? Object.assign(Object.assign({}, item.checkProps), { checked: false }) : undefined, children: item.children ? uncheckRecursive(item.children) : undefined })));
47
+ export const DataViewTreeFilter = ({ filterId, title, value = [], onChange, showToolbarItem, ouiaId = 'DataViewTreeFilter', items, defaultExpanded = false, onSelect, defaultSelected = [] }) => {
48
+ const classes = useStyles();
49
+ const [isOpen, setIsOpen] = useState(false);
50
+ const [treeData, setTreeData] = useState(items || []);
51
+ const menuRef = useRef(null);
52
+ const isInitialMount = useRef(true);
53
+ const hasCalledInitialOnChange = useRef(false);
54
+ // Initialize tree data with defaultExpanded and defaultSelected (only on first mount)
55
+ useEffect(() => {
56
+ if (!items) {
57
+ return;
58
+ }
59
+ let initializedData = [...items];
60
+ // Apply default expansion
61
+ if (defaultExpanded) {
62
+ initializedData = expandAllNodes(initializedData);
63
+ }
64
+ // Apply pre-selected items only on initial mount
65
+ if (isInitialMount.current && defaultSelected.length > 0) {
66
+ initializedData = setPreSelectedItems(initializedData, defaultSelected);
67
+ }
68
+ setTreeData(initializedData);
69
+ if (isInitialMount.current) {
70
+ isInitialMount.current = false;
71
+ }
72
+ }, [items, defaultExpanded]);
73
+ // Call onChange and onSelect after tree data is initialized with default selections
74
+ useEffect(() => {
75
+ if (!hasCalledInitialOnChange.current && defaultSelected.length > 0 && treeData.length > 0) {
76
+ const selectedValues = getAllCheckedLeafItems(treeData);
77
+ // Only call if there are actually selected values
78
+ if (selectedValues.length > 0) {
79
+ // Calculate both values synchronously before calling callbacks
80
+ const selectedItems = getAllCheckedItems(treeData);
81
+ // useEffect already runs after render, so this is safe
82
+ if (onChange) {
83
+ onChange(undefined, selectedValues);
84
+ }
85
+ if (onSelect) {
86
+ onSelect(selectedItems);
87
+ }
88
+ hasCalledInitialOnChange.current = true;
89
+ }
90
+ }
91
+ }, [treeData, onChange, onSelect, defaultSelected.length]);
92
+ // Sync tree checkboxes when value prop changes (when clearAllFilters is called)
93
+ useEffect(() => {
94
+ if (value.length === 0) {
95
+ setTreeData(currentTreeData => {
96
+ if (currentTreeData.length === 0) {
97
+ return currentTreeData;
98
+ }
99
+ const currentCheckedItems = getAllCheckedLeafItems(currentTreeData);
100
+ // Only update if there are checked items that need to be unchecked
101
+ if (currentCheckedItems.length > 0) {
102
+ return uncheckRecursive(currentTreeData);
103
+ }
104
+ return currentTreeData;
105
+ });
106
+ }
107
+ }, [value]);
108
+ // Check if all children are checked (recursive)
109
+ const areAllChildrenChecked = (item) => {
110
+ var _a, _b;
111
+ if (!((_a = item.children) === null || _a === void 0 ? void 0 : _a.length)) {
112
+ return ((_b = item.checkProps) === null || _b === void 0 ? void 0 : _b.checked) === true;
113
+ }
114
+ return item.children.every(child => areAllChildrenChecked(child));
115
+ };
116
+ // Check if some children are checked (recursive)
117
+ const areSomeChildrenChecked = (item) => {
118
+ var _a, _b;
119
+ if (!((_a = item.children) === null || _a === void 0 ? void 0 : _a.length)) {
120
+ return ((_b = item.checkProps) === null || _b === void 0 ? void 0 : _b.checked) === true;
121
+ }
122
+ return item.children.some(child => areSomeChildrenChecked(child));
123
+ };
124
+ // Find tree item by name
125
+ const findItemByName = (items, name) => {
126
+ for (const item of items) {
127
+ if (item.name === name) {
128
+ return item;
129
+ }
130
+ if (item.children) {
131
+ const found = findItemByName(item.children, name);
132
+ if (found) {
133
+ return found;
134
+ }
135
+ }
136
+ }
137
+ return null;
138
+ };
139
+ // Find parent item by child ID
140
+ const findParentById = (items, childId) => {
141
+ var _a;
142
+ for (const item of items) {
143
+ if ((_a = item.children) === null || _a === void 0 ? void 0 : _a.some(child => child.id === childId)) {
144
+ return item;
145
+ }
146
+ if (item.children) {
147
+ const found = findParentById(item.children, childId);
148
+ if (found) {
149
+ return found;
150
+ }
151
+ }
152
+ }
153
+ return null;
154
+ };
155
+ // Update parent checkbox states based on children (recursive)
156
+ const onCheckParentHandle = (childId) => {
157
+ const parent = findParentById(treeData, childId);
158
+ if (!parent) {
159
+ return;
160
+ }
161
+ if (parent.checkProps) {
162
+ const allChildrenChecked = areAllChildrenChecked(parent);
163
+ const someChildrenChecked = areSomeChildrenChecked(parent);
164
+ if (allChildrenChecked) {
165
+ parent.checkProps.checked = true;
166
+ }
167
+ else if (someChildrenChecked) {
168
+ parent.checkProps.checked = null;
169
+ }
170
+ else {
171
+ parent.checkProps.checked = false;
172
+ }
173
+ }
174
+ if (parent.id) {
175
+ onCheckParentHandle(parent.id);
176
+ }
177
+ };
178
+ // Check/uncheck item and all its children (recursive)
179
+ const onCheckHandle = (treeViewItem, checked) => {
180
+ var _a;
181
+ if (treeViewItem.checkProps) {
182
+ treeViewItem.checkProps.checked = checked;
183
+ }
184
+ (_a = treeViewItem.children) === null || _a === void 0 ? void 0 : _a.forEach(child => onCheckHandle(child, checked));
185
+ };
186
+ // Handle checkbox change event
187
+ const onCheck = (event, treeViewItem) => {
188
+ const checked = event.target.checked;
189
+ onCheckHandle(treeViewItem, checked);
190
+ if (treeViewItem.id) {
191
+ onCheckParentHandle(treeViewItem.id);
192
+ }
193
+ setTreeData(prev => [...prev]);
194
+ const selectedValues = getAllCheckedLeafItems(treeData);
195
+ onChange === null || onChange === void 0 ? void 0 : onChange(event, selectedValues);
196
+ if (onSelect) {
197
+ const selectedItems = getAllCheckedItems(treeData);
198
+ onSelect(selectedItems);
199
+ }
200
+ };
201
+ // Clear a specific filter by name (when label chip is removed)
202
+ const onFilterSelectorClear = (itemName) => {
203
+ const treeViewItem = findItemByName(treeData, itemName);
204
+ if (!treeViewItem) {
205
+ return;
206
+ }
207
+ onCheckHandle(treeViewItem, false);
208
+ if (treeViewItem.id) {
209
+ onCheckParentHandle(treeViewItem.id);
210
+ }
211
+ };
212
+ // Uncheck all items in the tree
213
+ const uncheckAllItems = () => {
214
+ const updatedTreeData = uncheckRecursive(treeData);
215
+ setTreeData(updatedTreeData);
216
+ onChange === null || onChange === void 0 ? void 0 : onChange(undefined, []);
217
+ };
218
+ const dropdown = (_jsx(Dropdown, { ref: menuRef, isOpen: isOpen, onOpenChange: (isOpen) => setIsOpen(isOpen), toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsOpen(!isOpen), isExpanded: isOpen, children: title })), ouiaId: ouiaId, shouldFocusToggleOnSelect: true, children: _jsx(TreeView, { hasAnimations: true, data: treeData, onCheck: onCheck, hasCheckboxes: true, className: classes.dataViewTreeFilterTreeView }) }));
219
+ return (_jsx(ToolbarFilter, { "data-ouia-component-id": ouiaId, labels: value.map(item => ({ key: item, node: item })), deleteLabel: (_, label) => {
220
+ const labelKey = typeof label === 'string' ? label : label.key;
221
+ onChange === null || onChange === void 0 ? void 0 : onChange(undefined, value.filter(item => item !== labelKey));
222
+ onFilterSelectorClear(labelKey);
223
+ }, deleteLabelGroup: uncheckAllItems, categoryName: title, showToolbarItem: showToolbarItem, children: dropdown }, filterId));
224
+ };
225
+ export default DataViewTreeFilter;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,166 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { render, screen, waitFor } from '@testing-library/react';
12
+ import '@testing-library/jest-dom';
13
+ import userEvent from '@testing-library/user-event';
14
+ import DataViewTreeFilter from './DataViewTreeFilter';
15
+ import DataViewToolbar from '../DataViewToolbar';
16
+ describe('DataViewTreeFilter component', () => {
17
+ const treeItems = [
18
+ {
19
+ name: 'Linux',
20
+ id: 'os-linux',
21
+ checkProps: { 'aria-label': 'linux-check', checked: false },
22
+ children: [
23
+ {
24
+ name: 'Ubuntu 22.04',
25
+ id: 'os-ubuntu',
26
+ checkProps: { checked: false }
27
+ },
28
+ {
29
+ name: 'RHEL 9',
30
+ id: 'os-rhel',
31
+ checkProps: { checked: false }
32
+ },
33
+ {
34
+ name: 'Debian 12',
35
+ id: 'os-debian',
36
+ checkProps: { checked: false }
37
+ },
38
+ {
39
+ name: 'CentOS 8',
40
+ id: 'os-centos',
41
+ checkProps: { checked: false }
42
+ },
43
+ {
44
+ name: 'Fedora 38',
45
+ id: 'os-fedora',
46
+ checkProps: { checked: false }
47
+ }
48
+ ],
49
+ defaultExpanded: true
50
+ },
51
+ {
52
+ name: 'Windows',
53
+ id: 'os-windows',
54
+ checkProps: { 'aria-label': 'windows-check', checked: false },
55
+ children: [
56
+ {
57
+ name: 'Windows Server 2022',
58
+ id: 'os-windows-2022',
59
+ checkProps: { checked: false }
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ name: 'macOS',
65
+ id: 'os-macos',
66
+ checkProps: { 'aria-label': 'macos-check', checked: false },
67
+ children: [
68
+ {
69
+ name: 'macOS Ventura',
70
+ id: 'os-macos-ventura',
71
+ checkProps: { checked: false }
72
+ },
73
+ {
74
+ name: 'macOS Sonoma',
75
+ id: 'os-macos-sonoma',
76
+ checkProps: { checked: false }
77
+ }
78
+ ]
79
+ }
80
+ ];
81
+ const defaultProps = {
82
+ filterId: 'test-tree-filter',
83
+ title: 'Test Tree Filter',
84
+ value: ['Linux'],
85
+ items: treeItems
86
+ };
87
+ beforeEach(() => {
88
+ jest.clearAllMocks();
89
+ });
90
+ it('should render correctly', () => {
91
+ const { container } = render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTreeFilter, Object.assign({}, defaultProps)) }));
92
+ expect(container).toMatchSnapshot();
93
+ });
94
+ describe('defaultExpanded', () => {
95
+ it('should have expanded items by default', () => __awaiter(void 0, void 0, void 0, function* () {
96
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTreeFilter, { filterId: "os", title: "Operating System", items: treeItems, defaultExpanded: true }) }));
97
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
98
+ yield userEvent.click(openMenu);
99
+ yield waitFor(() => {
100
+ const node = screen.getByText('Ubuntu 22.04');
101
+ expect(node).toHaveClass('pf-v6-c-tree-view__node-text');
102
+ expect(node).toBeInTheDocument();
103
+ });
104
+ }));
105
+ });
106
+ describe('onChange callback', () => {
107
+ it('onChange should be called on toggle of node', () => __awaiter(void 0, void 0, void 0, function* () {
108
+ const mockOnChange = jest.fn();
109
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTreeFilter, { filterId: "os", title: "Operating System", items: treeItems, defaultExpanded: true, onChange: mockOnChange }) }));
110
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
111
+ yield userEvent.click(openMenu);
112
+ yield waitFor(() => {
113
+ const node = screen.getByText('Ubuntu 22.04');
114
+ expect(node).toBeInTheDocument();
115
+ });
116
+ const node = screen.getByText('Ubuntu 22.04');
117
+ yield userEvent.click(node);
118
+ yield waitFor(() => {
119
+ expect(mockOnChange).toHaveBeenCalled();
120
+ });
121
+ }));
122
+ });
123
+ describe('onSelect callback', () => {
124
+ it('onSelect should return list of selected items when item is selected', () => __awaiter(void 0, void 0, void 0, function* () {
125
+ const mockOnSelect = jest.fn();
126
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTreeFilter, { filterId: "os", title: "Operating System", items: treeItems, defaultExpanded: true, onSelect: mockOnSelect }) }));
127
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
128
+ yield userEvent.click(openMenu);
129
+ yield waitFor(() => {
130
+ const node = screen.getByText('Ubuntu 22.04');
131
+ expect(node).toBeInTheDocument();
132
+ });
133
+ const node = screen.getByText('Ubuntu 22.04');
134
+ yield userEvent.click(node);
135
+ yield waitFor(() => {
136
+ expect(mockOnSelect).toHaveBeenCalled();
137
+ expect(mockOnSelect).toHaveBeenCalledWith(expect.arrayContaining([
138
+ expect.objectContaining({
139
+ name: 'Ubuntu 22.04',
140
+ id: 'os-ubuntu'
141
+ })
142
+ ]));
143
+ });
144
+ }));
145
+ });
146
+ describe('rendering all items', () => {
147
+ it('all tree items should be rendered', () => __awaiter(void 0, void 0, void 0, function* () {
148
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTreeFilter, { filterId: "os", title: "Operating System", items: treeItems, defaultExpanded: true }) }));
149
+ const openMenu = screen.getByRole('button', { name: /operating system/i });
150
+ yield userEvent.click(openMenu);
151
+ yield waitFor(() => {
152
+ expect(screen.getByText('Linux')).toBeInTheDocument();
153
+ expect(screen.getByText('Windows')).toBeInTheDocument();
154
+ expect(screen.getByText('macOS')).toBeInTheDocument();
155
+ expect(screen.getByText('Ubuntu 22.04')).toBeInTheDocument();
156
+ expect(screen.getByText('RHEL 9')).toBeInTheDocument();
157
+ expect(screen.getByText('Debian 12')).toBeInTheDocument();
158
+ expect(screen.getByText('CentOS 8')).toBeInTheDocument();
159
+ expect(screen.getByText('Fedora 38')).toBeInTheDocument();
160
+ expect(screen.getByText('Windows Server 2022')).toBeInTheDocument();
161
+ expect(screen.getByText('macOS Ventura')).toBeInTheDocument();
162
+ expect(screen.getByText('macOS Sonoma')).toBeInTheDocument();
163
+ });
164
+ }));
165
+ });
166
+ });
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewTreeFilter';
2
+ export * from './DataViewTreeFilter';
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewTreeFilter';
2
+ export * from './DataViewTreeFilter';
@@ -8,4 +8,5 @@ export declare const useDataViewSelection: (props?: UseDataViewSelectionProps) =
8
8
  selected: any[];
9
9
  onSelect: (isSelecting: boolean, items?: any[] | any) => void;
10
10
  isSelected: (item: any) => boolean;
11
+ setSelected: (items: any[]) => void;
11
12
  };