@patternfly/react-core 4.237.6 → 4.237.9
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 +24 -0
- 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/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 +528 -528
- package/dist/umd/react-core.min.js +2 -2
- package/package.json +6 -6
- package/src/components/Button/examples/Button.md +3 -15
- package/src/components/Button/examples/ButtonAriaDisabled.tsx +15 -2
- package/src/components/MenuToggle/MenuToggle.tsx +1 -0
- package/src/components/MenuToggle/__tests__/MenuToggle.test.tsx +27 -1
- package/src/components/Text/examples/Text.md +1 -1
- package/src/demos/Filters/FilterDemos.md +48 -0
- package/src/demos/Filters/examples/FilterCheckboxSelect.tsx +405 -0
- package/src/demos/Filters/examples/FilterSearchInput.tsx +331 -0
- package/src/demos/Filters/examples/FilterSingleSelect.tsx +392 -0
- package/src/components/Button/examples/ButtonAriaDisabledLinkTooltip.tsx +0 -10
- package/src/components/Button/examples/ButtonAriaDisabledTooltip.tsx +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/react-core",
|
|
3
|
-
"version": "4.237.
|
|
3
|
+
"version": "4.237.9",
|
|
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.
|
|
48
|
-
"@patternfly/react-styles": "^4.87.
|
|
49
|
-
"@patternfly/react-tokens": "^4.89.
|
|
47
|
+
"@patternfly/react-icons": "^4.88.9",
|
|
48
|
+
"@patternfly/react-styles": "^4.87.9",
|
|
49
|
+
"@patternfly/react-tokens": "^4.89.9",
|
|
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.
|
|
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": "
|
|
73
|
+
"gitHead": "0acf9a0fe23232defeddaaf3001cf6846702a8cf"
|
|
74
74
|
}
|
|
@@ -96,21 +96,9 @@ Router links can be used for in-app linking in React environments to prevent pag
|
|
|
96
96
|
|
|
97
97
|
Buttons that are aria-disabled are similar to normal disabled buttons, except they can receive focus. Every button variant can be aria-disabled using the `isAriaDisabled` property.
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Aria-disabled with tooltip
|
|
103
|
-
|
|
104
|
-
Unlike normal disabled buttons, aria-disabled buttons can support tooltips.
|
|
105
|
-
|
|
106
|
-
```ts file="./ButtonAriaDisabledTooltip.tsx"
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Aria-disabled link as button with tooltip
|
|
110
|
-
|
|
111
|
-
Aria-disabled buttons can operate as links, which also support tooltips.
|
|
99
|
+
Unlike normal disabled buttons, aria-disabled buttons can support tooltips. Furthermore, aria-disabled buttons can operate as links, which also support tooltips.
|
|
112
100
|
|
|
113
|
-
```ts file="./
|
|
101
|
+
```ts file="./ButtonAriaDisabled.tsx"
|
|
114
102
|
```
|
|
115
103
|
|
|
116
104
|
### Button with count
|
|
@@ -118,4 +106,4 @@ Aria-disabled buttons can operate as links, which also support tooltips.
|
|
|
118
106
|
Buttons can display a `count` in the form of a badge to indicate some value or number by passing in the `countOptions` prop as a `BadgeCountObject` object. The `BadgeCountObject` object will handle `count`, `isRead`, and `className` props for the badge count.
|
|
119
107
|
|
|
120
108
|
```ts file="./ButtonWithCount.tsx" isBeta
|
|
121
|
-
```
|
|
109
|
+
```
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Button } from '@patternfly/react-core';
|
|
2
|
+
import { Button, Tooltip } from '@patternfly/react-core';
|
|
3
3
|
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
|
|
4
4
|
import PlusCircleIcon from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
|
|
5
5
|
|
|
6
6
|
export const ButtonAriaDisabled: React.FunctionComponent = () => (
|
|
7
7
|
<React.Fragment>
|
|
8
|
-
<Button isAriaDisabled>Primary aria disabled</Button>
|
|
8
|
+
<Button isAriaDisabled>Primary aria disabled</Button>
|
|
9
|
+
<Button isAriaDisabled>Secondary aria disabled</Button>{' '}
|
|
9
10
|
<Button variant="secondary" isDanger isAriaDisabled>
|
|
10
11
|
Danger secondary aria disabled
|
|
11
12
|
</Button>{' '}
|
|
@@ -35,5 +36,17 @@ export const ButtonAriaDisabled: React.FunctionComponent = () => (
|
|
|
35
36
|
<Button isAriaDisabled variant="control">
|
|
36
37
|
Control aria disabled
|
|
37
38
|
</Button>
|
|
39
|
+
<br />
|
|
40
|
+
<br />
|
|
41
|
+
<Tooltip content="Aria-disabled buttons are like disabled buttons, but focusable. Allows for tooltip support.">
|
|
42
|
+
<Button isAriaDisabled variant="secondary">
|
|
43
|
+
Secondary button to core docs
|
|
44
|
+
</Button>
|
|
45
|
+
</Tooltip>{' '}
|
|
46
|
+
<Tooltip content="Aria-disabled link as button with tooltip">
|
|
47
|
+
<Button component="a" isAriaDisabled href="https://pf4.patternfly.org/" target="_blank" variant="tertiary">
|
|
48
|
+
Tertiary link as button to core docs
|
|
49
|
+
</Button>
|
|
50
|
+
</Tooltip>
|
|
38
51
|
</React.Fragment>
|
|
39
52
|
);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
2
|
import { MenuToggle } from '../MenuToggle';
|
|
3
|
+
import { MenuToggleCheckbox } from '../MenuToggleCheckbox';
|
|
4
4
|
import { Badge } from '../../Badge';
|
|
5
5
|
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
|
|
6
6
|
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
|
|
7
|
+
import userEvent from '@testing-library/user-event';
|
|
8
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
7
9
|
|
|
8
10
|
describe('menu toggle', () => {
|
|
9
11
|
test('renders successfully', () => {
|
|
@@ -54,4 +56,28 @@ describe('menu toggle', () => {
|
|
|
54
56
|
const { asFragment } = render(<MenuToggle badge={<Badge>badge</Badge>}>Toggle</MenuToggle>);
|
|
55
57
|
expect(asFragment()).toMatchSnapshot();
|
|
56
58
|
});
|
|
59
|
+
|
|
60
|
+
test('split toggle passes onClick', async () => {
|
|
61
|
+
const mockClick = jest.fn();
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
|
|
64
|
+
render(<MenuToggle
|
|
65
|
+
onClick={mockClick}
|
|
66
|
+
splitButtonOptions={{
|
|
67
|
+
items: [
|
|
68
|
+
<MenuToggleCheckbox
|
|
69
|
+
id="split-button-checkbox-with-text-disabled-example"
|
|
70
|
+
key="split-checkbox-with-text-disabled"
|
|
71
|
+
aria-label="Select all"
|
|
72
|
+
>
|
|
73
|
+
10 selected
|
|
74
|
+
</MenuToggleCheckbox>
|
|
75
|
+
]
|
|
76
|
+
}}
|
|
77
|
+
aria-label="Menu toggle with checkbox split button and text"
|
|
78
|
+
/>);
|
|
79
|
+
|
|
80
|
+
await user.click(screen.getByRole(`button`) as Element);
|
|
81
|
+
expect(mockClick).toHaveBeenCalled();
|
|
82
|
+
});
|
|
57
83
|
});
|
|
@@ -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
|
+
};
|