@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/esm/components/CalendarMonth/CalendarMonth.d.ts +3 -1
  3. package/dist/esm/components/CalendarMonth/CalendarMonth.d.ts.map +1 -1
  4. package/dist/esm/components/CalendarMonth/CalendarMonth.js +21 -14
  5. package/dist/esm/components/CalendarMonth/CalendarMonth.js.map +1 -1
  6. package/dist/esm/components/DatePicker/DatePicker.d.ts +14 -4
  7. package/dist/esm/components/DatePicker/DatePicker.d.ts.map +1 -1
  8. package/dist/esm/components/DatePicker/DatePicker.js +20 -8
  9. package/dist/esm/components/DatePicker/DatePicker.js.map +1 -1
  10. package/dist/esm/components/Label/Label.js +3 -3
  11. package/dist/esm/components/Label/Label.js.map +1 -1
  12. package/dist/esm/components/LabelGroup/LabelGroup.d.ts +2 -0
  13. package/dist/esm/components/LabelGroup/LabelGroup.d.ts.map +1 -1
  14. package/dist/esm/components/LabelGroup/LabelGroup.js +4 -3
  15. package/dist/esm/components/LabelGroup/LabelGroup.js.map +1 -1
  16. package/dist/esm/components/Popover/Popover.js +2 -2
  17. package/dist/esm/components/Popover/Popover.js.map +1 -1
  18. package/dist/esm/components/TimePicker/TimePicker.d.ts +6 -1
  19. package/dist/esm/components/TimePicker/TimePicker.d.ts.map +1 -1
  20. package/dist/esm/components/TimePicker/TimePicker.js +32 -19
  21. package/dist/esm/components/TimePicker/TimePicker.js.map +1 -1
  22. package/dist/js/components/CalendarMonth/CalendarMonth.d.ts +3 -1
  23. package/dist/js/components/CalendarMonth/CalendarMonth.d.ts.map +1 -1
  24. package/dist/js/components/CalendarMonth/CalendarMonth.js +21 -14
  25. package/dist/js/components/CalendarMonth/CalendarMonth.js.map +1 -1
  26. package/dist/js/components/DatePicker/DatePicker.d.ts +14 -4
  27. package/dist/js/components/DatePicker/DatePicker.d.ts.map +1 -1
  28. package/dist/js/components/DatePicker/DatePicker.js +20 -8
  29. package/dist/js/components/DatePicker/DatePicker.js.map +1 -1
  30. package/dist/js/components/Label/Label.js +3 -3
  31. package/dist/js/components/Label/Label.js.map +1 -1
  32. package/dist/js/components/LabelGroup/LabelGroup.d.ts +2 -0
  33. package/dist/js/components/LabelGroup/LabelGroup.d.ts.map +1 -1
  34. package/dist/js/components/LabelGroup/LabelGroup.js +4 -3
  35. package/dist/js/components/LabelGroup/LabelGroup.js.map +1 -1
  36. package/dist/js/components/Popover/Popover.js +1 -1
  37. package/dist/js/components/Popover/Popover.js.map +1 -1
  38. package/dist/js/components/TimePicker/TimePicker.d.ts +6 -1
  39. package/dist/js/components/TimePicker/TimePicker.d.ts.map +1 -1
  40. package/dist/js/components/TimePicker/TimePicker.js +32 -19
  41. package/dist/js/components/TimePicker/TimePicker.js.map +1 -1
  42. package/dist/styles/base-no-reset.css +5 -0
  43. package/dist/styles/base.css +5 -0
  44. package/dist/umd/react-core.css +255 -28
  45. package/dist/umd/react-core.js +100 -51
  46. package/dist/umd/react-core.min.css +255 -28
  47. package/dist/umd/react-core.min.js +5 -5
  48. package/package.json +6 -6
  49. package/src/components/CalendarMonth/CalendarMonth.tsx +21 -13
  50. package/src/components/DatePicker/DatePicker.tsx +36 -11
  51. package/src/components/Label/Label.tsx +3 -3
  52. package/src/components/LabelGroup/LabelGroup.tsx +5 -1
  53. package/src/components/LabelGroup/examples/LabelGroup.md +7 -0
  54. package/src/components/LabelGroup/examples/LabelGroupEditableAdd.tsx +74 -0
  55. package/src/components/LabelGroup/examples/LabelGroupEditableAddDropdown.tsx +138 -0
  56. package/src/components/LabelGroup/examples/LabelGroupEditableAddModal.tsx +356 -0
  57. package/src/components/Modal/examples/Modal.md +2 -0
  58. package/src/components/OverflowMenu/examples/OverflowMenu.md +8 -0
  59. package/src/components/Popover/Popover.tsx +2 -2
  60. package/src/components/TimePicker/TimePicker.tsx +36 -19
  61. package/src/components/Toolbar/examples/Toolbar.md +4 -0
  62. package/src/demos/Card/Card.md +2 -0
  63. package/src/demos/{DatePicker.md → DatePicker/DatePicker.md} +8 -0
  64. package/src/demos/DatePicker/examples/DateTimePickerInModal.tsx +51 -0
  65. package/src/demos/LabelGroupDemos.md +18 -0
  66. 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
+ };
@@ -876,3 +876,5 @@ If the content that you're passing to the modal is likely to overflow the modal
876
876
 
877
877
  ```ts file="ModalWithOverflowingContent.tsx"
878
878
  ```
879
+
880
+
@@ -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 { KEY_CODES } from '../../helpers/constants';
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.keyCode === KEY_CODES.ESCAPE_KEY && visible) {
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
- isOpen: boolean;
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
- isOpen: false,
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.isOpen && !(clickedOnToggle || clickedWithinMenu)) {
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 { isOpen, focusedIndex, scrollIndex } = this.state;
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 (!isOpen && event.key !== KeyTypes.Tab) {
162
+ if (!isTimeOptionsOpen && event.key !== KeyTypes.Tab && event.key !== KeyTypes.Escape) {
158
163
  this.onToggle(true);
159
- } else if (isOpen) {
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, isOpen, isInvalid, timeRegex } = this.state;
192
- const { time, is24Hour, delimiter, includeSeconds } = this.props;
193
- if (isOpen && !prevState.isOpen && timeState && !isInvalid) {
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
- isOpen: false,
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.isOpen) {
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, isOpen, isInvalid, minTimeState, maxTimeState } = this.state;
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={isOpen}
504
+ isVisible={isTimeOptionsOpen}
488
505
  />
489
506
  ) : (
490
507
  textInput
491
508
  )}
492
509
  </div>
493
- {isOpen && menuAppendTo === 'inline' && menuContainer}
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>
@@ -525,6 +525,8 @@ class CardViewBasic extends React.Component {
525
525
  isOpen={isLowerToolbarKebabDropdownOpen}
526
526
  isPlain
527
527
  dropdownItems={toolbarKebabDropdownItems}
528
+ isFlipEnabled
529
+ menuAppendTo="parent"
528
530
  />
529
531
  </OverflowMenuControl>
530
532
  </OverflowMenu>
@@ -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
+ };