Skip to content

Commit 2b2f1a3

Browse files
committed
feat: add checkbox filter
1 parent 541ed78 commit 2b2f1a3

File tree

3 files changed

+207
-15
lines changed

3 files changed

+207
-15
lines changed

packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/FiltersExample.tsx

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { useDataViewFilters, useDataViewPagination } from '@patternfly/react-dat
55
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
66
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
77
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';
99
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
10+
import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter';
1011

1112
const perPageOptions = [
1213
{ title: '5', value: 5 },
@@ -17,38 +18,51 @@ interface Repository {
1718
name: string;
1819
branch: string | null;
1920
prs: string | null;
20-
workspaces: string;
21+
workspace: string;
2122
lastCommit: string;
2223
}
2324

2425
interface RepositoryFilters {
2526
name: string,
26-
branch: string
27+
branch: string,
28+
workspace: string[]
2729
}
2830

2931
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' }
3638
];
3739

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' ];
3947

4048
const ouiaId = 'LayoutExample';
4149

4250
const MyTable: React.FunctionComponent = () => {
4351
const [ searchParams, setSearchParams ] = useSearchParams();
52+
const { filters, onSetFilters, clearAllFilters } = useDataViewFilters<RepositoryFilters>({ initialFilters: { name: '', branch: '', workspace: [] }, searchParams, setSearchParams });
4453
const pagination = useDataViewPagination({ perPage: 5 });
4554
const { page, perPage } = pagination;
46-
const { filters, onSetFilters, clearAllFilters } = useDataViewFilters<RepositoryFilters>({ initialFilters: { name: '', branch: '' }, searchParams, setSearchParams });
4755

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
5063
.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 ]);
5266

5367
return (
5468
<DataView>
@@ -66,6 +80,7 @@ const MyTable: React.FunctionComponent = () => {
6680
<DataViewFilters onChange={(_e, values) => onSetFilters(values)} values={filters}>
6781
<DataViewTextFilter filterId="name" title='Name' placeholder='Filter by name' />
6882
<DataViewTextFilter filterId="branch" title='Branch' placeholder='Filter by branch' />
83+
<DataViewCheckboxFilter filterId="workspace" title='Workspace' placeholder='Filter by workspace' options={filterOptions} />
6984
</DataViewFilters>
7085
}
7186
/>
@@ -76,7 +91,7 @@ const MyTable: React.FunctionComponent = () => {
7691
<Pagination
7792
isCompact
7893
perPageOptions={perPageOptions}
79-
itemCount={repositories.length}
94+
itemCount={filteredData.length}
8095
{...pagination}
8196
/>
8297
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './DataViewCheckboxFilter';
2+
export * from './DataViewCheckboxFilter';

0 commit comments

Comments
 (0)