@patternfly/react-core 4.222.13 → 4.222.14
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 +8 -0
- package/dist/esm/components/Label/Label.js +3 -3
- package/dist/esm/components/Label/Label.js.map +1 -1
- package/dist/esm/components/LabelGroup/LabelGroup.d.ts +2 -0
- package/dist/esm/components/LabelGroup/LabelGroup.d.ts.map +1 -1
- package/dist/esm/components/LabelGroup/LabelGroup.js +4 -3
- package/dist/esm/components/LabelGroup/LabelGroup.js.map +1 -1
- package/dist/js/components/Label/Label.js +3 -3
- package/dist/js/components/Label/Label.js.map +1 -1
- package/dist/js/components/LabelGroup/LabelGroup.d.ts +2 -0
- package/dist/js/components/LabelGroup/LabelGroup.d.ts.map +1 -1
- package/dist/js/components/LabelGroup/LabelGroup.js +4 -3
- package/dist/js/components/LabelGroup/LabelGroup.js.map +1 -1
- package/dist/umd/react-core.css +1 -0
- package/dist/umd/react-core.js +7 -6
- package/dist/umd/react-core.min.css +1058 -1057
- package/dist/umd/react-core.min.js +1 -1
- package/package.json +6 -6
- package/src/components/Label/Label.tsx +3 -3
- package/src/components/LabelGroup/LabelGroup.tsx +5 -1
- package/src/components/LabelGroup/examples/LabelGroup.md +7 -0
- package/src/components/LabelGroup/examples/LabelGroupEditableAdd.tsx +74 -0
- package/src/components/LabelGroup/examples/LabelGroupEditableAddDropdown.tsx +138 -0
- package/src/components/LabelGroup/examples/LabelGroupEditableAddModal.tsx +356 -0
- package/src/demos/LabelGroupDemos.md +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/react-core",
|
|
3
|
-
"version": "4.222.
|
|
3
|
+
"version": "4.222.14",
|
|
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",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"generate": "node scripts/copyStyles.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@patternfly/react-icons": "^4.73.
|
|
38
|
-
"@patternfly/react-styles": "^4.72.
|
|
39
|
-
"@patternfly/react-tokens": "^4.74.
|
|
37
|
+
"@patternfly/react-icons": "^4.73.14",
|
|
38
|
+
"@patternfly/react-styles": "^4.72.14",
|
|
39
|
+
"@patternfly/react-tokens": "^4.74.14",
|
|
40
40
|
"focus-trap": "6.9.2",
|
|
41
41
|
"react-dropzone": "9.0.0",
|
|
42
42
|
"tippy.js": "5.1.2",
|
|
43
43
|
"tslib": "^2.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@patternfly/patternfly": "4.198.
|
|
46
|
+
"@patternfly/patternfly": "4.198.2",
|
|
47
47
|
"@rollup/plugin-commonjs": "^21.0.0",
|
|
48
48
|
"@rollup/plugin-node-resolve": "^13.0.0",
|
|
49
49
|
"@rollup/plugin-replace": "^3.0.0",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"react": "^16.8.0 || ^17.0.0",
|
|
61
61
|
"react-dom": "^16.8.0 || ^17.0.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "0d04c7d4c7970f2be7e485e1f4bd0bda94ee5d46"
|
|
64
64
|
}
|
|
@@ -110,15 +110,15 @@ export const Label: React.FunctionComponent<LabelProps> = ({
|
|
|
110
110
|
const editableInputRef = React.useRef<HTMLInputElement>();
|
|
111
111
|
|
|
112
112
|
React.useEffect(() => {
|
|
113
|
-
document.addEventListener('
|
|
113
|
+
document.addEventListener('mousedown', onDocMouseDown);
|
|
114
114
|
document.addEventListener('keydown', onKeyDown);
|
|
115
115
|
return () => {
|
|
116
|
-
document.removeEventListener('
|
|
116
|
+
document.removeEventListener('mousedown', onDocMouseDown);
|
|
117
117
|
document.removeEventListener('keydown', onKeyDown);
|
|
118
118
|
};
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
const
|
|
121
|
+
const onDocMouseDown = (event: MouseEvent) => {
|
|
122
122
|
if (
|
|
123
123
|
isEditableActive &&
|
|
124
124
|
editableInputRef &&
|
|
@@ -58,6 +58,8 @@ export interface LabelGroupProps extends React.HTMLProps<HTMLUListElement> {
|
|
|
58
58
|
hasEditableTextArea?: boolean;
|
|
59
59
|
/** @beta Additional props passed to the editable textarea. */
|
|
60
60
|
editableTextAreaProps?: any;
|
|
61
|
+
/** @beta Control for adding new labels */
|
|
62
|
+
addLabelControl?: React.ReactNode;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
interface LabelGroupState {
|
|
@@ -149,6 +151,7 @@ export class LabelGroup extends React.Component<LabelGroupProps, LabelGroupState
|
|
|
149
151
|
isEditable,
|
|
150
152
|
hasEditableTextArea,
|
|
151
153
|
editableTextAreaProps,
|
|
154
|
+
addLabelControl,
|
|
152
155
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
153
156
|
...rest
|
|
154
157
|
} = this.props;
|
|
@@ -189,6 +192,7 @@ export class LabelGroup extends React.Component<LabelGroupProps, LabelGroupState
|
|
|
189
192
|
</Label>
|
|
190
193
|
</li>
|
|
191
194
|
)}
|
|
195
|
+
{addLabelControl && <li className={css(styles.labelGroupListItem)}>{addLabelControl}</li>}
|
|
192
196
|
{isEditable && hasEditableTextArea && (
|
|
193
197
|
<li className={css(styles.labelGroupListItem, styles.modifiers.textarea)}>
|
|
194
198
|
<textarea className={css(styles.labelGroupTextarea)} rows={1} tabIndex={0} {...editableTextAreaProps} />
|
|
@@ -228,7 +232,7 @@ export class LabelGroup extends React.Component<LabelGroupProps, LabelGroupState
|
|
|
228
232
|
);
|
|
229
233
|
};
|
|
230
234
|
|
|
231
|
-
return numChildren === 0 ? null : (
|
|
235
|
+
return numChildren === 0 && addLabelControl === undefined ? null : (
|
|
232
236
|
<GenerateId>{randomId => renderLabelGroup(this.props.id || randomId)}</GenerateId>
|
|
233
237
|
);
|
|
234
238
|
}
|
|
@@ -40,3 +40,10 @@ import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-i
|
|
|
40
40
|
|
|
41
41
|
```ts file="LabelGroupEditableLabels.tsx" isBeta
|
|
42
42
|
```
|
|
43
|
+
|
|
44
|
+
### Editable labels with add button
|
|
45
|
+
|
|
46
|
+
For additional documentation that showcases adding a new label, see [label group demos](/components/label-group/react-demos).
|
|
47
|
+
|
|
48
|
+
```ts file="LabelGroupEditableAdd.tsx"
|
|
49
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LabelGroup, Label } from '@patternfly/react-core';
|
|
3
|
+
|
|
4
|
+
export const LabelGroupEditableAdd: React.FunctionComponent = () => {
|
|
5
|
+
const [idIndex, setIdIndex] = React.useState<number>(3);
|
|
6
|
+
const [labels, setLabels] = React.useState<any>([
|
|
7
|
+
{ name: 'Label 1', id: 0 },
|
|
8
|
+
{ name: 'Label 2', id: 1 },
|
|
9
|
+
{
|
|
10
|
+
name: 'Label 3',
|
|
11
|
+
props: {
|
|
12
|
+
isEditable: true,
|
|
13
|
+
editableProps: {
|
|
14
|
+
'aria-label': 'label editable text'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
id: 2
|
|
18
|
+
}
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const onClose = (labelId: string) => {
|
|
22
|
+
setLabels(labels.filter((l: any) => l.id !== labelId));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const onEdit = (nextText: string, index: number) => {
|
|
26
|
+
const copy = [...labels];
|
|
27
|
+
copy[index] = { name: nextText, props: labels[index].props, id: labels[index].id };
|
|
28
|
+
setLabels(copy);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const onAdd = () => {
|
|
32
|
+
setLabels([
|
|
33
|
+
{
|
|
34
|
+
name: 'New Label',
|
|
35
|
+
props: {
|
|
36
|
+
isEditable: true,
|
|
37
|
+
editableProps: {
|
|
38
|
+
'aria-label': 'label editable text'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
id: idIndex
|
|
42
|
+
},
|
|
43
|
+
...labels
|
|
44
|
+
]);
|
|
45
|
+
setIdIndex(idIndex + 1);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<LabelGroup
|
|
50
|
+
categoryName="Label group 1"
|
|
51
|
+
numLabels={5}
|
|
52
|
+
isEditable
|
|
53
|
+
addLabelControl={
|
|
54
|
+
<Label color="blue" variant="outline" isOverflowLabel onClick={onAdd}>
|
|
55
|
+
Add label
|
|
56
|
+
</Label>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
{labels.map((label, index) => (
|
|
60
|
+
<Label
|
|
61
|
+
key={`${label.name}-${index}`}
|
|
62
|
+
id={`${label.name}-${index}`}
|
|
63
|
+
color="blue"
|
|
64
|
+
onClose={() => onClose(label.id)}
|
|
65
|
+
onEditCancel={prevText => onEdit(prevText, index)}
|
|
66
|
+
onEditComplete={newText => onEdit(newText, index)}
|
|
67
|
+
{...label.props}
|
|
68
|
+
>
|
|
69
|
+
{label.name}
|
|
70
|
+
</Label>
|
|
71
|
+
))}
|
|
72
|
+
</LabelGroup>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LabelGroup, Label, Menu, MenuContent, MenuList, MenuItem, Popper } from '@patternfly/react-core';
|
|
3
|
+
|
|
4
|
+
export const LabelGroupEditableAddDropdown: React.FunctionComponent = () => {
|
|
5
|
+
const toggleRef = React.useRef<HTMLDivElement>();
|
|
6
|
+
const menuRef = React.useRef<HTMLDivElement>();
|
|
7
|
+
const containerRef = React.useRef<HTMLDivElement>();
|
|
8
|
+
|
|
9
|
+
const [idIndex, setIdIndex] = React.useState<number>(3);
|
|
10
|
+
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
11
|
+
const [labels, setLabels] = React.useState<any>([
|
|
12
|
+
{ name: 'Label 1', id: 0 },
|
|
13
|
+
{ name: 'Label 2', id: 1 },
|
|
14
|
+
{
|
|
15
|
+
name: 'Label 3',
|
|
16
|
+
props: {
|
|
17
|
+
isEditable: true,
|
|
18
|
+
editableProps: {
|
|
19
|
+
'aria-label': 'label editable text'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
id: 2
|
|
23
|
+
}
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const onClose = (labelId: string) => {
|
|
27
|
+
setLabels(labels.filter((l: any) => l.id !== labelId));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const onEdit = (nextText: string, index: number) => {
|
|
31
|
+
const copy = [...labels];
|
|
32
|
+
copy[index] = { name: nextText, props: labels[index].props, id: labels[index].id };
|
|
33
|
+
setLabels(copy);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onAdd = (labelText: string) => {
|
|
37
|
+
setLabels([
|
|
38
|
+
{
|
|
39
|
+
name: labelText,
|
|
40
|
+
id: idIndex
|
|
41
|
+
},
|
|
42
|
+
...labels
|
|
43
|
+
]);
|
|
44
|
+
setIdIndex(idIndex + 1);
|
|
45
|
+
setIsOpen(!isOpen);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleMenuKeys = (event: KeyboardEvent) => {
|
|
49
|
+
if (isOpen && menuRef.current.contains(event.target as Node)) {
|
|
50
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
51
|
+
setIsOpen(!isOpen);
|
|
52
|
+
toggleRef.current.focus();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
58
|
+
if (
|
|
59
|
+
isOpen &&
|
|
60
|
+
!(menuRef.current.contains(event.target as Node) || toggleRef.current.contains(event.target as Node))
|
|
61
|
+
) {
|
|
62
|
+
setIsOpen(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
window.addEventListener('keydown', handleMenuKeys);
|
|
68
|
+
window.addEventListener('click', handleClickOutside);
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener('keydown', handleMenuKeys);
|
|
71
|
+
window.removeEventListener('click', handleClickOutside);
|
|
72
|
+
};
|
|
73
|
+
}, [isOpen, menuRef]);
|
|
74
|
+
|
|
75
|
+
const onToggleClick = () => {
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
if (menuRef.current) {
|
|
78
|
+
const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
|
|
79
|
+
firstElement && (firstElement as HTMLElement).focus();
|
|
80
|
+
}
|
|
81
|
+
}, 0);
|
|
82
|
+
setIsOpen(!isOpen);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const menu = (
|
|
86
|
+
<Menu ref={menuRef} onSelect={(_ev, itemId) => onAdd(itemId.toString())}>
|
|
87
|
+
<MenuContent>
|
|
88
|
+
<MenuList>
|
|
89
|
+
<MenuItem itemId="Label text 1">Label text 1</MenuItem>
|
|
90
|
+
<MenuItem itemId="Label text 2">Label text 2</MenuItem>
|
|
91
|
+
<MenuItem itemId="Label text 3">Label text 3</MenuItem>
|
|
92
|
+
<MenuItem itemId="Label text 4">Label text 4</MenuItem>
|
|
93
|
+
</MenuList>
|
|
94
|
+
</MenuContent>
|
|
95
|
+
</Menu>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const toggle = (
|
|
99
|
+
<div ref={toggleRef}>
|
|
100
|
+
<Label color="blue" variant="outline" isOverflowLabel onClick={onToggleClick}>
|
|
101
|
+
Add label
|
|
102
|
+
</Label>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div ref={containerRef}>
|
|
108
|
+
<LabelGroup
|
|
109
|
+
categoryName="Label group 1"
|
|
110
|
+
numLabels={5}
|
|
111
|
+
isEditable
|
|
112
|
+
addLabelControl={
|
|
113
|
+
<Popper
|
|
114
|
+
trigger={toggle}
|
|
115
|
+
popper={menu}
|
|
116
|
+
appendTo={containerRef.current}
|
|
117
|
+
isVisible={isOpen}
|
|
118
|
+
popperMatchesTriggerWidth={false}
|
|
119
|
+
/>
|
|
120
|
+
}
|
|
121
|
+
>
|
|
122
|
+
{labels.map((label, index) => (
|
|
123
|
+
<Label
|
|
124
|
+
key={`${label.name}-${index}`}
|
|
125
|
+
id={`${label.name}-${index}`}
|
|
126
|
+
color="blue"
|
|
127
|
+
onClose={() => onClose(label.id)}
|
|
128
|
+
onEditCancel={prevText => onEdit(prevText, index)}
|
|
129
|
+
onEditComplete={newText => onEdit(newText, index)}
|
|
130
|
+
{...label.props}
|
|
131
|
+
>
|
|
132
|
+
{label.name}
|
|
133
|
+
</Label>
|
|
134
|
+
))}
|
|
135
|
+
</LabelGroup>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LabelGroup,
|
|
4
|
+
Label,
|
|
5
|
+
Modal,
|
|
6
|
+
ModalVariant,
|
|
7
|
+
Button,
|
|
8
|
+
Form,
|
|
9
|
+
FormGroup,
|
|
10
|
+
TextInput,
|
|
11
|
+
Menu,
|
|
12
|
+
MenuContent,
|
|
13
|
+
MenuList,
|
|
14
|
+
MenuItem,
|
|
15
|
+
MenuToggle,
|
|
16
|
+
Radio,
|
|
17
|
+
Popper
|
|
18
|
+
} from '@patternfly/react-core';
|
|
19
|
+
import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon';
|
|
20
|
+
|
|
21
|
+
export const LabelGroupEditableAddModal: React.FunctionComponent = () => {
|
|
22
|
+
const [isModalOpen, setModalOpen] = React.useState<boolean>(false);
|
|
23
|
+
const [idIndex, setIdIndex] = React.useState<number>(3);
|
|
24
|
+
const [labelText, setLabelText] = React.useState<string>('');
|
|
25
|
+
const [color, setColor] = React.useState<string>();
|
|
26
|
+
const [icon, setIcon] = React.useState<any>();
|
|
27
|
+
const [labelType, setLabelType] = React.useState<string>('filled');
|
|
28
|
+
const [isClosable, setIsCloseable] = React.useState<boolean>(false);
|
|
29
|
+
const [isEditable, setIsEditable] = React.useState<boolean>(false);
|
|
30
|
+
const labelInputRef = React.useRef();
|
|
31
|
+
|
|
32
|
+
const [isColorOpen, setIsColorOpen] = React.useState<boolean>(false);
|
|
33
|
+
const colorMenuRef = React.useRef<HTMLDivElement>();
|
|
34
|
+
const colorContainerRef = React.useRef<HTMLDivElement>();
|
|
35
|
+
const colorToggleRef = React.useRef<HTMLButtonElement>();
|
|
36
|
+
|
|
37
|
+
const [isIconOpen, setIsIconOpen] = React.useState<boolean>(false);
|
|
38
|
+
const iconMenuRef = React.useRef<HTMLDivElement>();
|
|
39
|
+
const iconContainerRef = React.useRef<HTMLDivElement>();
|
|
40
|
+
const iconToggleRef = React.useRef<HTMLButtonElement>();
|
|
41
|
+
|
|
42
|
+
const [labels, setLabels] = React.useState<any>([
|
|
43
|
+
{ name: 'Label 1', id: 0 },
|
|
44
|
+
{ name: 'Label 2', id: 1 },
|
|
45
|
+
{
|
|
46
|
+
name: 'Label 3',
|
|
47
|
+
props: {
|
|
48
|
+
isEditable: true,
|
|
49
|
+
editableProps: {
|
|
50
|
+
'aria-label': 'label editable text'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
id: 2
|
|
54
|
+
}
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const onClose = (labelId: string) => {
|
|
58
|
+
setLabels(labels.filter((l: any) => l.id !== labelId));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onEdit = (nextText: string, index: number) => {
|
|
62
|
+
const copy = [...labels];
|
|
63
|
+
copy[index] = { name: nextText, props: labels[index].props, id: labels[index].id };
|
|
64
|
+
setLabels(copy);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const onAdd = () => {
|
|
68
|
+
let labelIcon = null;
|
|
69
|
+
if (icon === 'Info circle icon') {
|
|
70
|
+
labelIcon = <InfoCircleIcon />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setLabels([
|
|
74
|
+
{
|
|
75
|
+
name: labelText || 'New Label',
|
|
76
|
+
props: {
|
|
77
|
+
color: color ? color.toLowerCase() : 'gray',
|
|
78
|
+
icon: labelIcon,
|
|
79
|
+
variant: labelType || 'filled',
|
|
80
|
+
...(!isClosable && isClosable !== undefined && { onClose: null }),
|
|
81
|
+
isEditable: isEditable !== undefined ? isEditable : true,
|
|
82
|
+
...(isEditable && {
|
|
83
|
+
editableProps: {
|
|
84
|
+
'aria-label': 'label editable text'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
id: idIndex
|
|
89
|
+
},
|
|
90
|
+
...labels
|
|
91
|
+
]);
|
|
92
|
+
setModalOpen(!isModalOpen);
|
|
93
|
+
setIdIndex(idIndex + 1);
|
|
94
|
+
setLabelText('');
|
|
95
|
+
setColor(null);
|
|
96
|
+
setIcon(null);
|
|
97
|
+
setLabelType('filled');
|
|
98
|
+
setIsCloseable(false);
|
|
99
|
+
setIsEditable(false);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleModalToggle = () => {
|
|
103
|
+
setModalOpen(!isModalOpen);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
if (isModalOpen && labelInputRef && labelInputRef.current) {
|
|
108
|
+
(labelInputRef.current as HTMLInputElement).focus();
|
|
109
|
+
}
|
|
110
|
+
}, [isModalOpen]);
|
|
111
|
+
|
|
112
|
+
const handleMenuKeys = (event: KeyboardEvent) => {
|
|
113
|
+
if (isColorOpen && colorMenuRef.current.contains(event.target as Node)) {
|
|
114
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
115
|
+
setIsColorOpen(!isColorOpen);
|
|
116
|
+
colorToggleRef.current.focus();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (isIconOpen && iconMenuRef.current.contains(event.target as Node)) {
|
|
120
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
121
|
+
setIsIconOpen(!isIconOpen);
|
|
122
|
+
iconToggleRef.current.focus();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
128
|
+
if (isColorOpen && !colorMenuRef.current.contains(event.target as Node)) {
|
|
129
|
+
setIsColorOpen(false);
|
|
130
|
+
}
|
|
131
|
+
if (isIconOpen && !iconMenuRef.current.contains(event.target as Node)) {
|
|
132
|
+
setIsIconOpen(false);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
window.addEventListener('keydown', handleMenuKeys);
|
|
138
|
+
window.addEventListener('click', handleClickOutside);
|
|
139
|
+
return () => {
|
|
140
|
+
window.removeEventListener('keydown', handleMenuKeys);
|
|
141
|
+
window.removeEventListener('click', handleClickOutside);
|
|
142
|
+
};
|
|
143
|
+
}, [isColorOpen, isIconOpen, colorMenuRef, iconMenuRef]);
|
|
144
|
+
|
|
145
|
+
const onColorToggleClick = (ev: React.MouseEvent) => {
|
|
146
|
+
ev.stopPropagation(); // Stop handleClickOutside from handling
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
if (colorMenuRef.current) {
|
|
149
|
+
const firstElement = colorMenuRef.current.querySelector('li > button:not(:disabled)');
|
|
150
|
+
firstElement && (firstElement as HTMLElement).focus();
|
|
151
|
+
}
|
|
152
|
+
}, 0);
|
|
153
|
+
setIsColorOpen(!isColorOpen);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const colorToggle = (
|
|
157
|
+
<MenuToggle ref={colorToggleRef} onClick={onColorToggleClick} isExpanded={isColorOpen}>
|
|
158
|
+
{color || 'Select'}
|
|
159
|
+
</MenuToggle>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const colorMenu = (
|
|
163
|
+
<Menu
|
|
164
|
+
ref={colorMenuRef}
|
|
165
|
+
activeItemId={color}
|
|
166
|
+
onSelect={(_ev, itemId) => {
|
|
167
|
+
setColor(itemId.toString());
|
|
168
|
+
setIsColorOpen(false);
|
|
169
|
+
}}
|
|
170
|
+
selected={color}
|
|
171
|
+
>
|
|
172
|
+
<MenuContent>
|
|
173
|
+
<MenuList>
|
|
174
|
+
<MenuItem itemId="Gray">Gray</MenuItem>
|
|
175
|
+
<MenuItem itemId="Blue">Blue</MenuItem>
|
|
176
|
+
<MenuItem itemId="Green">Green</MenuItem>
|
|
177
|
+
<MenuItem itemId="Orange">Orange</MenuItem>
|
|
178
|
+
<MenuItem itemId="Red">Red</MenuItem>
|
|
179
|
+
<MenuItem itemId="Purple">Purple</MenuItem>
|
|
180
|
+
<MenuItem itemId="Cyan">Cyan</MenuItem>
|
|
181
|
+
</MenuList>
|
|
182
|
+
</MenuContent>
|
|
183
|
+
</Menu>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const onIconToggleClick = (ev: React.MouseEvent) => {
|
|
187
|
+
ev.stopPropagation(); // Stop handleClickOutside from handling
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
if (iconMenuRef.current) {
|
|
190
|
+
const firstElement = iconMenuRef.current.querySelector('li > button:not(:disabled)');
|
|
191
|
+
firstElement && (firstElement as HTMLElement).focus();
|
|
192
|
+
}
|
|
193
|
+
}, 0);
|
|
194
|
+
setIsIconOpen(!isIconOpen);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const iconToggle = (
|
|
198
|
+
<MenuToggle ref={iconToggleRef} onClick={onIconToggleClick} isExpanded={isIconOpen}>
|
|
199
|
+
{icon || 'Select'}
|
|
200
|
+
</MenuToggle>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const iconMenu = (
|
|
204
|
+
<Menu
|
|
205
|
+
ref={iconMenuRef}
|
|
206
|
+
activeItemId={icon}
|
|
207
|
+
onSelect={(_ev, itemId) => {
|
|
208
|
+
setIcon(itemId.toString());
|
|
209
|
+
setIsIconOpen(false);
|
|
210
|
+
}}
|
|
211
|
+
selected={icon}
|
|
212
|
+
>
|
|
213
|
+
<MenuContent>
|
|
214
|
+
<MenuList>
|
|
215
|
+
<MenuItem itemId="No icon">No icon</MenuItem>
|
|
216
|
+
<MenuItem itemId="Info circle icon">
|
|
217
|
+
<InfoCircleIcon />
|
|
218
|
+
Info circle icon
|
|
219
|
+
</MenuItem>
|
|
220
|
+
</MenuList>
|
|
221
|
+
</MenuContent>
|
|
222
|
+
</Menu>
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div>
|
|
227
|
+
<LabelGroup
|
|
228
|
+
categoryName="Label group 1"
|
|
229
|
+
numLabels={5}
|
|
230
|
+
isEditable
|
|
231
|
+
addLabelControl={
|
|
232
|
+
<Label color="blue" variant="outline" isOverflowLabel onClick={handleModalToggle}>
|
|
233
|
+
Add label
|
|
234
|
+
</Label>
|
|
235
|
+
}
|
|
236
|
+
>
|
|
237
|
+
{labels.map((label, index) => (
|
|
238
|
+
<Label
|
|
239
|
+
key={`${label.name}-${index}`}
|
|
240
|
+
id={`${label.name}-${index}`}
|
|
241
|
+
color="blue"
|
|
242
|
+
onClose={() => onClose(label.id)}
|
|
243
|
+
onEditCancel={prevText => onEdit(prevText, index)}
|
|
244
|
+
onEditComplete={newText => onEdit(newText, index)}
|
|
245
|
+
{...label.props}
|
|
246
|
+
>
|
|
247
|
+
{label.name}
|
|
248
|
+
</Label>
|
|
249
|
+
))}
|
|
250
|
+
</LabelGroup>
|
|
251
|
+
<Modal
|
|
252
|
+
variant={ModalVariant.small}
|
|
253
|
+
title="Add Label"
|
|
254
|
+
isOpen={isModalOpen}
|
|
255
|
+
onClose={handleModalToggle}
|
|
256
|
+
actions={[
|
|
257
|
+
<Button key="create" variant="primary" form="create-label-form" onClick={onAdd}>
|
|
258
|
+
Save
|
|
259
|
+
</Button>,
|
|
260
|
+
<Button key="cancel" variant="link" onClick={handleModalToggle}>
|
|
261
|
+
Cancel
|
|
262
|
+
</Button>
|
|
263
|
+
]}
|
|
264
|
+
>
|
|
265
|
+
<Form id="create-label-form">
|
|
266
|
+
<FormGroup label="Label text" fieldId="create-label-form-label-text">
|
|
267
|
+
<TextInput
|
|
268
|
+
type="text"
|
|
269
|
+
id="create-label-form-label-text"
|
|
270
|
+
name="create-label-form-label-text"
|
|
271
|
+
value={labelText}
|
|
272
|
+
onChange={setLabelText}
|
|
273
|
+
ref={labelInputRef}
|
|
274
|
+
/>
|
|
275
|
+
</FormGroup>
|
|
276
|
+
<FormGroup label="Color" fieldId="create-label-form-color">
|
|
277
|
+
<div ref={colorContainerRef}>
|
|
278
|
+
<Popper
|
|
279
|
+
trigger={colorToggle}
|
|
280
|
+
popper={colorMenu}
|
|
281
|
+
appendTo={colorContainerRef.current}
|
|
282
|
+
isVisible={isColorOpen}
|
|
283
|
+
popperMatchesTriggerWidth={false}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
</FormGroup>
|
|
287
|
+
<FormGroup label="Icon" fieldId="create-label-form-icon">
|
|
288
|
+
<div ref={iconContainerRef}>
|
|
289
|
+
<Popper
|
|
290
|
+
trigger={iconToggle}
|
|
291
|
+
popper={iconMenu}
|
|
292
|
+
appendTo={iconContainerRef.current}
|
|
293
|
+
isVisible={isIconOpen}
|
|
294
|
+
popperMatchesTriggerWidth={false}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
</FormGroup>
|
|
298
|
+
<FormGroup label="Label type" fieldId="create-label-form-label-type">
|
|
299
|
+
<Radio
|
|
300
|
+
isChecked={labelType === 'filled'}
|
|
301
|
+
name="filled-label"
|
|
302
|
+
onChange={() => setLabelType('filled')}
|
|
303
|
+
label="Filled"
|
|
304
|
+
id="radio-filled"
|
|
305
|
+
value="check1"
|
|
306
|
+
/>
|
|
307
|
+
<Radio
|
|
308
|
+
isChecked={labelType === 'outline'}
|
|
309
|
+
name="outline-label"
|
|
310
|
+
onChange={() => setLabelType('outline')}
|
|
311
|
+
label="Outlined"
|
|
312
|
+
id="radio-outline"
|
|
313
|
+
value="check2"
|
|
314
|
+
/>
|
|
315
|
+
</FormGroup>
|
|
316
|
+
<FormGroup label="Dismissable" fieldId="create-label-form-dismissable">
|
|
317
|
+
<Radio
|
|
318
|
+
isChecked={isClosable === true}
|
|
319
|
+
name="closeable-label"
|
|
320
|
+
onChange={() => setIsCloseable(true)}
|
|
321
|
+
label="Yes"
|
|
322
|
+
id="radio-closable"
|
|
323
|
+
value="check1"
|
|
324
|
+
/>
|
|
325
|
+
<Radio
|
|
326
|
+
isChecked={isClosable === false}
|
|
327
|
+
name="not-closeable-label"
|
|
328
|
+
onChange={() => setIsCloseable(false)}
|
|
329
|
+
label="No"
|
|
330
|
+
id="radio-not-closable"
|
|
331
|
+
value="check2"
|
|
332
|
+
/>
|
|
333
|
+
</FormGroup>
|
|
334
|
+
<FormGroup label="Editable" fieldId="create-label-form-editable">
|
|
335
|
+
<Radio
|
|
336
|
+
isChecked={isEditable === true}
|
|
337
|
+
name="editable-label"
|
|
338
|
+
onChange={() => setIsEditable(true)}
|
|
339
|
+
label="Yes"
|
|
340
|
+
id="radio-editable"
|
|
341
|
+
value="check3"
|
|
342
|
+
/>
|
|
343
|
+
<Radio
|
|
344
|
+
isChecked={isEditable === false}
|
|
345
|
+
name="not-editable-label"
|
|
346
|
+
onChange={() => setIsEditable(false)}
|
|
347
|
+
label="No"
|
|
348
|
+
id="radio-not-editable"
|
|
349
|
+
value="check4"
|
|
350
|
+
/>
|
|
351
|
+
</FormGroup>
|
|
352
|
+
</Form>
|
|
353
|
+
</Modal>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: Label group
|
|
3
|
+
section: components
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon';
|
|
7
|
+
|
|
8
|
+
## Demos
|
|
9
|
+
|
|
10
|
+
### Editable labels with add dropdown
|
|
11
|
+
|
|
12
|
+
```ts file="../components/LabelGroup/examples/LabelGroupEditableAddDropdown.tsx"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Editable labels with add modal
|
|
16
|
+
|
|
17
|
+
```ts file="../components/LabelGroup/examples/LabelGroupEditableAddModal.tsx"
|
|
18
|
+
```
|