Skip to content

Commit 81f752e

Browse files
authored
Feat: add ListManager component group (#766)
* feat: add drag and drop column component * feat: refactor Column into ColumnManagement and add patternfly-docs/examples * fix: address column accessibility issues * fix: add ul for DataListItem * chore: revert ul change * fix: implement DragDropSort and BulkSelect * chore: add react-drag-drop package * style: appease linter * feat: rename ColumnManagement to ListManager * test: remove unused listmanager cypress test * style: add ActionListItems and update isShown to isSelected * docs: update ListManager README with isSelected change * style: update with PF styles and fix unit tests
1 parent e5fb3bf commit 81f752e

File tree

8 files changed

+456
-19
lines changed

8 files changed

+456
-19
lines changed

package-lock.json

Lines changed: 112 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"whatwg-fetch": "^3.6.20"
8484
},
8585
"dependencies": {
86+
"@patternfly/react-drag-drop": "^6.3.0",
8687
"@patternfly/react-tokens": "^6.0.0",
8788
"sharp": "^0.34.0"
8889
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
# Sidenav top-level section
3+
# should be the same for all markdown files
4+
section: Component groups
5+
subsection: Helpers
6+
# Sidenav secondary level section
7+
# should be the same for all markdown files
8+
id: List manager
9+
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
10+
source: react
11+
# If you use typescript, the name of the interface to display props for
12+
# These are found through the sourceProps function provided in patternfly-docs.source.js
13+
propComponents: ['ListManager']
14+
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManager.md
15+
---
16+
17+
import ListManager from '@patternfly/react-component-groups/dist/dynamic/ListManager';
18+
import { FunctionComponent, useState } from 'react';
19+
20+
The **list manager** component can be used to implement customizable table columns. Columns can be configured to be enabled or disabled by default or be unhidable.
21+
22+
## Examples
23+
24+
### Basic column list
25+
26+
The order of the columns can be changed by dragging and dropping the columns themselves. This list can be used within a page or within a modal. Always make sure to set `isShownByDefault` and `isSelected` (if needed) to the same boolean value in the initial state.
27+
28+
```js file="./ListManagerExample.tsx"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { FunctionComponent, useState } from 'react';
2+
import { Column, ListManager } from '@patternfly/react-component-groups';
3+
4+
const DEFAULT_COLUMNS: Column[] = [
5+
{
6+
title: 'ID',
7+
key: 'id',
8+
isShownByDefault: true,
9+
isShown: true,
10+
isUntoggleable: true
11+
},
12+
{
13+
title: 'Publish date',
14+
key: 'publishDate',
15+
isShownByDefault: true,
16+
isShown: true
17+
},
18+
{
19+
title: 'Impact',
20+
key: 'impact',
21+
isShownByDefault: true,
22+
isShown: true
23+
},
24+
{
25+
title: 'Score',
26+
key: 'score',
27+
isShownByDefault: false,
28+
isShown: false
29+
}
30+
];
31+
32+
export const ColumnExample: FunctionComponent = () => {
33+
const [ columns, setColumns ] = useState(DEFAULT_COLUMNS);
34+
35+
return (
36+
<ListManager
37+
columns={columns}
38+
onOrderChange={setColumns}
39+
onSelect={(col) => {
40+
const newColumns = [ ...columns ];
41+
const changedColumn = newColumns.find(c => c.key === col.key);
42+
if (changedColumn) {
43+
changedColumn.isShown = col.isShown;
44+
}
45+
setColumns(newColumns);
46+
}}
47+
onSelectAll={(newColumns) => setColumns(newColumns)}
48+
onSave={(newColumns) => {
49+
setColumns(newColumns);
50+
alert('Changes saved!');
51+
}}
52+
onCancel={() => alert('Changes cancelled!')}
53+
/>
54+
);
55+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import '@testing-library/jest-dom';
4+
import ListManager from './ListManager';
5+
6+
jest.mock('@patternfly/react-drag-drop', () => {
7+
const originalModule = jest.requireActual('@patternfly/react-drag-drop');
8+
return {
9+
...originalModule,
10+
DragDropSort: ({ onDrop, items }) => {
11+
const handleDrop = () => {
12+
const reorderedItems = [ ...items ].reverse();
13+
onDrop({}, reorderedItems);
14+
};
15+
return <div onDrop={handleDrop}>{items.map(item => item.content)}</div>;
16+
},
17+
};
18+
});
19+
20+
const mockColumns = [
21+
{ key: 'name', title: 'Name', isSelected: true, isShownByDefault: true },
22+
{ key: 'status', title: 'Status', isSelected: true, isShownByDefault: true },
23+
{ key: 'version', title: 'Version', isSelected: false, isShownByDefault: false },
24+
];
25+
26+
describe('ListManager', () => {
27+
it('renders with initial columns', () => {
28+
render(<ListManager columns={mockColumns} />);
29+
expect(screen.getByTestId('column-check-name')).toBeChecked();
30+
expect(screen.getByTestId('column-check-status')).toBeChecked();
31+
expect(screen.getByTestId('column-check-version')).not.toBeChecked();
32+
});
33+
34+
it('renders a cancel button', async () => {
35+
const onCancel = jest.fn();
36+
render(<ListManager columns={mockColumns} onCancel={onCancel} />);
37+
const cancelButton = screen.getByText('Cancel');
38+
expect(cancelButton).toBeInTheDocument();
39+
await userEvent.click(cancelButton);
40+
expect(onCancel).toHaveBeenCalled();
41+
});
42+
43+
it('toggles a column', async () => {
44+
const onSelect = jest.fn();
45+
render(<ListManager columns={mockColumns} onSelect={onSelect} />);
46+
const nameCheckbox = screen.getByTestId('column-check-name');
47+
await userEvent.click(nameCheckbox);
48+
expect(nameCheckbox).not.toBeChecked();
49+
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: 'name', isSelected: false }));
50+
});
51+
52+
it('selects all columns', async () => {
53+
render(<ListManager columns={mockColumns} />);
54+
const menuToggle = screen.getByLabelText('Bulk select toggle');
55+
if (menuToggle) {
56+
await userEvent.click(menuToggle);
57+
}
58+
const selectAllButton = screen.getByText('Select all (3)');
59+
await userEvent.click(selectAllButton);
60+
expect(screen.getByTestId('column-check-name')).toBeChecked();
61+
expect(screen.getByTestId('column-check-status')).toBeChecked();
62+
expect(screen.getByTestId('column-check-version')).toBeChecked();
63+
});
64+
65+
it('selects no columns', async () => {
66+
render(<ListManager columns={mockColumns} />);
67+
const menuToggle = screen.getByLabelText('Bulk select toggle');
68+
if (menuToggle) {
69+
await userEvent.click(menuToggle);
70+
}
71+
const selectNoneButton = screen.getByText('Select none (0)');
72+
await userEvent.click(selectNoneButton);
73+
expect(screen.getByTestId('column-check-name')).not.toBeChecked();
74+
expect(screen.getByTestId('column-check-status')).not.toBeChecked();
75+
expect(screen.getByTestId('column-check-version')).not.toBeChecked();
76+
});
77+
78+
it('saves changes', async () => {
79+
const onSave = jest.fn();
80+
render(<ListManager columns={mockColumns} onSave={onSave} />);
81+
const saveButton = screen.getByText('Save');
82+
await userEvent.click(saveButton);
83+
expect(onSave).toHaveBeenCalledWith(expect.any(Array));
84+
});
85+
});

0 commit comments

Comments
 (0)