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

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 (79) hide show
  1. package/dist/cjs/DataViewTable/DataViewTable.d.ts +2 -1
  2. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.d.ts +11 -0
  3. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.js +46 -6
  4. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.test.js +47 -9
  5. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.d.ts +2 -0
  6. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.js +29 -1
  7. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.test.d.ts +1 -1
  8. package/dist/cjs/DataViewTextFilter/DataViewTextFilter.test.js +83 -0
  9. package/dist/cjs/DataViewTh/DataViewTh.d.ts +4 -4
  10. package/dist/cjs/DataViewTh/DataViewTh.js +8 -1
  11. package/dist/cjs/DataViewTh/index.d.ts +2 -0
  12. package/dist/cjs/DataViewTh/index.js +23 -0
  13. package/dist/cjs/DataViewToolbar/DataViewToolbar.js +13 -1
  14. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.d.ts +26 -0
  15. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.js +229 -0
  16. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.test.d.ts +1 -0
  17. package/dist/cjs/DataViewTreeFilter/DataViewTreeFilter.test.js +171 -0
  18. package/dist/cjs/DataViewTreeFilter/index.d.ts +2 -0
  19. package/dist/cjs/DataViewTreeFilter/index.js +23 -0
  20. package/dist/cjs/Hooks/selection.d.ts +8 -8
  21. package/dist/cjs/index.d.ts +6 -0
  22. package/dist/cjs/index.js +10 -1
  23. package/dist/dynamic/DataViewTh/package.json +1 -0
  24. package/dist/dynamic/DataViewTreeFilter/package.json +1 -0
  25. package/dist/dynamic-modules.json +62 -0
  26. package/dist/esm/DataViewTable/DataViewTable.d.ts +2 -1
  27. package/dist/esm/DataViewTableBasic/DataViewTableBasic.d.ts +11 -0
  28. package/dist/esm/DataViewTableBasic/DataViewTableBasic.js +48 -8
  29. package/dist/esm/DataViewTableBasic/DataViewTableBasic.test.js +45 -10
  30. package/dist/esm/DataViewTextFilter/DataViewTextFilter.d.ts +2 -0
  31. package/dist/esm/DataViewTextFilter/DataViewTextFilter.js +29 -1
  32. package/dist/esm/DataViewTextFilter/DataViewTextFilter.test.d.ts +1 -1
  33. package/dist/esm/DataViewTextFilter/DataViewTextFilter.test.js +84 -1
  34. package/dist/esm/DataViewTh/DataViewTh.d.ts +4 -4
  35. package/dist/esm/DataViewTh/DataViewTh.js +8 -1
  36. package/dist/esm/DataViewTh/index.d.ts +2 -0
  37. package/dist/esm/DataViewTh/index.js +2 -0
  38. package/dist/esm/DataViewToolbar/DataViewToolbar.js +13 -1
  39. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.d.ts +26 -0
  40. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.js +225 -0
  41. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.test.d.ts +1 -0
  42. package/dist/esm/DataViewTreeFilter/DataViewTreeFilter.test.js +166 -0
  43. package/dist/esm/DataViewTreeFilter/index.d.ts +2 -0
  44. package/dist/esm/DataViewTreeFilter/index.js +2 -0
  45. package/dist/esm/Hooks/selection.d.ts +8 -8
  46. package/dist/esm/index.d.ts +6 -0
  47. package/dist/esm/index.js +6 -0
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/generate-fed-package-json.js +18 -0
  50. package/generate-index.js +2 -2
  51. package/package.json +6 -6
  52. package/patternfly-docs/content/extensions/data-view/examples/DataView/DataView.md +10 -4
  53. package/patternfly-docs/content/extensions/data-view/examples/DataView/PredefinedLayoutFullExample.tsx +2 -1
  54. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExpandableExample.tsx +108 -0
  55. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableInteractiveExample.tsx +148 -0
  56. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickyExample.tsx +90 -0
  57. package/patternfly-docs/content/extensions/data-view/examples/Table/Table.md +63 -2
  58. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/FiltersExample.tsx +2 -1
  59. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/PaginationExample.tsx +1 -1
  60. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md +9 -2
  61. package/patternfly-docs/content/extensions/data-view/examples/Toolbar/TreeFilterExample.tsx +248 -0
  62. package/patternfly-docs/patternfly-docs.config.js +4 -1
  63. package/src/DataViewTable/DataViewTable.tsx +3 -1
  64. package/src/DataViewTable/__snapshots__/DataViewTable.test.tsx.snap +7 -7
  65. package/src/DataViewTableBasic/DataViewTableBasic.test.tsx +54 -12
  66. package/src/DataViewTableBasic/DataViewTableBasic.tsx +101 -10
  67. package/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap +10 -10
  68. package/src/DataViewTextFilter/DataViewTextFilter.test.tsx +129 -0
  69. package/src/DataViewTextFilter/DataViewTextFilter.tsx +58 -22
  70. package/src/DataViewTh/DataViewTh.tsx +15 -7
  71. package/src/DataViewTh/index.ts +2 -0
  72. package/src/DataViewToolbar/DataViewToolbar.tsx +17 -2
  73. package/src/DataViewToolbar/__snapshots__/DataViewToolbar.test.tsx.snap +288 -280
  74. package/src/DataViewTreeFilter/DataViewTreeFilter.test.tsx +222 -0
  75. package/src/DataViewTreeFilter/DataViewTreeFilter.tsx +361 -0
  76. package/src/DataViewTreeFilter/__snapshots__/DataViewTreeFilter.test.tsx.snap +199 -0
  77. package/src/DataViewTreeFilter/index.ts +2 -0
  78. package/src/Hooks/selection.ts +8 -8
  79. package/src/index.ts +9 -0
@@ -10,20 +10,32 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
- import { useMemo } from 'react';
14
- import { Table, Tbody, Td, Tr, } from '@patternfly/react-table';
13
+ import { useMemo, useState, useRef } from 'react';
14
+ import { ExpandableRowContent, InnerScrollContainer, OuterScrollContainer, Table, Tbody, Td, Tr, } from '@patternfly/react-table';
15
15
  import { useInternalContext } from '../InternalContext';
16
16
  import { DataViewTableHead } from '../DataViewTableHead';
17
17
  import { isDataViewTdObject, isDataViewTrObject } from '../DataViewTable';
18
18
  export const DataViewTableBasic = (_a) => {
19
- var { columns, rows, ouiaId = 'DataViewTableBasic', headStates, bodyStates, hasResizableColumns } = _a, props = __rest(_a, ["columns", "rows", "ouiaId", "headStates", "bodyStates", "hasResizableColumns"]);
19
+ var { columns, rows, expandedRows, ouiaId = 'DataViewTableBasic', headStates, bodyStates, hasResizableColumns, isExpandable = false, isSticky = false } = _a, props = __rest(_a, ["columns", "rows", "expandedRows", "ouiaId", "headStates", "bodyStates", "hasResizableColumns", "isExpandable", "isSticky"]);
20
20
  const { selection, activeState, isSelectable } = useInternalContext();
21
21
  const { onSelect, isSelected, isSelectDisabled } = selection !== null && selection !== void 0 ? selection : {};
22
22
  const activeHeadState = useMemo(() => activeState ? headStates === null || headStates === void 0 ? void 0 : headStates[activeState] : undefined, [activeState, headStates]);
23
23
  const activeBodyState = useMemo(() => activeState ? bodyStates === null || bodyStates === void 0 ? void 0 : bodyStates[activeState] : undefined, [activeState, bodyStates]);
24
+ const [expandedRowsState, setExpandedRowsState] = useState({});
25
+ const [expandedColumnIndex, setExpandedColumnIndex] = useState({});
26
+ const tableRef = useRef(null);
27
+ const needsSeparateTbody = isExpandable;
24
28
  const renderedRows = useMemo(() => rows.map((row, rowIndex) => {
25
29
  const rowIsObject = isDataViewTrObject(row);
26
- return (_jsxs(Tr, Object.assign({ ouiaId: `${ouiaId}-tr-${rowIndex}` }, (rowIsObject && (row === null || row === void 0 ? void 0 : row.props)), { children: [isSelectable && (_jsx(Td, { select: {
30
+ const isRowExpanded = expandedRowsState[rowIndex] || false;
31
+ const expandedColIndex = expandedColumnIndex[rowIndex];
32
+ // Get the first cell to extract the row ID
33
+ const rowData = rowIsObject ? row.row : row;
34
+ const firstCell = rowData[0];
35
+ const rowId = isDataViewTdObject(firstCell) ? firstCell.id : undefined;
36
+ // Find all expandable contents for this row
37
+ const rowExpandableContents = isExpandable ? expandedRows === null || expandedRows === void 0 ? void 0 : expandedRows.filter((content) => content.rowId === rowId) : [];
38
+ const rowContent = (_jsxs(Tr, Object.assign({ ouiaId: `${ouiaId}-tr-${rowIndex}` }, (rowIsObject && (row === null || row === void 0 ? void 0 : row.props)), { isContentExpanded: isRowExpanded, isControlRow: true, children: [isSelectable && (_jsx(Td, { select: {
27
39
  rowIndex,
28
40
  onSelect: (_event, isSelecting) => {
29
41
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(isSelecting, rowIsObject ? row : [row]);
@@ -33,9 +45,37 @@ export const DataViewTableBasic = (_a) => {
33
45
  } }, `select-${rowIndex}`)), (rowIsObject ? row.row : row).map((cell, colIndex) => {
34
46
  var _a;
35
47
  const cellIsObject = isDataViewTdObject(cell);
36
- return (_jsx(Td, Object.assign({}, (cellIsObject && ((_a = cell === null || cell === void 0 ? void 0 : cell.props) !== null && _a !== void 0 ? _a : {})), { "data-ouia-component-id": `${ouiaId}-td-${rowIndex}-${colIndex}`, children: cellIsObject ? cell.cell : cell }), colIndex));
37
- })] }), rowIndex));
38
- }), [rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId]);
39
- return (_jsxs(Table, Object.assign({ "aria-label": "Data table", ouiaId: ouiaId }, props, { children: [activeHeadState || _jsx(DataViewTableHead, { columns: columns, ouiaId: ouiaId, hasResizableColumns: hasResizableColumns }), activeBodyState || _jsx(Tbody, { children: renderedRows })] })));
48
+ const cellExpandableContent = isExpandable ? expandedRows === null || expandedRows === void 0 ? void 0 : expandedRows.find((content) => content.rowId === rowId && content.columnId === colIndex) : undefined;
49
+ return (_jsx(Td, Object.assign({}, (cellIsObject && ((_a = cell === null || cell === void 0 ? void 0 : cell.props) !== null && _a !== void 0 ? _a : {})), (cellExpandableContent != null && {
50
+ compoundExpand: {
51
+ isExpanded: isRowExpanded && expandedColIndex === colIndex,
52
+ expandId: `expandable-${rowIndex}`,
53
+ onToggle: () => {
54
+ setExpandedRowsState(prev => {
55
+ const isSameColumn = expandedColIndex === colIndex;
56
+ const wasExpanded = prev[rowIndex];
57
+ return Object.assign(Object.assign({}, prev), { [rowIndex]: isSameColumn ? !wasExpanded : true });
58
+ });
59
+ setExpandedColumnIndex(prev => (Object.assign(Object.assign({}, prev), { [rowIndex]: colIndex })));
60
+ },
61
+ rowIndex,
62
+ columnIndex: colIndex
63
+ }
64
+ }), { "data-ouia-component-id": `${ouiaId}-td-${rowIndex}-${colIndex}`, children: cellIsObject ? cell.cell : cell }), colIndex));
65
+ })] }), needsSeparateTbody ? undefined : rowIndex));
66
+ if (needsSeparateTbody) {
67
+ return (_jsxs(Tbody, { isExpanded: isRowExpanded, children: [rowContent, rowExpandableContents === null || rowExpandableContents === void 0 ? void 0 : rowExpandableContents.map((expandableContent) => (_jsx(Tr, { isExpanded: isRowExpanded && expandedColIndex === expandableContent.columnId, children: _jsx(Td, { colSpan: rowData.length + (isSelectable ? 1 : 0), "data-expanded-column-index": expandableContent.columnId, children: _jsx(ExpandableRowContent, { children: expandableContent.content }) }) }, `expand-${rowIndex}-${expandableContent.columnId}`)))] }, rowIndex));
68
+ }
69
+ else {
70
+ return rowContent;
71
+ }
72
+ }), [rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId, expandedRowsState, expandedColumnIndex, expandedRows, isExpandable, needsSeparateTbody]);
73
+ const bodyContent = activeBodyState || (needsSeparateTbody ? renderedRows : _jsx(Tbody, { children: renderedRows }));
74
+ if (isSticky) {
75
+ return (_jsx(OuterScrollContainer, { children: _jsx(InnerScrollContainer, { children: _jsxs(Table, Object.assign({ ref: tableRef, "aria-label": "Data table", ouiaId: ouiaId, isExpandable: isExpandable, hasAnimations: true }, props, { isStickyHeader: true, children: [activeHeadState || _jsx(DataViewTableHead, { columns: columns, ouiaId: ouiaId, hasResizableColumns: hasResizableColumns }), bodyContent] })) }) }));
76
+ }
77
+ else {
78
+ return (_jsxs(Table, Object.assign({ ref: tableRef, "aria-label": "Data table", ouiaId: ouiaId, isExpandable: isExpandable, hasAnimations: true }, props, { children: [activeHeadState || _jsx(DataViewTableHead, { columns: columns, ouiaId: ouiaId, hasResizableColumns: hasResizableColumns }), bodyContent] })));
79
+ }
40
80
  };
41
81
  export default DataViewTableBasic;
@@ -1,19 +1,36 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render } from '@testing-library/react';
11
+ import { render, screen } from '@testing-library/react';
12
+ import userEvent from '@testing-library/user-event';
3
13
  import { DataView } from '../DataView';
4
14
  import { DataViewTableBasic } from './DataViewTableBasic';
5
15
  const repositories = [
6
- { name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
7
- { name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
8
- { name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
9
- { name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
10
- { name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
11
- { name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
16
+ { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
17
+ { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
18
+ { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
19
+ { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
20
+ { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
21
+ { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
12
22
  ];
13
- const rows = repositories.map(repo => ({
14
- row: Object.values(repo),
15
- }));
23
+ const rows = repositories.map(({ id, name, branches, prs, workspaces, lastCommit }) => [
24
+ { id, cell: name },
25
+ branches,
26
+ prs,
27
+ workspaces,
28
+ lastCommit
29
+ ]);
16
30
  const columns = ['Repositories', 'Branches', 'Pull requests', 'Workspaces', 'Last commit'];
31
+ const expandableContents = [
32
+ { rowId: 1, columnId: 1, content: _jsx("div", { children: "Branch details for Repository one" }) },
33
+ ];
17
34
  const ouiaId = 'TableExample';
18
35
  describe('DataViewTable component', () => {
19
36
  test('should render correctly', () => {
@@ -32,4 +49,22 @@ describe('DataViewTable component', () => {
32
49
  const { container } = render(_jsx(DataView, { activeState: "loading", children: _jsx(DataViewTableBasic, { "aria-label": 'Repositories table', ouiaId: ouiaId, columns: columns, bodyStates: { loading: "Data is loading" }, rows: [] }) }));
33
50
  expect(container).toMatchSnapshot();
34
51
  });
52
+ test('when isExpandable cell should be clickable and expandable', () => __awaiter(void 0, void 0, void 0, function* () {
53
+ var _a, _b, _c;
54
+ const user = userEvent.setup();
55
+ render(_jsx(DataViewTableBasic, { "aria-label": 'Repositories table', ouiaId: ouiaId, columns: columns, rows: rows, isExpandable: true, expandedRows: expandableContents }));
56
+ // Initially, expandable content is rendered but should be hidden (not visible)
57
+ const initialBranchContent = screen.getByText('Branch details for Repository one');
58
+ expect((_a = initialBranchContent.closest('tr')) === null || _a === void 0 ? void 0 : _a.classList.contains('pf-m-expanded')).toBeFalsy();
59
+ // Find the first expandable button by ID
60
+ const branchExpandButton = document.getElementById('expandable-0-0-1');
61
+ expect(branchExpandButton).toBeTruthy();
62
+ // Verify the button is in the cell with "Branch one" text
63
+ expect((_b = branchExpandButton === null || branchExpandButton === void 0 ? void 0 : branchExpandButton.closest('td')) === null || _b === void 0 ? void 0 : _b.textContent).toContain('Branch one');
64
+ // Click the expand button for Branches column
65
+ yield user.click(branchExpandButton);
66
+ // After clicking, the expandable content should be visible
67
+ const branchContent = screen.getByText('Branch details for Repository one');
68
+ expect((_c = branchContent.closest('tr')) === null || _c === void 0 ? void 0 : _c.classList.contains('pf-m-expanded')).toBeTruthy();
69
+ }));
35
70
  });
@@ -16,6 +16,8 @@ export interface DataViewTextFilterProps extends SearchInputProps {
16
16
  trimValue?: boolean;
17
17
  /** Custom OUIA ID */
18
18
  ouiaId?: string;
19
+ /** Enable keyboard shortcut (/) to focus the filter. Defaults to true. */
20
+ enableShortcut?: boolean;
19
21
  }
20
22
  export declare const DataViewTextFilter: FC<DataViewTextFilterProps>;
21
23
  export default DataViewTextFilter;
@@ -10,9 +10,37 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { useEffect } from 'react';
13
14
  import { SearchInput, ToolbarFilter } from '@patternfly/react-core';
14
15
  export const DataViewTextFilter = (_a) => {
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
+ var { filterId, title, value = '', onChange, onClear = () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), showToolbarItem, trimValue = true, ouiaId = 'DataViewTextFilter', enableShortcut = true } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "onClear", "showToolbarItem", "trimValue", "ouiaId", "enableShortcut"]);
17
+ useEffect(() => {
18
+ if (!enableShortcut) {
19
+ return;
20
+ }
21
+ const handleKeyDown = (event) => {
22
+ // Only handle "/" key when not typing in an input, textarea, or contenteditable element
23
+ if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) {
24
+ const target = event.target;
25
+ const isInputElement = target.tagName === 'INPUT' ||
26
+ target.tagName === 'TEXTAREA' ||
27
+ target.isContentEditable;
28
+ // Only focus if the filter is visible and we're not already in an input field
29
+ if (showToolbarItem && !isInputElement) {
30
+ // Find the input element by its ID (searchInputId prop)
31
+ const inputElement = document.getElementById(filterId);
32
+ if (inputElement) {
33
+ event.preventDefault();
34
+ inputElement.focus();
35
+ }
36
+ }
37
+ }
38
+ };
39
+ window.addEventListener('keydown', handleKeyDown);
40
+ return () => {
41
+ window.removeEventListener('keydown', handleKeyDown);
42
+ };
43
+ }, [showToolbarItem, filterId, enableShortcut]);
16
44
  return (_jsx(ToolbarFilter, { "data-ouia-component-id": ouiaId, labels: value.length > 0 ? [{ key: title, node: value }] : [], deleteLabel: () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), categoryName: title, showToolbarItem: showToolbarItem, children: _jsx(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)) }, ouiaId));
17
45
  };
18
46
  export default DataViewTextFilter;
@@ -1 +1 @@
1
- export {};
1
+ import '@testing-library/jest-dom';
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
3
4
  import DataViewTextFilter from './DataViewTextFilter';
4
5
  import DataViewToolbar from '../DataViewToolbar';
5
6
  describe('DataViewTextFilter component', () => {
@@ -14,4 +15,86 @@ describe('DataViewTextFilter component', () => {
14
15
  const { container } = render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps)) }));
15
16
  expect(container).toMatchSnapshot();
16
17
  });
18
+ it('should focus the search input when "/" key is pressed and filter is visible', () => {
19
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true })) }));
20
+ const input = document.getElementById('test-filter');
21
+ expect(input).toBeInTheDocument();
22
+ // Simulate pressing "/" key by creating and dispatching a KeyboardEvent
23
+ const keyEvent = new KeyboardEvent('keydown', {
24
+ key: '/',
25
+ code: 'Slash',
26
+ bubbles: true,
27
+ cancelable: true,
28
+ });
29
+ window.dispatchEvent(keyEvent);
30
+ // Check that the input has focus
31
+ expect(document.activeElement).toBe(input);
32
+ });
33
+ it('should not focus the search input when "/" key is pressed if filter is not visible', () => {
34
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: false })) }));
35
+ const input = document.getElementById('test-filter');
36
+ // Simulate pressing "/" key
37
+ const keyEvent = new KeyboardEvent('keydown', {
38
+ key: '/',
39
+ code: 'Slash',
40
+ bubbles: true,
41
+ cancelable: true,
42
+ });
43
+ window.dispatchEvent(keyEvent);
44
+ if (input) {
45
+ expect(document.activeElement).not.toBe(input);
46
+ }
47
+ });
48
+ it('should not focus the search input when "/" key is pressed while typing in another input', () => {
49
+ const { container } = render(_jsxs("div", { children: [_jsx("input", { "data-testid": "other-input" }), _jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true })) })] }));
50
+ const otherInput = container.querySelector('[data-testid="other-input"]');
51
+ // Focus the other input first
52
+ otherInput.focus();
53
+ expect(document.activeElement).toBe(otherInput);
54
+ // Simulate pressing "/" key while focused on the other input
55
+ // The event target should be the input element
56
+ const keyEvent = new KeyboardEvent('keydown', {
57
+ key: '/',
58
+ code: 'Slash',
59
+ bubbles: true,
60
+ cancelable: true,
61
+ });
62
+ Object.defineProperty(keyEvent, 'target', {
63
+ value: otherInput,
64
+ enumerable: true,
65
+ });
66
+ window.dispatchEvent(keyEvent);
67
+ // The search input should not be focused since we're already in an input field
68
+ expect(document.activeElement).toBe(otherInput);
69
+ });
70
+ it('should not focus the search input when enableShortcut is false', () => {
71
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true, enableShortcut: false })) }));
72
+ const input = document.getElementById('test-filter');
73
+ expect(input).toBeInTheDocument();
74
+ // Simulate pressing "/" key
75
+ const keyEvent = new KeyboardEvent('keydown', {
76
+ key: '/',
77
+ code: 'Slash',
78
+ bubbles: true,
79
+ cancelable: true,
80
+ });
81
+ window.dispatchEvent(keyEvent);
82
+ // The input should not be focused since the shortcut is disabled
83
+ expect(document.activeElement).not.toBe(input);
84
+ });
85
+ it('should focus the search input when enableShortcut is true (default)', () => {
86
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true })) }));
87
+ const input = document.getElementById('test-filter');
88
+ expect(input).toBeInTheDocument();
89
+ // Simulate pressing "/" key
90
+ const keyEvent = new KeyboardEvent('keydown', {
91
+ key: '/',
92
+ code: 'Slash',
93
+ bubbles: true,
94
+ cancelable: true,
95
+ });
96
+ window.dispatchEvent(keyEvent);
97
+ // The input should be focused since the shortcut is enabled by default
98
+ expect(document.activeElement).toBe(input);
99
+ });
17
100
  });
@@ -5,13 +5,13 @@ export interface DataViewThResizableProps {
5
5
  isResizable?: boolean;
6
6
  /** Callback after the column is resized. Returns the triggering event, the column id passed in via cell props, and the new width of the column. */
7
7
  onResize?: (event: ReactMouseEvent | MouseEvent | ReactKeyboardEvent | KeyboardEvent | TouchEvent, id: string | number | undefined, width: number) => void;
8
- /** Width of the column */
8
+ /** Starting width in pixels of the column */
9
9
  width?: number;
10
- /** Minimum width of the column */
10
+ /** Minimum resize width in pixels of the column */
11
11
  minWidth?: number;
12
- /** Increment for keyboard navigation */
12
+ /** Increment in pixels for keyboard navigation */
13
13
  increment?: number;
14
- /** Increment for keyboard navigation while shift is held */
14
+ /** Increment in pixels for keyboard navigation while shift is held */
15
15
  shiftIncrement?: number;
16
16
  /** Provides an accessible name for the resizable column via a human readable string. */
17
17
  resizeButtonAriaLabel?: string;
@@ -210,6 +210,13 @@ export const DataViewTh = (_a) => {
210
210
  onResize && onResize(e, thProps === null || thProps === void 0 ? void 0 : thProps.id, newSize);
211
211
  };
212
212
  const resizableContent = (_jsxs(Fragment, { children: [_jsx("div", { "aria-live": "polite", className: "pf-v6-screen-reader", children: screenReaderText }), _jsx(Button, { ref: resizeButtonRef, variant: "plain", hasNoPadding: true, icon: _jsx(ResizeIcon, {}), onMouseDown: handleMousedown, onKeyDown: handleKeys, onTouchStart: handleTouchStart, "aria-label": resizeButtonAriaLabel, className: classes.dataViewResizableButton })] }));
213
- return (_jsx(Th, Object.assign({}, thProps, props, { style: width > 0 ? { minWidth: width } : undefined, ref: thRef, modifier: "truncate", className: dataViewThClassName }, (isResizable && { additionalContent: resizableContent }), { children: content })));
213
+ const classNames = [];
214
+ if (thProps === null || thProps === void 0 ? void 0 : thProps.className) {
215
+ classNames.push(thProps.className);
216
+ }
217
+ if (dataViewThClassName) {
218
+ classNames.push(dataViewThClassName);
219
+ }
220
+ return (_jsx(Th, Object.assign({ modifier: "truncate" }, thProps, props, { ref: thRef, style: width > 0 ? Object.assign(Object.assign({}, thProps === null || thProps === void 0 ? void 0 : thProps.style), { minWidth: width }) : thProps === null || thProps === void 0 ? void 0 : thProps.style, className: classNames.length > 0 ? classNames.join(' ') : undefined }, (isResizable && { additionalContent: resizableContent }), { children: content })));
214
221
  };
215
222
  export default DataViewTh;
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewTh';
2
+ export * from './DataViewTh';
@@ -0,0 +1,2 @@
1
+ export { default } from './DataViewTh';
2
+ export * from './DataViewTh';
@@ -12,9 +12,21 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import { useRef } from 'react';
14
14
  import { Button, Toolbar, ToolbarContent, ToolbarItem, ToolbarItemVariant } from '@patternfly/react-core';
15
+ import { createUseStyles } from 'react-jss';
16
+ const useStyles = createUseStyles({
17
+ dataViewToolbarPagination: {
18
+ flexBasis: '100%',
19
+ width: '100%'
20
+ },
21
+ dataViewToolbarPaginationWrapper: {
22
+ flexBasis: '100%',
23
+ width: '100%'
24
+ }
25
+ });
15
26
  export const DataViewToolbar = (_a) => {
16
27
  var { className, ouiaId = 'DataViewToolbar', bulkSelect, actions, toggleGroup, pagination, filters, customLabelGroupContent, clearAllFilters, children } = _a, props = __rest(_a, ["className", "ouiaId", "bulkSelect", "actions", "toggleGroup", "pagination", "filters", "customLabelGroupContent", "clearAllFilters", "children"]);
28
+ const classes = useStyles();
17
29
  const defaultClearFilters = useRef(_jsx(ToolbarItem, { children: _jsx(Button, { ouiaId: `${ouiaId}-clear-all-filters`, variant: "link", onClick: clearAllFilters, isInline: true, children: "Clear filters" }) }));
18
- return (_jsx(Toolbar, Object.assign({ ouiaId: ouiaId, className: className, customLabelGroupContent: customLabelGroupContent !== null && customLabelGroupContent !== void 0 ? customLabelGroupContent : defaultClearFilters.current }, props, { children: _jsxs(ToolbarContent, { children: [bulkSelect && (_jsx(ToolbarItem, { "data-ouia-component-id": `${ouiaId}-bulk-select`, children: bulkSelect })), filters && (_jsx(ToolbarItem, { children: filters })), actions && (_jsx(ToolbarItem, { children: actions })), toggleGroup && (_jsx(ToolbarItem, { children: toggleGroup })), pagination && (_jsx(ToolbarItem, { variant: ToolbarItemVariant.pagination, "data-ouia-component-id": `${ouiaId}-pagination`, children: pagination })), children] }) })));
30
+ return (_jsx(Toolbar, Object.assign({ ouiaId: ouiaId, className: className, customLabelGroupContent: customLabelGroupContent !== null && customLabelGroupContent !== void 0 ? customLabelGroupContent : defaultClearFilters.current }, props, { children: _jsxs(ToolbarContent, { children: [bulkSelect && (_jsx(ToolbarItem, { "data-ouia-component-id": `${ouiaId}-bulk-select`, children: bulkSelect })), filters && (_jsx(ToolbarItem, { children: filters })), actions && (_jsx(ToolbarItem, { children: actions })), toggleGroup && (_jsx(ToolbarItem, { children: toggleGroup })), pagination && (_jsx(ToolbarItem, { variant: ToolbarItemVariant.pagination, "data-ouia-component-id": `${ouiaId}-pagination`, className: classes.dataViewToolbarPagination, children: _jsx("div", { className: classes.dataViewToolbarPaginationWrapper, children: pagination }) })), children] }) })));
19
31
  };
20
32
  export default DataViewToolbar;
@@ -0,0 +1,26 @@
1
+ import { ToolbarFilterProps, TreeViewDataItem } from '@patternfly/react-core';
2
+ import React, { FC } from 'react';
3
+ export interface DataViewTreeFilterProps {
4
+ /** Unique key for the filter attribute */
5
+ filterId: string;
6
+ /** Array of current filter values */
7
+ value?: string[];
8
+ /** Filter title displayed in the toolbar */
9
+ title: string;
10
+ /** Callback for when the selection changes */
11
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
12
+ /** Controls visibility of the filter in the toolbar */
13
+ showToolbarItem?: ToolbarFilterProps['showToolbarItem'];
14
+ /** Custom OUIA ID */
15
+ ouiaId?: string;
16
+ /** Hierarchical data items for the tree structure */
17
+ items?: TreeViewDataItem[];
18
+ /** When true, expands all tree nodes by default */
19
+ defaultExpanded?: boolean;
20
+ /** Callback for when tree items are selected/deselected, provides all currently selected nodes */
21
+ onSelect?: (selectedItems: TreeViewDataItem[]) => void;
22
+ /** Array of pre-selected item id's to be checked on initial render */
23
+ defaultSelected?: string[];
24
+ }
25
+ export declare const DataViewTreeFilter: FC<DataViewTreeFilterProps>;
26
+ export default DataViewTreeFilter;
@@ -0,0 +1,225 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Dropdown, MenuToggle, ToolbarFilter, TreeView } from '@patternfly/react-core';
3
+ import { useState, useRef, useEffect } from 'react';
4
+ import { createUseStyles } from 'react-jss';
5
+ /** This style is needed so the tree filter dropdown looks like the basic filter dropdow */
6
+ const useStyles = createUseStyles({
7
+ dataViewTreeFilterTreeView: {
8
+ '& .pf-v6-c-tree-view__node::after': {
9
+ borderRadius: 0,
10
+ borderRightStyle: 'none',
11
+ borderLeftStyle: 'none'
12
+ },
13
+ '& .pf-v6-c-tree-view__content': {
14
+ borderRadius: 0
15
+ }
16
+ }
17
+ });
18
+ // Generic helper to collect items from tree based on predicate
19
+ const collectTreeItems = (items, predicate, leafOnly = false) => {
20
+ const collected = [];
21
+ const collect = (item) => {
22
+ var _a;
23
+ const isLeaf = !item.children || item.children.length === 0;
24
+ if (predicate(item) && (!leafOnly || isLeaf)) {
25
+ collected.push(item);
26
+ }
27
+ (_a = item.children) === null || _a === void 0 ? void 0 : _a.forEach(child => collect(child));
28
+ };
29
+ items.forEach(item => collect(item));
30
+ return collected;
31
+ };
32
+ // Helper function to get all checked items (not just leaf nodes)
33
+ const getAllCheckedItems = (items) => collectTreeItems(items, item => { var _a; return ((_a = item.checkProps) === null || _a === void 0 ? void 0 : _a.checked) === true; }, false);
34
+ // Get all checked leaf items (returns array of names)
35
+ const getAllCheckedLeafItems = (items) => collectTreeItems(items, item => { var _a; return ((_a = item.checkProps) === null || _a === void 0 ? void 0 : _a.checked) === true; }, true).map(item => String(item.name));
36
+ // Helper function to expand all nodes in the tree
37
+ const expandAllNodes = (items) => items.map(item => (Object.assign(Object.assign({}, item), { defaultExpanded: true, children: item.children ? expandAllNodes(item.children) : undefined })));
38
+ // Helper function to set pre-selected items
39
+ const setPreSelectedItems = (items, selectedIds) => items.map(item => {
40
+ var _a, _b;
41
+ const isSelected = selectedIds.includes(String(item.id));
42
+ const hasSelectedChildren = (_b = (_a = item.children) === null || _a === void 0 ? void 0 : _a.some(child => selectedIds.includes(String(child.id)))) !== null && _b !== void 0 ? _b : false;
43
+ return Object.assign(Object.assign({}, item), { checkProps: item.checkProps ? Object.assign(Object.assign({}, item.checkProps), { checked: isSelected || hasSelectedChildren }) : undefined, children: item.children ? setPreSelectedItems(item.children, selectedIds) : undefined });
44
+ });
45
+ // Helper function to uncheck all items recursively
46
+ const uncheckRecursive = (items) => items.map(item => (Object.assign(Object.assign({}, item), { checkProps: item.checkProps ? Object.assign(Object.assign({}, item.checkProps), { checked: false }) : undefined, children: item.children ? uncheckRecursive(item.children) : undefined })));
47
+ export const DataViewTreeFilter = ({ filterId, title, value = [], onChange, showToolbarItem, ouiaId = 'DataViewTreeFilter', items, defaultExpanded = false, onSelect, defaultSelected = [] }) => {
48
+ const classes = useStyles();
49
+ const [isOpen, setIsOpen] = useState(false);
50
+ const [treeData, setTreeData] = useState(items || []);
51
+ const menuRef = useRef(null);
52
+ const isInitialMount = useRef(true);
53
+ const hasCalledInitialOnChange = useRef(false);
54
+ // Initialize tree data with defaultExpanded and defaultSelected (only on first mount)
55
+ useEffect(() => {
56
+ if (!items) {
57
+ return;
58
+ }
59
+ let initializedData = [...items];
60
+ // Apply default expansion
61
+ if (defaultExpanded) {
62
+ initializedData = expandAllNodes(initializedData);
63
+ }
64
+ // Apply pre-selected items only on initial mount
65
+ if (isInitialMount.current && defaultSelected.length > 0) {
66
+ initializedData = setPreSelectedItems(initializedData, defaultSelected);
67
+ }
68
+ setTreeData(initializedData);
69
+ if (isInitialMount.current) {
70
+ isInitialMount.current = false;
71
+ }
72
+ }, [items, defaultExpanded]);
73
+ // Call onChange and onSelect after tree data is initialized with default selections
74
+ useEffect(() => {
75
+ if (!hasCalledInitialOnChange.current && defaultSelected.length > 0 && treeData.length > 0) {
76
+ const selectedValues = getAllCheckedLeafItems(treeData);
77
+ // Only call if there are actually selected values
78
+ if (selectedValues.length > 0) {
79
+ // Calculate both values synchronously before calling callbacks
80
+ const selectedItems = getAllCheckedItems(treeData);
81
+ // useEffect already runs after render, so this is safe
82
+ if (onChange) {
83
+ onChange(undefined, selectedValues);
84
+ }
85
+ if (onSelect) {
86
+ onSelect(selectedItems);
87
+ }
88
+ hasCalledInitialOnChange.current = true;
89
+ }
90
+ }
91
+ }, [treeData, onChange, onSelect, defaultSelected.length]);
92
+ // Sync tree checkboxes when value prop changes (when clearAllFilters is called)
93
+ useEffect(() => {
94
+ if (value.length === 0) {
95
+ setTreeData(currentTreeData => {
96
+ if (currentTreeData.length === 0) {
97
+ return currentTreeData;
98
+ }
99
+ const currentCheckedItems = getAllCheckedLeafItems(currentTreeData);
100
+ // Only update if there are checked items that need to be unchecked
101
+ if (currentCheckedItems.length > 0) {
102
+ return uncheckRecursive(currentTreeData);
103
+ }
104
+ return currentTreeData;
105
+ });
106
+ }
107
+ }, [value]);
108
+ // Check if all children are checked (recursive)
109
+ const areAllChildrenChecked = (item) => {
110
+ var _a, _b;
111
+ if (!((_a = item.children) === null || _a === void 0 ? void 0 : _a.length)) {
112
+ return ((_b = item.checkProps) === null || _b === void 0 ? void 0 : _b.checked) === true;
113
+ }
114
+ return item.children.every(child => areAllChildrenChecked(child));
115
+ };
116
+ // Check if some children are checked (recursive)
117
+ const areSomeChildrenChecked = (item) => {
118
+ var _a, _b;
119
+ if (!((_a = item.children) === null || _a === void 0 ? void 0 : _a.length)) {
120
+ return ((_b = item.checkProps) === null || _b === void 0 ? void 0 : _b.checked) === true;
121
+ }
122
+ return item.children.some(child => areSomeChildrenChecked(child));
123
+ };
124
+ // Find tree item by name
125
+ const findItemByName = (items, name) => {
126
+ for (const item of items) {
127
+ if (item.name === name) {
128
+ return item;
129
+ }
130
+ if (item.children) {
131
+ const found = findItemByName(item.children, name);
132
+ if (found) {
133
+ return found;
134
+ }
135
+ }
136
+ }
137
+ return null;
138
+ };
139
+ // Find parent item by child ID
140
+ const findParentById = (items, childId) => {
141
+ var _a;
142
+ for (const item of items) {
143
+ if ((_a = item.children) === null || _a === void 0 ? void 0 : _a.some(child => child.id === childId)) {
144
+ return item;
145
+ }
146
+ if (item.children) {
147
+ const found = findParentById(item.children, childId);
148
+ if (found) {
149
+ return found;
150
+ }
151
+ }
152
+ }
153
+ return null;
154
+ };
155
+ // Update parent checkbox states based on children (recursive)
156
+ const onCheckParentHandle = (childId) => {
157
+ const parent = findParentById(treeData, childId);
158
+ if (!parent) {
159
+ return;
160
+ }
161
+ if (parent.checkProps) {
162
+ const allChildrenChecked = areAllChildrenChecked(parent);
163
+ const someChildrenChecked = areSomeChildrenChecked(parent);
164
+ if (allChildrenChecked) {
165
+ parent.checkProps.checked = true;
166
+ }
167
+ else if (someChildrenChecked) {
168
+ parent.checkProps.checked = null;
169
+ }
170
+ else {
171
+ parent.checkProps.checked = false;
172
+ }
173
+ }
174
+ if (parent.id) {
175
+ onCheckParentHandle(parent.id);
176
+ }
177
+ };
178
+ // Check/uncheck item and all its children (recursive)
179
+ const onCheckHandle = (treeViewItem, checked) => {
180
+ var _a;
181
+ if (treeViewItem.checkProps) {
182
+ treeViewItem.checkProps.checked = checked;
183
+ }
184
+ (_a = treeViewItem.children) === null || _a === void 0 ? void 0 : _a.forEach(child => onCheckHandle(child, checked));
185
+ };
186
+ // Handle checkbox change event
187
+ const onCheck = (event, treeViewItem) => {
188
+ const checked = event.target.checked;
189
+ onCheckHandle(treeViewItem, checked);
190
+ if (treeViewItem.id) {
191
+ onCheckParentHandle(treeViewItem.id);
192
+ }
193
+ setTreeData(prev => [...prev]);
194
+ const selectedValues = getAllCheckedLeafItems(treeData);
195
+ onChange === null || onChange === void 0 ? void 0 : onChange(event, selectedValues);
196
+ if (onSelect) {
197
+ const selectedItems = getAllCheckedItems(treeData);
198
+ onSelect(selectedItems);
199
+ }
200
+ };
201
+ // Clear a specific filter by name (when label chip is removed)
202
+ const onFilterSelectorClear = (itemName) => {
203
+ const treeViewItem = findItemByName(treeData, itemName);
204
+ if (!treeViewItem) {
205
+ return;
206
+ }
207
+ onCheckHandle(treeViewItem, false);
208
+ if (treeViewItem.id) {
209
+ onCheckParentHandle(treeViewItem.id);
210
+ }
211
+ };
212
+ // Uncheck all items in the tree
213
+ const uncheckAllItems = () => {
214
+ const updatedTreeData = uncheckRecursive(treeData);
215
+ setTreeData(updatedTreeData);
216
+ onChange === null || onChange === void 0 ? void 0 : onChange(undefined, []);
217
+ };
218
+ const dropdown = (_jsx(Dropdown, { ref: menuRef, isOpen: isOpen, onOpenChange: (isOpen) => setIsOpen(isOpen), toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsOpen(!isOpen), isExpanded: isOpen, children: title })), ouiaId: ouiaId, shouldFocusToggleOnSelect: true, children: _jsx(TreeView, { hasAnimations: true, data: treeData, onCheck: onCheck, hasCheckboxes: true, className: classes.dataViewTreeFilterTreeView }) }));
219
+ return (_jsx(ToolbarFilter, { "data-ouia-component-id": ouiaId, labels: value.map(item => ({ key: item, node: item })), deleteLabel: (_, label) => {
220
+ const labelKey = typeof label === 'string' ? label : label.key;
221
+ onChange === null || onChange === void 0 ? void 0 : onChange(undefined, value.filter(item => item !== labelKey));
222
+ onFilterSelectorClear(labelKey);
223
+ }, deleteLabelGroup: uncheckAllItems, categoryName: title, showToolbarItem: showToolbarItem, children: dropdown }, filterId));
224
+ };
225
+ export default DataViewTreeFilter;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';