• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

carbon-design-system / carbon-addons-iot-react / 9956995662

16 Jul 2024 12:38PM UTC coverage: 97.418% (-0.004%) from 97.422%
9956995662

push

github

carbon-bot
v2.154.0-next.30

7839 of 8189 branches covered (95.73%)

Branch coverage included in aggregate %.

9520 of 9630 relevant lines covered (98.86%)

2462.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.69
/packages/react/src/components/DateTimePicker/DateTimePickerV2WithTimeSpinner.jsx
1
import React, { useEffect, useState, useMemo, useRef } from 'react';
2
import PropTypes from 'prop-types';
3
import {
4
  DatePicker,
5
  DatePickerInput,
6
  RadioButtonGroup,
7
  RadioButton,
8
  FormGroup,
9
  Select,
10
  SelectItem,
11
  NumberInput,
12
  TooltipDefinition,
13
  OrderedList,
14
  ListItem,
15
} from 'carbon-components-react';
16
import { Calendar16, WarningFilled16, ErrorFilled16 } from '@carbon/icons-react';
17
import classnames from 'classnames';
18
import { v4 as uuidv4 } from 'uuid';
19
import warning from 'warning';
20
import { useLangDirection } from 'use-lang-direction';
21

22
import TimePickerSpinner from '../TimePickerSpinner/TimePickerSpinner';
23
import TimePickerDropdown from '../TimePicker/TimePickerDropdown';
24
import { settings } from '../../constants/Settings';
25
import dayjs from '../../utils/dayjs';
26
import {
27
  PICKER_KINDS,
28
  PRESET_VALUES,
29
  INTERVAL_VALUES,
30
  RELATIVE_VALUES,
31
} from '../../constants/DateConstants';
32
import Button from '../Button/Button';
33
import FlyoutMenu, { FlyoutMenuDirection } from '../FlyoutMenu/FlyoutMenu';
34
import { handleSpecificKeyDown, useOnClickOutside } from '../../utils/componentUtilityFunctions';
35
import { Tooltip } from '../Tooltip';
36

37
import {
38
  getIntervalValue,
39
  invalidEndDate,
40
  invalidStartDate,
41
  onDatePickerClose,
42
  parseValue,
43
  useAbsoluteDateTimeValue,
44
  useDateTimePickerFocus,
45
  useDateTimePickerKeyboardInteraction,
46
  useDateTimePickerRangeKind,
47
  useDateTimePickerRef,
48
  useDateTimePickerTooltip,
49
  useRelativeDateTimeValue,
50
  useDateTimePickerClickOutside,
51
  useCloseDropdown,
52
  useCustomHeight,
53
} from './dateTimePickerUtils';
54

55
const { iotPrefix, prefix } = settings;
48✔
56

57
export const DateTimePickerDefaultValuePropTypes = PropTypes.oneOfType([
48✔
58
  PropTypes.exact({
59
    timeRangeKind: PropTypes.oneOf([PICKER_KINDS.PRESET]).isRequired,
60
    timeRangeValue: PropTypes.exact({
61
      id: PropTypes.string,
62
      label: PropTypes.string.isRequired,
63
      /** offset is in minutes */
64
      offset: PropTypes.number.isRequired,
65
    }).isRequired,
66
  }).isRequired,
67
  PropTypes.exact({
68
    timeRangeKind: PropTypes.oneOf([PICKER_KINDS.RELATIVE]).isRequired,
69
    timeRangeValue: PropTypes.exact({
70
      lastNumber: PropTypes.number.isRequired,
71
      lastInterval: PropTypes.string.isRequired,
72
      relativeToWhen: PropTypes.string.isRequired,
73
      relativeToTime: PropTypes.string.isRequired,
74
    }).isRequired,
75
  }).isRequired,
76
  PropTypes.exact({
77
    timeRangeKind: PropTypes.oneOf([PICKER_KINDS.ABSOLUTE]).isRequired,
78
    timeRangeValue: PropTypes.exact({
79
      startDate: PropTypes.string.isRequired,
80
      startTime: PropTypes.string.isRequired,
81
      /** Can be a full parsable DateTime string or a Date object */
82
      start: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
83
      /** Can be a full parsable DateTime string or a Date object */
84
      end: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
85
      endDate: PropTypes.string.isRequired,
86
      endTime: PropTypes.string.isRequired,
87
    }).isRequired,
88
  }).isRequired,
89
  PropTypes.exact({
90
    timeRangeKind: PropTypes.oneOf([PICKER_KINDS.SINGLE]).isRequired,
91
    timeSingleValue: PropTypes.exact({
92
      /** Can be a full parsable DateTime string or a Date object */
93
      start: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
94
      startDate: PropTypes.string.isRequired,
95
      startTime: PropTypes.string.isRequired,
96
    }).isRequired,
97
  }).isRequired,
98
]);
99

100
export const propTypes = {
48✔
101
  testId: PropTypes.string,
102
  /** default value for the picker */
103
  defaultValue: DateTimePickerDefaultValuePropTypes,
104
  /** the dayjs.js format for the human readable interval value */
105
  dateTimeMask: PropTypes.string,
106
  /** a list of options to for the default presets */
107
  presets: PropTypes.arrayOf(
108
    PropTypes.shape({
109
      id: PropTypes.string,
110
      label: PropTypes.string,
111
      offset: PropTypes.number,
112
    })
113
  ),
114
  /** a list of options to put on the 'Last' interval dropdown */
115
  intervals: PropTypes.arrayOf(
116
    PropTypes.shape({
117
      label: PropTypes.string,
118
      value: PropTypes.string,
119
    })
120
  ),
121
  /** a list of options to put on the 'Relative to' dropdown */
122
  relatives: PropTypes.arrayOf(
123
    PropTypes.shape({
124
      label: PropTypes.string,
125
      value: PropTypes.string,
126
    })
127
  ),
128
  /** show the picker in the expanded state */
129
  expanded: PropTypes.bool,
130
  /** hide the back button and display cancel button while only using absolute range selector */
131
  hideBackButton: PropTypes.bool,
132
  /** disable the input */
133
  disabled: PropTypes.bool,
134
  /** specify the input in invalid state */
135
  invalid: PropTypes.bool,
136
  /** show the relative custom range picker */
137
  showRelativeOption: PropTypes.bool,
138
  /** show the custom range link */
139
  showCustomRangeLink: PropTypes.bool,
140
  /** show time input fields */
141
  hasTimeInput: PropTypes.bool,
142
  /**
143
   * Function hook used to provide the appropriate tooltip content for the preset time
144
   * picker. This function takes in the currentValue and should return a string message.
145
   */
146
  renderPresetTooltipText: PropTypes.func,
147
  /** triggered on cancel */
148
  onCancel: PropTypes.func,
149
  /** triggered on apply with returning object with similar signature to defaultValue */
150
  onApply: PropTypes.func,
151
  /** call back function for clear values in single select */
152
  onClear: PropTypes.func,
153
  /** All the labels that need translation */
154
  i18n: PropTypes.shape({
155
    toLabel: PropTypes.string,
156
    toNowLabel: PropTypes.string,
157
    calendarLabel: PropTypes.string,
158
    presetLabels: PropTypes.arrayOf(PropTypes.string),
159
    intervalLabels: PropTypes.arrayOf(PropTypes.string),
160
    relativeLabels: PropTypes.arrayOf(PropTypes.string),
161
    customRangeLinkLabel: PropTypes.string,
162
    customRangeLabel: PropTypes.string,
163
    relativeLabel: PropTypes.string,
164
    lastLabel: PropTypes.string,
165
    invalidNumberLabel: PropTypes.string,
166
    relativeToLabel: PropTypes.string,
167
    absoluteLabel: PropTypes.string,
168
    startTimeLabel: PropTypes.string,
169
    endTimeLabel: PropTypes.string,
170
    applyBtnLabel: PropTypes.string,
171
    cancelBtnLabel: PropTypes.string,
172
    backBtnLabel: PropTypes.string,
173
    resetBtnLabel: PropTypes.string,
174
    increment: PropTypes.string,
175
    decrement: PropTypes.string,
176
    hours: PropTypes.string,
177
    minutes: PropTypes.string,
178
    number: PropTypes.string,
179
    timePickerInvalidText: PropTypes.string,
180
    invalidText: PropTypes.string,
181
    amString: PropTypes.string,
182
    pmString: PropTypes.string,
183
  }),
184
  /** Light version  */
185
  light: PropTypes.bool,
186
  /** The language locale used to format the days of the week, months, and numbers. */
187
  locale: PropTypes.string,
188
  /** Unique id of the component */
189
  id: PropTypes.string,
190
  /** Optionally renders only an icon rather than displaying the current selected time */
191
  hasIconOnly: PropTypes.bool,
192
  /** Allow repositioning the flyout menu */
193
  menuOffset: PropTypes.shape({
194
    left: PropTypes.number,
195
    top: PropTypes.number,
196
    inputTop: PropTypes.number,
197
    inputBottom: PropTypes.number,
198
  }),
199
  /** Date picker types are single and range, default is range */
200
  datePickerType: PropTypes.string,
201
  /** If set to true it will render outside of the current DOM in a portal, otherwise render as a child */
202
  renderInPortal: PropTypes.bool,
203
  /** Auto reposition if flyout menu offscreen */
204
  useAutoPositioning: PropTypes.bool,
205
  style: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
206
};
207

208
export const defaultProps = {
48✔
209
  testId: 'date-time-picker',
210
  defaultValue: null,
211
  dateTimeMask: 'YYYY-MM-DD HH:mm',
212
  presets: PRESET_VALUES,
213
  intervals: [
214
    {
215
      label: 'minutes',
216
      value: INTERVAL_VALUES.MINUTES,
217
    },
218
    {
219
      label: 'hours',
220
      value: INTERVAL_VALUES.HOURS,
221
    },
222
    {
223
      label: 'days',
224
      value: INTERVAL_VALUES.DAYS,
225
    },
226
    {
227
      label: 'weeks',
228
      value: INTERVAL_VALUES.WEEKS,
229
    },
230
    {
231
      label: 'months',
232
      value: INTERVAL_VALUES.MONTHS,
233
    },
234
    {
235
      label: 'years',
236
      value: INTERVAL_VALUES.YEARS,
237
    },
238
  ],
239
  relatives: [
240
    {
241
      label: 'Today',
242
      value: RELATIVE_VALUES.TODAY,
243
    },
244
    {
245
      label: 'Yesterday',
246
      value: RELATIVE_VALUES.YESTERDAY,
247
    },
248
  ],
249
  expanded: false,
250
  disabled: false,
251
  invalid: false,
252
  showRelativeOption: true,
253
  hideBackButton: false,
254
  showCustomRangeLink: true,
255
  hasTimeInput: true,
256
  renderPresetTooltipText: null,
257
  onCancel: null,
258
  onApply: null,
259
  onClear: null,
260
  i18n: {
261
    toLabel: 'to',
262
    toNowLabel: 'to Now',
263
    calendarLabel: 'Calendar',
264
    presetLabels: [],
265
    intervalLabels: [],
266
    relativeLabels: [],
267
    customRangeLinkLabel: 'Custom Range',
268
    customRangeLabel: 'Custom range',
269
    relativeLabel: 'Relative',
270
    lastLabel: 'Last',
271
    invalidNumberLabel: 'Number is not valid',
272
    relativeToLabel: 'Relative to',
273
    absoluteLabel: 'Absolute',
274
    startTimeLabel: 'Start time',
275
    startAriaLabel: 'Date time start',
276
    endAriaLabel: 'Date time end',
277
    endTimeLabel: 'End time',
278
    applyBtnLabel: 'Apply',
279
    cancelBtnLabel: 'Cancel',
280
    backBtnLabel: 'Back',
281
    resetBtnLabel: 'Clear',
282
    increment: 'Increment',
283
    decrement: 'Decrement',
284
    hours: 'hours',
285
    minutes: 'minutes',
286
    number: 'number',
287
    timePickerInvalidText: undefined,
288
    invalidText: 'Time is required',
289
    invalidDateText: 'Date is required',
290
    amString: 'AM',
291
    pmString: 'PM',
292
  },
293
  light: false,
294
  locale: 'en',
295
  id: undefined,
296
  hasIconOnly: false,
297
  menuOffset: undefined,
298
  datePickerType: 'range',
299
  renderInPortal: true,
300
  useAutoPositioning: false,
301
  style: {},
302
};
303

304
const DateTimePicker = ({
48✔
305
  testId,
306
  defaultValue,
307
  dateTimeMask,
308
  presets,
309
  intervals,
310
  relatives,
311
  expanded,
312
  disabled,
313
  invalid,
314
  showRelativeOption,
315
  showCustomRangeLink,
316
  hasTimeInput,
317
  renderPresetTooltipText,
318
  hideBackButton,
319
  onCancel,
320
  onApply,
321
  onClear,
322
  i18n,
323
  light,
324
  locale,
325
  id = uuidv4(),
590✔
326
  hasIconOnly,
327
  menuOffset,
328
  datePickerType,
329
  renderInPortal,
330
  useAutoPositioning,
331
  style,
332
  ...others
333
}) => {
334
  React.useEffect(() => {
14,235✔
335
    if (__DEV__) {
492✔
336
      warning(
432✔
337
        false,
338
        'The `DateTimePickerV2` is an experimental component and could be lacking unit test and documentation. Be aware that minor version bumps could introduce breaking changes. For the reasons listed above use of this component in production is highly discouraged'
339
      );
340
    }
341
  }, []);
342

343
  const langDir = useLangDirection();
14,235✔
344
  const mergedI18n = useMemo(
14,235✔
345
    () => ({
492✔
346
      ...defaultProps.i18n,
347
      ...i18n,
348
    }),
349
    [i18n]
350
  );
351

352
  const is24hours = useMemo(() => {
14,235✔
353
    const [, time] = dateTimeMask.split(' ');
492✔
354
    const hoursMask = time?.split(':')[0];
492✔
355
    return hoursMask ? hoursMask.includes('H') : false;
492!
356
  }, [dateTimeMask]);
357
  const isSingleSelect = useMemo(() => datePickerType === 'single', [datePickerType]);
14,235✔
358

359
  // initialize the dayjs locale
360
  useEffect(() => {
14,235✔
361
    dayjs.locale(locale);
492✔
362
  }, [locale]);
363

364
  // State
365
  const [customRangeKind, setCustomRangeKind, onCustomRangeChange] =
366
    useDateTimePickerRangeKind(showRelativeOption);
14,235✔
367
  const [isCustomRange, setIsCustomRange] = useState(false);
14,235✔
368
  const [selectedPreset, setSelectedPreset] = useState(null);
14,235✔
369
  const [currentValue, setCurrentValue] = useState(null);
14,235✔
370
  const [lastAppliedValue, setLastAppliedValue] = useState(null);
14,235✔
371
  const [humanValue, setHumanValue] = useState(null);
14,235✔
372
  const [defaultTimeValueUpdate, setDefaultTimeValueUpdate] = useState(false);
14,235✔
373
  const [invalidState, setInvalidState] = useState(invalid);
14,235✔
374
  const [datePickerElem, handleDatePickerRef] = useDateTimePickerRef({ id, v2: true });
14,235✔
375
  const [focusOnFirstField, setFocusOnFirstField] = useDateTimePickerFocus(datePickerElem);
14,235✔
376
  const relativeSelect = useRef(null);
14,235✔
377
  const containerRef = useRef();
14,235✔
378
  const dropdownRef = useRef();
14,235✔
379
  const updatedStyle = useMemo(
14,235✔
380
    () => ({
492✔
381
      ...style,
382
      '--zIndex': style.zIndex ?? 0,
879✔
383
    }),
384
    [style]
385
  );
386
  const {
387
    absoluteValue,
388
    setAbsoluteValue,
389
    resetAbsoluteValue,
390
    isValid12HourTime,
391
    isValid24HourTime,
392
  } = useAbsoluteDateTimeValue();
14,235✔
393

394
  const {
395
    relativeValue,
396
    setRelativeValue,
397
    relativeToTimeInvalid,
398
    resetRelativeValue,
399
    relativeLastNumberInvalid,
400
    onRelativeLastNumberChange,
401
    onRelativeLastIntervalChange,
402
    onRelativeToWhenChange,
403
    onRelativeToTimeChange,
404
  } = useRelativeDateTimeValue({
14,235✔
405
    defaultInterval: intervals[0].value,
406
    defaultRelativeTo: relatives[0].value,
407
  });
408

409
  const {
410
    isExpanded,
411
    setIsExpanded,
412
    presetListRef,
413
    onFieldInteraction,
414
    onNavigateRadioButton,
415
    onNavigatePresets,
416
    onFieldClick,
417
  } = useDateTimePickerKeyboardInteraction({ expanded, setCustomRangeKind });
14,235✔
418
  const [isTooltipOpen, toggleTooltip, setIsTooltipOpen] = useDateTimePickerTooltip({ isExpanded });
14,235✔
419

420
  const [singleDateValue, setSingleDateValue] = useState(null);
14,235✔
421
  const [singleTimeValue, setSingleTimeValue] = useState(null);
14,235✔
422
  const [rangeStartTimeValue, setRangeStartTimeValue] = useState(null);
14,235✔
423
  const [rangeEndTimeValue, setRangeEndTimeValue] = useState(null);
14,235✔
424
  const [invalidRangeStartTime, setInvalidRangeStartTime] = useState(false);
14,235✔
425
  const [invalidRangeEndTime, setInvalidRangeEndTime] = useState(false);
14,235✔
426
  const [invalidRangeStartDate, setInvalidRangeStartDate] = useState(false);
14,235✔
427

428
  const dateTimePickerBaseValue = {
14,235✔
429
    kind: '',
430
    preset: {
431
      id: presets[0].id,
432
      label: presets[0].label,
433
      offset: presets[0].offset,
434
    },
435
    relative: {
436
      lastNumber: null,
437
      lastInterval: intervals[0].value,
438
      relativeToWhen: relatives[0].value,
439
      relativeToTime: null,
440
    },
441
    absolute: {
442
      startDate: null,
443
      startTime: null,
444
      endDate: null,
445
      endTime: null,
446
    },
447
    single: {
448
      startDate: null,
449
      startTime: null,
450
    },
451
  };
452

453
  const translatedMeridian = {
14,235✔
454
    AM: mergedI18n.amString,
455
    am: mergedI18n.amString,
456
    PM: mergedI18n.pmString,
457
    pm: mergedI18n.pmString,
458
  };
459

460
  const getLocalizedTimeValue = (timeValue) =>
14,235✔
461
    !is24hours && timeValue
28,084✔
462
      ? timeValue?.replace(/am|AM|pm|PM/g, (matched) => translatedMeridian[matched])
5,135✔
463
      : timeValue;
464

465
  const getTranslatedTimeValue = (timeValue) => {
14,235✔
466
    if (!timeValue) {
16,820✔
467
      return timeValue;
893✔
468
    }
469
    const localizedMeridian = {
15,927✔
470
      [mergedI18n.amString]: 'AM',
471
      [mergedI18n.pmString]: 'PM',
472
    };
473
    const time = timeValue.split(' ')[0];
15,927✔
474
    const meridian = localizedMeridian[timeValue.split(' ')[1]];
15,927✔
475

476
    return is24hours ? timeValue : `${time} ${meridian}`;
15,927✔
477
  };
478

479
  /**
480
   * Transforms a default or selected value into a full blown returnable object
481
   * @param {Object} [preset] clicked preset
482
   * @param {string} preset.label preset label
483
   * @param {number} preset.offset preset offset in minutes
484
   * @returns {Object} the augmented value itself and the human readable value
485
   */
486
  const renderValue = (clickedPreset = null) => {
14,235✔
487
    const value = { ...dateTimePickerBaseValue };
3,920✔
488
    if (isCustomRange) {
3,920✔
489
      if (customRangeKind === PICKER_KINDS.RELATIVE) {
3,450✔
490
        value.relative = relativeValue;
59✔
491
      } else if (customRangeKind === PICKER_KINDS.ABSOLUTE) {
3,391✔
492
        value.absolute = {
3,042✔
493
          ...absoluteValue,
494
          startTime: hasTimeInput ? rangeStartTimeValue : null,
3,042✔
495
          endTime: hasTimeInput ? rangeEndTimeValue : null,
3,042✔
496
        };
497
      } else {
498
        value.single = {
349✔
499
          ...singleDateValue,
500
          startTime: hasTimeInput && singleTimeValue !== '' ? singleTimeValue : null,
1,047✔
501
        };
502
      }
503
      value.kind = customRangeKind;
3,450✔
504
    } else {
505
      const preset = presets
470✔
506
        .filter((p) => {
507
          let filteredPreset;
508
          if (p.id) {
2,350✔
509
            filteredPreset = p.id === (clickedPreset ? clickedPreset.id : selectedPreset);
2,325✔
510
          } else {
511
            filteredPreset = p.offset === (clickedPreset ? clickedPreset.offset : selectedPreset);
25✔
512
          }
513
          return filteredPreset;
2,350✔
514
        })
515
        .pop();
516
      value.preset = preset;
470✔
517
      value.kind = PICKER_KINDS.PRESET;
470✔
518
    }
519
    setCurrentValue(value);
3,920✔
520
    const parsedValue = parseValue(value, dateTimeMask, mergedI18n.toLabel, hasTimeInput);
3,920✔
521
    setHumanValue(getLocalizedTimeValue(parsedValue.readableValue));
3,920✔
522

523
    return {
3,920✔
524
      ...value,
525
      ...parsedValue,
526
    };
527
  };
528

529
  useEffect(
14,235✔
530
    () => {
531
      if (
4,025✔
532
        absoluteValue ||
6,796✔
533
        relativeValue ||
534
        singleDateValue ||
535
        singleTimeValue ||
536
        rangeStartTimeValue ||
537
        rangeEndTimeValue
538
      ) {
539
        renderValue();
3,533✔
540
      }
541
    },
542
    // eslint-disable-next-line react-hooks/exhaustive-deps
543
    [
544
      absoluteValue,
545
      relativeValue,
546
      singleDateValue,
547
      singleTimeValue,
548
      rangeStartTimeValue,
549
      rangeEndTimeValue,
550
    ]
551
  );
552

553
  const onDatePickerChange = ([start, end], _, flatpickr) => {
14,235✔
554
    const calendarInFocus = document?.activeElement?.closest(
246✔
555
      `.${iotPrefix}--date-time-picker__datepicker`
556
    );
557

558
    const daysDidntChange =
559
      start &&
246✔
560
      end &&
561
      dayjs(absoluteValue.start).isSame(dayjs(start)) &&
562
      dayjs(absoluteValue.end).isSame(dayjs(end));
563

564
    if (daysDidntChange || !calendarInFocus) {
246✔
565
      // jump back to start to fix bug where flatpickr will change the month to the start
566
      // after it loses focus if you click outside the calendar
567
      if (focusOnFirstField) {
102!
568
        flatpickr.jumpToDate(start);
×
569
      } else {
570
        flatpickr.jumpToDate(end);
102✔
571
      }
572

573
      // In some situations, when the calendar loses focus flatpickr is firing the onChange event
574
      // again, but the dates reset to where both start and end are the same. This fixes that.
575
      if (!calendarInFocus && dayjs(start).isSame(dayjs(end))) {
102!
576
        flatpickr.setDate([absoluteValue.start, absoluteValue.end]);
×
577
      }
578
      return;
102✔
579
    }
580

581
    const newAbsolute = { ...absoluteValue };
144✔
582
    newAbsolute.start = start;
144✔
583
    newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
144✔
584
    const prevFocusOnFirstField = focusOnFirstField;
144✔
585
    if (end) {
144✔
586
      setFocusOnFirstField(!focusOnFirstField);
124✔
587
      newAbsolute.start = start;
124✔
588
      newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
124✔
589
      newAbsolute.end = end;
124✔
590
      newAbsolute.endDate = dayjs(newAbsolute.end).format('MM/DD/YYYY');
124✔
591
      if (prevFocusOnFirstField) {
124✔
592
        flatpickr.jumpToDate(newAbsolute.start, true);
103✔
593
      } else {
594
        flatpickr.jumpToDate(newAbsolute.end, true);
21✔
595
      }
596
    } else {
597
      setFocusOnFirstField(false);
20✔
598
      flatpickr.jumpToDate(newAbsolute.start, true);
20✔
599
    }
600

601
    setAbsoluteValue(newAbsolute);
144✔
602
    setInvalidRangeStartTime(
144✔
603
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
604
    );
605
    setInvalidRangeEndTime(
144✔
606
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
607
    );
608
  };
609

610
  const onSingleDatePickerChange = (start) => {
14,235✔
611
    const newSingleDate = { ...singleDateValue };
×
612
    newSingleDate.start = start;
×
613
    newSingleDate.startDate = dayjs(newSingleDate.start).format('MM/DD/YYYY');
×
614

615
    setSingleDateValue(newSingleDate);
×
616
    setInvalidRangeStartDate(!newSingleDate.startDate);
×
617
  };
618

619
  const onPresetClick = (preset) => {
14,235✔
620
    setSelectedPreset(preset.id ?? preset.offset);
217✔
621
    renderValue(preset);
217✔
622
  };
623

624
  const parseDefaultValue = (parsableValue) => {
14,235✔
625
    const currentCustomRangeKind = showRelativeOption
586✔
626
      ? PICKER_KINDS.RELATIVE
627
      : datePickerType === 'range'
53✔
628
      ? PICKER_KINDS.ABSOLUTE
629
      : PICKER_KINDS.SINGLE;
630
    if (parsableValue !== null) {
586✔
631
      if (parsableValue.timeRangeKind === PICKER_KINDS.PRESET) {
398✔
632
        // preset
633
        resetAbsoluteValue();
5✔
634
        resetRelativeValue();
5✔
635
        setCustomRangeKind(currentCustomRangeKind);
5✔
636
        onPresetClick(parsableValue.timeRangeValue);
5✔
637
      }
638
      if (parsableValue.timeRangeKind === PICKER_KINDS.RELATIVE) {
398✔
639
        // relative
640
        resetAbsoluteValue();
6✔
641
        setIsCustomRange(true);
6✔
642
        setCustomRangeKind(currentCustomRangeKind);
6✔
643
        setRelativeValue(parsableValue.timeRangeValue);
6✔
644
      }
645
      if (parsableValue.timeRangeKind === PICKER_KINDS.ABSOLUTE) {
398✔
646
        // absolute
647
        // range
648
        const absolute = { ...parsableValue.timeRangeValue };
297✔
649
        resetRelativeValue();
297✔
650
        setIsCustomRange(true);
297✔
651
        setCustomRangeKind(PICKER_KINDS.ABSOLUTE);
297✔
652
        if (!absolute.hasOwnProperty('start')) {
297✔
653
          absolute.start = dayjs(`${absolute.startDate} ${absolute.startTime}`).valueOf();
42✔
654
        }
655
        if (!absolute.hasOwnProperty('end')) {
297✔
656
          absolute.end = dayjs(`${absolute.endDate} ${absolute.endTime}`).valueOf();
42✔
657
        }
658
        absolute.startDate = dayjs(absolute.start).format('MM/DD/YYYY');
297✔
659
        absolute.startTime = is24hours
297✔
660
          ? dayjs(absolute.start).format('HH:mm')
661
          : dayjs(absolute.start).format('hh:mm A');
662
        absolute.endDate = dayjs(absolute.end).format('MM/DD/YYYY');
297✔
663
        absolute.endTime = is24hours
297✔
664
          ? dayjs(absolute.end).format('HH:mm')
665
          : dayjs(absolute.end).format('hh:mm A');
666
        setAbsoluteValue(absolute);
297✔
667
        setRangeStartTimeValue(absolute.startTime);
297✔
668
        setRangeEndTimeValue(absolute.endTime);
297✔
669
      }
670

671
      if (parsableValue.timeRangeKind === PICKER_KINDS.SINGLE) {
398✔
672
        // single
673
        const single = { ...parsableValue.timeSingleValue };
90✔
674
        resetRelativeValue();
90✔
675
        setIsCustomRange(true);
90✔
676
        setCustomRangeKind(PICKER_KINDS.SINGLE);
90✔
677
        if (!single.hasOwnProperty('start') && single.startDate && single.startTime) {
90✔
678
          single.start = dayjs(`${single.startDate} ${single.startTime}`).valueOf();
84✔
679
        }
680
        single.startDate = single.start ? dayjs(single.start).format('MM/DD/YYYY') : null;
90✔
681
        single.startTime = single.start
90✔
682
          ? is24hours
89✔
683
            ? dayjs(single.start).format('HH:mm')
684
            : dayjs(single.start).format('hh:mm A')
685
          : null;
686
        setSingleDateValue(single);
90✔
687
        setSingleTimeValue(single.startTime);
90✔
688
      }
689
    } else {
690
      resetAbsoluteValue();
188✔
691
      resetRelativeValue();
188✔
692
      setCustomRangeKind(currentCustomRangeKind);
188✔
693
      onPresetClick(presets[0]);
188✔
694
    }
695
  };
696

697
  const toggleIsCustomRange = (event) => {
14,235✔
698
    // stop the event from bubbling
699
    event.stopPropagation();
60✔
700
    setIsCustomRange(!isCustomRange);
60✔
701

702
    // If value was changed reset when going back to Preset
703
    if (absoluteValue.startDate !== '' || relativeValue.lastNumber > 0) {
60✔
704
      if (selectedPreset) {
3✔
705
        onPresetClick(presets.filter((p) => p.id ?? p.offset === selectedPreset)[0]);
5!
706
        resetAbsoluteValue();
1✔
707
        resetRelativeValue();
1✔
708
      } else {
709
        onPresetClick(presets[0]);
2✔
710
        resetAbsoluteValue();
2✔
711
        resetRelativeValue();
2✔
712
      }
713
    }
714
  };
715

716
  useEffect(
14,235✔
717
    () => {
718
      /* istanbul ignore else */
719
      if (defaultValue || humanValue === null) {
493✔
720
        parseDefaultValue(defaultValue);
493✔
721
        setLastAppliedValue(defaultValue);
493✔
722
      }
723
    },
724
    // eslint-disable-next-line react-hooks/exhaustive-deps
725
    [defaultValue]
726
  );
727

728
  const tooltipValue = renderPresetTooltipText
14,235✔
729
    ? renderPresetTooltipText(currentValue)
730
    : datePickerType === 'range'
14,232✔
731
    ? getIntervalValue({ currentValue, mergedI18n, dateTimeMask, humanValue })
732
    : isSingleSelect
1,256!
733
    ? humanValue
734
    : dateTimeMask;
735

736
  const disableAbsoluteApply =
737
    isCustomRange &&
14,235✔
738
    customRangeKind === PICKER_KINDS.ABSOLUTE &&
739
    (invalidRangeStartTime ||
740
      invalidRangeEndTime ||
741
      (absoluteValue?.startDate === '' && absoluteValue?.endDate === '') ||
742
      (hasTimeInput ? !rangeStartTimeValue || !rangeEndTimeValue : false));
13,313✔
743

744
  const disableRelativeApply =
745
    isCustomRange &&
14,235✔
746
    customRangeKind === PICKER_KINDS.RELATIVE &&
747
    (relativeLastNumberInvalid || relativeToTimeInvalid);
748

749
  const disableApply = disableRelativeApply || disableAbsoluteApply;
14,235✔
750

751
  useEffect(() => setInvalidState(invalid), [invalid]);
14,235✔
752

753
  const onApplyClick = () => {
14,235✔
754
    const value = renderValue();
170✔
755
    const returnValue = {
170✔
756
      timeRangeKind: value.kind,
757
      timeRangeValue: null,
758
      timeSingleValue: null,
759
    };
760

761
    let isValid = true;
170✔
762
    switch (value.kind) {
170✔
763
      case PICKER_KINDS.ABSOLUTE:
764
        value.absolute.startTime = getLocalizedTimeValue(value.absolute.startTime);
130✔
765
        value.absolute.endTime = getLocalizedTimeValue(value.absolute.endTime);
130✔
766
        returnValue.timeRangeValue = {
130✔
767
          ...value.absolute,
768
          humanValue,
769
          tooltipValue,
770
          ISOStart: value.absolute.start?.toISOString(),
771
          ISOEnd: value.absolute.end?.toISOString(),
772
        };
773
        break;
130✔
774
      case PICKER_KINDS.SINGLE:
775
        isValid =
38✔
776
          value.single.startDate &&
48✔
777
          !invalidRangeStartDate &&
778
          (hasTimeInput ? !invalidRangeStartTime && value.single.startTime : true);
15!
779

780
        setInvalidRangeStartTime(hasTimeInput ? !value.single.startTime : false);
38!
781
        setInvalidRangeStartDate(!value.single.startDate);
38✔
782

783
        value.single.startTime = getLocalizedTimeValue(value.single.startTime);
38✔
784
        returnValue.timeSingleValue = {
38✔
785
          ...value.single,
786
          humanValue,
787
          tooltipValue,
788
          ISOStart: new Date(value.single.start).toISOString(),
789
        };
790
        setDefaultTimeValueUpdate(!defaultTimeValueUpdate);
38✔
791
        break;
38✔
792

793
      case PICKER_KINDS.RELATIVE:
794
        returnValue.timeRangeValue = {
1✔
795
          ...value.relative,
796
          humanValue,
797
          tooltipValue,
798
        };
799
        break;
1✔
800
      default:
801
        returnValue.timeRangeValue = {
1✔
802
          ...value.preset,
803
          tooltipValue,
804
        };
805
        break;
1✔
806
    }
807
    setLastAppliedValue(returnValue);
170✔
808

809
    if (onApply && isValid) {
170✔
810
      setIsExpanded(false);
137✔
811
      onApply(returnValue);
137✔
812
    }
813
  };
814

815
  const onCancelClick = () => {
14,235✔
816
    parseDefaultValue(lastAppliedValue);
2✔
817
    setIsExpanded(false);
2✔
818

819
    /* istanbul ignore else */
820
    if (onCancel) {
2✔
821
      onCancel();
2✔
822
    }
823
  };
824

825
  const onClearClick = () => {
14,235✔
826
    setSingleDateValue({ start: null, startDate: null });
43✔
827
    setSingleTimeValue(null);
43✔
828
    setDefaultTimeValueUpdate(!defaultTimeValueUpdate);
43✔
829
    setInvalidRangeStartDate(false);
43✔
830
    setIsExpanded(false);
43✔
831
    const returnValue = {
43✔
832
      timeRangeKind: PICKER_KINDS.SINGLE,
833
      timeRangeValue: null,
834
      timeSingleValue: null,
835
    };
836

837
    returnValue.timeSingleValue = {
43✔
838
      ISOStart: null,
839
      humanValue: dateTimeMask,
840
      start: null,
841
      startDate: null,
842
      startTime: null,
843
      tooltipValue: dateTimeMask,
844
    };
845

846
    onClear(returnValue);
43✔
847
  };
848

849
  // Close tooltip if dropdown was closed by click outside
850
  const onFieldBlur = (evt) => {
14,235✔
851
    if (evt.target !== evt.currentTarget) {
1,338✔
852
      setIsTooltipOpen(false);
1,141✔
853
    }
854
  };
855

856
  const closeDropdown = useCloseDropdown({
14,235✔
857
    isExpanded,
858
    isCustomRange,
859
    setIsCustomRange,
860
    setIsExpanded,
861
    parseDefaultValue,
862
    defaultValue,
863
    setCustomRangeKind,
864
    lastAppliedValue,
865
    singleTimeValue,
866
    setSingleDateValue,
867
    setSingleTimeValue,
868
  });
869

870
  const onClickOutside = useDateTimePickerClickOutside(closeDropdown, containerRef);
14,235✔
871

872
  useOnClickOutside(dropdownRef, onClickOutside);
14,235✔
873

874
  // eslint-disable-next-line react/prop-types
875
  const CustomFooter = () => {
14,235✔
876
    return (
7,209✔
877
      <div className={`${iotPrefix}--date-time-picker__menu-btn-set`}>
878
        {isCustomRange && !isSingleSelect && !hideBackButton ? (
27,781✔
879
          <Button
880
            kind="secondary"
881
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-back`}
882
            size="field"
883
            {...others}
884
            onClick={toggleIsCustomRange}
885
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], toggleIsCustomRange)}
886
          >
887
            {mergedI18n.backBtnLabel}
888
          </Button>
889
        ) : isSingleSelect ? (
785✔
890
          <Button
891
            kind="secondary"
892
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-reset`}
893
            size="field"
894
            {...others}
895
            onClick={onClearClick}
896
            onMouseDown={(e) => e.preventDefault()}
43✔
897
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], onClearClick)}
898
          >
899
            {mergedI18n.resetBtnLabel}
900
          </Button>
901
        ) : (
902
          <Button
903
            kind="secondary"
904
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-cancel`}
905
            onClick={onCancelClick}
906
            size="field"
907
            {...others}
908
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], onCancelClick)}
909
          >
910
            {mergedI18n.cancelBtnLabel}
911
          </Button>
912
        )}
913
        <Button
914
          kind="primary"
915
          className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-apply`}
916
          {...others}
917
          onClick={onApplyClick}
918
          onKeyUp={handleSpecificKeyDown(['Enter', ' '], onApplyClick)}
919
          onMouseDown={(e) => e.preventDefault()}
143✔
920
          size="field"
921
          disabled={customRangeKind === PICKER_KINDS.SINGLE ? false : disableApply}
7,209✔
922
        >
923
          {mergedI18n.applyBtnLabel}
924
        </Button>
925
      </div>
926
    );
927
  };
928

929
  const handleRangeTimeValueChange = (startState, endState) => {
14,235✔
930
    const translatedStartTimeValue = getTranslatedTimeValue(startState);
8,179✔
931
    const translatedEndTimeValue = getTranslatedTimeValue(endState);
8,179✔
932
    setRangeStartTimeValue(translatedStartTimeValue);
8,179✔
933
    setRangeEndTimeValue(translatedEndTimeValue);
8,179✔
934
    setInvalidRangeStartTime(
8,179✔
935
      (absoluteValue &&
22,018✔
936
        invalidStartDate(translatedStartTimeValue, translatedEndTimeValue, absoluteValue)) ||
937
        (is24hours
5,693✔
938
          ? !isValid24HourTime(translatedStartTimeValue)
939
          : !isValid12HourTime(translatedStartTimeValue))
940
    );
941
    setInvalidRangeEndTime(
8,179✔
942
      (absoluteValue &&
21,313✔
943
        invalidEndDate(translatedStartTimeValue, translatedEndTimeValue, absoluteValue)) ||
944
        (is24hours
4,988✔
945
          ? !isValid24HourTime(translatedEndTimeValue)
946
          : !isValid12HourTime(translatedEndTimeValue))
947
    );
948
  };
949

950
  const handleSingleTimeValueChange = (startState) => {
14,235✔
951
    const translatedTimeValue = getTranslatedTimeValue(startState);
462✔
952
    setSingleTimeValue(translatedTimeValue);
462✔
953
    setInvalidRangeStartTime(
462✔
954
      is24hours ? !isValid24HourTime(translatedTimeValue) : !isValid12HourTime(translatedTimeValue)
462✔
955
    );
956
  };
957

958
  const menuOffsetLeft = menuOffset?.left
14,235!
959
    ? menuOffset.left
960
    : langDir === 'ltr'
14,235!
961
    ? 0
962
    : hasIconOnly
×
963
    ? -15
964
    : 288;
965

966
  const menuOffsetTop = menuOffset?.top ? menuOffset.top : 0;
14,235!
967

968
  const [
969
    offTop,
970
    ,
971
    inputTop,
972
    inputBottom,
973
    customHeight,
974
    maxHeight,
975
    invalidDateWarningHeight,
976
    invalidTimeWarningHeight,
977
    timeInputHeight,
978
  ] = useCustomHeight({
14,235✔
979
    containerRef,
980
    isSingleSelect,
981
    isCustomRange,
982
    showRelativeOption,
983
    customRangeKind,
984
    setIsExpanded,
985
  });
986

987
  const direction = useAutoPositioning
14,235✔
988
    ? offTop
70✔
989
      ? FlyoutMenuDirection.BottomEnd
990
      : FlyoutMenuDirection.TopEnd
991
    : FlyoutMenuDirection.BottomEnd;
992

993
  return (
14,235✔
994
    <div className={`${iotPrefix}--date-time-pickerv2`} ref={containerRef}>
995
      <div
996
        data-testid={testId}
997
        id={`${id}-${iotPrefix}--date-time-pickerv2__wrapper`}
998
        className={classnames(`${iotPrefix}--date-time-pickerv2__wrapper`, {
999
          [`${iotPrefix}--date-time-pickerv2__wrapper--disabled`]: disabled,
1000
          [`${iotPrefix}--date-time-pickerv2__wrapper--invalid`]: invalidState,
1001
        })}
1002
        style={{ '--wrapper-width': hasIconOnly ? '3rem' : '20rem' }}
14,235✔
1003
        role="button"
1004
        onClick={onFieldClick}
1005
        onKeyDown={handleSpecificKeyDown(['Enter', ' ', 'Escape', 'ArrowDown'], (event) => {
1006
          // the onApplyClick event gets blocked when called via the keyboard from the flyout menu's
1007
          // custom footer. This is a catch to ensure the onApplyCLick is called correctly for preset
1008
          // ranges via the keyboard.
1009
          if (
79!
1010
            (event.key === 'Enter' || event.key === ' ') &&
158!
1011
            event.target.classList.contains(`${iotPrefix}--date-time-picker__menu-btn-apply`) &&
1012
            !isCustomRange
1013
          ) {
1014
            onApplyClick();
×
1015
          }
1016

1017
          onFieldInteraction(event);
79✔
1018
        })}
1019
        onFocus={toggleTooltip}
1020
        onBlur={onFieldBlur}
1021
        onMouseEnter={toggleTooltip}
1022
        onMouseLeave={toggleTooltip}
1023
        tabIndex={0}
1024
      >
1025
        <div
1026
          className={classnames({
1027
            [`${iotPrefix}--date-time-picker__box--full`]: !hasIconOnly,
1028
            [`${iotPrefix}--date-time-picker__box--light`]: light,
1029
            [`${iotPrefix}--date-time-picker__box--disabled`]: disabled,
1030
            [`${iotPrefix}--date-time-picker__box--invalid`]: invalidState,
1031
          })}
1032
        >
1033
          {!hasIconOnly ? (
14,235✔
1034
            <div
1035
              data-testid={`${testId}__field`}
1036
              className={`${iotPrefix}--date-time-picker__field`}
1037
            >
1038
              {isExpanded || (currentValue && currentValue.kind !== PICKER_KINDS.PRESET) ? (
34,715✔
1039
                <span
1040
                  className={classnames({
1041
                    [`${iotPrefix}--date-time-picker__disabled`]:
1042
                      disabled ||
26,659✔
1043
                      (isSingleSelect &&
1044
                        (!singleDateValue?.startDate || (hasTimeInput ? !singleTimeValue : false))), // singleDateValue might be null or undefined
557!
1045
                  })}
1046
                  title={humanValue}
1047
                >
1048
                  {humanValue}
1049
                </span>
1050
              ) : humanValue ? (
1,560✔
1051
                <TooltipDefinition
1052
                  align="start"
1053
                  direction="bottom"
1054
                  tooltipText={tooltipValue}
1055
                  triggerClassName={disabled ? `${iotPrefix}--date-time-picker__disabled` : ''}
773✔
1056
                >
1057
                  {humanValue}
1058
                </TooltipDefinition>
1059
              ) : null}
1060
              {!isExpanded && isTooltipOpen && !isSingleSelect ? (
32,315✔
1061
                <Tooltip
1062
                  open={isTooltipOpen}
1063
                  showIcon={false}
1064
                  focusTrap={false}
1065
                  menuOffset={{ top: 16, left: 16 }}
1066
                  triggerClassName={`${iotPrefix}--date-time-picker__tooltip-trigger`}
1067
                  className={`${iotPrefix}--date-time-picker__tooltip`}
1068
                >
1069
                  {tooltipValue}
1070
                </Tooltip>
1071
              ) : null}
1072
            </div>
1073
          ) : null}
1074

1075
          <FlyoutMenu
1076
            isOpen={isExpanded}
1077
            buttonSize={hasIconOnly ? 'default' : 'small'}
14,235✔
1078
            renderIcon={invalidState ? WarningFilled16 : Calendar16}
14,235✔
1079
            disabled={disabled}
1080
            buttonProps={{
1081
              tooltipPosition: 'top',
1082
              tabIndex: -1,
1083
              className: classnames(`${iotPrefix}--date-time-picker--trigger-button`, {
1084
                [`${iotPrefix}--date-time-picker--trigger-button--invalid`]: invalid,
1085
                [`${iotPrefix}--date-time-picker--trigger-button--disabled`]: disabled,
1086
              }),
1087
            }}
1088
            hideTooltip
1089
            iconDescription={mergedI18n.calendarLabel}
1090
            passive={false}
1091
            triggerId={`test-trigger-${id}`}
1092
            light={light}
1093
            menuOffset={{
1094
              top: menuOffsetTop,
1095
              left: menuOffsetLeft,
1096
              inputTop,
1097
              inputBottom,
1098
            }}
1099
            testId={`${testId}-datepicker-flyout`}
1100
            direction={direction}
1101
            customFooter={CustomFooter}
1102
            tooltipFocusTrap={false}
1103
            renderInPortal={renderInPortal}
1104
            useAutoPositioning={useAutoPositioning}
1105
            tooltipClassName={classnames(`${iotPrefix}--date-time-picker--tooltip`, {
1106
              [`${iotPrefix}--date-time-picker--tooltip--icon`]: hasIconOnly,
1107
            })}
1108
            tooltipContentClassName={`${iotPrefix}--date-time-picker--menu`}
1109
            style={updatedStyle}
1110
          >
1111
            <div
1112
              ref={dropdownRef}
1113
              className={`${iotPrefix}--date-time-picker__menu-scroll`}
1114
              style={{
1115
                '--wrapper-width': '20rem',
1116
                height: customHeight,
1117
                maxHeight:
1118
                  maxHeight +
1119
                  (invalidRangeStartTime || invalidRangeEndTime ? invalidTimeWarningHeight : 0) +
39,501✔
1120
                  (invalidRangeStartDate ? invalidDateWarningHeight : 0) -
14,235✔
1121
                  (!hasTimeInput ? timeInputHeight : 0),
14,235✔
1122
              }}
1123
              role="presentation"
1124
              onClick={(event) => event.stopPropagation()} // need to stop the event so that it will not close the menu
1,058✔
1125
              onKeyDown={(event) => event.stopPropagation()} // need to stop the event so that it will not close the menu
3,047✔
1126
              tabIndex="-1"
1127
            >
1128
              {!isCustomRange ? (
14,235✔
1129
                // Catch bubbled Up/Down keys from the preset list and move focus.
1130
                // eslint-disable-next-line jsx-a11y/no-static-element-interactions
1131
                <div
1132
                  ref={presetListRef}
1133
                  onKeyDown={handleSpecificKeyDown(['ArrowUp', 'ArrowDown'], onNavigatePresets)}
1134
                >
1135
                  <OrderedList nested={false}>
1136
                    {tooltipValue ? (
1,635✔
1137
                      <ListItem
1138
                        className={`${iotPrefix}--date-time-picker__listitem ${iotPrefix}--date-time-picker__listitem--current`}
1139
                      >
1140
                        {tooltipValue}
1141
                      </ListItem>
1142
                    ) : null}
1143
                    {showCustomRangeLink ? (
1,635✔
1144
                      <ListItem
1145
                        onClick={toggleIsCustomRange}
1146
                        onKeyDown={handleSpecificKeyDown(['Enter', ' '], toggleIsCustomRange)}
1147
                        className={`${iotPrefix}--date-time-picker__listitem ${iotPrefix}--date-time-picker__listitem--preset ${iotPrefix}--date-time-picker__listitem--custom`}
1148
                        tabIndex={0}
1149
                      >
1150
                        {mergedI18n.customRangeLinkLabel}
1151
                      </ListItem>
1152
                    ) : null}
1153
                    {presets.map((preset, i) => {
1154
                      return (
8,185✔
1155
                        <ListItem
1156
                          key={i}
1157
                          onClick={() => onPresetClick(preset)}
21✔
1158
                          onKeyDown={handleSpecificKeyDown(['Enter', ' '], () =>
1159
                            onPresetClick(preset)
×
1160
                          )}
1161
                          className={classnames(
1162
                            `${iotPrefix}--date-time-picker__listitem ${iotPrefix}--date-time-picker__listitem--preset`,
1163
                            {
1164
                              [`${iotPrefix}--date-time-picker__listitem--preset-selected`]:
1165
                                selectedPreset === (preset.id ?? preset.offset),
8,250✔
1166
                            }
1167
                          )}
1168
                          tabIndex={0}
1169
                        >
1170
                          {mergedI18n.presetLabels[i] || preset.label}
16,272✔
1171
                        </ListItem>
1172
                      );
1173
                    })}
1174
                  </OrderedList>
1175
                </div>
1176
              ) : (
1177
                <div
1178
                  className={`${iotPrefix}--date-time-picker__custom-wrapper`}
1179
                  style={{ '--wrapper-width': '20rem' }}
1180
                >
1181
                  {showRelativeOption ? (
12,600✔
1182
                    <FormGroup
1183
                      legendText={mergedI18n.customRangeLabel}
1184
                      className={`${iotPrefix}--date-time-picker__menu-formgroup`}
1185
                    >
1186
                      <RadioButtonGroup
1187
                        valueSelected={customRangeKind}
1188
                        onChange={onCustomRangeChange}
1189
                        name={`${id}-radiogroup`}
1190
                      >
1191
                        <RadioButton
1192
                          value={PICKER_KINDS.RELATIVE}
1193
                          id={`${id}-relative`}
1194
                          labelText={mergedI18n.relativeLabel}
1195
                          onKeyDown={handleSpecificKeyDown(
1196
                            ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'],
1197
                            onNavigateRadioButton
1198
                          )}
1199
                        />
1200
                        <RadioButton
1201
                          value={PICKER_KINDS.ABSOLUTE}
1202
                          id={`${id}-absolute`}
1203
                          labelText={mergedI18n.absoluteLabel}
1204
                          onKeyDown={handleSpecificKeyDown(
1205
                            ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'],
1206
                            onNavigateRadioButton
1207
                          )}
1208
                        />
1209
                      </RadioButtonGroup>
1210
                    </FormGroup>
1211
                  ) : null}
1212
                  {showRelativeOption && customRangeKind === PICKER_KINDS.RELATIVE ? (
37,143✔
1213
                    <>
1214
                      <FormGroup
1215
                        legendText={mergedI18n.lastLabel}
1216
                        className={`${iotPrefix}--date-time-picker__menu-formgroup`}
1217
                      >
1218
                        <div className={`${iotPrefix}--date-time-picker__fields-wrapper`}>
1219
                          <NumberInput
1220
                            id={`${id}-last-number`}
1221
                            invalidText={mergedI18n.invalidNumberLabel}
1222
                            step={1}
1223
                            min={0}
1224
                            value={relativeValue ? relativeValue.lastNumber : 0}
357!
1225
                            onChange={onRelativeLastNumberChange}
1226
                            translateWithId={(messageId) =>
1227
                              messageId === 'increment.number'
590✔
1228
                                ? `${i18n.increment} ${i18n.number}`
1229
                                : messageId === 'decrement.number'
295!
1230
                                ? `${i18n.decrement} ${i18n.number}`
1231
                                : null
1232
                            }
1233
                            light
1234
                          />
1235
                          <Select
1236
                            {...others}
1237
                            id={`${id}-last-interval`}
1238
                            defaultValue={
1239
                              relativeValue ? relativeValue.lastInterval : INTERVAL_VALUES.MINUTES
357!
1240
                            }
1241
                            onChange={onRelativeLastIntervalChange}
1242
                            hideLabel
1243
                            light
1244
                          >
1245
                            {intervals.map((interval, i) => {
1246
                              return (
2,122✔
1247
                                <SelectItem
1248
                                  key={i}
1249
                                  value={interval.value}
1250
                                  text={mergedI18n.intervalLabels[i] || interval.label}
4,244✔
1251
                                />
1252
                              );
1253
                            })}
1254
                          </Select>
1255
                        </div>
1256
                      </FormGroup>
1257
                      <FormGroup
1258
                        legendText={mergedI18n.relativeToLabel}
1259
                        className={`${iotPrefix}--date-time-picker__menu-formgroup`}
1260
                      >
1261
                        <div className={`${iotPrefix}--date-time-picker__fields-wrapper`}>
1262
                          <Select
1263
                            {...others}
1264
                            ref={relativeSelect}
1265
                            id={`${id}-relative-to-when`}
1266
                            defaultValue={relativeValue ? relativeValue.relativeToWhen : ''}
357!
1267
                            onChange={onRelativeToWhenChange}
1268
                            hideLabel
1269
                            light
1270
                          >
1271
                            {relatives.map((relative, i) => {
1272
                              return (
707✔
1273
                                <SelectItem
1274
                                  key={i}
1275
                                  value={relative.value}
1276
                                  text={mergedI18n.relativeLabels[i] || relative.label}
1,414✔
1277
                                />
1278
                              );
1279
                            })}
1280
                          </Select>
1281
                          {hasTimeInput ? (
357✔
1282
                            <TimePickerSpinner
1283
                              id={`${id}-relative-to-time`}
1284
                              invalid={relativeToTimeInvalid}
1285
                              value={relativeValue ? relativeValue.relativeToTime : ''}
330!
1286
                              i18n={i18n}
1287
                              onChange={onRelativeToTimeChange}
1288
                              spinner
1289
                              autoComplete="off"
1290
                              light
1291
                            />
1292
                          ) : null}
1293
                        </div>
1294
                      </FormGroup>
1295
                    </>
1296
                  ) : (
1297
                    <div data-testid={`${testId}-datepicker`}>
1298
                      <div
1299
                        id={`${id}-${iotPrefix}--date-time-picker__datepicker`}
1300
                        className={`${iotPrefix}--date-time-picker__datepicker`}
1301
                      >
1302
                        <DatePicker
1303
                          datePickerType={datePickerType}
1304
                          dateFormat="m/d/Y"
1305
                          ref={handleDatePickerRef}
1306
                          onChange={
1307
                            datePickerType === 'single'
12,243✔
1308
                              ? onSingleDatePickerChange
1309
                              : onDatePickerChange
1310
                          }
1311
                          onClose={onDatePickerClose}
1312
                          value={
1313
                            absoluteValue && datePickerType === 'range'
35,371✔
1314
                              ? [absoluteValue.startDate, absoluteValue.endDate]
1315
                              : singleDateValue && datePickerType === 'single'
4,074✔
1316
                              ? [singleDateValue?.startDate]
1317
                              : null
1318
                          }
1319
                          locale={locale}
1320
                        >
1321
                          <DatePickerInput
1322
                            labelText=""
1323
                            aria-label={mergedI18n.startAriaLabel}
1324
                            id={`${id}-date-picker-input-start`}
1325
                            hideLabel
1326
                          />
1327

1328
                          {datePickerType === 'range' ? (
12,243✔
1329
                            <DatePickerInput
1330
                              labelText=""
1331
                              aria-label={mergedI18n.endAriaLabel}
1332
                              id={`${id}-date-picker-input-end`}
1333
                              hideLabel
1334
                            />
1335
                          ) : null}
1336
                        </DatePicker>
1337
                        {invalidRangeStartDate ? (
12,243✔
1338
                          <div
1339
                            className={classnames(
1340
                              `${iotPrefix}--date-time-picker__datepicker--invalid`
1341
                            )}
1342
                          >
1343
                            <ErrorFilled16 />
1344
                            <p
1345
                              className={classnames(
1346
                                `${iotPrefix}--date-time-picker__helper-text--invalid`
1347
                              )}
1348
                            >
1349
                              {mergedI18n.invalidDateText}
1350
                            </p>
1351
                          </div>
1352
                        ) : null}
1353
                      </div>
1354
                      {hasTimeInput ? (
12,243✔
1355
                        <TimePickerDropdown
1356
                          className={`${iotPrefix}--time-picker-dropdown`}
1357
                          id={id}
1358
                          key={defaultTimeValueUpdate}
1359
                          value={
1360
                            isSingleSelect
11,933✔
1361
                              ? getLocalizedTimeValue(singleTimeValue)
1362
                              : getLocalizedTimeValue(rangeStartTimeValue)
1363
                          }
1364
                          secondaryValue={getLocalizedTimeValue(rangeEndTimeValue)}
1365
                          hideLabel={!mergedI18n.startTimeLabel}
1366
                          hideSecondaryLabel={!mergedI18n.endTimeLabel}
1367
                          onChange={(startState, endState) =>
1368
                            isSingleSelect
8,641✔
1369
                              ? handleSingleTimeValueChange(startState)
1370
                              : handleRangeTimeValueChange(startState, endState)
1371
                          }
1372
                          type={isSingleSelect ? 'single' : 'range'}
11,933✔
1373
                          invalid={[invalidRangeStartTime, invalidRangeEndTime]}
1374
                          i18n={{
1375
                            labelText: mergedI18n.startTimeLabel,
1376
                            secondaryLabelText: mergedI18n.endTimeLabel,
1377
                            invalidText: mergedI18n.timePickerInvalidText,
1378
                            amString: mergedI18n.amString,
1379
                            pmString: mergedI18n.pmString,
1380
                          }}
1381
                          size="sm"
1382
                          testId={testId}
1383
                          style={{ zIndex: (style.zIndex ?? 0) + 6000 }}
18,495✔
1384
                          is24hours={is24hours}
1385
                        />
1386
                      ) : (
1387
                        <div className={`${iotPrefix}--date-time-picker__no-formgroup`} />
1388
                      )}
1389
                    </div>
1390
                  )}
1391
                </div>
1392
              )}
1393
            </div>
1394
          </FlyoutMenu>
1395
        </div>
1396
      </div>
1397
      {invalidState && !hasIconOnly ? (
28,473✔
1398
        <p
1399
          className={classnames(
1400
            `${prefix}--form__helper-text`,
1401
            `${iotPrefix}--date-time-picker__helper-text--invalid`
1402
          )}
1403
        >
1404
          {mergedI18n.invalidText}
1405
        </p>
1406
      ) : null}
1407
    </div>
1408
  );
1409
};
1410

1411
DateTimePicker.propTypes = propTypes;
48✔
1412
DateTimePicker.defaultProps = defaultProps;
48✔
1413

1414
export default DateTimePicker;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc