@patternfly/react-data-view 5.5.1 → 5.7.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.
- package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.d.ts +29 -0
- package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.js +70 -0
- package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.test.d.ts +1 -0
- package/dist/cjs/DataViewCheckboxFilter/DataViewCheckboxFilter.test.js +25 -0
- package/dist/cjs/DataViewCheckboxFilter/index.d.ts +2 -0
- package/dist/cjs/DataViewCheckboxFilter/index.js +23 -0
- package/dist/cjs/DataViewFilters/DataViewFilters.d.ts +7 -1
- package/dist/cjs/DataViewFilters/DataViewFilters.js +16 -1
- package/dist/cjs/DataViewTextFilter/DataViewTextFilter.js +1 -1
- package/dist/cjs/Hooks/filters.js +13 -14
- package/dist/cjs/Hooks/index.d.ts +1 -0
- package/dist/cjs/Hooks/index.js +1 -0
- package/dist/cjs/Hooks/sort.d.ts +32 -0
- package/dist/cjs/Hooks/sort.js +47 -0
- package/dist/cjs/Hooks/sort.test.d.ts +1 -0
- package/dist/cjs/Hooks/sort.test.js +68 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +4 -1
- package/dist/dynamic/DataViewCheckboxFilter/package.json +1 -0
- package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.d.ts +29 -0
- package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.js +62 -0
- package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.d.ts +1 -0
- package/dist/esm/DataViewCheckboxFilter/DataViewCheckboxFilter.test.js +20 -0
- package/dist/esm/DataViewCheckboxFilter/index.d.ts +2 -0
- package/dist/esm/DataViewCheckboxFilter/index.js +2 -0
- package/dist/esm/DataViewFilters/DataViewFilters.d.ts +7 -1
- package/dist/esm/DataViewFilters/DataViewFilters.js +16 -1
- package/dist/esm/DataViewTextFilter/DataViewTextFilter.js +1 -1
- package/dist/esm/Hooks/filters.js +13 -14
- package/dist/esm/Hooks/index.d.ts +1 -0
- package/dist/esm/Hooks/index.js +1 -0
- package/dist/esm/Hooks/sort.d.ts +32 -0
- package/dist/esm/Hooks/sort.js +43 -0
- package/dist/esm/Hooks/sort.test.d.ts +1 -0
- package/dist/esm/Hooks/sort.test.js +66 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/data-view/examples/Functionality/FiltersExample.tsx +31 -16
- package/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md +36 -4
- package/patternfly-docs/content/extensions/data-view/examples/Functionality/SortingExample.tsx +87 -0
- package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.test.tsx +24 -0
- package/src/DataViewCheckboxFilter/DataViewCheckboxFilter.tsx +175 -0
- package/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap +194 -0
- package/src/DataViewCheckboxFilter/index.ts +2 -0
- package/src/DataViewFilters/DataViewFilters.tsx +26 -7
- package/src/DataViewTextFilter/DataViewTextFilter.tsx +1 -0
- package/src/Hooks/filters.ts +14 -13
- package/src/Hooks/index.ts +1 -0
- package/src/Hooks/sort.test.tsx +84 -0
- package/src/Hooks/sort.ts +87 -0
- package/src/index.ts +3 -0
|
@@ -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
|
+
`;
|
|
@@ -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
|
-
|
|
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, '')}
|
package/src/Hooks/filters.ts
CHANGED
|
@@ -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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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/Hooks/index.ts
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { useDataViewSort, UseDataViewSortProps, DataViewSortConfig, DataViewSortParams } from './sort';
|
|
4
|
+
|
|
5
|
+
describe('useDataViewSort', () => {
|
|
6
|
+
const initialSort: DataViewSortConfig = { sortBy: 'name', direction: 'asc' };
|
|
7
|
+
|
|
8
|
+
it('should initialize with provided initial sort config', () => {
|
|
9
|
+
const { result } = renderHook(() => useDataViewSort({ initialSort }));
|
|
10
|
+
expect(result.current).toEqual(expect.objectContaining(initialSort));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should initialize with empty sort config if no initialSort is provided', () => {
|
|
14
|
+
const { result } = renderHook(() => useDataViewSort());
|
|
15
|
+
expect(result.current).toEqual(expect.objectContaining({ sortBy: undefined, direction: 'asc' }));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should update sort state when onSort is called', () => {
|
|
19
|
+
const { result } = renderHook(() => useDataViewSort({ initialSort }));
|
|
20
|
+
act(() => {
|
|
21
|
+
result.current.onSort(undefined, 'age', 'desc');
|
|
22
|
+
});
|
|
23
|
+
expect(result.current).toEqual(expect.objectContaining({ sortBy: 'age', direction: 'desc' }));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should sync with URL search params if isUrlSyncEnabled', () => {
|
|
27
|
+
const searchParams = new URLSearchParams();
|
|
28
|
+
const setSearchParams = jest.fn();
|
|
29
|
+
const props: UseDataViewSortProps = {
|
|
30
|
+
initialSort,
|
|
31
|
+
searchParams,
|
|
32
|
+
setSearchParams,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const { result } = renderHook(() => useDataViewSort(props));
|
|
36
|
+
|
|
37
|
+
expect(setSearchParams).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(result.current).toEqual(expect.objectContaining(initialSort));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should validate direction and fallback to default direction if invalid direction is provided', () => {
|
|
42
|
+
const searchParams = new URLSearchParams();
|
|
43
|
+
searchParams.set(DataViewSortParams.SORT_BY, 'name');
|
|
44
|
+
searchParams.set(DataViewSortParams.DIRECTION, 'invalid-direction');
|
|
45
|
+
const { result } = renderHook(() => useDataViewSort({ searchParams, defaultDirection: 'desc' }));
|
|
46
|
+
|
|
47
|
+
expect(result.current).toEqual(expect.objectContaining({ sortBy: 'name', direction: 'desc' }));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should update search params when URL sync is enabled and sort changes', () => {
|
|
51
|
+
const searchParams = new URLSearchParams();
|
|
52
|
+
const setSearchParams = jest.fn();
|
|
53
|
+
const props: UseDataViewSortProps = {
|
|
54
|
+
initialSort,
|
|
55
|
+
searchParams,
|
|
56
|
+
setSearchParams,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const { result } = renderHook(() => useDataViewSort(props));
|
|
60
|
+
act(() => {
|
|
61
|
+
expect(setSearchParams).toHaveBeenCalledTimes(1);
|
|
62
|
+
result.current.onSort(undefined, 'priority', 'desc');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(setSearchParams).toHaveBeenCalledTimes(2);
|
|
66
|
+
expect(result.current).toEqual(expect.objectContaining({ sortBy: 'priority', direction: 'desc' }));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should prioritize searchParams values', () => {
|
|
70
|
+
const searchParams = new URLSearchParams();
|
|
71
|
+
searchParams.set(DataViewSortParams.SORT_BY, 'category');
|
|
72
|
+
searchParams.set(DataViewSortParams.DIRECTION, 'desc');
|
|
73
|
+
|
|
74
|
+
const { result } = renderHook(
|
|
75
|
+
(props: UseDataViewSortProps) => useDataViewSort(props),
|
|
76
|
+
{ initialProps: { initialSort, searchParams } }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result.current).toEqual(expect.objectContaining({
|
|
80
|
+
sortBy: 'category',
|
|
81
|
+
direction: 'desc',
|
|
82
|
+
}));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ISortBy } from "@patternfly/react-table";
|
|
2
|
+
import { useState, useEffect, useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
export enum DataViewSortParams {
|
|
5
|
+
SORT_BY = 'sortBy',
|
|
6
|
+
DIRECTION = 'direction'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const validateDirection = (direction: string | null | undefined, defaultDirection: ISortBy['direction']): ISortBy['direction'] => (
|
|
10
|
+
direction === 'asc' || direction === 'desc' ? direction : defaultDirection
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export interface DataViewSortConfig {
|
|
14
|
+
/** Attribute to sort the entries by */
|
|
15
|
+
sortBy: string | undefined;
|
|
16
|
+
/** Sort direction */
|
|
17
|
+
direction: ISortBy['direction'];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface UseDataViewSortProps {
|
|
21
|
+
/** Initial sort config */
|
|
22
|
+
initialSort?: DataViewSortConfig;
|
|
23
|
+
/** Current search parameters as a string */
|
|
24
|
+
searchParams?: URLSearchParams;
|
|
25
|
+
/** Function to set search parameters */
|
|
26
|
+
setSearchParams?: (params: URLSearchParams) => void;
|
|
27
|
+
/** Default direction */
|
|
28
|
+
defaultDirection?: ISortBy['direction'];
|
|
29
|
+
/** Sort by URL param name */
|
|
30
|
+
sortByParam?: string;
|
|
31
|
+
/** Direction URL param name */
|
|
32
|
+
directionParam?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const useDataViewSort = (props?: UseDataViewSortProps) => {
|
|
36
|
+
const {
|
|
37
|
+
initialSort,
|
|
38
|
+
searchParams,
|
|
39
|
+
setSearchParams,
|
|
40
|
+
defaultDirection = 'asc',
|
|
41
|
+
sortByParam = DataViewSortParams.SORT_BY,
|
|
42
|
+
directionParam = DataViewSortParams.DIRECTION
|
|
43
|
+
} = props ?? {};
|
|
44
|
+
|
|
45
|
+
const isUrlSyncEnabled = useMemo(() => searchParams && !!setSearchParams, [ searchParams, setSearchParams ]);
|
|
46
|
+
|
|
47
|
+
const [ state, setState ] = useState<DataViewSortConfig>({
|
|
48
|
+
sortBy: searchParams?.get(sortByParam) ?? initialSort?.sortBy,
|
|
49
|
+
direction: validateDirection(searchParams?.get(directionParam) as ISortBy['direction'], initialSort?.direction),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const updateSearchParams = (sortBy: string, direction: ISortBy['direction']) => {
|
|
53
|
+
if (isUrlSyncEnabled && sortBy) {
|
|
54
|
+
const params = new URLSearchParams(searchParams);
|
|
55
|
+
params.set(sortByParam, `${sortBy}`);
|
|
56
|
+
params.set(directionParam, `${direction}`);
|
|
57
|
+
setSearchParams?.(params);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
state.sortBy && state.direction && updateSearchParams(state.sortBy, state.direction);
|
|
63
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const currentSortBy = searchParams?.get(sortByParam) || state.sortBy;
|
|
68
|
+
const currentDirection = searchParams?.get(directionParam) as ISortBy['direction'] || state.direction;
|
|
69
|
+
const validDirection = validateDirection(currentDirection, defaultDirection);
|
|
70
|
+
currentSortBy !== state.sortBy || validDirection !== state.direction && setState({ sortBy: currentSortBy, direction: validDirection });
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
}, [ searchParams?.toString() ]);
|
|
73
|
+
|
|
74
|
+
const onSort = (
|
|
75
|
+
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined,
|
|
76
|
+
newSortBy: string,
|
|
77
|
+
newSortDirection: ISortBy['direction']
|
|
78
|
+
) => {
|
|
79
|
+
setState({ sortBy: newSortBy, direction: newSortDirection });
|
|
80
|
+
updateSearchParams(newSortBy, newSortDirection);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
onSort
|
|
86
|
+
};
|
|
87
|
+
};
|
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';
|