@patternfly/react-core 4.247.0 → 4.248.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/CHANGELOG.md +19 -0
- package/dist/esm/components/Menu/Menu.d.ts.map +1 -1
- package/dist/esm/components/Menu/Menu.js +10 -3
- package/dist/esm/components/Menu/Menu.js.map +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.js +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/js/components/Menu/Menu.d.ts.map +1 -1
- package/dist/js/components/Menu/Menu.js +10 -3
- package/dist/js/components/Menu/Menu.js.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.js +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/umd/react-core.min.css +1942 -1942
- package/dist/umd/react-core.min.js +1 -1
- package/package.json +2 -2
- package/src/components/Menu/Menu.tsx +13 -7
- package/src/components/MenuToggle/MenuToggle.tsx +1 -0
- package/src/demos/Filters/FilterDemos.md +27 -5
- package/src/demos/Filters/examples/FilterAttributeSearch.tsx +701 -0
- package/src/demos/Filters/examples/FilterCheckboxSelect.tsx +66 -53
- package/src/demos/Filters/examples/FilterFaceted.tsx +512 -0
- package/src/demos/Filters/examples/FilterMixedSelectGroup.tsx +569 -0
- package/src/demos/Filters/examples/FilterSameSelectGroup.tsx +542 -0
- package/src/demos/Filters/examples/FilterSearchInput.tsx +91 -68
- package/src/demos/Filters/examples/FilterSingleSelect.tsx +63 -52
- package/src/demos/Page.md +47 -0
- package/src/demos/examples/DashboardWrapper.js +3 -1
- package/src/demos/examples/Page/PageStickySectionBreadcrumb.tsx +305 -0
- package/src/demos/examples/Page/PageStickySectionGroup.tsx +306 -0
- package/src/demos/examples/Page/PageStickySectionGroupAlternate.tsx +301 -0
- package/src/demos/examples/Page/PageStickySectionGroupUsingPageHeader.tsx +293 -0
- package/src/demos/Page/Page.md +0 -1184
- package/src/demos/Page/imgAvatar.svg +0 -33
- package/src/demos/Page/imgBrand.svg +0 -1
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
Title,
|
|
18
18
|
EmptyStateBody,
|
|
19
19
|
EmptyStatePrimary,
|
|
20
|
-
Button
|
|
20
|
+
Button,
|
|
21
|
+
Bullseye
|
|
21
22
|
} from '@patternfly/react-core';
|
|
22
23
|
import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
|
|
23
24
|
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
|
|
@@ -36,7 +37,7 @@ const repositories: Repository[] = [
|
|
|
36
37
|
{ name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },
|
|
37
38
|
{ name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },
|
|
38
39
|
{ name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },
|
|
39
|
-
{ name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs
|
|
40
|
+
{ name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },
|
|
40
41
|
{ name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },
|
|
41
42
|
{ name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },
|
|
42
43
|
{ name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },
|
|
@@ -56,11 +57,32 @@ const columnNames = {
|
|
|
56
57
|
|
|
57
58
|
/* eslint-disable patternfly-react/no-anonymous-functions */
|
|
58
59
|
export const FilterSearchInput: React.FunctionComponent = () => {
|
|
59
|
-
|
|
60
|
-
const
|
|
60
|
+
// Set up repo filtering
|
|
61
|
+
const [searchValue, setSearchValue] = React.useState('');
|
|
62
|
+
|
|
63
|
+
const onSearchChange = (value: string) => {
|
|
64
|
+
setSearchValue(value);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const onFilter = (repo: Repository) => {
|
|
68
|
+
if (searchValue === '') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
61
71
|
|
|
72
|
+
let input: RegExp;
|
|
73
|
+
try {
|
|
74
|
+
input = new RegExp(searchValue, 'i');
|
|
75
|
+
} catch (err) {
|
|
76
|
+
input = new RegExp(searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
77
|
+
}
|
|
78
|
+
return repo.name.search(input) >= 0;
|
|
79
|
+
};
|
|
80
|
+
const filteredRepos = repositories.filter(onFilter);
|
|
81
|
+
|
|
82
|
+
// Set up table row selection
|
|
62
83
|
// In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.
|
|
63
84
|
// This is to prevent state from being based on row order index in case we later add sorting.
|
|
85
|
+
const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example
|
|
64
86
|
const [selectedRepoNames, setSelectedRepoNames] = React.useState<string[]>([]);
|
|
65
87
|
const setRepoSelected = (repo: Repository, isSelecting = true) =>
|
|
66
88
|
setSelectedRepoNames(prevSelected => {
|
|
@@ -68,8 +90,8 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
68
90
|
return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;
|
|
69
91
|
});
|
|
70
92
|
const selectAllRepos = (isSelecting = true) =>
|
|
71
|
-
setSelectedRepoNames(isSelecting ?
|
|
72
|
-
const areAllReposSelected = selectedRepoNames.length ===
|
|
93
|
+
setSelectedRepoNames(isSelecting ? filteredRepos.map(r => r.name) : []); // Selecting all should only select all currently filtered rows
|
|
94
|
+
const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;
|
|
73
95
|
const areSomeReposSelected = selectedRepoNames.length > 0;
|
|
74
96
|
const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);
|
|
75
97
|
|
|
@@ -113,21 +135,13 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
113
135
|
};
|
|
114
136
|
}, []);
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
const
|
|
138
|
+
// Set up bulk selection menu
|
|
139
|
+
const bulkSelectMenuRef = React.useRef<HTMLDivElement>(null);
|
|
140
|
+
const bulkSelectToggleRef = React.useRef<any>(null);
|
|
141
|
+
const bulkSelectContainerRef = React.useRef<HTMLDivElement>(null);
|
|
119
142
|
|
|
120
143
|
const [isBulkSelectOpen, setIsBulkSelectOpen] = React.useState<boolean>(false);
|
|
121
144
|
|
|
122
|
-
React.useEffect(() => {
|
|
123
|
-
window.addEventListener('keydown', handleBulkSelectMenuKeys);
|
|
124
|
-
window.addEventListener('click', handleBulkSelectClickOutside);
|
|
125
|
-
return () => {
|
|
126
|
-
window.removeEventListener('keydown', handleBulkSelectMenuKeys);
|
|
127
|
-
window.removeEventListener('click', handleBulkSelectClickOutside);
|
|
128
|
-
};
|
|
129
|
-
}, [isBulkSelectOpen, bulkSelectMenuRef]);
|
|
130
|
-
|
|
131
145
|
const handleBulkSelectClickOutside = (event: MouseEvent) => {
|
|
132
146
|
if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {
|
|
133
147
|
setIsBulkSelectOpen(false);
|
|
@@ -144,11 +158,20 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
144
158
|
) {
|
|
145
159
|
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
146
160
|
setIsBulkSelectOpen(!isBulkSelectOpen);
|
|
147
|
-
bulkSelectToggleRef.current?.focus();
|
|
161
|
+
bulkSelectToggleRef.current?.querySelector('button').focus();
|
|
148
162
|
}
|
|
149
163
|
}
|
|
150
164
|
};
|
|
151
165
|
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
window.addEventListener('keydown', handleBulkSelectMenuKeys);
|
|
168
|
+
window.addEventListener('click', handleBulkSelectClickOutside);
|
|
169
|
+
return () => {
|
|
170
|
+
window.removeEventListener('keydown', handleBulkSelectMenuKeys);
|
|
171
|
+
window.removeEventListener('click', handleBulkSelectClickOutside);
|
|
172
|
+
};
|
|
173
|
+
}, [isBulkSelectOpen, bulkSelectMenuRef]);
|
|
174
|
+
|
|
152
175
|
const onBulkSelectToggleClick = (ev: React.MouseEvent) => {
|
|
153
176
|
ev.stopPropagation(); // Stop handleClickOutside from handling
|
|
154
177
|
setTimeout(() => {
|
|
@@ -175,8 +198,8 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
175
198
|
splitButtonOptions={{
|
|
176
199
|
items: [
|
|
177
200
|
<MenuToggleCheckbox
|
|
178
|
-
id="select
|
|
179
|
-
key="select
|
|
201
|
+
id="search-input-bulk-select"
|
|
202
|
+
key="search-input-bulk-select"
|
|
180
203
|
aria-label="Select all"
|
|
181
204
|
isChecked={menuToggleCheckmark}
|
|
182
205
|
onChange={(checked, _event) => selectAllRepos(checked)}
|
|
@@ -188,12 +211,13 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
188
211
|
);
|
|
189
212
|
|
|
190
213
|
const bulkSelectMenu = (
|
|
191
|
-
// eslint-disable-next-line no-console
|
|
192
214
|
<Menu
|
|
215
|
+
id="search-input-bulk-select"
|
|
193
216
|
ref={bulkSelectMenuRef}
|
|
194
217
|
onSelect={(_ev, itemId) => {
|
|
195
218
|
selectAllRepos(itemId === 1 || itemId === 2);
|
|
196
219
|
setIsBulkSelectOpen(!isBulkSelectOpen);
|
|
220
|
+
bulkSelectToggleRef.current?.querySelector('button').focus();
|
|
197
221
|
}}
|
|
198
222
|
>
|
|
199
223
|
<MenuContent>
|
|
@@ -207,37 +231,17 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
207
231
|
);
|
|
208
232
|
|
|
209
233
|
const toolbarBulkSelect = (
|
|
210
|
-
<div ref={
|
|
234
|
+
<div ref={bulkSelectContainerRef}>
|
|
211
235
|
<Popper
|
|
212
236
|
trigger={bulkSelectToggle}
|
|
213
237
|
popper={bulkSelectMenu}
|
|
214
|
-
appendTo={
|
|
238
|
+
appendTo={bulkSelectContainerRef.current || undefined}
|
|
215
239
|
isVisible={isBulkSelectOpen}
|
|
216
240
|
popperMatchesTriggerWidth={false}
|
|
217
241
|
/>
|
|
218
242
|
</div>
|
|
219
243
|
);
|
|
220
244
|
|
|
221
|
-
const [searchValue, setSearchValue] = React.useState('');
|
|
222
|
-
|
|
223
|
-
const onSearchChange = (value: string) => {
|
|
224
|
-
setSearchValue(value);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const onFilter = (repo: Repository) => {
|
|
228
|
-
if (searchValue === '') {
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let input: RegExp;
|
|
233
|
-
try {
|
|
234
|
-
input = new RegExp(searchValue, 'i');
|
|
235
|
-
} catch (err) {
|
|
236
|
-
input = new RegExp(searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
237
|
-
}
|
|
238
|
-
return repo.name.search(input) >= 0;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
245
|
const searchInput = (
|
|
242
246
|
<SearchInput
|
|
243
247
|
placeholder="Filter by server name"
|
|
@@ -269,25 +273,6 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
269
273
|
</Toolbar>
|
|
270
274
|
);
|
|
271
275
|
|
|
272
|
-
const filteredRepos = repositories.filter(onFilter).map((repo, rowIndex) => (
|
|
273
|
-
<Tr key={repo.name}>
|
|
274
|
-
<Td
|
|
275
|
-
select={{
|
|
276
|
-
rowIndex,
|
|
277
|
-
onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),
|
|
278
|
-
isSelected: isRepoSelected(repo),
|
|
279
|
-
disable: !isRepoSelectable(repo)
|
|
280
|
-
}}
|
|
281
|
-
/>
|
|
282
|
-
<Td dataLabel={columnNames.name}>{repo.name}</Td>
|
|
283
|
-
<Td dataLabel={columnNames.threads}>{repo.threads}</Td>
|
|
284
|
-
<Td dataLabel={columnNames.apps}>{repo.apps}</Td>
|
|
285
|
-
<Td dataLabel={columnNames.workspaces}>{repo.workspaces}</Td>
|
|
286
|
-
<Td dataLabel={columnNames.status}>{repo.status}</Td>
|
|
287
|
-
<Td dataLabel={columnNames.location}>{repo.location}</Td>
|
|
288
|
-
</Tr>
|
|
289
|
-
));
|
|
290
|
-
|
|
291
276
|
const emptyState = (
|
|
292
277
|
<EmptyState>
|
|
293
278
|
<EmptyStateIcon icon={SearchIcon} />
|
|
@@ -316,16 +301,54 @@ export const FilterSearchInput: React.FunctionComponent = () => {
|
|
|
316
301
|
<Tr>
|
|
317
302
|
<Th />
|
|
318
303
|
<Th width={20}>{columnNames.name}</Th>
|
|
319
|
-
<Th width={
|
|
320
|
-
<Th width={
|
|
321
|
-
<Th width={
|
|
304
|
+
<Th width={10}>{columnNames.threads}</Th>
|
|
305
|
+
<Th width={10}>{columnNames.apps}</Th>
|
|
306
|
+
<Th width={10}>{columnNames.workspaces}</Th>
|
|
322
307
|
<Th width={20}>{columnNames.status}</Th>
|
|
323
308
|
<Th width={20}>{columnNames.location}</Th>
|
|
324
309
|
</Tr>
|
|
325
310
|
</Thead>
|
|
326
|
-
<Tbody>
|
|
311
|
+
<Tbody>
|
|
312
|
+
{filteredRepos.length > 0 &&
|
|
313
|
+
filteredRepos.map((repo, rowIndex) => (
|
|
314
|
+
<Tr key={repo.name}>
|
|
315
|
+
<Td
|
|
316
|
+
select={{
|
|
317
|
+
rowIndex,
|
|
318
|
+
onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),
|
|
319
|
+
isSelected: isRepoSelected(repo),
|
|
320
|
+
disable: !isRepoSelectable(repo)
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
323
|
+
<Td dataLabel={columnNames.name} modifier="truncate">
|
|
324
|
+
{repo.name}
|
|
325
|
+
</Td>
|
|
326
|
+
<Td dataLabel={columnNames.threads} modifier="truncate">
|
|
327
|
+
{repo.threads}
|
|
328
|
+
</Td>
|
|
329
|
+
<Td dataLabel={columnNames.apps} modifier="truncate">
|
|
330
|
+
{repo.apps}
|
|
331
|
+
</Td>
|
|
332
|
+
<Td dataLabel={columnNames.workspaces} modifier="truncate">
|
|
333
|
+
{repo.workspaces}
|
|
334
|
+
</Td>
|
|
335
|
+
<Td dataLabel={columnNames.status} modifier="truncate">
|
|
336
|
+
{repo.status}
|
|
337
|
+
</Td>
|
|
338
|
+
<Td dataLabel={columnNames.location} modifier="truncate">
|
|
339
|
+
{repo.location}
|
|
340
|
+
</Td>
|
|
341
|
+
</Tr>
|
|
342
|
+
))}
|
|
343
|
+
{filteredRepos.length === 0 && (
|
|
344
|
+
<Tr>
|
|
345
|
+
<Td colSpan={8}>
|
|
346
|
+
<Bullseye>{emptyState}</Bullseye>
|
|
347
|
+
</Td>
|
|
348
|
+
</Tr>
|
|
349
|
+
)}
|
|
350
|
+
</Tbody>
|
|
327
351
|
</TableComposable>
|
|
328
|
-
{filteredRepos.length === 0 && emptyState}
|
|
329
352
|
</React.Fragment>
|
|
330
353
|
);
|
|
331
354
|
};
|
|
@@ -29,7 +29,7 @@ const repositories: Repository[] = [
|
|
|
29
29
|
{ name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },
|
|
30
30
|
{ name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },
|
|
31
31
|
{ name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },
|
|
32
|
-
{ name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs
|
|
32
|
+
{ name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },
|
|
33
33
|
{ name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },
|
|
34
34
|
{ name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },
|
|
35
35
|
{ name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },
|
|
@@ -49,12 +49,28 @@ const columnNames = {
|
|
|
49
49
|
|
|
50
50
|
/* eslint-disable patternfly-react/no-anonymous-functions */
|
|
51
51
|
export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
52
|
-
// Set up
|
|
53
|
-
const
|
|
54
|
-
const
|
|
52
|
+
// Set up repo filtering
|
|
53
|
+
const [searchValue, setSearchValue] = React.useState('');
|
|
54
|
+
const onFilter = (repo: Repository) => {
|
|
55
|
+
if (searchValue === 'all') {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
55
58
|
|
|
59
|
+
let input: RegExp;
|
|
60
|
+
try {
|
|
61
|
+
input = new RegExp(searchValue, 'i');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
input = new RegExp(searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
64
|
+
}
|
|
65
|
+
return repo.status.search(input) >= 0;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const filteredRepos = repositories.filter(onFilter);
|
|
69
|
+
|
|
70
|
+
// Set up table selection
|
|
56
71
|
// In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.
|
|
57
72
|
// This is to prevent state from being based on row order index in case we later add sorting.
|
|
73
|
+
const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example
|
|
58
74
|
const [selectedRepoNames, setSelectedRepoNames] = React.useState<string[]>([]);
|
|
59
75
|
const setRepoSelected = (repo: Repository, isSelecting = true) =>
|
|
60
76
|
setSelectedRepoNames(prevSelected => {
|
|
@@ -62,8 +78,8 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
62
78
|
return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;
|
|
63
79
|
});
|
|
64
80
|
const selectAllRepos = (isSelecting = true) =>
|
|
65
|
-
setSelectedRepoNames(isSelecting ?
|
|
66
|
-
const areAllReposSelected = selectedRepoNames.length ===
|
|
81
|
+
setSelectedRepoNames(isSelecting ? filteredRepos.map(r => r.name) : []);
|
|
82
|
+
const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;
|
|
67
83
|
const areSomeReposSelected = selectedRepoNames.length > 0;
|
|
68
84
|
const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);
|
|
69
85
|
|
|
@@ -108,21 +124,12 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
108
124
|
}, []);
|
|
109
125
|
|
|
110
126
|
// Set up bulk selection menu
|
|
111
|
-
const bulkSelectMenuRef = React.
|
|
112
|
-
const bulkSelectToggleRef = React.
|
|
113
|
-
const
|
|
127
|
+
const bulkSelectMenuRef = React.useRef<HTMLDivElement>(null);
|
|
128
|
+
const bulkSelectToggleRef = React.useRef<any>(null);
|
|
129
|
+
const bulkSelectContainerRef = React.useRef<HTMLDivElement>(null);
|
|
114
130
|
|
|
115
131
|
const [isBulkSelectOpen, setIsBulkSelectOpen] = React.useState<boolean>(false);
|
|
116
132
|
|
|
117
|
-
React.useEffect(() => {
|
|
118
|
-
window.addEventListener('keydown', handleBulkSelectMenuKeys);
|
|
119
|
-
window.addEventListener('click', handleBulkSelectClickOutside);
|
|
120
|
-
return () => {
|
|
121
|
-
window.removeEventListener('keydown', handleBulkSelectMenuKeys);
|
|
122
|
-
window.removeEventListener('click', handleBulkSelectClickOutside);
|
|
123
|
-
};
|
|
124
|
-
}, [isBulkSelectOpen, bulkSelectMenuRef]);
|
|
125
|
-
|
|
126
133
|
const handleBulkSelectClickOutside = (event: MouseEvent) => {
|
|
127
134
|
if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {
|
|
128
135
|
setIsBulkSelectOpen(false);
|
|
@@ -139,11 +146,20 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
139
146
|
) {
|
|
140
147
|
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
141
148
|
setIsBulkSelectOpen(!isBulkSelectOpen);
|
|
142
|
-
bulkSelectToggleRef.current?.focus();
|
|
149
|
+
bulkSelectToggleRef.current?.querySelector('button').focus();
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
};
|
|
146
153
|
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
window.addEventListener('keydown', handleBulkSelectMenuKeys);
|
|
156
|
+
window.addEventListener('click', handleBulkSelectClickOutside);
|
|
157
|
+
return () => {
|
|
158
|
+
window.removeEventListener('keydown', handleBulkSelectMenuKeys);
|
|
159
|
+
window.removeEventListener('click', handleBulkSelectClickOutside);
|
|
160
|
+
};
|
|
161
|
+
}, [isBulkSelectOpen, bulkSelectMenuRef]);
|
|
162
|
+
|
|
147
163
|
const onBulkSelectToggleClick = (ev: React.MouseEvent) => {
|
|
148
164
|
ev.stopPropagation(); // Stop handleClickOutside from handling
|
|
149
165
|
setTimeout(() => {
|
|
@@ -170,8 +186,8 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
170
186
|
splitButtonOptions={{
|
|
171
187
|
items: [
|
|
172
188
|
<MenuToggleCheckbox
|
|
173
|
-
id="select
|
|
174
|
-
key="select
|
|
189
|
+
id="single-bulk-select"
|
|
190
|
+
key="single-bulk-select"
|
|
175
191
|
aria-label="Select all"
|
|
176
192
|
isChecked={menuToggleCheckmark}
|
|
177
193
|
onChange={(checked, _event) => selectAllRepos(checked)}
|
|
@@ -183,12 +199,12 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
183
199
|
);
|
|
184
200
|
|
|
185
201
|
const bulkSelectMenu = (
|
|
186
|
-
// eslint-disable-next-line no-console
|
|
187
202
|
<Menu
|
|
188
203
|
ref={bulkSelectMenuRef}
|
|
189
204
|
onSelect={(_ev, itemId) => {
|
|
190
205
|
selectAllRepos(itemId === 1 || itemId === 2);
|
|
191
206
|
setIsBulkSelectOpen(!isBulkSelectOpen);
|
|
207
|
+
bulkSelectToggleRef.current?.querySelector('button').focus();
|
|
192
208
|
}}
|
|
193
209
|
>
|
|
194
210
|
<MenuContent>
|
|
@@ -202,34 +218,17 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
202
218
|
);
|
|
203
219
|
|
|
204
220
|
const toolbarBulkSelect = (
|
|
205
|
-
<div ref={
|
|
221
|
+
<div ref={bulkSelectContainerRef}>
|
|
206
222
|
<Popper
|
|
207
223
|
trigger={bulkSelectToggle}
|
|
208
224
|
popper={bulkSelectMenu}
|
|
209
|
-
appendTo={
|
|
225
|
+
appendTo={bulkSelectContainerRef.current || undefined}
|
|
210
226
|
isVisible={isBulkSelectOpen}
|
|
211
227
|
popperMatchesTriggerWidth={false}
|
|
212
228
|
/>
|
|
213
229
|
</div>
|
|
214
230
|
);
|
|
215
231
|
|
|
216
|
-
// Set up table filtering
|
|
217
|
-
const [searchValue, setSearchValue] = React.useState('');
|
|
218
|
-
|
|
219
|
-
const onFilter = (repo: Repository) => {
|
|
220
|
-
if (searchValue === 'all') {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let input: RegExp;
|
|
225
|
-
try {
|
|
226
|
-
input = new RegExp(searchValue, 'i');
|
|
227
|
-
} catch (err) {
|
|
228
|
-
input = new RegExp(searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
229
|
-
}
|
|
230
|
-
return repo.status.search(input) >= 0;
|
|
231
|
-
};
|
|
232
|
-
|
|
233
232
|
// Set up single select menu & state
|
|
234
233
|
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
235
234
|
const [menuSelection, setMenuSelection] = React.useState<string>('all');
|
|
@@ -308,7 +307,7 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
308
307
|
);
|
|
309
308
|
|
|
310
309
|
const menu = (
|
|
311
|
-
<Menu ref={menuRef} id="select-menu" onSelect={onSelect} selected={menuSelection}>
|
|
310
|
+
<Menu ref={menuRef} id="single-select-menu" onSelect={onSelect} selected={menuSelection}>
|
|
312
311
|
<MenuContent>
|
|
313
312
|
<MenuList>
|
|
314
313
|
<MenuItem itemId="all">All statuses</MenuItem>
|
|
@@ -359,15 +358,15 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
359
358
|
<Tr>
|
|
360
359
|
<Th />
|
|
361
360
|
<Th width={20}>{columnNames.name}</Th>
|
|
362
|
-
<Th width={
|
|
363
|
-
<Th width={
|
|
364
|
-
<Th width={
|
|
361
|
+
<Th width={10}>{columnNames.threads}</Th>
|
|
362
|
+
<Th width={10}>{columnNames.apps}</Th>
|
|
363
|
+
<Th width={10}>{columnNames.workspaces}</Th>
|
|
365
364
|
<Th width={20}>{columnNames.status}</Th>
|
|
366
365
|
<Th width={20}>{columnNames.location}</Th>
|
|
367
366
|
</Tr>
|
|
368
367
|
</Thead>
|
|
369
368
|
<Tbody>
|
|
370
|
-
{
|
|
369
|
+
{filteredRepos.map((repo, rowIndex) => (
|
|
371
370
|
<Tr key={repo.name}>
|
|
372
371
|
<Td
|
|
373
372
|
select={{
|
|
@@ -377,12 +376,24 @@ export const FilterSingleSelect: React.FunctionComponent = () => {
|
|
|
377
376
|
disable: !isRepoSelectable(repo)
|
|
378
377
|
}}
|
|
379
378
|
/>
|
|
380
|
-
<Td dataLabel={columnNames.name}>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
<Td dataLabel={columnNames.
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
<Td dataLabel={columnNames.name} modifier="truncate">
|
|
380
|
+
{repo.name}
|
|
381
|
+
</Td>
|
|
382
|
+
<Td dataLabel={columnNames.threads} modifier="truncate">
|
|
383
|
+
{repo.threads}
|
|
384
|
+
</Td>
|
|
385
|
+
<Td dataLabel={columnNames.apps} modifier="truncate">
|
|
386
|
+
{repo.apps}
|
|
387
|
+
</Td>
|
|
388
|
+
<Td dataLabel={columnNames.workspaces} modifier="truncate">
|
|
389
|
+
{repo.workspaces}
|
|
390
|
+
</Td>
|
|
391
|
+
<Td dataLabel={columnNames.status} modifier="truncate">
|
|
392
|
+
{repo.status}
|
|
393
|
+
</Td>
|
|
394
|
+
<Td dataLabel={columnNames.location} modifier="truncate">
|
|
395
|
+
{repo.location}
|
|
396
|
+
</Td>
|
|
386
397
|
</Tr>
|
|
387
398
|
))}
|
|
388
399
|
</Tbody>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: Page
|
|
3
|
+
section: components
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
|
|
7
|
+
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
|
|
8
|
+
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
|
|
9
|
+
import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
|
|
10
|
+
import imgBrand from '@patternfly/react-core/src/demos/examples/pfColorLogo.svg';
|
|
11
|
+
import imgAvatar from '@patternfly/react-core/src/components/Avatar/examples/avatarImg.svg';
|
|
12
|
+
import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
|
|
13
|
+
import AttentionBellIcon from '@patternfly/react-icons/dist/esm/icons/attention-bell-icon';
|
|
14
|
+
import LightbulbIcon from '@patternfly/react-icons/dist/esm/icons/lightbulb-icon';
|
|
15
|
+
|
|
16
|
+
- All examples set the `isManagedSidebar` prop on the Page component to have the sidebar automatically close for smaller screen widths. You can also manually control this behavior by not adding the `isManagedSidebar` prop and instead:
|
|
17
|
+
|
|
18
|
+
1. Add an onNavToggle callback to PageHeader
|
|
19
|
+
2. Pass a boolean into the isNavOpen prop to PageSidebar
|
|
20
|
+
|
|
21
|
+
- To make the page take up the full height, it is recommended to set the height of all ancestor elements up to the page component to `100%`
|
|
22
|
+
|
|
23
|
+
## Layouts
|
|
24
|
+
|
|
25
|
+
This demonstrates a variety of navigation patterns in the context of a full page layout. These can be used as a basis for choosing the most appropriate page template for your application.
|
|
26
|
+
|
|
27
|
+
### Sticky section group
|
|
28
|
+
|
|
29
|
+
```ts file='./examples/Page/PageStickySectionGroup.tsx' isFullscreen
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Sticky section group (alternate syntax)
|
|
33
|
+
|
|
34
|
+
```ts file='./examples/Page/PageStickySectionGroupAlternate.tsx' isFullscreen
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Sticky section group (using PageHeader)
|
|
38
|
+
|
|
39
|
+
This demo is provided becuase PageHeader and PageHeaderTools are still in use; however, going forward Masthead and Toolbar should be used to make headers rather than PageHeader and PageHeaderTools.
|
|
40
|
+
|
|
41
|
+
```ts file='./examples/Page/PageStickySectionGroupUsingPageHeader.tsx' isFullscreen
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Sticky section breadcrumb (with breakpoints)
|
|
45
|
+
|
|
46
|
+
```ts file='./examples/Page/PageStickySectionBreadcrumb.tsx' isFullscreen
|
|
47
|
+
```
|
|
@@ -60,7 +60,8 @@ export default class DashboardWrapper extends React.Component {
|
|
|
60
60
|
hasNoBreadcrumb,
|
|
61
61
|
notificationDrawer,
|
|
62
62
|
isNotificationDrawerExpanded,
|
|
63
|
-
hasPageTemplateTitle
|
|
63
|
+
hasPageTemplateTitle,
|
|
64
|
+
...pageProps
|
|
64
65
|
} = this.props;
|
|
65
66
|
|
|
66
67
|
let renderedBreadcrumb;
|
|
@@ -108,6 +109,7 @@ export default class DashboardWrapper extends React.Component {
|
|
|
108
109
|
notificationDrawer={notificationDrawer}
|
|
109
110
|
isNotificationDrawerExpanded={isNotificationDrawerExpanded}
|
|
110
111
|
onPageResize={onPageResize}
|
|
112
|
+
{...pageProps}
|
|
111
113
|
>
|
|
112
114
|
{hasPageTemplateTitle && PageTemplateTitle}
|
|
113
115
|
{children}
|