@patternfly/react-core 4.237.7 → 4.237.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-core",
3
- "version": "4.237.7",
3
+ "version": "4.237.8",
4
4
  "description": "This library provides a set of common React components for use with the PatternFly reference implementation.",
5
5
  "main": "dist/js/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -44,16 +44,16 @@
44
44
  "generate": "node scripts/copyStyles.js"
45
45
  },
46
46
  "dependencies": {
47
- "@patternfly/react-icons": "^4.88.7",
48
- "@patternfly/react-styles": "^4.87.7",
49
- "@patternfly/react-tokens": "^4.89.7",
47
+ "@patternfly/react-icons": "^4.88.8",
48
+ "@patternfly/react-styles": "^4.87.8",
49
+ "@patternfly/react-tokens": "^4.89.8",
50
50
  "focus-trap": "6.9.2",
51
51
  "react-dropzone": "9.0.0",
52
52
  "tippy.js": "5.1.2",
53
53
  "tslib": "^2.0.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@patternfly/patternfly": "4.214.1",
56
+ "@patternfly/patternfly": "4.215.0",
57
57
  "@rollup/plugin-commonjs": "^21.0.0",
58
58
  "@rollup/plugin-node-resolve": "^13.0.0",
59
59
  "@rollup/plugin-replace": "^3.0.0",
@@ -70,5 +70,5 @@
70
70
  "react": "^16.8.0 || ^17.0.0",
71
71
  "react-dom": "^16.8.0 || ^17.0.0"
72
72
  },
73
- "gitHead": "6a8c488ab5de48d31ab8070271ebf17f9e6a625a"
73
+ "gitHead": "655ddf36fb109c26211e803531627a92841587d1"
74
74
  }
@@ -0,0 +1,48 @@
1
+ ---
2
+ id: Filters
3
+ section: demos
4
+ ---
5
+
6
+ import {
7
+ Popper,
8
+ SearchInput,
9
+ Toolbar,
10
+ ToolbarContent,
11
+ ToolbarItem,
12
+ Menu,
13
+ MenuContent,
14
+ MenuList,
15
+ MenuItem,
16
+ MenuToggle,
17
+ MenuToggleCheckbox,
18
+ ToolbarGroup,
19
+ ToolbarFilter,
20
+ Badge,
21
+ Pagination,
22
+ EmptyState,
23
+ EmptyStateIcon,
24
+ Title,
25
+ EmptyStateBody,
26
+ EmptyStatePrimary,
27
+ Button
28
+ } from '@patternfly/react-core';
29
+ import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';
30
+ import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
31
+ import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
32
+
33
+ ## Demos
34
+
35
+ ### Filtering with a search input
36
+
37
+ ```ts file="./examples/FilterSearchInput.tsx"
38
+ ```
39
+
40
+ ### Filtering with a single select
41
+
42
+ ```ts file="./examples/FilterSingleSelect.tsx"
43
+ ```
44
+
45
+ ### Filtering with a checkbox select
46
+
47
+ ```ts file="./examples/FilterCheckboxSelect.tsx"
48
+ ```
@@ -0,0 +1,405 @@
1
+ import React from 'react';
2
+ import {
3
+ Menu,
4
+ MenuContent,
5
+ MenuList,
6
+ MenuItem,
7
+ MenuToggle,
8
+ MenuToggleCheckbox,
9
+ Popper,
10
+ Toolbar,
11
+ ToolbarContent,
12
+ ToolbarGroup,
13
+ ToolbarFilter,
14
+ ToolbarItem,
15
+ Badge,
16
+ Pagination
17
+ } from '@patternfly/react-core';
18
+ import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
19
+ import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';
20
+
21
+ interface Repository {
22
+ name: string;
23
+ threads: string;
24
+ apps: string;
25
+ workspaces: string;
26
+ status: string;
27
+ location: string;
28
+ }
29
+
30
+ // In real usage, this data would come from some external source like an API via props.
31
+ const repositories: Repository[] = [
32
+ { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },
33
+ { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },
34
+ { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },
35
+ { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintainence', location: 'Raleigh' },
36
+ { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },
37
+ { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },
38
+ { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },
39
+ { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },
40
+ { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },
41
+ { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }
42
+ ];
43
+
44
+ const columnNames = {
45
+ name: 'Servers',
46
+ threads: 'Threads',
47
+ apps: 'Applications',
48
+ workspaces: 'Workspaces',
49
+ status: 'Status',
50
+ location: 'Location'
51
+ };
52
+
53
+ /* eslint-disable patternfly-react/no-anonymous-functions */
54
+ export const FilterCheckboxSelect: React.FunctionComponent = () => {
55
+ // Set up table selection
56
+ const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example
57
+ const selectableRepos = repositories.filter(isRepoSelectable);
58
+
59
+ // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.
60
+ // This is to prevent state from being based on row order index in case we later add sorting.
61
+ const [selectedRepoNames, setSelectedRepoNames] = React.useState<string[]>([]);
62
+ const setRepoSelected = (repo: Repository, isSelecting = true) =>
63
+ setSelectedRepoNames(prevSelected => {
64
+ const otherSelectedRepoNames = prevSelected.filter(r => r !== repo.name);
65
+ return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;
66
+ });
67
+ const selectAllRepos = (isSelecting = true) =>
68
+ setSelectedRepoNames(isSelecting ? selectableRepos.map(r => r.name) : []);
69
+ const areAllReposSelected = selectedRepoNames.length === selectableRepos.length;
70
+ const areSomeReposSelected = selectedRepoNames.length > 0;
71
+ const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);
72
+
73
+ // To allow shift+click to select/deselect multiple rows
74
+ const [recentSelectedRowIndex, setRecentSelectedRowIndex] = React.useState<number | null>(null);
75
+ const [shifting, setShifting] = React.useState(false);
76
+
77
+ const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {
78
+ // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected
79
+ if (shifting && recentSelectedRowIndex !== null) {
80
+ const numberSelected = rowIndex - recentSelectedRowIndex;
81
+ const intermediateIndexes =
82
+ numberSelected > 0
83
+ ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)
84
+ : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);
85
+ intermediateIndexes.forEach(index => setRepoSelected(repositories[index], isSelecting));
86
+ } else {
87
+ setRepoSelected(repo, isSelecting);
88
+ }
89
+ setRecentSelectedRowIndex(rowIndex);
90
+ };
91
+
92
+ React.useEffect(() => {
93
+ const onKeyDown = (e: KeyboardEvent) => {
94
+ if (e.key === 'Shift') {
95
+ setShifting(true);
96
+ }
97
+ };
98
+ const onKeyUp = (e: KeyboardEvent) => {
99
+ if (e.key === 'Shift') {
100
+ setShifting(false);
101
+ }
102
+ };
103
+
104
+ document.addEventListener('keydown', onKeyDown);
105
+ document.addEventListener('keyup', onKeyUp);
106
+
107
+ return () => {
108
+ document.removeEventListener('keydown', onKeyDown);
109
+ document.removeEventListener('keyup', onKeyUp);
110
+ };
111
+ }, []);
112
+
113
+ // Set up bulk selection menu
114
+ const bulkSelectMenuRef = React.createRef<HTMLDivElement>();
115
+ const bulkSelectToggleRef = React.createRef<any>();
116
+ const bulkSelectcontainerRef = React.createRef<HTMLDivElement>();
117
+
118
+ const [isBulkSelectOpen, setIsBulkSelectOpen] = React.useState<boolean>(false);
119
+
120
+ React.useEffect(() => {
121
+ window.addEventListener('keydown', handleBulkSelectMenuKeys);
122
+ window.addEventListener('click', handleBulkSelectClickOutside);
123
+ return () => {
124
+ window.removeEventListener('keydown', handleBulkSelectMenuKeys);
125
+ window.removeEventListener('click', handleBulkSelectClickOutside);
126
+ };
127
+ }, [isBulkSelectOpen, bulkSelectMenuRef]);
128
+
129
+ const handleBulkSelectClickOutside = (event: MouseEvent) => {
130
+ if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {
131
+ setIsBulkSelectOpen(false);
132
+ }
133
+ };
134
+
135
+ const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {
136
+ if (!isBulkSelectOpen) {
137
+ return;
138
+ }
139
+ if (
140
+ bulkSelectMenuRef.current?.contains(event.target as Node) ||
141
+ bulkSelectToggleRef.current?.contains(event.target as Node)
142
+ ) {
143
+ if (event.key === 'Escape' || event.key === 'Tab') {
144
+ setIsBulkSelectOpen(!isBulkSelectOpen);
145
+ bulkSelectToggleRef.current?.focus();
146
+ }
147
+ }
148
+ };
149
+
150
+ const onBulkSelectToggleClick = (ev: React.MouseEvent) => {
151
+ ev.stopPropagation(); // Stop handleClickOutside from handling
152
+ setTimeout(() => {
153
+ if (bulkSelectMenuRef.current) {
154
+ const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');
155
+ firstElement && (firstElement as HTMLElement).focus();
156
+ }
157
+ }, 0);
158
+ setIsBulkSelectOpen(!isBulkSelectOpen);
159
+ };
160
+
161
+ let menuToggleCheckmark: boolean | null = false;
162
+ if (areAllReposSelected) {
163
+ menuToggleCheckmark = true;
164
+ } else if (areSomeReposSelected) {
165
+ menuToggleCheckmark = null;
166
+ }
167
+
168
+ const bulkSelectToggle = (
169
+ <MenuToggle
170
+ ref={bulkSelectToggleRef}
171
+ onClick={onBulkSelectToggleClick}
172
+ isExpanded={isBulkSelectOpen}
173
+ splitButtonOptions={{
174
+ items: [
175
+ <MenuToggleCheckbox
176
+ id="select-checkbox"
177
+ key="select-checkbox"
178
+ aria-label="Select all"
179
+ isChecked={menuToggleCheckmark}
180
+ onChange={(checked, _event) => selectAllRepos(checked)}
181
+ />
182
+ ]
183
+ }}
184
+ aria-label="Full table selection checkbox"
185
+ />
186
+ );
187
+
188
+ const bulkSelectMenu = (
189
+ // eslint-disable-next-line no-console
190
+ <Menu
191
+ ref={bulkSelectMenuRef}
192
+ onSelect={(_ev, itemId) => {
193
+ selectAllRepos(itemId === 1 || itemId === 2);
194
+ setIsBulkSelectOpen(!isBulkSelectOpen);
195
+ }}
196
+ >
197
+ <MenuContent>
198
+ <MenuList>
199
+ <MenuItem itemId={0}>Select none (0 items)</MenuItem>
200
+ <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>
201
+ <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>
202
+ </MenuList>
203
+ </MenuContent>
204
+ </Menu>
205
+ );
206
+
207
+ const toolbarBulkSelect = (
208
+ <div ref={bulkSelectcontainerRef}>
209
+ <Popper
210
+ trigger={bulkSelectToggle}
211
+ popper={bulkSelectMenu}
212
+ appendTo={bulkSelectcontainerRef.current || undefined}
213
+ isVisible={isBulkSelectOpen}
214
+ popperMatchesTriggerWidth={false}
215
+ />
216
+ </div>
217
+ );
218
+
219
+ // Set up single select menu & state
220
+ const [isOpen, setIsOpen] = React.useState<boolean>(false);
221
+ const [selections, setSelections] = React.useState<string[]>([]);
222
+ const toggleRef = React.useRef<HTMLButtonElement>(null);
223
+ const menuRef = React.useRef<HTMLDivElement>(null);
224
+ const containerRef = React.useRef<HTMLDivElement>(null);
225
+
226
+ const onFilter = (repo: Repository) => {
227
+ if (selections.length === 0) {
228
+ return true;
229
+ }
230
+
231
+ return selections.some(searchValue => {
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.location.search(input) >= 0;
239
+ });
240
+ };
241
+
242
+ const handleMenuKeys = (event: KeyboardEvent) => {
243
+ if (isOpen && menuRef.current?.contains(event.target as Node)) {
244
+ if (event.key === 'Escape' || event.key === 'Tab') {
245
+ setIsOpen(!isOpen);
246
+ toggleRef.current?.focus();
247
+ }
248
+ }
249
+ };
250
+
251
+ const handleClickOutside = (event: MouseEvent) => {
252
+ if (isOpen && !menuRef.current?.contains(event.target as Node)) {
253
+ setIsOpen(false);
254
+ }
255
+ };
256
+
257
+ React.useEffect(() => {
258
+ window.addEventListener('keydown', handleMenuKeys);
259
+ window.addEventListener('click', handleClickOutside);
260
+ return () => {
261
+ window.removeEventListener('keydown', handleMenuKeys);
262
+ window.removeEventListener('click', handleClickOutside);
263
+ };
264
+ }, [isOpen, menuRef]);
265
+
266
+ const onToggleClick = (ev: React.MouseEvent) => {
267
+ ev.stopPropagation(); // Stop handleClickOutside from handling
268
+ setTimeout(() => {
269
+ if (menuRef.current) {
270
+ const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
271
+ firstElement && (firstElement as HTMLElement).focus();
272
+ }
273
+ }, 0);
274
+ setIsOpen(!isOpen);
275
+ };
276
+
277
+ function onSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {
278
+ if (typeof itemId === 'undefined') {
279
+ return;
280
+ }
281
+
282
+ const itemStr = itemId.toString();
283
+ setSelections(
284
+ selections.includes(itemStr) ? selections.filter(selection => selection !== itemStr) : [itemStr, ...selections]
285
+ );
286
+ }
287
+
288
+ const toggle = (
289
+ <MenuToggle
290
+ ref={toggleRef}
291
+ onClick={onToggleClick}
292
+ isExpanded={isOpen}
293
+ icon={<FilterIcon />}
294
+ {...(selections.length > 0 && { badge: <Badge isRead>{selections.length}</Badge> })}
295
+ style={
296
+ {
297
+ width: '200px'
298
+ } as React.CSSProperties
299
+ }
300
+ >
301
+ Location
302
+ </MenuToggle>
303
+ );
304
+
305
+ const menu = (
306
+ <Menu ref={menuRef} id="select-menu" onSelect={onSelect} selected={selections}>
307
+ <MenuContent>
308
+ <MenuList>
309
+ <MenuItem hasCheck isSelected={selections.includes('Bangalore')} itemId="Bangalore">
310
+ Bangalore
311
+ </MenuItem>
312
+ <MenuItem hasCheck isSelected={selections.includes('Boston')} itemId="Boston">
313
+ Boston
314
+ </MenuItem>
315
+ <MenuItem hasCheck isSelected={selections.includes('Brno')} itemId="Brno">
316
+ Brno
317
+ </MenuItem>
318
+ <MenuItem hasCheck isSelected={selections.includes('Raleigh')} itemId="Raleigh">
319
+ Raleigh
320
+ </MenuItem>
321
+ <MenuItem hasCheck isSelected={selections.includes('Westford')} itemId="Westford">
322
+ Westford
323
+ </MenuItem>
324
+ </MenuList>
325
+ </MenuContent>
326
+ </Menu>
327
+ );
328
+
329
+ const select = (
330
+ <div ref={containerRef}>
331
+ <Popper trigger={toggle} popper={menu} appendTo={containerRef.current || undefined} isVisible={isOpen} />
332
+ </div>
333
+ );
334
+
335
+ // Set up pagination
336
+ const toolbarPagination = (
337
+ <Pagination
338
+ titles={{ paginationTitle: 'Checkbox filter pagination' }}
339
+ perPageComponent="button"
340
+ itemCount={repositories.length}
341
+ perPage={10}
342
+ page={1}
343
+ widgetId="checkbox-select-mock-pagination"
344
+ isCompact
345
+ />
346
+ );
347
+
348
+ const toolbar = (
349
+ <Toolbar id="checkbox-filter-toolbar" clearAllFilters={() => setSelections([])}>
350
+ <ToolbarContent>
351
+ <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>
352
+ <ToolbarGroup variant="filter-group">
353
+ <ToolbarFilter
354
+ chips={selections}
355
+ deleteChip={(category, chip) => onSelect(undefined, chip as string)}
356
+ deleteChipGroup={() => setSelections([])}
357
+ categoryName="Location"
358
+ >
359
+ {select}
360
+ </ToolbarFilter>
361
+ </ToolbarGroup>
362
+ <ToolbarItem variant="pagination">{toolbarPagination}</ToolbarItem>
363
+ </ToolbarContent>
364
+ </Toolbar>
365
+ );
366
+
367
+ return (
368
+ <React.Fragment>
369
+ {toolbar}
370
+ <TableComposable aria-label="Selectable table">
371
+ <Thead>
372
+ <Tr>
373
+ <Th />
374
+ <Th width={20}>{columnNames.name}</Th>
375
+ <Th width={20}>{columnNames.threads}</Th>
376
+ <Th width={20}>{columnNames.apps}</Th>
377
+ <Th width={20}>{columnNames.workspaces}</Th>
378
+ <Th width={20}>{columnNames.status}</Th>
379
+ <Th width={20}>{columnNames.location}</Th>
380
+ </Tr>
381
+ </Thead>
382
+ <Tbody>
383
+ {repositories.filter(onFilter).map((repo, rowIndex) => (
384
+ <Tr key={repo.name}>
385
+ <Td
386
+ select={{
387
+ rowIndex,
388
+ onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),
389
+ isSelected: isRepoSelected(repo),
390
+ disable: !isRepoSelectable(repo)
391
+ }}
392
+ />
393
+ <Td dataLabel={columnNames.name}>{repo.name}</Td>
394
+ <Td dataLabel={columnNames.threads}>{repo.threads}</Td>
395
+ <Td dataLabel={columnNames.apps}>{repo.apps}</Td>
396
+ <Td dataLabel={columnNames.workspaces}>{repo.workspaces}</Td>
397
+ <Td dataLabel={columnNames.status}>{repo.status}</Td>
398
+ <Td dataLabel={columnNames.location}>{repo.location}</Td>
399
+ </Tr>
400
+ ))}
401
+ </Tbody>
402
+ </TableComposable>
403
+ </React.Fragment>
404
+ );
405
+ };