@patternfly/react-data-view 5.5.1 → 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 (36) 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/DataViewTextFilter/DataViewTextFilter.js +1 -1
  10. package/dist/cjs/Hooks/filters.js +13 -14
  11. package/dist/cjs/index.d.ts +2 -0
  12. package/dist/cjs/index.js +4 -1
  13. package/dist/dynamic/DataViewCheckboxFilter/package.json +1 -0
  14. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.d.ts +29 -0
  15. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.js +62 -0
  16. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.d.ts +1 -0
  17. package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.js +20 -0
  18. package/dist/esm/DataViewCheckboxFilter/index.d.ts +2 -0
  19. package/dist/esm/DataViewCheckboxFilter/index.js +2 -0
  20. package/dist/esm/DataViewFilters/DataViewFilters.d.ts +7 -1
  21. package/dist/esm/DataViewFilters/DataViewFilters.js +16 -1
  22. package/dist/esm/DataViewTextFilter/DataViewTextFilter.js +1 -1
  23. package/dist/esm/Hooks/filters.js +13 -14
  24. package/dist/esm/index.d.ts +2 -0
  25. package/dist/esm/index.js +2 -0
  26. package/package.json +1 -1
  27. package/patternfly-docs/content/extensions/data-view/examples/Functionality/FiltersExample.tsx +31 -16
  28. package/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md +4 -3
  29. package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.test.tsx +24 -0
  30. package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.tsx +175 -0
  31. package/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap +194 -0
  32. package/src/DataViewCheckboxFilter/index.ts +2 -0
  33. package/src/DataViewFilters/DataViewFilters.tsx +26 -7
  34. package/src/DataViewTextFilter/DataViewTextFilter.tsx +1 -0
  35. package/src/Hooks/filters.ts +14 -13
  36. 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;
@@ -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;
@@ -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;
@@ -2,25 +2,24 @@ import { useState, useCallback, useEffect, useMemo } from "react";
2
2
  ;
3
3
  export const useDataViewFilters = ({ initialFilters = {}, searchParams, setSearchParams, }) => {
4
4
  const isUrlSyncEnabled = useMemo(() => searchParams && !!setSearchParams, [searchParams, setSearchParams]);
5
- const getInitialFilters = useCallback(() => isUrlSyncEnabled ? Object.keys(initialFilters).reduce((loadedFilters, key) => {
6
- const urlValue = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(key);
7
- loadedFilters[key] = urlValue
8
- ? urlValue
9
- : initialFilters[key];
10
- return loadedFilters;
11
- // eslint-disable-next-line react-hooks/exhaustive-deps
12
- }, Object.assign({}, initialFilters)) : initialFilters, [isUrlSyncEnabled, JSON.stringify(initialFilters), searchParams === null || searchParams === void 0 ? void 0 : searchParams.toString()]);
5
+ const getInitialFilters = useCallback(() => isUrlSyncEnabled
6
+ ? Object.keys(initialFilters).reduce((loadedFilters, key) => {
7
+ const urlValue = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(key);
8
+ const isArrayFilter = Array.isArray(initialFilters[key]);
9
+ // eslint-disable-next-line no-nested-ternary
10
+ loadedFilters[key] = urlValue
11
+ ? (isArrayFilter && !Array.isArray(urlValue) ? [urlValue] : urlValue)
12
+ : initialFilters[key];
13
+ return loadedFilters;
14
+ }, Object.assign({}, initialFilters))
15
+ : initialFilters, [isUrlSyncEnabled, initialFilters, searchParams]);
13
16
  const [filters, setFilters] = useState(getInitialFilters());
14
17
  const updateSearchParams = useCallback((newFilters) => {
15
18
  if (isUrlSyncEnabled) {
16
19
  const params = new URLSearchParams(searchParams);
17
20
  Object.entries(newFilters).forEach(([key, value]) => {
18
- if (value) {
19
- params.set(key, Array.isArray(value) ? value.join(',') : value);
20
- }
21
- else {
22
- params.delete(key);
23
- }
21
+ params.delete(key);
22
+ (Array.isArray(value) ? value : [value]).forEach((val) => value && params.append(key, val));
24
23
  });
25
24
  setSearchParams === null || setSearchParams === void 0 ? void 0 : setSearchParams(params);
26
25
  }
@@ -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/esm/index.js CHANGED
@@ -16,5 +16,7 @@ export { default as DataViewTable } from './DataViewTable';
16
16
  export * from './DataViewTable';
17
17
  export { default as DataViewEventsContext } from './DataViewEventsContext';
18
18
  export * from './DataViewEventsContext';
19
+ export { default as DataViewCheckboxFilter } from './DataViewCheckboxFilter';
20
+ export * from './DataViewCheckboxFilter';
19
21
  export { default as DataView } from './DataView';
20
22
  export * from './DataView';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-data-view",
3
- "version": "5.5.1",
3
+ "version": "5.6.0",
4
4
  "description": "Data view used for Red Hat projects.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -5,8 +5,9 @@ import { useDataViewFilters, useDataViewPagination } from '@patternfly/react-dat
5
5
  import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
6
6
  import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
7
7
  import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
8
- import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
8
+ import { DataViewFilterOption, DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
9
9
  import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
10
+ import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter';
10
11
 
11
12
  const perPageOptions = [
12
13
  { title: '5', value: 5 },
@@ -17,38 +18,51 @@ interface Repository {
17
18
  name: string;
18
19
  branch: string | null;
19
20
  prs: string | null;
20
- workspaces: string;
21
+ workspace: string;
21
22
  lastCommit: string;
22
23
  }
23
24
 
24
25
  interface RepositoryFilters {
25
26
  name: string,
26
- branch: string
27
+ branch: string,
28
+ workspace: string[]
27
29
  }
28
30
 
29
31
  const repositories: Repository[] = [
30
- { name: 'Repository one', branch: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
31
- { name: 'Repository two', branch: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
32
- { name: 'Repository three', branch: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
33
- { name: 'Repository four', branch: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
34
- { name: 'Repository five', branch: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
35
- { name: 'Repository six', branch: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
32
+ { name: 'Repository one', branch: 'Branch one', prs: 'Pull request one', workspace: 'Workspace one', lastCommit: 'Timestamp one' },
33
+ { name: 'Repository two', branch: 'Branch two', prs: 'Pull request two', workspace: 'Workspace two', lastCommit: 'Timestamp two' },
34
+ { name: 'Repository three', branch: 'Branch three', prs: 'Pull request three', workspace: 'Workspace one', lastCommit: 'Timestamp three' },
35
+ { name: 'Repository four', branch: 'Branch four', prs: 'Pull request four', workspace: 'Workspace one', lastCommit: 'Timestamp four' },
36
+ { name: 'Repository five', branch: 'Branch five', prs: 'Pull request five', workspace: 'Workspace two', lastCommit: 'Timestamp five' },
37
+ { name: 'Repository six', branch: 'Branch six', prs: 'Pull request six', workspace: 'Workspace three', lastCommit: 'Timestamp six' }
36
38
  ];
37
39
 
38
- const columns = [ 'Name', 'Branch', 'Pull requests', 'Workspaces', 'Last commit' ];
40
+ const filterOptions: DataViewFilterOption[] = [
41
+ { label: 'Workspace one', value: 'workspace-one' },
42
+ { label: 'Workspace two', value: 'workspace-two' },
43
+ { label: 'Workspace three', value: 'workspace-three' }
44
+ ];
45
+
46
+ const columns = [ 'Name', 'Branch', 'Pull requests', 'Workspace', 'Last commit' ];
39
47
 
40
48
  const ouiaId = 'LayoutExample';
41
49
 
42
50
  const MyTable: React.FunctionComponent = () => {
43
51
  const [ searchParams, setSearchParams ] = useSearchParams();
52
+ const { filters, onSetFilters, clearAllFilters } = useDataViewFilters<RepositoryFilters>({ initialFilters: { name: '', branch: '', workspace: [] }, searchParams, setSearchParams });
44
53
  const pagination = useDataViewPagination({ perPage: 5 });
45
54
  const { page, perPage } = pagination;
46
- const { filters, onSetFilters, clearAllFilters } = useDataViewFilters<RepositoryFilters>({ initialFilters: { name: '', branch: '' }, searchParams, setSearchParams });
47
55
 
48
- const pageRows = useMemo(() => repositories
49
- .filter(item => (!filters.name || item.name?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) && (!filters.branch || item.branch?.toLocaleLowerCase().includes(filters.branch?.toLocaleLowerCase())))
56
+ const filteredData = useMemo(() => repositories.filter(item =>
57
+ (!filters.name || item.name?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) &&
58
+ (!filters.branch || item.branch?.toLocaleLowerCase().includes(filters.branch?.toLocaleLowerCase())) &&
59
+ (!filters.workspace || filters.workspace.length === 0 || filters.workspace.includes(String(filterOptions.find(option => option.label === item.workspace)?.value)))
60
+ ), [ filters ]);
61
+
62
+ const pageRows = useMemo(() => filteredData
50
63
  .slice((page - 1) * perPage, ((page - 1) * perPage) + perPage)
51
- .map(item => Object.values(item)), [ page, perPage, filters ]);
64
+ .map(item => Object.values(item)),
65
+ [ page, perPage, filteredData ]);
52
66
 
53
67
  return (
54
68
  <DataView>
@@ -58,7 +72,7 @@ const MyTable: React.FunctionComponent = () => {
58
72
  pagination={
59
73
  <Pagination
60
74
  perPageOptions={perPageOptions}
61
- itemCount={repositories.length}
75
+ itemCount={filteredData.length}
62
76
  {...pagination}
63
77
  />
64
78
  }
@@ -66,6 +80,7 @@ const MyTable: React.FunctionComponent = () => {
66
80
  <DataViewFilters onChange={(_e, values) => onSetFilters(values)} values={filters}>
67
81
  <DataViewTextFilter filterId="name" title='Name' placeholder='Filter by name' />
68
82
  <DataViewTextFilter filterId="branch" title='Branch' placeholder='Filter by branch' />
83
+ <DataViewCheckboxFilter filterId="workspace" title='Workspace' placeholder='Filter by workspace' options={filterOptions} />
69
84
  </DataViewFilters>
70
85
  }
71
86
  />
@@ -76,7 +91,7 @@ const MyTable: React.FunctionComponent = () => {
76
91
  <Pagination
77
92
  isCompact
78
93
  perPageOptions={perPageOptions}
79
- itemCount={repositories.length}
94
+ itemCount={filteredData.length}
80
95
  {...pagination}
81
96
  />
82
97
  }
@@ -11,7 +11,7 @@ source: react
11
11
  # If you use typescript, the name of the interface to display props for
12
12
  # These are found through the sourceProps function provided in patternfly-docs.source.js
13
13
  sortValue: 3
14
- propComponents: ['DataViewFilters', 'DataViewTextFilter']
14
+ propComponents: ['DataViewFilters', 'DataViewTextFilter', 'DataViewCheckboxFilter']
15
15
  sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
16
16
  ---
17
17
  import { useMemo } from 'react';
@@ -23,6 +23,7 @@ import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataVi
23
23
  import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
24
24
  import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
25
25
  import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
26
+ import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter';
26
27
 
27
28
  This is a list of functionality you can use to manage data displayed in the **data view**.
28
29
 
@@ -92,7 +93,7 @@ The `useDataViewSelection` hook manages the selection state of the data view.
92
93
  Enables filtering of data records in the data view and displays the applied filter chips.
93
94
 
94
95
  ### Toolbar usage
95
- The data view toolbar can include a set of filters by passing a React node to the `filters` property. You can use predefined components `DataViewFilters` and `DataViewTextFilter` to customize and handle filtering directly in the toolbar. The `DataViewFilters` is a wrapper allowing conditional filtering using multiple attributes. If you need just a single filter, you can use `DataViewTextFilter` or a different filter component alone. Props of these filter components are listed at the bottom of this page.
96
+ The data view toolbar can include a set of filters by passing a React node to the `filters` property. You can use predefined components `DataViewFilters`, `DataViewTextFilter` and `DataViewCheckboxFilter` to customize and handle filtering directly in the toolbar. The `DataViewFilters` is a wrapper allowing conditional filtering using multiple attributes. If you need just a single filter, you can use `DataViewTextFilter`, `DataViewCheckboxFilter` or a different filter component alone. Props of these filter components are listed at the bottom of this page.
96
97
 
97
98
  You can decide between passing `value` and `onChange` event to every filter separately or pass `values` and `onChange` to the `DataViewFilters` wrapper which make them available to its children. Props directly passed to child filters have a higher priority than the "inherited" ones.
98
99
 
@@ -101,7 +102,7 @@ You can decide between passing `value` and `onChange` event to every filter sepa
101
102
  The `useDataViewFilters` hook manages the filter state of the data view. It allows you to define default filter values, synchronize filter state with URL parameters, and handle filter changes efficiently.
102
103
 
103
104
  **Initial values:**
104
- - `initialFilters` object with default filter values
105
+ - `initialFilters` object with default filter values (if the filter param allows multiple values, pass an array)
105
106
  - optional `searchParams` object for managing URL-based filter state
106
107
  - optional `setSearchParams` function to update the URL when filters are modified
107
108
 
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import DataViewCheckboxFilter, { DataViewCheckboxFilterProps } from './DataViewCheckboxFilter';
4
+ import DataViewToolbar from '../DataViewToolbar';
5
+
6
+ describe('DataViewCheckboxFilter component', () => {
7
+ const defaultProps: DataViewCheckboxFilterProps = {
8
+ filterId: 'test-checkbox-filter',
9
+ title: 'Test Checkbox Filter',
10
+ value: [ 'workspace-one' ],
11
+ options: [
12
+ { label: 'Workspace one', value: 'workspace-one' },
13
+ { label: 'Workspace two', value: 'workspace-two' },
14
+ { label: 'Workspace three', value: 'workspace-three' },
15
+ ],
16
+ };
17
+
18
+ it('should render correctly', () => {
19
+ const { container } = render(
20
+ <DataViewToolbar filters={<DataViewCheckboxFilter {...defaultProps} />} />
21
+ );
22
+ expect(container).toMatchSnapshot();
23
+ });
24
+ });
@@ -0,0 +1,175 @@
1
+ import React from 'react';
2
+ import {
3
+ Badge,
4
+ Menu,
5
+ MenuContent,
6
+ MenuItem,
7
+ MenuList,
8
+ MenuProps,
9
+ MenuToggle,
10
+ Popper,
11
+ ToolbarChip,
12
+ ToolbarFilter,
13
+ } from '@patternfly/react-core';
14
+ import { FilterIcon } from '@patternfly/react-icons';
15
+ import { DataViewFilterOption } from '../DataViewFilters';
16
+
17
+ const isToolbarChip = (chip: string | ToolbarChip): chip is ToolbarChip =>
18
+ typeof chip === 'object' && 'key' in chip;
19
+
20
+ export const isDataViewFilterOption = (obj: unknown): obj is DataViewFilterOption =>
21
+ !!obj &&
22
+ typeof obj === 'object' &&
23
+ 'label' in obj &&
24
+ 'value' in obj &&
25
+ typeof (obj as DataViewFilterOption).value === 'string';
26
+
27
+ /** extends MenuProps */
28
+ export interface DataViewCheckboxFilterProps extends Omit<MenuProps, 'onSelect' | 'onChange'> {
29
+ /** Unique key for the filter attribute */
30
+ filterId: string;
31
+ /** Array of current filter values */
32
+ value?: string[];
33
+ /** Filter title displayed in the toolbar */
34
+ title: string;
35
+ /** Placeholder text of the menu */
36
+ placeholder?: string;
37
+ /** Filter options displayed */
38
+ options: (DataViewFilterOption | string)[];
39
+ /** Callback for updating when item selection changes. */
40
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
41
+ /** Controls visibility of the filter in the toolbar */
42
+ showToolbarItem?: boolean;
43
+ /** Controls visibility of the filter icon */
44
+ showIcon?: boolean;
45
+ /** Controls visibility of the selected items badge */
46
+ showBadge?: boolean;
47
+ /** Custom OUIA ID */
48
+ ouiaId?: string;
49
+ }
50
+
51
+ export const DataViewCheckboxFilter: React.FC<DataViewCheckboxFilterProps> = ({
52
+ filterId,
53
+ title,
54
+ value = [],
55
+ onChange,
56
+ placeholder,
57
+ options = [],
58
+ showToolbarItem,
59
+ showIcon = !placeholder,
60
+ showBadge = !placeholder,
61
+ ouiaId = 'DataViewCheckboxFilter',
62
+ ...props
63
+ }: DataViewCheckboxFilterProps) => {
64
+ const [ isOpen, setIsOpen ] = React.useState(false);
65
+ const toggleRef = React.useRef<HTMLButtonElement>(null);
66
+ const menuRef = React.useRef<HTMLDivElement>(null);
67
+ const containerRef = React.useRef<HTMLDivElement>(null);
68
+
69
+ const normalizeOptions = React.useMemo(
70
+ () =>
71
+ options.map(option =>
72
+ typeof option === 'string'
73
+ ? { label: option, value: option }
74
+ : option
75
+ ),
76
+ [ options ]
77
+ );
78
+
79
+ const handleToggleClick = (event: React.MouseEvent) => {
80
+ event.stopPropagation();
81
+ setTimeout(() => {
82
+ const firstElement = menuRef.current?.querySelector('li > button:not(:disabled)') as HTMLElement;
83
+ firstElement?.focus();
84
+ }, 0);
85
+ setIsOpen(prev => !prev);
86
+ };
87
+
88
+ const handleSelect = (event?: React.MouseEvent, itemId?: string | number) => {
89
+ const activeItem = String(itemId);
90
+ const isSelected = value.includes(activeItem);
91
+
92
+ onChange?.(
93
+ event,
94
+ isSelected ? value.filter(item => item !== activeItem) : [ activeItem, ...value ]
95
+ );
96
+ };
97
+
98
+ const handleClickOutside = (event: MouseEvent) =>
99
+ isOpen &&
100
+ menuRef.current && toggleRef.current &&
101
+ !menuRef.current.contains(event.target as Node) && !toggleRef.current.contains(event.target as Node)
102
+ && setIsOpen(false);
103
+
104
+
105
+ React.useEffect(() => {
106
+ window.addEventListener('click', handleClickOutside);
107
+ return () => {
108
+ window.removeEventListener('click', handleClickOutside);
109
+ };
110
+ }, [ isOpen ]); // eslint-disable-line react-hooks/exhaustive-deps
111
+
112
+ return (
113
+ <ToolbarFilter
114
+ key={ouiaId}
115
+ data-ouia-component-id={ouiaId}
116
+ chips={value.map(item => {
117
+ const activeOption = normalizeOptions.find(option => option.value === item);
118
+ return ({ key: activeOption?.value as string, node: activeOption?.label })
119
+ })}
120
+ deleteChip={(_, chip) =>
121
+ onChange?.(undefined, value.filter(item => item !== (isToolbarChip(chip) ? chip.key : chip)))
122
+ }
123
+ categoryName={title}
124
+ showToolbarItem={showToolbarItem}
125
+ >
126
+ <Popper
127
+ trigger={
128
+ <MenuToggle
129
+ ouiaId={`${ouiaId}-toggle`}
130
+ ref={toggleRef}
131
+ onClick={handleToggleClick}
132
+ isExpanded={isOpen}
133
+ icon={showIcon ? <FilterIcon /> : undefined}
134
+ badge={value.length > 0 && showBadge ? <Badge data-ouia-component-id={`${ouiaId}-badge`} isRead>{value.length}</Badge> : undefined}
135
+ style={{ width: '200px' }}
136
+ >
137
+ {placeholder ?? title}
138
+ </MenuToggle>
139
+ }
140
+ triggerRef={toggleRef}
141
+ popper={
142
+ <Menu
143
+ ref={menuRef}
144
+ ouiaId={`${ouiaId}-menu`}
145
+ onSelect={handleSelect}
146
+ selected={value}
147
+ {...props}
148
+ >
149
+ <MenuContent>
150
+ <MenuList>
151
+ {normalizeOptions.map(option => (
152
+ <MenuItem
153
+ data-ouia-component-id={`${ouiaId}-filter-item-${option.value}`}
154
+ key={option.value}
155
+ itemId={option.value}
156
+ isSelected={value.includes(option.value)}
157
+ hasCheckbox
158
+ >
159
+ {option.label}
160
+ </MenuItem>
161
+ ))}
162
+ </MenuList>
163
+ </MenuContent>
164
+ </Menu>
165
+ }
166
+ popperRef={menuRef}
167
+ appendTo={containerRef.current || undefined}
168
+ aria-label={`${title ?? filterId} filter`}
169
+ isVisible={isOpen}
170
+ />
171
+ </ToolbarFilter>
172
+ );
173
+ };
174
+
175
+ export default DataViewCheckboxFilter;
@@ -0,0 +1,194 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`DataViewCheckboxFilter component should render correctly 1`] = `
4
+ <div>
5
+ <div
6
+ class="pf-v5-c-toolbar"
7
+ data-ouia-component-id="DataViewToolbar"
8
+ data-ouia-component-type="PF5/Toolbar"
9
+ data-ouia-safe="true"
10
+ id="pf-random-id-0"
11
+ >
12
+ <div
13
+ class="pf-v5-c-toolbar__content"
14
+ >
15
+ <div
16
+ class="pf-v5-c-toolbar__content-section"
17
+ >
18
+ <div
19
+ class="pf-v5-c-toolbar__item pf-m-search-filter"
20
+ >
21
+ <div
22
+ class="pf-v5-c-toolbar__item"
23
+ data-ouia-component-id="DataViewCheckboxFilter"
24
+ >
25
+ <button
26
+ aria-expanded="false"
27
+ class="pf-v5-c-menu-toggle"
28
+ data-ouia-component-id="DataViewCheckboxFilter-toggle"
29
+ data-ouia-component-type="PF5/MenuToggle"
30
+ data-ouia-safe="true"
31
+ style="width: 200px;"
32
+ type="button"
33
+ >
34
+ <span
35
+ class="pf-v5-c-menu-toggle__icon"
36
+ >
37
+ <svg
38
+ aria-hidden="true"
39
+ class="pf-v5-svg"
40
+ fill="currentColor"
41
+ height="1em"
42
+ role="img"
43
+ viewBox="0 0 512 512"
44
+ width="1em"
45
+ >
46
+ <path
47
+ d="M487.976 0H24.028C2.71 0-8.047 25.866 7.058 40.971L192 225.941V432c0 7.831 3.821 15.17 10.237 19.662l80 55.98C298.02 518.69 320 507.493 320 487.98V225.941l184.947-184.97C520.021 25.896 509.338 0 487.976 0z"
48
+ />
49
+ </svg>
50
+ </span>
51
+ <span
52
+ class="pf-v5-c-menu-toggle__text"
53
+ >
54
+ Test Checkbox Filter
55
+ </span>
56
+ <span
57
+ class="pf-v5-c-menu-toggle__count"
58
+ >
59
+ <span
60
+ class="pf-v5-c-badge pf-m-read"
61
+ data-ouia-component-id="DataViewCheckboxFilter-badge"
62
+ >
63
+ 1
64
+ </span>
65
+ </span>
66
+ <span
67
+ class="pf-v5-c-menu-toggle__controls"
68
+ >
69
+ <span
70
+ class="pf-v5-c-menu-toggle__toggle-icon"
71
+ >
72
+ <svg
73
+ aria-hidden="true"
74
+ class="pf-v5-svg"
75
+ fill="currentColor"
76
+ height="1em"
77
+ role="img"
78
+ viewBox="0 0 320 512"
79
+ width="1em"
80
+ >
81
+ <path
82
+ d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"
83
+ />
84
+ </svg>
85
+ </span>
86
+ </span>
87
+ </button>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ <div
93
+ class="pf-v5-c-toolbar__content pf-m-chip-container"
94
+ >
95
+ <div
96
+ class="pf-v5-c-toolbar__group"
97
+ >
98
+ <div
99
+ class="pf-v5-c-toolbar__item pf-m-chip-group"
100
+ >
101
+ <div
102
+ aria-labelledby="pf-random-id-1"
103
+ class="pf-v5-c-chip-group pf-m-category"
104
+ data-ouia-component-type="PF5/ChipGroup"
105
+ data-ouia-safe="true"
106
+ role="group"
107
+ >
108
+ <div
109
+ class="pf-v5-c-chip-group__main"
110
+ >
111
+ <span
112
+ class="pf-v5-c-chip-group__label"
113
+ id="pf-random-id-1"
114
+ >
115
+ Test Checkbox Filter
116
+ </span>
117
+ <ul
118
+ aria-labelledby="pf-random-id-1"
119
+ class="pf-v5-c-chip-group__list"
120
+ role="list"
121
+ >
122
+ <li
123
+ class="pf-v5-c-chip-group__list-item"
124
+ >
125
+ <div
126
+ class="pf-v5-c-chip"
127
+ data-ouia-component-id="OUIA-Generated-Chip-1"
128
+ data-ouia-component-type="PF5/Chip"
129
+ data-ouia-safe="true"
130
+ >
131
+ <span
132
+ class="pf-v5-c-chip__content"
133
+ >
134
+ <span
135
+ class="pf-v5-c-chip__text"
136
+ id="pf-random-id-2"
137
+ >
138
+ Workspace one
139
+ </span>
140
+ </span>
141
+ <span
142
+ class="pf-v5-c-chip__actions"
143
+ >
144
+ <button
145
+ aria-disabled="false"
146
+ aria-label="close"
147
+ aria-labelledby="remove_pf-random-id-2 pf-random-id-2"
148
+ class="pf-v5-c-button pf-m-plain"
149
+ data-ouia-component-id="close"
150
+ data-ouia-component-type="PF5/Button"
151
+ data-ouia-safe="true"
152
+ id="remove_pf-random-id-2"
153
+ type="button"
154
+ >
155
+ <svg
156
+ aria-hidden="true"
157
+ class="pf-v5-svg"
158
+ fill="currentColor"
159
+ height="1em"
160
+ role="img"
161
+ viewBox="0 0 352 512"
162
+ width="1em"
163
+ >
164
+ <path
165
+ d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"
166
+ />
167
+ </svg>
168
+ </button>
169
+ </span>
170
+ </div>
171
+ </li>
172
+ </ul>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ <div
178
+ class="pf-v5-c-toolbar__item"
179
+ >
180
+ <button
181
+ aria-disabled="false"
182
+ class="pf-v5-c-button pf-m-link pf-m-inline"
183
+ data-ouia-component-id="DataViewToolbar-clear-all-filters"
184
+ data-ouia-component-type="PF5/Button"
185
+ data-ouia-safe="true"
186
+ type="button"
187
+ >
188
+ Clear filters
189
+ </button>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ `;
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewCheckboxFilter';
2
+ export * from './DataViewCheckboxFilter';
@@ -1,9 +1,16 @@
1
- import React, { useMemo, useState, useRef, useEffect, ReactElement } from 'react';
1
+ import React, { useMemo, useState, useRef, useEffect, ReactElement, ReactNode } from 'react';
2
2
  import {
3
3
  Menu, MenuContent, MenuItem, MenuList, MenuToggle, Popper, ToolbarGroup, ToolbarToggleGroup, ToolbarToggleGroupProps,
4
4
  } from '@patternfly/react-core';
5
5
  import { FilterIcon } from '@patternfly/react-icons';
6
6
 
7
+ export interface DataViewFilterOption {
8
+ /** Filter option label */
9
+ label: ReactNode;
10
+ /** Filter option value */
11
+ value: string;
12
+ }
13
+
7
14
  // helper interface to generate attribute menu
8
15
  interface DataViewFilterIdentifiers {
9
16
  filterId: string;
@@ -57,6 +64,19 @@ export const DataViewFilters = <T extends object>({
57
64
  filterItems.length > 0 && setActiveAttributeMenu(filterItems[0].title);
58
65
  }, [ filterItems ]);
59
66
 
67
+ const handleClickOutside = (event: MouseEvent) =>
68
+ isAttributeMenuOpen &&
69
+ !attributeMenuRef.current?.contains(event.target as Node) &&
70
+ !attributeToggleRef.current?.contains(event.target as Node)
71
+ && setIsAttributeMenuOpen(false);
72
+
73
+ useEffect(() => {
74
+ window.addEventListener('click', handleClickOutside);
75
+ return () => {
76
+ window.removeEventListener('click', handleClickOutside);
77
+ };
78
+ }, [ isAttributeMenuOpen ]); // eslint-disable-line react-hooks/exhaustive-deps
79
+
60
80
  const attributeToggle = (
61
81
  <MenuToggle
62
82
  ref={attributeToggleRef}
@@ -102,9 +122,9 @@ export const DataViewFilters = <T extends object>({
102
122
  isVisible={isAttributeMenuOpen}
103
123
  />
104
124
  </div>
105
- {React.Children.map(children, (child) => (
106
- React.isValidElement(child) ? (
107
- React.cloneElement(child as ReactElement<{
125
+ {React.Children.map(children, (child) =>
126
+ React.isValidElement(child)
127
+ ? React.cloneElement(child as ReactElement<{
108
128
  showToolbarItem: boolean;
109
129
  onChange: (_e: unknown, values: unknown) => void;
110
130
  value: unknown;
@@ -114,9 +134,8 @@ export const DataViewFilters = <T extends object>({
114
134
  value: values?.[child.props.filterId],
115
135
  ...child.props
116
136
  })
117
- ) : child
118
- ))}
119
-
137
+ : child
138
+ )}
120
139
  </ToolbarGroup>
121
140
  </ToolbarToggleGroup>
122
141
  );
@@ -31,6 +31,7 @@ export const DataViewTextFilter: React.FC<DataViewTextFilterProps> = ({
31
31
  ...props
32
32
  }: DataViewTextFilterProps) => (
33
33
  <ToolbarFilter
34
+ key={ouiaId}
34
35
  data-ouia-component-id={ouiaId}
35
36
  chips={value.length > 0 ? [ { key: title, node: value } ] : []}
36
37
  deleteChip={() => onChange?.(undefined, '')}
@@ -16,15 +16,19 @@ export const useDataViewFilters = <T extends object>({
16
16
  }: UseDataViewFiltersProps<T>) => {
17
17
  const isUrlSyncEnabled = useMemo(() => searchParams && !!setSearchParams, [ searchParams, setSearchParams ]);
18
18
 
19
- const getInitialFilters = useCallback((): T => isUrlSyncEnabled ? Object.keys(initialFilters).reduce((loadedFilters, key) => {
20
- const urlValue = searchParams?.get(key);
21
- loadedFilters[key as keyof T] = urlValue
22
- ? (urlValue as T[keyof T] | T[keyof T])
23
- : initialFilters[key as keyof T];
24
- return loadedFilters;
25
- // eslint-disable-next-line react-hooks/exhaustive-deps
26
- }, { ...initialFilters }) : initialFilters, [ isUrlSyncEnabled, JSON.stringify(initialFilters), searchParams?.toString() ]);
19
+ const getInitialFilters = useCallback((): T => isUrlSyncEnabled
20
+ ? Object.keys(initialFilters).reduce((loadedFilters, key) => {
21
+ const urlValue = searchParams?.get(key);
22
+ const isArrayFilter = Array.isArray(initialFilters[key]);
27
23
 
24
+ // eslint-disable-next-line no-nested-ternary
25
+ loadedFilters[key] = urlValue
26
+ ? (isArrayFilter && !Array.isArray(urlValue) ? [ urlValue ] : urlValue)
27
+ : initialFilters[key];
28
+
29
+ return loadedFilters;
30
+ }, { ...initialFilters })
31
+ : initialFilters, [ isUrlSyncEnabled, initialFilters, searchParams ]);
28
32
  const [ filters, setFilters ] = useState<T>(getInitialFilters());
29
33
 
30
34
  const updateSearchParams = useCallback(
@@ -32,11 +36,8 @@ export const useDataViewFilters = <T extends object>({
32
36
  if (isUrlSyncEnabled) {
33
37
  const params = new URLSearchParams(searchParams);
34
38
  Object.entries(newFilters).forEach(([ key, value ]) => {
35
- if (value) {
36
- params.set(key, Array.isArray(value) ? value.join(',') : value);
37
- } else {
38
- params.delete(key);
39
- }
39
+ params.delete(key);
40
+ (Array.isArray(value) ? value : [ value ]).forEach((val) => value && params.append(key, val));
40
41
  });
41
42
  setSearchParams?.(params);
42
43
  }
package/src/index.ts CHANGED
@@ -25,5 +25,8 @@ export * from './DataViewTable';
25
25
  export { default as DataViewEventsContext } from './DataViewEventsContext';
26
26
  export * from './DataViewEventsContext';
27
27
 
28
+ export { default as DataViewCheckboxFilter } from './DataViewCheckboxFilter';
29
+ export * from './DataViewCheckboxFilter';
30
+
28
31
  export { default as DataView } from './DataView';
29
32
  export * from './DataView';