@patternfly/react-data-view 5.5.0 → 5.6.0

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 (39) hide show
  1. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.d.ts +29 -0
  2. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.js +70 -0
  3. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.test.d.ts +1 -0
  4. package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.test.js +25 -0
  5. package/dist/cjs/DataViewCheckboxFilter/index.d.ts +2 -0
  6. package/dist/cjs/DataViewCheckboxFilter/index.js +23 -0
  7. package/dist/cjs/DataViewFilters/DataViewFilters.d.ts +7 -1
  8. package/dist/cjs/DataViewFilters/DataViewFilters.js +16 -1
  9. package/dist/cjs/DataViewTableTree/DataViewTableTree.js +26 -14
  10. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.js +1 -1
  11. package/dist/cjs/Hooks/filters.js +13 -14
  12. package/dist/cjs/index.d.ts +2 -0
  13. package/dist/cjs/index.js +4 -1
  14. package/dist/dynamic/DataViewCheckboxFilter/package.json +1 -0
  15. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.d.ts +29 -0
  16. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.js +62 -0
  17. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.d.ts +1 -0
  18. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.js +20 -0
  19. package/dist/esm/DataViewCheckboxFilter/index.d.ts +2 -0
  20. package/dist/esm/DataViewCheckboxFilter/index.js +2 -0
  21. package/dist/esm/DataViewFilters/DataViewFilters.d.ts +7 -1
  22. package/dist/esm/DataViewFilters/DataViewFilters.js +16 -1
  23. package/dist/esm/DataViewTableTree/DataViewTableTree.js +26 -14
  24. package/dist/esm/DataViewTextFilter/DataViewTextFilter.js +1 -1
  25. package/dist/esm/Hooks/filters.js +13 -14
  26. package/dist/esm/index.d.ts +2 -0
  27. package/dist/esm/index.js +2 -0
  28. package/package.json +1 -1
  29. package/patternfly-docs/content/extensions/data-view/examples/Functionality/FiltersExample.tsx +31 -16
  30. package/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md +4 -3
  31. package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.test.tsx +24 -0
  32. package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.tsx +175 -0
  33. package/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap +194 -0
  34. package/src/DataViewCheckboxFilter/index.ts +2 -0
  35. package/src/DataViewFilters/DataViewFilters.tsx +26 -7
  36. package/src/DataViewTableTree/DataViewTableTree.tsx +39 -18
  37. package/src/DataViewTextFilter/DataViewTextFilter.tsx +1 -0
  38. package/src/Hooks/filters.ts +14 -13
  39. package/src/index.ts +3 -0
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { MenuProps } from '@patternfly/react-core';
3
+ import { DataViewFilterOption } from '../DataViewFilters';
4
+ export declare const isDataViewFilterOption: (obj: unknown) => obj is DataViewFilterOption;
5
+ /** extends MenuProps */
6
+ export interface DataViewCheckboxFilterProps extends Omit<MenuProps, 'onSelect' | 'onChange'> {
7
+ /** Unique key for the filter attribute */
8
+ filterId: string;
9
+ /** Array of current filter values */
10
+ value?: string[];
11
+ /** Filter title displayed in the toolbar */
12
+ title: string;
13
+ /** Placeholder text of the menu */
14
+ placeholder?: string;
15
+ /** Filter options displayed */
16
+ options: (DataViewFilterOption | string)[];
17
+ /** Callback for updating when item selection changes. */
18
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
19
+ /** Controls visibility of the filter in the toolbar */
20
+ showToolbarItem?: boolean;
21
+ /** Controls visibility of the filter icon */
22
+ showIcon?: boolean;
23
+ /** Controls visibility of the selected items badge */
24
+ showBadge?: boolean;
25
+ /** Custom OUIA ID */
26
+ ouiaId?: string;
27
+ }
28
+ export declare const DataViewCheckboxFilter: React.FC<DataViewCheckboxFilterProps>;
29
+ export default DataViewCheckboxFilter;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.DataViewCheckboxFilter = exports.isDataViewFilterOption = void 0;
18
+ const react_1 = __importDefault(require("react"));
19
+ const react_core_1 = require("@patternfly/react-core");
20
+ const react_icons_1 = require("@patternfly/react-icons");
21
+ const isToolbarChip = (chip) => typeof chip === 'object' && 'key' in chip;
22
+ const isDataViewFilterOption = (obj) => !!obj &&
23
+ typeof obj === 'object' &&
24
+ 'label' in obj &&
25
+ 'value' in obj &&
26
+ typeof obj.value === 'string';
27
+ exports.isDataViewFilterOption = isDataViewFilterOption;
28
+ const DataViewCheckboxFilter = (_a) => {
29
+ var { filterId, title, value = [], onChange, placeholder, options = [], showToolbarItem, showIcon = !placeholder, showBadge = !placeholder, ouiaId = 'DataViewCheckboxFilter' } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "placeholder", "options", "showToolbarItem", "showIcon", "showBadge", "ouiaId"]);
30
+ const [isOpen, setIsOpen] = react_1.default.useState(false);
31
+ const toggleRef = react_1.default.useRef(null);
32
+ const menuRef = react_1.default.useRef(null);
33
+ const containerRef = react_1.default.useRef(null);
34
+ const normalizeOptions = react_1.default.useMemo(() => options.map(option => typeof option === 'string'
35
+ ? { label: option, value: option }
36
+ : option), [options]);
37
+ const handleToggleClick = (event) => {
38
+ event.stopPropagation();
39
+ setTimeout(() => {
40
+ var _a;
41
+ const firstElement = (_a = menuRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('li > button:not(:disabled)');
42
+ firstElement === null || firstElement === void 0 ? void 0 : firstElement.focus();
43
+ }, 0);
44
+ setIsOpen(prev => !prev);
45
+ };
46
+ const handleSelect = (event, itemId) => {
47
+ const activeItem = String(itemId);
48
+ const isSelected = value.includes(activeItem);
49
+ onChange === null || onChange === void 0 ? void 0 : onChange(event, isSelected ? value.filter(item => item !== activeItem) : [activeItem, ...value]);
50
+ };
51
+ const handleClickOutside = (event) => isOpen &&
52
+ menuRef.current && toggleRef.current &&
53
+ !menuRef.current.contains(event.target) && !toggleRef.current.contains(event.target)
54
+ && setIsOpen(false);
55
+ react_1.default.useEffect(() => {
56
+ window.addEventListener('click', handleClickOutside);
57
+ return () => {
58
+ window.removeEventListener('click', handleClickOutside);
59
+ };
60
+ }, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
61
+ return (react_1.default.createElement(react_core_1.ToolbarFilter, { key: ouiaId, "data-ouia-component-id": ouiaId, chips: value.map(item => {
62
+ const activeOption = normalizeOptions.find(option => option.value === item);
63
+ return ({ key: activeOption === null || activeOption === void 0 ? void 0 : activeOption.value, node: activeOption === null || activeOption === void 0 ? void 0 : activeOption.label });
64
+ }), deleteChip: (_, chip) => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, value.filter(item => item !== (isToolbarChip(chip) ? chip.key : chip))), categoryName: title, showToolbarItem: showToolbarItem },
65
+ react_1.default.createElement(react_core_1.Popper, { trigger: react_1.default.createElement(react_core_1.MenuToggle, { ouiaId: `${ouiaId}-toggle`, ref: toggleRef, onClick: handleToggleClick, isExpanded: isOpen, icon: showIcon ? react_1.default.createElement(react_icons_1.FilterIcon, null) : undefined, badge: value.length > 0 && showBadge ? react_1.default.createElement(react_core_1.Badge, { "data-ouia-component-id": `${ouiaId}-badge`, isRead: true }, value.length) : undefined, style: { width: '200px' } }, placeholder !== null && placeholder !== void 0 ? placeholder : title), triggerRef: toggleRef, popper: react_1.default.createElement(react_core_1.Menu, Object.assign({ ref: menuRef, ouiaId: `${ouiaId}-menu`, onSelect: handleSelect, selected: value }, props),
66
+ react_1.default.createElement(react_core_1.MenuContent, null,
67
+ react_1.default.createElement(react_core_1.MenuList, null, normalizeOptions.map(option => (react_1.default.createElement(react_core_1.MenuItem, { "data-ouia-component-id": `${ouiaId}-filter-item-${option.value}`, key: option.value, itemId: option.value, isSelected: value.includes(option.value), hasCheckbox: true }, option.label)))))), popperRef: menuRef, appendTo: containerRef.current || undefined, "aria-label": `${title !== null && title !== void 0 ? title : filterId} filter`, isVisible: isOpen })));
68
+ };
69
+ exports.DataViewCheckboxFilter = DataViewCheckboxFilter;
70
+ exports.default = exports.DataViewCheckboxFilter;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const react_1 = __importDefault(require("react"));
7
+ const react_2 = require("@testing-library/react");
8
+ const DataViewCheckboxFilter_1 = __importDefault(require("./DataViewCheckboxFilter"));
9
+ const DataViewToolbar_1 = __importDefault(require("../DataViewToolbar"));
10
+ describe('DataViewCheckboxFilter component', () => {
11
+ const defaultProps = {
12
+ filterId: 'test-checkbox-filter',
13
+ title: 'Test Checkbox Filter',
14
+ value: ['workspace-one'],
15
+ options: [
16
+ { label: 'Workspace one', value: 'workspace-one' },
17
+ { label: 'Workspace two', value: 'workspace-two' },
18
+ { label: 'Workspace three', value: 'workspace-three' },
19
+ ],
20
+ };
21
+ it('should render correctly', () => {
22
+ const { container } = (0, react_2.render)(react_1.default.createElement(DataViewToolbar_1.default, { filters: react_1.default.createElement(DataViewCheckboxFilter_1.default, Object.assign({}, defaultProps)) }));
23
+ expect(container).toMatchSnapshot();
24
+ });
25
+ });
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewCheckboxFilter';
2
+ export * from './DataViewCheckboxFilter';
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.default = void 0;
21
+ var DataViewCheckboxFilter_1 = require("./DataViewCheckboxFilter");
22
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(DataViewCheckboxFilter_1).default; } });
23
+ __exportStar(require("./DataViewCheckboxFilter"), exports);
@@ -1,5 +1,11 @@
1
- import React from 'react';
1
+ import React, { ReactNode } from 'react';
2
2
  import { ToolbarToggleGroupProps } from '@patternfly/react-core';
3
+ export interface DataViewFilterOption {
4
+ /** Filter option label */
5
+ label: ReactNode;
6
+ /** Filter option value */
7
+ value: string;
8
+ }
3
9
  /** extends ToolbarToggleGroupProps */
4
10
  export interface DataViewFiltersProps<T extends object> extends Omit<ToolbarToggleGroupProps, 'toggleIcon' | 'breakpoint' | 'onChange'> {
5
11
  /** Content rendered inside the data view */
@@ -52,6 +52,19 @@ const DataViewFilters = (_a) => {
52
52
  (0, react_1.useEffect)(() => {
53
53
  filterItems.length > 0 && setActiveAttributeMenu(filterItems[0].title);
54
54
  }, [filterItems]);
55
+ const handleClickOutside = (event) => {
56
+ var _a, _b;
57
+ return isAttributeMenuOpen &&
58
+ !((_a = attributeMenuRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target)) &&
59
+ !((_b = attributeToggleRef.current) === null || _b === void 0 ? void 0 : _b.contains(event.target))
60
+ && setIsAttributeMenuOpen(false);
61
+ };
62
+ (0, react_1.useEffect)(() => {
63
+ window.addEventListener('click', handleClickOutside);
64
+ return () => {
65
+ window.removeEventListener('click', handleClickOutside);
66
+ };
67
+ }, [isAttributeMenuOpen]); // eslint-disable-line react-hooks/exhaustive-deps
55
68
  const attributeToggle = (react_1.default.createElement(react_core_1.MenuToggle, { ref: attributeToggleRef, onClick: () => setIsAttributeMenuOpen(!isAttributeMenuOpen), isExpanded: isAttributeMenuOpen, icon: toggleIcon }, activeAttributeMenu));
56
69
  const attributeMenu = (react_1.default.createElement(react_core_1.Menu, { ref: attributeMenuRef, onSelect: (_ev, itemId) => {
57
70
  const selectedItem = filterItems.find(item => item.filterId === itemId);
@@ -64,7 +77,9 @@ const DataViewFilters = (_a) => {
64
77
  react_1.default.createElement(react_core_1.ToolbarGroup, { variant: "filter-group" },
65
78
  react_1.default.createElement("div", { ref: attributeContainerRef },
66
79
  react_1.default.createElement(react_core_1.Popper, { trigger: attributeToggle, triggerRef: attributeToggleRef, popper: attributeMenu, popperRef: attributeMenuRef, appendTo: attributeContainerRef.current || undefined, isVisible: isAttributeMenuOpen })),
67
- react_1.default.Children.map(children, (child) => (react_1.default.isValidElement(child) ? (react_1.default.cloneElement(child, Object.assign({ showToolbarItem: activeAttributeMenu === child.props.title, onChange: (event, value) => onChange === null || onChange === void 0 ? void 0 : onChange(event, { [child.props.filterId]: value }), value: values === null || values === void 0 ? void 0 : values[child.props.filterId] }, child.props))) : child)))));
80
+ react_1.default.Children.map(children, (child) => react_1.default.isValidElement(child)
81
+ ? react_1.default.cloneElement(child, Object.assign({ showToolbarItem: activeAttributeMenu === child.props.title, onChange: (event, value) => onChange === null || onChange === void 0 ? void 0 : onChange(event, { [child.props.filterId]: value }), value: values === null || values === void 0 ? void 0 : values[child.props.filterId] }, child.props))
82
+ : child))));
68
83
  };
69
84
  exports.DataViewFilters = DataViewFilters;
70
85
  exports.default = exports.DataViewFilters;
@@ -40,19 +40,31 @@ const react_table_1 = require("@patternfly/react-table");
40
40
  const InternalContext_1 = require("../InternalContext");
41
41
  const DataViewTableHead_1 = require("../DataViewTableHead");
42
42
  const DataViewTable_1 = require("../DataViewTable");
43
- const getDescendants = (node) => (!node.children || !node.children.length) ? [node] : node.children.flatMap(getDescendants);
44
- const isNodeChecked = (node, isSelected) => {
45
- let allSelected = true;
46
- let someSelected = false;
47
- for (const descendant of getDescendants(node)) {
48
- const selected = !!(isSelected === null || isSelected === void 0 ? void 0 : isSelected(descendant));
49
- someSelected || (someSelected = selected);
50
- allSelected && (allSelected = selected);
51
- if (!allSelected && someSelected) {
52
- return null;
43
+ const getNodesAffectedBySelection = (allRows, node, isChecking, isSelected) => {
44
+ const getDescendants = (node) => node.children ? node.children.flatMap(getDescendants).concat(node) : [node];
45
+ const findParent = (child, rows) => {
46
+ var _a;
47
+ return (_a = rows.find(row => { var _a; return (_a = row.children) === null || _a === void 0 ? void 0 : _a.some(c => c === child); })) !== null && _a !== void 0 ? _a : rows.flatMap(row => { var _a; return (_a = row.children) !== null && _a !== void 0 ? _a : []; }).map(c => findParent(child, [c])).find(p => p);
48
+ };
49
+ const getAncestors = (node) => {
50
+ const ancestors = [];
51
+ let parent = findParent(node, allRows);
52
+ while (parent) {
53
+ ancestors.push(parent);
54
+ parent = findParent(parent, allRows);
53
55
  }
54
- }
55
- return allSelected;
56
+ return ancestors;
57
+ };
58
+ const affectedNodes = new Set([node, ...getDescendants(node)]);
59
+ getAncestors(node).forEach(ancestor => {
60
+ var _a, _b;
61
+ const allChildrenSelected = (_a = ancestor.children) === null || _a === void 0 ? void 0 : _a.every(child => (isSelected === null || isSelected === void 0 ? void 0 : isSelected(child)) || affectedNodes.has(child));
62
+ const anyChildAffected = (_b = ancestor.children) === null || _b === void 0 ? void 0 : _b.some(child => affectedNodes.has(child) || child.id === node.id);
63
+ if (isChecking ? !(isSelected === null || isSelected === void 0 ? void 0 : isSelected(ancestor)) && allChildrenSelected : (isSelected === null || isSelected === void 0 ? void 0 : isSelected(ancestor)) && anyChildAffected) {
64
+ affectedNodes.add(ancestor);
65
+ }
66
+ });
67
+ return Array.from(affectedNodes);
56
68
  };
57
69
  const DataViewTableTree = (_a) => {
58
70
  var { columns, rows, headStates, bodyStates, leafIcon = null, expandedIcon = null, collapsedIcon = null, ouiaId = 'DataViewTableTree' } = _a, props = __rest(_a, ["columns", "rows", "headStates", "bodyStates", "leafIcon", "expandedIcon", "collapsedIcon", "ouiaId"]);
@@ -70,7 +82,7 @@ const DataViewTableTree = (_a) => {
70
82
  }
71
83
  const isExpanded = expandedNodeIds.includes(node.id);
72
84
  const isDetailsExpanded = expandedDetailsNodeNames.includes(node.id);
73
- const isChecked = isSelected && isNodeChecked(node, isSelected);
85
+ const isChecked = isSelected === null || isSelected === void 0 ? void 0 : isSelected(node);
74
86
  let icon = leafIcon;
75
87
  if (node.children) {
76
88
  icon = isExpanded ? expandedIcon : collapsedIcon;
@@ -84,7 +96,7 @@ const DataViewTableTree = (_a) => {
84
96
  const otherDetailsExpandedNodeIds = prevDetailsExpanded.filter(id => id !== node.id);
85
97
  return isDetailsExpanded ? otherDetailsExpandedNodeIds : [...otherDetailsExpandedNodeIds, node.id];
86
98
  }),
87
- onCheckChange: ((isSelectDisabled === null || isSelectDisabled === void 0 ? void 0 : isSelectDisabled(node)) || !onSelect) ? undefined : (_event, isChecking) => onSelect === null || onSelect === void 0 ? void 0 : onSelect(isChecking, getDescendants(node)),
99
+ onCheckChange: ((isSelectDisabled === null || isSelectDisabled === void 0 ? void 0 : isSelectDisabled(node)) || !onSelect) ? undefined : (_event, isChecking) => onSelect === null || onSelect === void 0 ? void 0 : onSelect(isChecking, getNodesAffectedBySelection(rows, node, isChecking, isSelected)),
88
100
  rowIndex,
89
101
  props: {
90
102
  isExpanded,
@@ -19,7 +19,7 @@ const react_1 = __importDefault(require("react"));
19
19
  const react_core_1 = require("@patternfly/react-core");
20
20
  const DataViewTextFilter = (_a) => {
21
21
  var { filterId, title, value = '', onChange, onClear = () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), showToolbarItem, trimValue = true, ouiaId = 'DataViewTextFilter' } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "onClear", "showToolbarItem", "trimValue", "ouiaId"]);
22
- return (react_1.default.createElement(react_core_1.ToolbarFilter, { "data-ouia-component-id": ouiaId, chips: value.length > 0 ? [{ key: title, node: value }] : [], deleteChip: () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), categoryName: title, showToolbarItem: showToolbarItem },
22
+ return (react_1.default.createElement(react_core_1.ToolbarFilter, { key: ouiaId, "data-ouia-component-id": ouiaId, chips: value.length > 0 ? [{ key: title, node: value }] : [], deleteChip: () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), categoryName: title, showToolbarItem: showToolbarItem },
23
23
  react_1.default.createElement(react_core_1.SearchInput, Object.assign({ searchInputId: filterId, value: value, onChange: (e, inputValue) => onChange === null || onChange === void 0 ? void 0 : onChange(e, trimValue ? inputValue.trim() : inputValue), onClear: onClear, placeholder: `Filter by ${title}`, "aria-label": `${title !== null && title !== void 0 ? title : filterId} filter`, "data-ouia-component-id": `${ouiaId}-input` }, props))));
24
24
  };
25
25
  exports.DataViewTextFilter = DataViewTextFilter;
@@ -5,25 +5,24 @@ const react_1 = require("react");
5
5
  ;
6
6
  const useDataViewFilters = ({ initialFilters = {}, searchParams, setSearchParams, }) => {
7
7
  const isUrlSyncEnabled = (0, react_1.useMemo)(() => searchParams && !!setSearchParams, [searchParams, setSearchParams]);
8
- const getInitialFilters = (0, react_1.useCallback)(() => isUrlSyncEnabled ? Object.keys(initialFilters).reduce((loadedFilters, key) => {
9
- const urlValue = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(key);
10
- loadedFilters[key] = urlValue
11
- ? urlValue
12
- : initialFilters[key];
13
- return loadedFilters;
14
- // eslint-disable-next-line react-hooks/exhaustive-deps
15
- }, Object.assign({}, initialFilters)) : initialFilters, [isUrlSyncEnabled, JSON.stringify(initialFilters), searchParams === null || searchParams === void 0 ? void 0 : searchParams.toString()]);
8
+ const getInitialFilters = (0, react_1.useCallback)(() => isUrlSyncEnabled
9
+ ? Object.keys(initialFilters).reduce((loadedFilters, key) => {
10
+ const urlValue = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(key);
11
+ const isArrayFilter = Array.isArray(initialFilters[key]);
12
+ // eslint-disable-next-line no-nested-ternary
13
+ loadedFilters[key] = urlValue
14
+ ? (isArrayFilter && !Array.isArray(urlValue) ? [urlValue] : urlValue)
15
+ : initialFilters[key];
16
+ return loadedFilters;
17
+ }, Object.assign({}, initialFilters))
18
+ : initialFilters, [isUrlSyncEnabled, initialFilters, searchParams]);
16
19
  const [filters, setFilters] = (0, react_1.useState)(getInitialFilters());
17
20
  const updateSearchParams = (0, react_1.useCallback)((newFilters) => {
18
21
  if (isUrlSyncEnabled) {
19
22
  const params = new URLSearchParams(searchParams);
20
23
  Object.entries(newFilters).forEach(([key, value]) => {
21
- if (value) {
22
- params.set(key, Array.isArray(value) ? value.join(',') : value);
23
- }
24
- else {
25
- params.delete(key);
26
- }
24
+ params.delete(key);
25
+ (Array.isArray(value) ? value : [value]).forEach((val) => value && params.append(key, val));
27
26
  });
28
27
  setSearchParams === null || setSearchParams === void 0 ? void 0 : setSearchParams(params);
29
28
  }
@@ -15,5 +15,7 @@ export { default as DataViewTable } from './DataViewTable';
15
15
  export * from './DataViewTable';
16
16
  export { default as DataViewEventsContext } from './DataViewEventsContext';
17
17
  export * from './DataViewEventsContext';
18
+ export { default as DataViewCheckboxFilter } from './DataViewCheckboxFilter';
19
+ export * from './DataViewCheckboxFilter';
18
20
  export { default as DataView } from './DataView';
19
21
  export * from './DataView';
package/dist/cjs/index.js CHANGED
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.DataView = exports.DataViewEventsContext = exports.DataViewTable = exports.DataViewTableBasic = exports.DataViewTableHead = exports.DataViewTableTree = exports.DataViewTextFilter = exports.DataViewToolbar = exports.InternalContext = void 0;
21
+ exports.DataView = exports.DataViewCheckboxFilter = exports.DataViewEventsContext = exports.DataViewTable = exports.DataViewTableBasic = exports.DataViewTableHead = exports.DataViewTableTree = exports.DataViewTextFilter = exports.DataViewToolbar = exports.InternalContext = void 0;
22
22
  var InternalContext_1 = require("./InternalContext");
23
23
  Object.defineProperty(exports, "InternalContext", { enumerable: true, get: function () { return __importDefault(InternalContext_1).default; } });
24
24
  __exportStar(require("./InternalContext"), exports);
@@ -44,6 +44,9 @@ __exportStar(require("./DataViewTable"), exports);
44
44
  var DataViewEventsContext_1 = require("./DataViewEventsContext");
45
45
  Object.defineProperty(exports, "DataViewEventsContext", { enumerable: true, get: function () { return __importDefault(DataViewEventsContext_1).default; } });
46
46
  __exportStar(require("./DataViewEventsContext"), exports);
47
+ var DataViewCheckboxFilter_1 = require("./DataViewCheckboxFilter");
48
+ Object.defineProperty(exports, "DataViewCheckboxFilter", { enumerable: true, get: function () { return __importDefault(DataViewCheckboxFilter_1).default; } });
49
+ __exportStar(require("./DataViewCheckboxFilter"), exports);
47
50
  var DataView_1 = require("./DataView");
48
51
  Object.defineProperty(exports, "DataView", { enumerable: true, get: function () { return __importDefault(DataView_1).default; } });
49
52
  __exportStar(require("./DataView"), exports);
@@ -0,0 +1 @@
1
+ {"main":"../../cjs/DataViewCheckboxFilter/index.js","module":"../../esm/DataViewCheckboxFilter/index.js","typings":"../../esm/DataViewCheckboxFilter/index.d.ts"}
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { MenuProps } from '@patternfly/react-core';
3
+ import { DataViewFilterOption } from '../DataViewFilters';
4
+ export declare const isDataViewFilterOption: (obj: unknown) => obj is DataViewFilterOption;
5
+ /** extends MenuProps */
6
+ export interface DataViewCheckboxFilterProps extends Omit<MenuProps, 'onSelect' | 'onChange'> {
7
+ /** Unique key for the filter attribute */
8
+ filterId: string;
9
+ /** Array of current filter values */
10
+ value?: string[];
11
+ /** Filter title displayed in the toolbar */
12
+ title: string;
13
+ /** Placeholder text of the menu */
14
+ placeholder?: string;
15
+ /** Filter options displayed */
16
+ options: (DataViewFilterOption | string)[];
17
+ /** Callback for updating when item selection changes. */
18
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
19
+ /** Controls visibility of the filter in the toolbar */
20
+ showToolbarItem?: boolean;
21
+ /** Controls visibility of the filter icon */
22
+ showIcon?: boolean;
23
+ /** Controls visibility of the selected items badge */
24
+ showBadge?: boolean;
25
+ /** Custom OUIA ID */
26
+ ouiaId?: string;
27
+ }
28
+ export declare const DataViewCheckboxFilter: React.FC<DataViewCheckboxFilterProps>;
29
+ export default DataViewCheckboxFilter;
@@ -0,0 +1,62 @@
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 React from 'react';
13
+ import { Badge, Menu, MenuContent, MenuItem, MenuList, MenuToggle, Popper, ToolbarFilter, } from '@patternfly/react-core';
14
+ import { FilterIcon } from '@patternfly/react-icons';
15
+ const isToolbarChip = (chip) => typeof chip === 'object' && 'key' in chip;
16
+ export const isDataViewFilterOption = (obj) => !!obj &&
17
+ typeof obj === 'object' &&
18
+ 'label' in obj &&
19
+ 'value' in obj &&
20
+ typeof obj.value === 'string';
21
+ export const DataViewCheckboxFilter = (_a) => {
22
+ var { filterId, title, value = [], onChange, placeholder, options = [], showToolbarItem, showIcon = !placeholder, showBadge = !placeholder, ouiaId = 'DataViewCheckboxFilter' } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "placeholder", "options", "showToolbarItem", "showIcon", "showBadge", "ouiaId"]);
23
+ const [isOpen, setIsOpen] = React.useState(false);
24
+ const toggleRef = React.useRef(null);
25
+ const menuRef = React.useRef(null);
26
+ const containerRef = React.useRef(null);
27
+ const normalizeOptions = React.useMemo(() => options.map(option => typeof option === 'string'
28
+ ? { label: option, value: option }
29
+ : option), [options]);
30
+ const handleToggleClick = (event) => {
31
+ event.stopPropagation();
32
+ setTimeout(() => {
33
+ var _a;
34
+ const firstElement = (_a = menuRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('li > button:not(:disabled)');
35
+ firstElement === null || firstElement === void 0 ? void 0 : firstElement.focus();
36
+ }, 0);
37
+ setIsOpen(prev => !prev);
38
+ };
39
+ const handleSelect = (event, itemId) => {
40
+ const activeItem = String(itemId);
41
+ const isSelected = value.includes(activeItem);
42
+ onChange === null || onChange === void 0 ? void 0 : onChange(event, isSelected ? value.filter(item => item !== activeItem) : [activeItem, ...value]);
43
+ };
44
+ const handleClickOutside = (event) => isOpen &&
45
+ menuRef.current && toggleRef.current &&
46
+ !menuRef.current.contains(event.target) && !toggleRef.current.contains(event.target)
47
+ && setIsOpen(false);
48
+ React.useEffect(() => {
49
+ window.addEventListener('click', handleClickOutside);
50
+ return () => {
51
+ window.removeEventListener('click', handleClickOutside);
52
+ };
53
+ }, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
54
+ return (React.createElement(ToolbarFilter, { key: ouiaId, "data-ouia-component-id": ouiaId, chips: value.map(item => {
55
+ const activeOption = normalizeOptions.find(option => option.value === item);
56
+ return ({ key: activeOption === null || activeOption === void 0 ? void 0 : activeOption.value, node: activeOption === null || activeOption === void 0 ? void 0 : activeOption.label });
57
+ }), deleteChip: (_, chip) => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, value.filter(item => item !== (isToolbarChip(chip) ? chip.key : chip))), categoryName: title, showToolbarItem: showToolbarItem },
58
+ React.createElement(Popper, { trigger: React.createElement(MenuToggle, { ouiaId: `${ouiaId}-toggle`, ref: toggleRef, onClick: handleToggleClick, isExpanded: isOpen, icon: showIcon ? React.createElement(FilterIcon, null) : undefined, badge: value.length > 0 && showBadge ? React.createElement(Badge, { "data-ouia-component-id": `${ouiaId}-badge`, isRead: true }, value.length) : undefined, style: { width: '200px' } }, placeholder !== null && placeholder !== void 0 ? placeholder : title), triggerRef: toggleRef, popper: React.createElement(Menu, Object.assign({ ref: menuRef, ouiaId: `${ouiaId}-menu`, onSelect: handleSelect, selected: value }, props),
59
+ React.createElement(MenuContent, null,
60
+ React.createElement(MenuList, null, normalizeOptions.map(option => (React.createElement(MenuItem, { "data-ouia-component-id": `${ouiaId}-filter-item-${option.value}`, key: option.value, itemId: option.value, isSelected: value.includes(option.value), hasCheckbox: true }, option.label)))))), popperRef: menuRef, appendTo: containerRef.current || undefined, "aria-label": `${title !== null && title !== void 0 ? title : filterId} filter`, isVisible: isOpen })));
61
+ };
62
+ export default DataViewCheckboxFilter;
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import DataViewCheckboxFilter from './DataViewCheckboxFilter';
4
+ import DataViewToolbar from '../DataViewToolbar';
5
+ describe('DataViewCheckboxFilter component', () => {
6
+ const defaultProps = {
7
+ filterId: 'test-checkbox-filter',
8
+ title: 'Test Checkbox Filter',
9
+ value: ['workspace-one'],
10
+ options: [
11
+ { label: 'Workspace one', value: 'workspace-one' },
12
+ { label: 'Workspace two', value: 'workspace-two' },
13
+ { label: 'Workspace three', value: 'workspace-three' },
14
+ ],
15
+ };
16
+ it('should render correctly', () => {
17
+ const { container } = render(React.createElement(DataViewToolbar, { filters: React.createElement(DataViewCheckboxFilter, Object.assign({}, defaultProps)) }));
18
+ expect(container).toMatchSnapshot();
19
+ });
20
+ });
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewCheckboxFilter';
2
+ export * from './DataViewCheckboxFilter';
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewCheckboxFilter';
2
+ export * from './DataViewCheckboxFilter';
@@ -1,5 +1,11 @@
1
- import React from 'react';
1
+ import React, { ReactNode } from 'react';
2
2
  import { ToolbarToggleGroupProps } from '@patternfly/react-core';
3
+ export interface DataViewFilterOption {
4
+ /** Filter option label */
5
+ label: ReactNode;
6
+ /** Filter option value */
7
+ value: string;
8
+ }
3
9
  /** extends ToolbarToggleGroupProps */
4
10
  export interface DataViewFiltersProps<T extends object> extends Omit<ToolbarToggleGroupProps, 'toggleIcon' | 'breakpoint' | 'onChange'> {
5
11
  /** Content rendered inside the data view */
@@ -26,6 +26,19 @@ export const DataViewFilters = (_a) => {
26
26
  useEffect(() => {
27
27
  filterItems.length > 0 && setActiveAttributeMenu(filterItems[0].title);
28
28
  }, [filterItems]);
29
+ const handleClickOutside = (event) => {
30
+ var _a, _b;
31
+ return isAttributeMenuOpen &&
32
+ !((_a = attributeMenuRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target)) &&
33
+ !((_b = attributeToggleRef.current) === null || _b === void 0 ? void 0 : _b.contains(event.target))
34
+ && setIsAttributeMenuOpen(false);
35
+ };
36
+ useEffect(() => {
37
+ window.addEventListener('click', handleClickOutside);
38
+ return () => {
39
+ window.removeEventListener('click', handleClickOutside);
40
+ };
41
+ }, [isAttributeMenuOpen]); // eslint-disable-line react-hooks/exhaustive-deps
29
42
  const attributeToggle = (React.createElement(MenuToggle, { ref: attributeToggleRef, onClick: () => setIsAttributeMenuOpen(!isAttributeMenuOpen), isExpanded: isAttributeMenuOpen, icon: toggleIcon }, activeAttributeMenu));
30
43
  const attributeMenu = (React.createElement(Menu, { ref: attributeMenuRef, onSelect: (_ev, itemId) => {
31
44
  const selectedItem = filterItems.find(item => item.filterId === itemId);
@@ -38,6 +51,8 @@ export const DataViewFilters = (_a) => {
38
51
  React.createElement(ToolbarGroup, { variant: "filter-group" },
39
52
  React.createElement("div", { ref: attributeContainerRef },
40
53
  React.createElement(Popper, { trigger: attributeToggle, triggerRef: attributeToggleRef, popper: attributeMenu, popperRef: attributeMenuRef, appendTo: attributeContainerRef.current || undefined, isVisible: isAttributeMenuOpen })),
41
- React.Children.map(children, (child) => (React.isValidElement(child) ? (React.cloneElement(child, Object.assign({ showToolbarItem: activeAttributeMenu === child.props.title, onChange: (event, value) => onChange === null || onChange === void 0 ? void 0 : onChange(event, { [child.props.filterId]: value }), value: values === null || values === void 0 ? void 0 : values[child.props.filterId] }, child.props))) : child)))));
54
+ React.Children.map(children, (child) => React.isValidElement(child)
55
+ ? React.cloneElement(child, Object.assign({ showToolbarItem: activeAttributeMenu === child.props.title, onChange: (event, value) => onChange === null || onChange === void 0 ? void 0 : onChange(event, { [child.props.filterId]: value }), value: values === null || values === void 0 ? void 0 : values[child.props.filterId] }, child.props))
56
+ : child))));
42
57
  };
43
58
  export default DataViewFilters;
@@ -14,19 +14,31 @@ import { Table, Tbody, Td, TreeRowWrapper, } from '@patternfly/react-table';
14
14
  import { useInternalContext } from '../InternalContext';
15
15
  import { DataViewTableHead } from '../DataViewTableHead';
16
16
  import { isDataViewTdObject } from '../DataViewTable';
17
- const getDescendants = (node) => (!node.children || !node.children.length) ? [node] : node.children.flatMap(getDescendants);
18
- const isNodeChecked = (node, isSelected) => {
19
- let allSelected = true;
20
- let someSelected = false;
21
- for (const descendant of getDescendants(node)) {
22
- const selected = !!(isSelected === null || isSelected === void 0 ? void 0 : isSelected(descendant));
23
- someSelected || (someSelected = selected);
24
- allSelected && (allSelected = selected);
25
- if (!allSelected && someSelected) {
26
- return null;
17
+ const getNodesAffectedBySelection = (allRows, node, isChecking, isSelected) => {
18
+ const getDescendants = (node) => node.children ? node.children.flatMap(getDescendants).concat(node) : [node];
19
+ const findParent = (child, rows) => {
20
+ var _a;
21
+ return (_a = rows.find(row => { var _a; return (_a = row.children) === null || _a === void 0 ? void 0 : _a.some(c => c === child); })) !== null && _a !== void 0 ? _a : rows.flatMap(row => { var _a; return (_a = row.children) !== null && _a !== void 0 ? _a : []; }).map(c => findParent(child, [c])).find(p => p);
22
+ };
23
+ const getAncestors = (node) => {
24
+ const ancestors = [];
25
+ let parent = findParent(node, allRows);
26
+ while (parent) {
27
+ ancestors.push(parent);
28
+ parent = findParent(parent, allRows);
27
29
  }
28
- }
29
- return allSelected;
30
+ return ancestors;
31
+ };
32
+ const affectedNodes = new Set([node, ...getDescendants(node)]);
33
+ getAncestors(node).forEach(ancestor => {
34
+ var _a, _b;
35
+ const allChildrenSelected = (_a = ancestor.children) === null || _a === void 0 ? void 0 : _a.every(child => (isSelected === null || isSelected === void 0 ? void 0 : isSelected(child)) || affectedNodes.has(child));
36
+ const anyChildAffected = (_b = ancestor.children) === null || _b === void 0 ? void 0 : _b.some(child => affectedNodes.has(child) || child.id === node.id);
37
+ if (isChecking ? !(isSelected === null || isSelected === void 0 ? void 0 : isSelected(ancestor)) && allChildrenSelected : (isSelected === null || isSelected === void 0 ? void 0 : isSelected(ancestor)) && anyChildAffected) {
38
+ affectedNodes.add(ancestor);
39
+ }
40
+ });
41
+ return Array.from(affectedNodes);
30
42
  };
31
43
  export const DataViewTableTree = (_a) => {
32
44
  var { columns, rows, headStates, bodyStates, leafIcon = null, expandedIcon = null, collapsedIcon = null, ouiaId = 'DataViewTableTree' } = _a, props = __rest(_a, ["columns", "rows", "headStates", "bodyStates", "leafIcon", "expandedIcon", "collapsedIcon", "ouiaId"]);
@@ -44,7 +56,7 @@ export const DataViewTableTree = (_a) => {
44
56
  }
45
57
  const isExpanded = expandedNodeIds.includes(node.id);
46
58
  const isDetailsExpanded = expandedDetailsNodeNames.includes(node.id);
47
- const isChecked = isSelected && isNodeChecked(node, isSelected);
59
+ const isChecked = isSelected === null || isSelected === void 0 ? void 0 : isSelected(node);
48
60
  let icon = leafIcon;
49
61
  if (node.children) {
50
62
  icon = isExpanded ? expandedIcon : collapsedIcon;
@@ -58,7 +70,7 @@ export const DataViewTableTree = (_a) => {
58
70
  const otherDetailsExpandedNodeIds = prevDetailsExpanded.filter(id => id !== node.id);
59
71
  return isDetailsExpanded ? otherDetailsExpandedNodeIds : [...otherDetailsExpandedNodeIds, node.id];
60
72
  }),
61
- onCheckChange: ((isSelectDisabled === null || isSelectDisabled === void 0 ? void 0 : isSelectDisabled(node)) || !onSelect) ? undefined : (_event, isChecking) => onSelect === null || onSelect === void 0 ? void 0 : onSelect(isChecking, getDescendants(node)),
73
+ onCheckChange: ((isSelectDisabled === null || isSelectDisabled === void 0 ? void 0 : isSelectDisabled(node)) || !onSelect) ? undefined : (_event, isChecking) => onSelect === null || onSelect === void 0 ? void 0 : onSelect(isChecking, getNodesAffectedBySelection(rows, node, isChecking, isSelected)),
62
74
  rowIndex,
63
75
  props: {
64
76
  isExpanded,
@@ -13,7 +13,7 @@ import React from 'react';
13
13
  import { SearchInput, ToolbarFilter } from '@patternfly/react-core';
14
14
  export const DataViewTextFilter = (_a) => {
15
15
  var { filterId, title, value = '', onChange, onClear = () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), showToolbarItem, trimValue = true, ouiaId = 'DataViewTextFilter' } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "onClear", "showToolbarItem", "trimValue", "ouiaId"]);
16
- return (React.createElement(ToolbarFilter, { "data-ouia-component-id": ouiaId, chips: value.length > 0 ? [{ key: title, node: value }] : [], deleteChip: () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), categoryName: title, showToolbarItem: showToolbarItem },
16
+ return (React.createElement(ToolbarFilter, { key: ouiaId, "data-ouia-component-id": ouiaId, chips: value.length > 0 ? [{ key: title, node: value }] : [], deleteChip: () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), categoryName: title, showToolbarItem: showToolbarItem },
17
17
  React.createElement(SearchInput, Object.assign({ searchInputId: filterId, value: value, onChange: (e, inputValue) => onChange === null || onChange === void 0 ? void 0 : onChange(e, trimValue ? inputValue.trim() : inputValue), onClear: onClear, placeholder: `Filter by ${title}`, "aria-label": `${title !== null && title !== void 0 ? title : filterId} filter`, "data-ouia-component-id": `${ouiaId}-input` }, props))));
18
18
  };
19
19
  export default DataViewTextFilter;