@patternfly/react-core 4.222.12 → 4.222.15
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/CalendarMonth/CalendarMonth.d.ts +3 -1
- package/dist/esm/components/CalendarMonth/CalendarMonth.d.ts.map +1 -1
- package/dist/esm/components/CalendarMonth/CalendarMonth.js +21 -14
- package/dist/esm/components/CalendarMonth/CalendarMonth.js.map +1 -1
- package/dist/esm/components/DatePicker/DatePicker.d.ts +14 -4
- package/dist/esm/components/DatePicker/DatePicker.d.ts.map +1 -1
- package/dist/esm/components/DatePicker/DatePicker.js +20 -8
- package/dist/esm/components/DatePicker/DatePicker.js.map +1 -1
- 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/esm/components/Popover/Popover.js +2 -2
- package/dist/esm/components/Popover/Popover.js.map +1 -1
- package/dist/esm/components/TimePicker/TimePicker.d.ts +6 -1
- package/dist/esm/components/TimePicker/TimePicker.d.ts.map +1 -1
- package/dist/esm/components/TimePicker/TimePicker.js +32 -19
- package/dist/esm/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/js/components/CalendarMonth/CalendarMonth.d.ts +3 -1
- package/dist/js/components/CalendarMonth/CalendarMonth.d.ts.map +1 -1
- package/dist/js/components/CalendarMonth/CalendarMonth.js +21 -14
- package/dist/js/components/CalendarMonth/CalendarMonth.js.map +1 -1
- package/dist/js/components/DatePicker/DatePicker.d.ts +14 -4
- package/dist/js/components/DatePicker/DatePicker.d.ts.map +1 -1
- package/dist/js/components/DatePicker/DatePicker.js +20 -8
- package/dist/js/components/DatePicker/DatePicker.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/js/components/Popover/Popover.js +1 -1
- package/dist/js/components/Popover/Popover.js.map +1 -1
- package/dist/js/components/TimePicker/TimePicker.d.ts +6 -1
- package/dist/js/components/TimePicker/TimePicker.d.ts.map +1 -1
- package/dist/js/components/TimePicker/TimePicker.js +32 -19
- package/dist/js/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/styles/base-no-reset.css +5 -0
- package/dist/styles/base.css +5 -0
- package/dist/umd/react-core.css +255 -28
- package/dist/umd/react-core.js +100 -51
- package/dist/umd/react-core.min.css +255 -28
- package/dist/umd/react-core.min.js +5 -5
- package/package.json +6 -6
- package/src/components/CalendarMonth/CalendarMonth.tsx +21 -13
- package/src/components/DatePicker/DatePicker.tsx +36 -11
- 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/components/Modal/examples/Modal.md +2 -0
- package/src/components/OverflowMenu/examples/OverflowMenu.md +8 -0
- package/src/components/Popover/Popover.tsx +2 -2
- package/src/components/TimePicker/TimePicker.tsx +36 -19
- package/src/components/Toolbar/examples/Toolbar.md +4 -0
- package/src/demos/Card/Card.md +2 -0
- package/src/demos/{DatePicker.md → DatePicker/DatePicker.md} +8 -0
- package/src/demos/DatePicker/examples/DateTimePickerInModal.tsx +51 -0
- package/src/demos/LabelGroupDemos.md +18 -0
- package/src/demos/PrimaryDetail.md +10 -6
|
@@ -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
|
+
};
|
|
@@ -61,6 +61,8 @@ class SimpleOverflowMenu extends React.Component {
|
|
|
61
61
|
isOpen={isOpen}
|
|
62
62
|
isPlain
|
|
63
63
|
dropdownItems={dropdownItems}
|
|
64
|
+
isFlipEnabled
|
|
65
|
+
menuAppendTo="parent"
|
|
64
66
|
/>
|
|
65
67
|
</OverflowMenuControl>
|
|
66
68
|
</OverflowMenu>
|
|
@@ -153,6 +155,8 @@ class OverflowMenuGroupTypes extends React.Component {
|
|
|
153
155
|
isOpen={isOpen}
|
|
154
156
|
isPlain
|
|
155
157
|
dropdownItems={dropdownItems}
|
|
158
|
+
isFlipEnabled
|
|
159
|
+
menuAppendTo="parent"
|
|
156
160
|
/>
|
|
157
161
|
</OverflowMenuControl>
|
|
158
162
|
</OverflowMenu>
|
|
@@ -239,6 +243,8 @@ class OverflowMenuAdditionalOptions extends React.Component {
|
|
|
239
243
|
isOpen={isOpen}
|
|
240
244
|
isPlain
|
|
241
245
|
dropdownItems={dropdownItems}
|
|
246
|
+
isFlipEnabled
|
|
247
|
+
menuAppendTo="parent"
|
|
242
248
|
/>
|
|
243
249
|
</OverflowMenuControl>
|
|
244
250
|
</OverflowMenu>
|
|
@@ -301,6 +307,8 @@ class OverflowMenuPersist extends React.Component {
|
|
|
301
307
|
isOpen={isOpen}
|
|
302
308
|
isPlain
|
|
303
309
|
dropdownItems={dropdownItems}
|
|
310
|
+
isFlipEnabled
|
|
311
|
+
menuAppendTo="parent"
|
|
304
312
|
/>
|
|
305
313
|
</OverflowMenuControl>
|
|
306
314
|
</OverflowMenu>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { KeyTypes } from '../../helpers/constants';
|
|
4
4
|
import styles from '@patternfly/react-styles/css/components/Popover/popover';
|
|
5
5
|
import { css } from '@patternfly/react-styles';
|
|
6
6
|
import { PopoverContext } from './PopoverContext';
|
|
@@ -330,7 +330,7 @@ export const Popover: React.FunctionComponent<PopoverProps> = ({
|
|
|
330
330
|
const hasCustomMinWidth = minWidth !== popoverMinWidth.value;
|
|
331
331
|
const hasCustomMaxWidth = maxWidth !== popoverMaxWidth.value;
|
|
332
332
|
const onDocumentKeyDown = (event: KeyboardEvent) => {
|
|
333
|
-
if (event.
|
|
333
|
+
if (event.key === KeyTypes.Escape && visible) {
|
|
334
334
|
if (triggerManually) {
|
|
335
335
|
shouldClose(null, hide, event);
|
|
336
336
|
} else {
|
|
@@ -67,11 +67,15 @@ export interface TimePickerProps
|
|
|
67
67
|
maxTime?: string | Date;
|
|
68
68
|
/** Includes number of seconds with the chosen time and allows users to manually edit the seconds value. */
|
|
69
69
|
includeSeconds?: boolean;
|
|
70
|
+
/** Flag to control the opened state of the time picker menu */
|
|
71
|
+
isOpen?: boolean;
|
|
72
|
+
/** Handler invoked each time the open state of time picker updates */
|
|
73
|
+
setIsOpen?: (isOpen?: boolean) => void;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
interface TimePickerState {
|
|
73
77
|
isInvalid: boolean;
|
|
74
|
-
|
|
78
|
+
isTimeOptionsOpen: boolean;
|
|
75
79
|
timeState: string;
|
|
76
80
|
focusedIndex: number;
|
|
77
81
|
scrollIndex: number;
|
|
@@ -102,12 +106,13 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
102
106
|
stepMinutes: 30,
|
|
103
107
|
inputProps: {},
|
|
104
108
|
minTime: '',
|
|
105
|
-
maxTime: ''
|
|
109
|
+
maxTime: '',
|
|
110
|
+
setIsOpen: () => {}
|
|
106
111
|
};
|
|
107
112
|
|
|
108
113
|
constructor(props: TimePickerProps) {
|
|
109
114
|
super(props);
|
|
110
|
-
const { is24Hour, delimiter, time, includeSeconds } = this.props;
|
|
115
|
+
const { is24Hour, delimiter, time, includeSeconds, isOpen } = this.props;
|
|
111
116
|
let { minTime, maxTime } = this.props;
|
|
112
117
|
if (minTime === '') {
|
|
113
118
|
const minSeconds = includeSeconds ? `${delimiter}00` : '';
|
|
@@ -120,7 +125,7 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
120
125
|
const timeRegex = this.getRegExp();
|
|
121
126
|
this.state = {
|
|
122
127
|
isInvalid: false,
|
|
123
|
-
|
|
128
|
+
isTimeOptionsOpen: isOpen,
|
|
124
129
|
timeState: parseTime(time, timeRegex, delimiter, !is24Hour, includeSeconds),
|
|
125
130
|
focusedIndex: null,
|
|
126
131
|
scrollIndex: 0,
|
|
@@ -145,18 +150,18 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
145
150
|
onDocClick = (event: MouseEvent | TouchEvent) => {
|
|
146
151
|
const clickedOnToggle = this.toggleRef?.current?.contains(event.target as Node);
|
|
147
152
|
const clickedWithinMenu = this.menuRef?.current?.contains(event.target as Node);
|
|
148
|
-
if (this.state.
|
|
153
|
+
if (this.state.isTimeOptionsOpen && !(clickedOnToggle || clickedWithinMenu)) {
|
|
149
154
|
this.onToggle(false);
|
|
150
155
|
}
|
|
151
156
|
};
|
|
152
157
|
|
|
153
158
|
handleGlobalKeys = (event: KeyboardEvent) => {
|
|
154
|
-
const {
|
|
159
|
+
const { isTimeOptionsOpen, focusedIndex, scrollIndex } = this.state;
|
|
155
160
|
// keyboard pressed while focus on toggle
|
|
156
161
|
if (this.inputRef?.current?.contains(event.target as Node)) {
|
|
157
|
-
if (!
|
|
162
|
+
if (!isTimeOptionsOpen && event.key !== KeyTypes.Tab && event.key !== KeyTypes.Escape) {
|
|
158
163
|
this.onToggle(true);
|
|
159
|
-
} else if (
|
|
164
|
+
} else if (isTimeOptionsOpen) {
|
|
160
165
|
if (event.key === KeyTypes.Escape || event.key === KeyTypes.Tab) {
|
|
161
166
|
this.onToggle(false);
|
|
162
167
|
} else if (event.key === KeyTypes.Enter) {
|
|
@@ -188,9 +193,13 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
188
193
|
};
|
|
189
194
|
|
|
190
195
|
componentDidUpdate(prevProps: TimePickerProps, prevState: TimePickerState) {
|
|
191
|
-
const { timeState,
|
|
192
|
-
const { time, is24Hour, delimiter, includeSeconds } = this.props;
|
|
193
|
-
if (isOpen
|
|
196
|
+
const { timeState, isTimeOptionsOpen, isInvalid, timeRegex } = this.state;
|
|
197
|
+
const { time, is24Hour, delimiter, includeSeconds, isOpen } = this.props;
|
|
198
|
+
if (prevProps.isOpen !== isOpen) {
|
|
199
|
+
this.onToggle(isOpen);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isTimeOptionsOpen && !prevState.isTimeOptionsOpen && timeState && !isInvalid) {
|
|
194
203
|
this.scrollToSelection(timeState);
|
|
195
204
|
}
|
|
196
205
|
if (delimiter !== prevProps.delimiter) {
|
|
@@ -340,18 +349,21 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
340
349
|
const { timeRegex, isInvalid } = prevState;
|
|
341
350
|
const { delimiter, is24Hour, includeSeconds } = this.props;
|
|
342
351
|
const time = parseTime(prevState.timeState, timeRegex, delimiter, !is24Hour, includeSeconds);
|
|
343
|
-
|
|
344
352
|
return {
|
|
345
|
-
isOpen,
|
|
353
|
+
isTimeOptionsOpen: isOpen,
|
|
346
354
|
timeState: time,
|
|
347
355
|
isInvalid: isOpen ? isInvalid : !this.isValid(time)
|
|
348
356
|
};
|
|
349
357
|
});
|
|
358
|
+
this.props.setIsOpen(isOpen);
|
|
359
|
+
if (!isOpen) {
|
|
360
|
+
this.inputRef.current.focus();
|
|
361
|
+
}
|
|
350
362
|
};
|
|
351
363
|
|
|
352
364
|
onSelect = (e: any) => {
|
|
353
365
|
const { timeRegex, timeState } = this.state;
|
|
354
|
-
const { delimiter, is24Hour, includeSeconds } = this.props;
|
|
366
|
+
const { delimiter, is24Hour, includeSeconds, setIsOpen } = this.props;
|
|
355
367
|
const time = parseTime(e.target.textContent, timeRegex, delimiter, !is24Hour, includeSeconds);
|
|
356
368
|
if (time !== timeState) {
|
|
357
369
|
this.onInputChange(time);
|
|
@@ -359,13 +371,14 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
359
371
|
|
|
360
372
|
this.inputRef.current.focus();
|
|
361
373
|
this.setState({
|
|
362
|
-
|
|
374
|
+
isTimeOptionsOpen: false,
|
|
363
375
|
isInvalid: false
|
|
364
376
|
});
|
|
377
|
+
setIsOpen(false);
|
|
365
378
|
};
|
|
366
379
|
|
|
367
380
|
onInputClick = (e: any) => {
|
|
368
|
-
if (!this.state.
|
|
381
|
+
if (!this.state.isTimeOptionsOpen) {
|
|
369
382
|
this.onToggle(true);
|
|
370
383
|
}
|
|
371
384
|
e.stopPropagation();
|
|
@@ -417,6 +430,10 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
417
430
|
inputProps,
|
|
418
431
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
419
432
|
onChange,
|
|
433
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
434
|
+
setIsOpen,
|
|
435
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
436
|
+
isOpen,
|
|
420
437
|
time,
|
|
421
438
|
validateTime,
|
|
422
439
|
minTime,
|
|
@@ -425,7 +442,7 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
425
442
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
426
443
|
...props
|
|
427
444
|
} = this.props;
|
|
428
|
-
const { timeState,
|
|
445
|
+
const { timeState, isTimeOptionsOpen, isInvalid, minTimeState, maxTimeState } = this.state;
|
|
429
446
|
const style = { '--pf-c-date-picker__input--c-form-control--Width': width } as React.CSSProperties;
|
|
430
447
|
const options = makeTimeOptions(stepMinutes, !is24Hour, delimiter, minTimeState, maxTimeState, includeSeconds);
|
|
431
448
|
const isValidFormat = this.isValidFormat(timeState);
|
|
@@ -484,13 +501,13 @@ export class TimePicker extends React.Component<TimePickerProps, TimePickerState
|
|
|
484
501
|
appendTo={menuAppendTo === 'parent' ? getParentElement() : menuAppendTo}
|
|
485
502
|
trigger={textInput}
|
|
486
503
|
popper={menuContainer}
|
|
487
|
-
isVisible={
|
|
504
|
+
isVisible={isTimeOptionsOpen}
|
|
488
505
|
/>
|
|
489
506
|
) : (
|
|
490
507
|
textInput
|
|
491
508
|
)}
|
|
492
509
|
</div>
|
|
493
|
-
{
|
|
510
|
+
{isTimeOptionsOpen && menuAppendTo === 'inline' && menuContainer}
|
|
494
511
|
</div>
|
|
495
512
|
</InputGroup>
|
|
496
513
|
{isInvalid && (
|
|
@@ -811,6 +811,7 @@ import {
|
|
|
811
811
|
Dropdown,
|
|
812
812
|
DropdownItem,
|
|
813
813
|
DropdownSeparator,
|
|
814
|
+
DropdownPosition,
|
|
814
815
|
KebabToggle,
|
|
815
816
|
SearchInput
|
|
816
817
|
} from '@patternfly/react-core';
|
|
@@ -1036,6 +1037,7 @@ class ToolbarWithFilterExample extends React.Component {
|
|
|
1036
1037
|
isOpen={kebabIsOpen}
|
|
1037
1038
|
isPlain
|
|
1038
1039
|
dropdownItems={dropdownItems}
|
|
1040
|
+
position={DropdownPosition.right}
|
|
1039
1041
|
/>
|
|
1040
1042
|
</ToolbarItem>
|
|
1041
1043
|
</React.Fragment>
|
|
@@ -1080,6 +1082,7 @@ import {
|
|
|
1080
1082
|
DropdownToggle,
|
|
1081
1083
|
DropdownToggleCheckbox,
|
|
1082
1084
|
DropdownItem,
|
|
1085
|
+
DropdownPosition,
|
|
1083
1086
|
Divider,
|
|
1084
1087
|
OverflowMenu,
|
|
1085
1088
|
OverflowMenuContent,
|
|
@@ -1293,6 +1296,7 @@ class ToolbarStacked extends React.Component {
|
|
|
1293
1296
|
isOpen={kebabIsOpen}
|
|
1294
1297
|
isPlain
|
|
1295
1298
|
dropdownItems={dropdownItems}
|
|
1299
|
+
position={DropdownPosition.right}
|
|
1296
1300
|
/>
|
|
1297
1301
|
</OverflowMenuControl>
|
|
1298
1302
|
</OverflowMenu>
|
package/src/demos/Card/Card.md
CHANGED
|
@@ -146,3 +146,11 @@ DateTimeRangePicker = () => {
|
|
|
146
146
|
}
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
|
|
150
|
+
### Date and time pickers in modal
|
|
151
|
+
Modals trap focus and watch a few document level events. In order to place a date picker in a modal:
|
|
152
|
+
- To avoid the modal's escape press event handler from overruling the date picker's escape press handlers, use the `DatePickerRef` to close the calendar when it is open and the escape key is pressed.
|
|
153
|
+
- Append the calendar to the modal to keep it as close to the date picker in the DOM while maintaining correct layouts visually
|
|
154
|
+
In order to place a time picker in the modal, its menu must be appended to the time picker's parent.
|
|
155
|
+
```ts file="./examples/DateTimePickerInModal.tsx"
|
|
156
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DatePicker, Modal, ModalVariant, Button, TimePicker, InputGroup } from '@patternfly/react-core';
|
|
3
|
+
|
|
4
|
+
export const SimpleModal = () => {
|
|
5
|
+
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
6
|
+
const [isTimePickerOpen, setIsTimePickerOpen] = React.useState(false);
|
|
7
|
+
const dateRef = React.useRef(null);
|
|
8
|
+
|
|
9
|
+
const handleModalToggle = () => {
|
|
10
|
+
setIsModalOpen(!isModalOpen);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const onEscapePress = (event: KeyboardEvent) => {
|
|
14
|
+
if (dateRef && dateRef.current && dateRef.current.isCalendarOpen) {
|
|
15
|
+
dateRef.current.toggleCalendar(false, event.key);
|
|
16
|
+
} else if (isTimePickerOpen) {
|
|
17
|
+
setIsTimePickerOpen(false);
|
|
18
|
+
} else {
|
|
19
|
+
handleModalToggle();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<React.Fragment>
|
|
25
|
+
<Button variant="primary" onClick={handleModalToggle}>
|
|
26
|
+
Launch modal
|
|
27
|
+
</Button>
|
|
28
|
+
<Modal
|
|
29
|
+
id="date-time-picker-modal"
|
|
30
|
+
variant={ModalVariant.small}
|
|
31
|
+
title="Generic modal header"
|
|
32
|
+
isOpen={isModalOpen}
|
|
33
|
+
onEscapePress={onEscapePress}
|
|
34
|
+
onClose={handleModalToggle}
|
|
35
|
+
actions={[
|
|
36
|
+
<Button key="confirm" variant="primary" onClick={handleModalToggle}>
|
|
37
|
+
Confirm
|
|
38
|
+
</Button>,
|
|
39
|
+
<Button key="cancel" variant="link" onClick={handleModalToggle}>
|
|
40
|
+
Cancel
|
|
41
|
+
</Button>
|
|
42
|
+
]}
|
|
43
|
+
>
|
|
44
|
+
<InputGroup>
|
|
45
|
+
<DatePicker ref={dateRef} appendTo={() => document.getElementById('date-time-picker-modal')} />
|
|
46
|
+
<TimePicker menuAppendTo="parent" isOpen={isTimePickerOpen} setIsOpen={setIsTimePickerOpen} />
|
|
47
|
+
</InputGroup>
|
|
48
|
+
</Modal>
|
|
49
|
+
</React.Fragment>
|
|
50
|
+
);
|
|
51
|
+
};
|