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

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

11 Sep 2023 01:09PM UTC coverage: 97.488% (+0.006%) from 97.482%
6147354289

Pull #3825

github

Marcelo Blechner
fix(IdleTimer): Preventing the IdleTimer logic to start its timer if timeout is 0.
Pull Request #3825: fix(IdleTimer): Preventing the IdleTimer logic to start its timer if timeout is 0.

7624 of 7956 branches covered (0.0%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

9141 of 9241 relevant lines covered (98.92%)

2104.61 hits per line

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

94.79
/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 } 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;
47✔
56

57
export const DateTimePickerDefaultValuePropTypes = PropTypes.oneOfType([
47✔
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 = {
47✔
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
  /** disable the input */
131
  disabled: PropTypes.bool,
132
  /** specify the input in invalid state */
133
  invalid: PropTypes.bool,
134
  /** show the relative custom range picker */
135
  showRelativeOption: PropTypes.bool,
136
  /** show the custom range link */
137
  showCustomRangeLink: PropTypes.bool,
138
  /** show time input fields */
139
  hasTimeInput: PropTypes.bool,
140
  /**
141
   * Function hook used to provide the appropriate tooltip content for the preset time
142
   * picker. This function takes in the currentValue and should return a string message.
143
   */
144
  renderPresetTooltipText: PropTypes.func,
145
  /** triggered on cancel */
146
  onCancel: PropTypes.func,
147
  /** triggered on apply with returning object with similar signature to defaultValue */
148
  onApply: PropTypes.func,
149
  /** All the labels that need translation */
150
  i18n: PropTypes.shape({
151
    toLabel: PropTypes.string,
152
    toNowLabel: PropTypes.string,
153
    calendarLabel: PropTypes.string,
154
    presetLabels: PropTypes.arrayOf(PropTypes.string),
155
    intervalLabels: PropTypes.arrayOf(PropTypes.string),
156
    relativeLabels: PropTypes.arrayOf(PropTypes.string),
157
    customRangeLinkLabel: PropTypes.string,
158
    customRangeLabel: PropTypes.string,
159
    relativeLabel: PropTypes.string,
160
    lastLabel: PropTypes.string,
161
    invalidNumberLabel: PropTypes.string,
162
    relativeToLabel: PropTypes.string,
163
    absoluteLabel: PropTypes.string,
164
    startTimeLabel: PropTypes.string,
165
    endTimeLabel: PropTypes.string,
166
    applyBtnLabel: PropTypes.string,
167
    cancelBtnLabel: PropTypes.string,
168
    backBtnLabel: PropTypes.string,
169
    resetBtnLabel: PropTypes.string,
170
    increment: PropTypes.string,
171
    decrement: PropTypes.string,
172
    hours: PropTypes.string,
173
    minutes: PropTypes.string,
174
    number: PropTypes.string,
175
    timePickerInvalidText: PropTypes.string,
176
    invalidText: PropTypes.string,
177
    amString: PropTypes.string,
178
    pmString: PropTypes.string,
179
  }),
180
  /** Light version  */
181
  light: PropTypes.bool,
182
  /** The language locale used to format the days of the week, months, and numbers. */
183
  locale: PropTypes.string,
184
  /** Unique id of the component */
185
  id: PropTypes.string,
186
  /** Optionally renders only an icon rather than displaying the current selected time */
187
  hasIconOnly: PropTypes.bool,
188
  /** Allow repositioning the flyout menu */
189
  menuOffset: PropTypes.shape({
190
    left: PropTypes.number,
191
    top: PropTypes.number,
192
    inputTop: PropTypes.number,
193
    inputBottom: PropTypes.number,
194
  }),
195
  /** Date picker types are single and range, default is range */
196
  datePickerType: PropTypes.string,
197
  /** If set to true it will render outside of the current DOM in a portal, otherwise render as a child */
198
  renderInPortal: PropTypes.bool,
199
  /** Auto reposition if flyout menu offscreen */
200
  useAutoPositioning: PropTypes.bool,
201
  style: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
202
};
203

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

295
const DateTimePicker = ({
47✔
296
  testId,
297
  defaultValue,
298
  dateTimeMask,
299
  presets,
300
  intervals,
301
  relatives,
302
  expanded,
303
  disabled,
304
  invalid,
305
  showRelativeOption,
306
  showCustomRangeLink,
307
  hasTimeInput,
308
  renderPresetTooltipText,
309
  onCancel,
310
  onApply,
311
  i18n,
312
  light,
313
  locale,
314
  id = uuidv4(),
578✔
315
  hasIconOnly,
316
  menuOffset,
317
  datePickerType,
318
  renderInPortal,
319
  useAutoPositioning,
320
  style,
321
  ...others
322
}) => {
323
  React.useEffect(() => {
13,420✔
324
    if (__DEV__) {
452✔
325
      warning(
394✔
326
        false,
327
        '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'
328
      );
329
    }
330
  }, []);
331

332
  const langDir = useLangDirection();
13,420✔
333
  const mergedI18n = useMemo(
13,420✔
334
    () => ({
452✔
335
      ...defaultProps.i18n,
336
      ...i18n,
337
    }),
338
    [i18n]
339
  );
340

341
  const is24hours = useMemo(() => {
13,420✔
342
    const [, time] = dateTimeMask.split(' ');
452✔
343
    const hoursMask = time?.split(':')[0];
452✔
344
    return hoursMask ? hoursMask.includes('H') : false;
452!
345
  }, [dateTimeMask]);
346
  const isSingleSelect = useMemo(() => datePickerType === 'single', [datePickerType]);
13,420✔
347

348
  // initialize the dayjs locale
349
  useEffect(() => {
13,420✔
350
    dayjs.locale(locale);
452✔
351
  }, [locale]);
352

353
  // State
354
  const [customRangeKind, setCustomRangeKind, onCustomRangeChange] = useDateTimePickerRangeKind(
13,420✔
355
    showRelativeOption
356
  );
357
  const [isCustomRange, setIsCustomRange] = useState(false);
13,420✔
358
  const [selectedPreset, setSelectedPreset] = useState(null);
13,420✔
359
  const [currentValue, setCurrentValue] = useState(null);
13,420✔
360
  const [lastAppliedValue, setLastAppliedValue] = useState(null);
13,420✔
361
  const [humanValue, setHumanValue] = useState(null);
13,420✔
362
  const [defaultSingleDateValue, SetDefaultSingleDateValue] = useState(false);
13,420✔
363
  const [invalidState, setInvalidState] = useState(invalid);
13,420✔
364
  const [datePickerElem, handleDatePickerRef] = useDateTimePickerRef({ id, v2: true });
13,420✔
365
  const [focusOnFirstField, setFocusOnFirstField] = useDateTimePickerFocus(datePickerElem);
13,420✔
366
  const relativeSelect = useRef(null);
13,420✔
367
  const containerRef = useRef();
13,420✔
368
  const dropdownRef = useRef();
13,420✔
369
  const updatedStyle = useMemo(
13,420✔
370
    () => ({
452✔
371
      ...style,
372
      '--zIndex': style.zIndex ?? 0,
801✔
373
    }),
374
    [style]
375
  );
376
  const {
377
    absoluteValue,
378
    setAbsoluteValue,
379
    resetAbsoluteValue,
380
    isValid12HourTime,
381
    isValid24HourTime,
382
  } = useAbsoluteDateTimeValue();
13,420✔
383

384
  const {
385
    relativeValue,
386
    setRelativeValue,
387
    relativeToTimeInvalid,
388
    resetRelativeValue,
389
    relativeLastNumberInvalid,
390
    onRelativeLastNumberChange,
391
    onRelativeLastIntervalChange,
392
    onRelativeToWhenChange,
393
    onRelativeToTimeChange,
394
  } = useRelativeDateTimeValue({
13,420✔
395
    defaultInterval: intervals[0].value,
396
    defaultRelativeTo: relatives[0].value,
397
  });
398

399
  const {
400
    isExpanded,
401
    setIsExpanded,
402
    presetListRef,
403
    onFieldInteraction,
404
    onNavigateRadioButton,
405
    onNavigatePresets,
406
  } = useDateTimePickerKeyboardInteraction({ expanded, setCustomRangeKind });
13,420✔
407
  const [isTooltipOpen, toggleTooltip, setIsTooltipOpen] = useDateTimePickerTooltip({ isExpanded });
13,420✔
408

409
  const [singleDateValue, setSingleDateValue] = useState(null);
13,420✔
410
  const [singleTimeValue, setSingleTimeValue] = useState(null);
13,420✔
411
  const [rangeStartTimeValue, setRangeStartTimeValue] = useState(null);
13,420✔
412
  const [rangeEndTimeValue, setRangeEndTimeValue] = useState(null);
13,420✔
413
  const [invalidRangeStartTime, setInvalidRangeStartTime] = useState(false);
13,420✔
414
  const [invalidRangeEndTime, setInvalidRangeEndTime] = useState(false);
13,420✔
415

416
  const dateTimePickerBaseValue = {
13,420✔
417
    kind: '',
418
    preset: {
419
      id: presets[0].id,
420
      label: presets[0].label,
421
      offset: presets[0].offset,
422
    },
423
    relative: {
424
      lastNumber: null,
425
      lastInterval: intervals[0].value,
426
      relativeToWhen: relatives[0].value,
427
      relativeToTime: null,
428
    },
429
    absolute: {
430
      startDate: null,
431
      startTime: null,
432
      endDate: null,
433
      endTime: null,
434
    },
435
    single: {
436
      startDate: null,
437
      startTime: null,
438
    },
439
  };
440

441
  const translatedMeridian = {
13,420✔
442
    AM: mergedI18n.amString,
443
    am: mergedI18n.amString,
444
    PM: mergedI18n.pmString,
445
    pm: mergedI18n.pmString,
446
  };
447

448
  const getLocalizedTimeValue = (timeValue) =>
13,420✔
449
    !is24hours && timeValue
26,293✔
450
      ? timeValue?.replace(/am|AM|pm|PM/g, (matched) => translatedMeridian[matched])
5,004✔
451
      : timeValue;
452

453
  const getTranslatedTimeValue = (timeValue) => {
13,420✔
454
    if (!timeValue) {
16,298✔
455
      return timeValue;
779✔
456
    }
457
    const localizedMeridian = {
15,519✔
458
      [mergedI18n.amString]: 'AM',
459
      [mergedI18n.pmString]: 'PM',
460
    };
461
    const time = timeValue.split(' ')[0];
15,519✔
462
    const meridian = localizedMeridian[timeValue.split(' ')[1]];
15,519✔
463

464
    return is24hours ? timeValue : `${time} ${meridian}`;
15,519✔
465
  };
466

467
  /**
468
   * Transforms a default or selected value into a full blown returnable object
469
   * @param {Object} [preset] clicked preset
470
   * @param {string} preset.label preset label
471
   * @param {number} preset.offset preset offset in minutes
472
   * @returns {Object} the augmented value itself and the human readable value
473
   */
474
  const renderValue = (clickedPreset = null) => {
13,420✔
475
    const value = { ...dateTimePickerBaseValue };
3,716✔
476
    if (isCustomRange) {
3,716✔
477
      if (customRangeKind === PICKER_KINDS.RELATIVE) {
3,246✔
478
        value.relative = relativeValue;
59✔
479
      } else if (customRangeKind === PICKER_KINDS.ABSOLUTE) {
3,187✔
480
        value.absolute = {
2,973✔
481
          ...absoluteValue,
482
          startTime: hasTimeInput ? rangeStartTimeValue : null,
2,973✔
483
          endTime: hasTimeInput ? rangeEndTimeValue : null,
2,973✔
484
        };
485
      } else {
486
        value.single = {
214✔
487
          ...singleDateValue,
488
          startTime: hasTimeInput && singleTimeValue !== '' ? singleTimeValue : null,
642✔
489
        };
490
      }
491
      value.kind = customRangeKind;
3,246✔
492
    } else {
493
      const preset = presets
470✔
494
        .filter((p) => {
495
          let filteredPreset;
496
          if (p.id) {
2,350✔
497
            filteredPreset = p.id === (clickedPreset ? clickedPreset.id : selectedPreset);
2,325✔
498
          } else {
499
            filteredPreset = p.offset === (clickedPreset ? clickedPreset.offset : selectedPreset);
25✔
500
          }
501
          return filteredPreset;
2,350✔
502
        })
503
        .pop();
504
      value.preset = preset;
470✔
505
      value.kind = PICKER_KINDS.PRESET;
470✔
506
    }
507
    setCurrentValue(value);
3,716✔
508
    const parsedValue = parseValue(value, dateTimeMask, mergedI18n.toLabel);
3,716✔
509
    setHumanValue(getLocalizedTimeValue(parsedValue.readableValue));
3,716✔
510

511
    return {
3,716✔
512
      ...value,
513
      ...parsedValue,
514
    };
515
  };
516

517
  useEffect(
13,420✔
518
    () => {
519
      if (
3,816✔
520
        absoluteValue ||
6,285✔
521
        relativeValue ||
522
        singleDateValue ||
523
        singleTimeValue ||
524
        rangeStartTimeValue ||
525
        rangeEndTimeValue
526
      ) {
527
        renderValue();
3,364✔
528
      }
529
    },
530
    // eslint-disable-next-line react-hooks/exhaustive-deps
531
    [
532
      absoluteValue,
533
      relativeValue,
534
      singleDateValue,
535
      singleTimeValue,
536
      rangeStartTimeValue,
537
      rangeEndTimeValue,
538
    ]
539
  );
540

541
  const onDatePickerChange = ([start, end], _, flatpickr) => {
13,420✔
542
    const calendarInFocus = document?.activeElement?.closest(
242✔
543
      `.${iotPrefix}--date-time-picker__datepicker`
544
    );
545

546
    const daysDidntChange =
547
      start &&
242✔
548
      end &&
549
      dayjs(absoluteValue.start).isSame(dayjs(start)) &&
550
      dayjs(absoluteValue.end).isSame(dayjs(end));
551

552
    if (daysDidntChange || !calendarInFocus) {
242✔
553
      // jump back to start to fix bug where flatpickr will change the month to the start
554
      // after it loses focus if you click outside the calendar
555
      if (focusOnFirstField) {
100!
556
        flatpickr.jumpToDate(start);
×
557
      } else {
558
        flatpickr.jumpToDate(end);
100✔
559
      }
560

561
      // In some situations, when the calendar loses focus flatpickr is firing the onChange event
562
      // again, but the dates reset to where both start and end are the same. This fixes that.
563
      if (!calendarInFocus && dayjs(start).isSame(dayjs(end))) {
100!
564
        flatpickr.setDate([absoluteValue.start, absoluteValue.end]);
×
565
      }
566
      return;
100✔
567
    }
568

569
    const newAbsolute = { ...absoluteValue };
142✔
570
    newAbsolute.start = start;
142✔
571
    newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
142✔
572
    const prevFocusOnFirstField = focusOnFirstField;
142✔
573
    if (end) {
142✔
574
      setFocusOnFirstField(!focusOnFirstField);
122✔
575
      newAbsolute.start = start;
122✔
576
      newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
122✔
577
      newAbsolute.end = end;
122✔
578
      newAbsolute.endDate = dayjs(newAbsolute.end).format('MM/DD/YYYY');
122✔
579
      if (prevFocusOnFirstField) {
122✔
580
        flatpickr.jumpToDate(newAbsolute.start, true);
101✔
581
      } else {
582
        flatpickr.jumpToDate(newAbsolute.end, true);
21✔
583
      }
584
    } else {
585
      setFocusOnFirstField(false);
20✔
586
      flatpickr.jumpToDate(newAbsolute.start, true);
20✔
587
    }
588

589
    setAbsoluteValue(newAbsolute);
142✔
590
    setInvalidRangeStartTime(
142✔
591
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
592
    );
593
    setInvalidRangeEndTime(
142✔
594
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
595
    );
596
  };
597

598
  const onSingleDatePickerChange = (start) => {
13,420✔
599
    const newSingleDate = { ...singleDateValue };
×
600
    newSingleDate.start = start;
×
601
    newSingleDate.startDate = dayjs(newSingleDate.start).format('MM/DD/YYYY');
×
602

603
    setSingleDateValue(newSingleDate);
×
604
  };
605

606
  const onPresetClick = (preset) => {
13,420✔
607
    setSelectedPreset(preset.id ?? preset.offset);
217✔
608
    renderValue(preset);
217✔
609
  };
610

611
  const parseDefaultValue = (parsableValue) => {
13,420✔
612
    const currentCustomRangeKind = showRelativeOption
546✔
613
      ? PICKER_KINDS.RELATIVE
614
      : datePickerType === 'range'
18✔
615
      ? PICKER_KINDS.ABSOLUTE
616
      : PICKER_KINDS.SINGLE;
617
    if (parsableValue !== null) {
546✔
618
      if (parsableValue.timeRangeKind === PICKER_KINDS.PRESET) {
358✔
619
        // preset
620
        resetAbsoluteValue();
5✔
621
        resetRelativeValue();
5✔
622
        setCustomRangeKind(currentCustomRangeKind);
5✔
623
        onPresetClick(parsableValue.timeRangeValue);
5✔
624
      }
625
      if (parsableValue.timeRangeKind === PICKER_KINDS.RELATIVE) {
358✔
626
        // relative
627
        resetAbsoluteValue();
6✔
628
        setIsCustomRange(true);
6✔
629
        setCustomRangeKind(currentCustomRangeKind);
6✔
630
        setRelativeValue(parsableValue.timeRangeValue);
6✔
631
      }
632
      if (parsableValue.timeRangeKind === PICKER_KINDS.ABSOLUTE) {
358✔
633
        // absolute
634
        // range
635
        const absolute = { ...parsableValue.timeRangeValue };
292✔
636
        resetRelativeValue();
292✔
637
        setIsCustomRange(true);
292✔
638
        setCustomRangeKind(PICKER_KINDS.ABSOLUTE);
292✔
639
        if (!absolute.hasOwnProperty('start')) {
292✔
640
          absolute.start = dayjs(`${absolute.startDate} ${absolute.startTime}`).valueOf();
41✔
641
        }
642
        if (!absolute.hasOwnProperty('end')) {
292✔
643
          absolute.end = dayjs(`${absolute.endDate} ${absolute.endTime}`).valueOf();
41✔
644
        }
645
        absolute.startDate = dayjs(absolute.start).format('MM/DD/YYYY');
292✔
646
        absolute.startTime = is24hours
292✔
647
          ? dayjs(absolute.start).format('HH:mm')
648
          : dayjs(absolute.start).format('hh:mm A');
649
        absolute.endDate = dayjs(absolute.end).format('MM/DD/YYYY');
292✔
650
        absolute.endTime = is24hours
292✔
651
          ? dayjs(absolute.end).format('HH:mm')
652
          : dayjs(absolute.end).format('hh:mm A');
653
        setAbsoluteValue(absolute);
292✔
654
        setRangeStartTimeValue(absolute.startTime);
292✔
655
        setRangeEndTimeValue(absolute.endTime);
292✔
656
      }
657

658
      if (parsableValue.timeRangeKind === PICKER_KINDS.SINGLE) {
358✔
659
        // single
660
        const single = { ...parsableValue.timeSingleValue };
55✔
661
        resetRelativeValue();
55✔
662
        setIsCustomRange(true);
55✔
663
        setCustomRangeKind(PICKER_KINDS.SINGLE);
55✔
664
        if (!single.hasOwnProperty('start') && single.startDate && single.startTime) {
55✔
665
          single.start = dayjs(`${single.startDate} ${single.startTime}`).valueOf();
49✔
666
        }
667
        single.startDate = single.start ? dayjs(single.start).format('MM/DD/YYYY') : null;
55✔
668
        single.startTime = single.start
55✔
669
          ? is24hours
54✔
670
            ? dayjs(single.start).format('HH:mm')
671
            : dayjs(single.start).format('hh:mm A')
672
          : null;
673
        setSingleDateValue(single);
55✔
674
        setSingleTimeValue(single.startTime);
55✔
675
      }
676
    } else {
677
      resetAbsoluteValue();
188✔
678
      resetRelativeValue();
188✔
679
      setCustomRangeKind(currentCustomRangeKind);
188✔
680
      onPresetClick(presets[0]);
188✔
681
    }
682
  };
683

684
  const toggleIsCustomRange = (event) => {
13,420✔
685
    // stop the event from bubbling
686
    event.stopPropagation();
60✔
687
    setIsCustomRange(!isCustomRange);
60✔
688

689
    // If value was changed reset when going back to Preset
690
    if (absoluteValue.startDate !== '' || relativeValue.lastNumber > 0) {
60✔
691
      if (selectedPreset) {
3✔
692
        onPresetClick(presets.filter((p) => p.id ?? p.offset === selectedPreset)[0]);
5!
693
        resetAbsoluteValue();
1✔
694
        resetRelativeValue();
1✔
695
      } else {
696
        onPresetClick(presets[0]);
2✔
697
        resetAbsoluteValue();
2✔
698
        resetRelativeValue();
2✔
699
      }
700
    }
701
  };
702

703
  useEffect(
13,420✔
704
    () => {
705
      /* istanbul ignore else */
706
      if (defaultValue || humanValue === null) {
453✔
707
        parseDefaultValue(defaultValue);
453✔
708
        setLastAppliedValue(defaultValue);
453✔
709
      }
710
    },
711
    // eslint-disable-next-line react-hooks/exhaustive-deps
712
    [defaultValue]
713
  );
714

715
  const tooltipValue = renderPresetTooltipText
13,420✔
716
    ? renderPresetTooltipText(currentValue)
717
    : datePickerType === 'range'
13,417✔
718
    ? getIntervalValue({ currentValue, mergedI18n, dateTimeMask, humanValue })
719
    : isSingleSelect
683!
720
    ? humanValue
721
    : dateTimeMask;
722

723
  const disableRelativeApply =
724
    isCustomRange &&
13,420✔
725
    customRangeKind === PICKER_KINDS.RELATIVE &&
726
    (relativeLastNumberInvalid || relativeToTimeInvalid);
727

728
  const disableAbsoluteApply =
729
    isCustomRange &&
13,420✔
730
    customRangeKind === PICKER_KINDS.ABSOLUTE &&
731
    (invalidRangeStartTime ||
732
      invalidRangeEndTime ||
733
      (absoluteValue?.startDate === '' && absoluteValue?.endDate === '') ||
734
      (hasTimeInput ? !rangeStartTimeValue || !rangeEndTimeValue : false));
13,058✔
735

736
  const disableSingleApply =
737
    isCustomRange &&
13,420✔
738
    customRangeKind === PICKER_KINDS.SINGLE &&
739
    (invalidRangeStartTime ||
740
      (!singleDateValue.start && !singleDateValue.startDate) ||
741
      (hasTimeInput ? !singleTimeValue : false));
507!
742

743
  const disableApply = disableRelativeApply || disableAbsoluteApply || disableSingleApply;
13,420✔
744

745
  useEffect(() => setInvalidState(invalid), [invalid]);
13,420✔
746

747
  const onApplyClick = () => {
13,420✔
748
    setIsExpanded(false);
135✔
749
    const value = renderValue();
135✔
750
    setLastAppliedValue(value);
135✔
751
    const returnValue = {
135✔
752
      timeRangeKind: value.kind,
753
      timeRangeValue: null,
754
      timeSingleValue: null,
755
    };
756

757
    switch (value.kind) {
135✔
758
      case PICKER_KINDS.ABSOLUTE:
759
        value.absolute.startTime = getLocalizedTimeValue(value.absolute.startTime);
128✔
760
        value.absolute.endTime = getLocalizedTimeValue(value.absolute.endTime);
128✔
761
        returnValue.timeRangeValue = {
128✔
762
          ...value.absolute,
763
          humanValue,
764
          tooltipValue,
765
          ISOStart: new Date(value.absolute.start).toISOString(),
766
          ISOEnd: new Date(value.absolute.end).toISOString(),
767
        };
768
        break;
128✔
769
      case PICKER_KINDS.SINGLE:
770
        value.single.startTime = getLocalizedTimeValue(value.single.startTime);
5✔
771
        returnValue.timeSingleValue = {
5✔
772
          ...value.single,
773
          humanValue,
774
          tooltipValue,
775
          ISOStart: new Date(value.single.start).toISOString(),
776
        };
777
        break;
5✔
778
      case PICKER_KINDS.RELATIVE:
779
        returnValue.timeRangeValue = {
1✔
780
          ...value.relative,
781
          humanValue,
782
          tooltipValue,
783
        };
784
        break;
1✔
785
      default:
786
        returnValue.timeRangeValue = {
1✔
787
          ...value.preset,
788
          tooltipValue,
789
        };
790
        break;
1✔
791
    }
792

793
    if (onApply) {
135!
794
      onApply(returnValue);
135✔
795
    }
796
  };
797

798
  const onCancelClick = () => {
13,420✔
799
    parseDefaultValue(lastAppliedValue);
2✔
800
    setIsExpanded(false);
2✔
801

802
    /* istanbul ignore else */
803
    if (onCancel) {
2✔
804
      onCancel();
2✔
805
    }
806
  };
807

808
  const onClearClick = () => {
13,420✔
809
    setSingleDateValue({ start: null, startDate: null });
9✔
810
    setSingleTimeValue(null);
9✔
811
    SetDefaultSingleDateValue(true);
9✔
812
    setIsExpanded(false);
9✔
813
  };
814

815
  // Close tooltip if dropdown was closed by click outside
816
  const onFieldBlur = (evt) => {
13,420✔
817
    if (evt.target !== evt.currentTarget) {
1,322✔
818
      setIsTooltipOpen(false);
1,128✔
819
    }
820
  };
821

822
  const closeDropdown = useCloseDropdown({
13,420✔
823
    isExpanded,
824
    isCustomRange,
825
    setIsCustomRange,
826
    setIsExpanded,
827
    parseDefaultValue,
828
    defaultValue,
829
    setCustomRangeKind,
830
    lastAppliedValue,
831
    singleTimeValue,
832
    setSingleDateValue,
833
    setSingleTimeValue,
834
  });
835

836
  const onClickOutside = useDateTimePickerClickOutside(closeDropdown, containerRef);
13,420✔
837

838
  useOnClickOutside(dropdownRef, onClickOutside);
13,420✔
839

840
  // eslint-disable-next-line react/prop-types
841
  const CustomFooter = () => {
13,420✔
842
    return (
6,840✔
843
      <div className={`${iotPrefix}--date-time-picker__menu-btn-set`}>
844
        {isCustomRange && !isSingleSelect ? (
20,250✔
845
          <Button
846
            kind="secondary"
847
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-back`}
848
            size="field"
849
            {...others}
850
            onClick={toggleIsCustomRange}
851
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], toggleIsCustomRange)}
852
          >
853
            {mergedI18n.backBtnLabel}
854
          </Button>
855
        ) : isSingleSelect ? (
552✔
856
          <Button
857
            kind="secondary"
858
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-reset`}
859
            size="field"
860
            {...others}
861
            onClick={onClearClick}
862
            onMouseDown={(e) => e.preventDefault()}
9✔
863
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], onClearClick)}
864
          >
865
            {mergedI18n.resetBtnLabel}
866
          </Button>
867
        ) : (
868
          <Button
869
            kind="secondary"
870
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-cancel`}
871
            onClick={onCancelClick}
872
            size="field"
873
            {...others}
874
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], onCancelClick)}
875
          >
876
            {mergedI18n.cancelBtnLabel}
877
          </Button>
878
        )}
879
        <Button
880
          kind="primary"
881
          className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-apply`}
882
          {...others}
883
          onClick={onApplyClick}
884
          onKeyUp={handleSpecificKeyDown(['Enter', ' '], onApplyClick)}
885
          onMouseDown={(e) => e.preventDefault()}
108✔
886
          size="field"
887
          disabled={disableApply}
888
        >
889
          {mergedI18n.applyBtnLabel}
890
        </Button>
891
      </div>
892
    );
893
  };
894

895
  const handleRangeTimeValueChange = (startState, endState) => {
13,420✔
896
    const translatedStartTimeValue = getTranslatedTimeValue(startState);
7,984✔
897
    const translatedEndTimeValue = getTranslatedTimeValue(endState);
7,984✔
898
    setRangeStartTimeValue(translatedStartTimeValue);
7,984✔
899
    setRangeEndTimeValue(translatedEndTimeValue);
7,984✔
900
    setInvalidRangeStartTime(
7,984✔
901
      (absoluteValue &&
21,496✔
902
        invalidStartDate(translatedStartTimeValue, translatedEndTimeValue, absoluteValue)) ||
903
        (is24hours
5,561✔
904
          ? !isValid24HourTime(translatedStartTimeValue)
905
          : !isValid12HourTime(translatedStartTimeValue))
906
    );
907
    setInvalidRangeEndTime(
7,984✔
908
      (absoluteValue &&
20,803✔
909
        invalidEndDate(translatedStartTimeValue, translatedEndTimeValue, absoluteValue)) ||
910
        (is24hours
4,868✔
911
          ? !isValid24HourTime(translatedEndTimeValue)
912
          : !isValid12HourTime(translatedEndTimeValue))
913
    );
914
  };
915

916
  const handleSingleTimeValueChange = (startState) => {
13,420✔
917
    const translatedTimeValue = getTranslatedTimeValue(startState);
330✔
918
    setSingleTimeValue(translatedTimeValue);
330✔
919
    setInvalidRangeStartTime(
330✔
920
      is24hours ? !isValid24HourTime(translatedTimeValue) : !isValid12HourTime(translatedTimeValue)
330✔
921
    );
922
  };
923

924
  const menuOffsetLeft = menuOffset?.left
13,420!
925
    ? menuOffset.left
926
    : langDir === 'ltr'
13,420!
927
    ? 0
928
    : hasIconOnly
×
929
    ? -15
930
    : 288;
931

932
  const menuOffsetTop = menuOffset?.top ? menuOffset.top : 0;
13,420!
933

934
  const [offTop, , inputTop, inputBottom, customHeight, maxHeight] = useCustomHeight({
13,420✔
935
    containerRef,
936
    isSingleSelect,
937
    isCustomRange,
938
    showRelativeOption,
939
    customRangeKind,
940
    setIsExpanded,
941
  });
942

943
  const direction = useAutoPositioning
13,420✔
944
    ? offTop
70✔
945
      ? FlyoutMenuDirection.BottomEnd
946
      : FlyoutMenuDirection.TopEnd
947
    : FlyoutMenuDirection.BottomEnd;
948

949
  return (
13,420✔
950
    <div className={`${iotPrefix}--date-time-pickerv2`} ref={containerRef}>
951
      <div
952
        data-testid={testId}
953
        id={`${id}-${iotPrefix}--date-time-pickerv2__wrapper`}
954
        className={classnames(`${iotPrefix}--date-time-pickerv2__wrapper`, {
955
          [`${iotPrefix}--date-time-pickerv2__wrapper--disabled`]: disabled,
956
          [`${iotPrefix}--date-time-pickerv2__wrapper--invalid`]: invalidState,
957
        })}
958
        style={{ '--wrapper-width': hasIconOnly ? '3rem' : '20rem' }}
13,420✔
959
        role="button"
960
        onClick={onFieldInteraction}
961
        onKeyDown={handleSpecificKeyDown(['Enter', ' ', 'Escape', 'ArrowDown'], (event) => {
962
          // the onApplyClick event gets blocked when called via the keyboard from the flyout menu's
963
          // custom footer. This is a catch to ensure the onApplyCLick is called correctly for preset
964
          // ranges via the keyboard.
965
          if (
79!
966
            (event.key === 'Enter' || event.key === ' ') &&
158!
967
            event.target.classList.contains(`${iotPrefix}--date-time-picker__menu-btn-apply`) &&
968
            !isCustomRange
969
          ) {
970
            onApplyClick();
×
971
          }
972

973
          onFieldInteraction(event);
79✔
974
        })}
975
        onFocus={toggleTooltip}
976
        onBlur={onFieldBlur}
977
        onMouseEnter={toggleTooltip}
978
        onMouseLeave={toggleTooltip}
979
        tabIndex={0}
980
      >
981
        <div
982
          className={classnames({
983
            [`${iotPrefix}--date-time-picker__box--full`]: !hasIconOnly,
984
            [`${iotPrefix}--date-time-picker__box--light`]: light,
985
            [`${iotPrefix}--date-time-picker__box--disabled`]: disabled,
986
            [`${iotPrefix}--date-time-picker__box--invalid`]: invalidState,
987
          })}
988
        >
989
          {!hasIconOnly ? (
13,420✔
990
            <div
991
              data-testid={`${testId}__field`}
992
              className={`${iotPrefix}--date-time-picker__field`}
993
            >
994
              {isExpanded || (currentValue && currentValue.kind !== PICKER_KINDS.PRESET) ? (
32,543✔
995
                <span
996
                  className={classnames({
997
                    [`${iotPrefix}--date-time-picker__disabled`]:
998
                      disabled || (isSingleSelect && !singleDateValue.startDate),
24,129✔
999
                  })}
1000
                  title={humanValue}
1001
                >
1002
                  {humanValue}
1003
                </span>
1004
              ) : humanValue ? (
1,480✔
1005
                <TooltipDefinition
1006
                  align="start"
1007
                  direction="bottom"
1008
                  tooltipText={tooltipValue}
1009
                  triggerClassName={disabled ? `${iotPrefix}--date-time-picker__disabled` : ''}
773✔
1010
                >
1011
                  {humanValue}
1012
                </TooltipDefinition>
1013
              ) : null}
1014
              {!isExpanded && isTooltipOpen && !isSingleSelect ? (
30,302✔
1015
                <Tooltip
1016
                  open={isTooltipOpen}
1017
                  showIcon={false}
1018
                  focusTrap={false}
1019
                  menuOffset={{ top: 16, left: 16 }}
1020
                  triggerClassName={`${iotPrefix}--date-time-picker__tooltip-trigger`}
1021
                  className={`${iotPrefix}--date-time-picker__tooltip`}
1022
                >
1023
                  {tooltipValue}
1024
                </Tooltip>
1025
              ) : null}
1026
            </div>
1027
          ) : null}
1028

1029
          <FlyoutMenu
1030
            isOpen={isExpanded}
1031
            buttonSize={hasIconOnly ? 'default' : 'small'}
13,420✔
1032
            renderIcon={invalidState ? WarningFilled16 : Calendar16}
13,420✔
1033
            disabled={disabled}
1034
            buttonProps={{
1035
              tooltipPosition: 'top',
1036
              tabIndex: -1,
1037
              className: classnames(`${iotPrefix}--date-time-picker--trigger-button`, {
1038
                [`${iotPrefix}--date-time-picker--trigger-button--invalid`]: invalid,
1039
                [`${iotPrefix}--date-time-picker--trigger-button--disabled`]: disabled,
1040
              }),
1041
            }}
1042
            hideTooltip
1043
            iconDescription={mergedI18n.calendarLabel}
1044
            passive={false}
1045
            triggerId="test-trigger-id-2"
1046
            light={light}
1047
            menuOffset={{
1048
              top: menuOffsetTop,
1049
              left: menuOffsetLeft,
1050
              inputTop,
1051
              inputBottom,
1052
            }}
1053
            testId={`${testId}-datepicker-flyout`}
1054
            direction={direction}
1055
            customFooter={CustomFooter}
1056
            tooltipFocusTrap={false}
1057
            renderInPortal={renderInPortal}
1058
            useAutoPositioning={useAutoPositioning}
1059
            tooltipClassName={classnames(`${iotPrefix}--date-time-picker--tooltip`, {
1060
              [`${iotPrefix}--date-time-picker--tooltip--icon`]: hasIconOnly,
1061
            })}
1062
            tooltipContentClassName={`${iotPrefix}--date-time-picker--menu`}
1063
            style={updatedStyle}
1064
          >
1065
            <div
1066
              ref={dropdownRef}
1067
              className={`${iotPrefix}--date-time-picker__menu-scroll`}
1068
              style={{
1069
                '--wrapper-width': '20rem',
1070
                height: customHeight,
1071
                maxHeight,
1072
              }}
1073
              role="listbox"
1074
              onClick={(event) => event.stopPropagation()} // need to stop the event so that it will not close the menu
1,044✔
1075
              onKeyDown={(event) => event.stopPropagation()} // need to stop the event so that it will not close the menu
2,982✔
1076
              tabIndex="-1"
1077
            >
1078
              {!isCustomRange ? (
13,420✔
1079
                // Catch bubbled Up/Down keys from the preset list and move focus.
1080
                // eslint-disable-next-line jsx-a11y/no-static-element-interactions
1081
                <div
1082
                  ref={presetListRef}
1083
                  onKeyDown={handleSpecificKeyDown(['ArrowUp', 'ArrowDown'], onNavigatePresets)}
1084
                >
1085
                  <OrderedList nested={false}>
1086
                    {tooltipValue ? (
1,595✔
1087
                      <ListItem
1088
                        className={`${iotPrefix}--date-time-picker__listitem ${iotPrefix}--date-time-picker__listitem--current`}
1089
                      >
1090
                        {tooltipValue}
1091
                      </ListItem>
1092
                    ) : null}
1093
                    {showCustomRangeLink ? (
1,595✔
1094
                      <ListItem
1095
                        onClick={toggleIsCustomRange}
1096
                        onKeyDown={handleSpecificKeyDown(['Enter', ' '], toggleIsCustomRange)}
1097
                        className={`${iotPrefix}--date-time-picker__listitem ${iotPrefix}--date-time-picker__listitem--preset ${iotPrefix}--date-time-picker__listitem--custom`}
1098
                        tabIndex={0}
1099
                      >
1100
                        {mergedI18n.customRangeLinkLabel}
1101
                      </ListItem>
1102
                    ) : null}
1103
                    {presets.map((preset, i) => {
1104
                      return (
7,985✔
1105
                        <ListItem
1106
                          key={i}
1107
                          onClick={() => onPresetClick(preset)}
21✔
1108
                          onKeyDown={handleSpecificKeyDown(['Enter', ' '], () =>
1109
                            onPresetClick(preset)
×
1110
                          )}
1111
                          className={classnames(
1112
                            `${iotPrefix}--date-time-picker__listitem ${iotPrefix}--date-time-picker__listitem--preset`,
1113
                            {
1114
                              [`${iotPrefix}--date-time-picker__listitem--preset-selected`]:
1115
                                selectedPreset === (preset.id ?? preset.offset),
8,050✔
1116
                            }
1117
                          )}
1118
                          tabIndex={0}
1119
                        >
1120
                          {mergedI18n.presetLabels[i] || preset.label}
15,872✔
1121
                        </ListItem>
1122
                      );
1123
                    })}
1124
                  </OrderedList>
1125
                </div>
1126
              ) : (
1127
                <div
1128
                  className={`${iotPrefix}--date-time-picker__custom-wrapper`}
1129
                  style={{ '--wrapper-width': '20rem' }}
1130
                >
1131
                  {showRelativeOption ? (
11,825✔
1132
                    <FormGroup
1133
                      legendText={mergedI18n.customRangeLabel}
1134
                      className={`${iotPrefix}--date-time-picker__menu-formgroup`}
1135
                    >
1136
                      <RadioButtonGroup
1137
                        valueSelected={customRangeKind}
1138
                        onChange={onCustomRangeChange}
1139
                        name={`${id}-radiogroup`}
1140
                      >
1141
                        <RadioButton
1142
                          value={PICKER_KINDS.RELATIVE}
1143
                          id={`${id}-relative`}
1144
                          labelText={mergedI18n.relativeLabel}
1145
                          onKeyDown={handleSpecificKeyDown(
1146
                            ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'],
1147
                            onNavigateRadioButton
1148
                          )}
1149
                        />
1150
                        <RadioButton
1151
                          value={PICKER_KINDS.ABSOLUTE}
1152
                          id={`${id}-absolute`}
1153
                          labelText={mergedI18n.absoluteLabel}
1154
                          onKeyDown={handleSpecificKeyDown(
1155
                            ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'],
1156
                            onNavigateRadioButton
1157
                          )}
1158
                        />
1159
                      </RadioButtonGroup>
1160
                    </FormGroup>
1161
                  ) : null}
1162
                  {showRelativeOption && customRangeKind === PICKER_KINDS.RELATIVE ? (
35,356✔
1163
                    <>
1164
                      <FormGroup
1165
                        legendText={mergedI18n.lastLabel}
1166
                        className={`${iotPrefix}--date-time-picker__menu-formgroup`}
1167
                      >
1168
                        <div className={`${iotPrefix}--date-time-picker__fields-wrapper`}>
1169
                          <NumberInput
1170
                            id={`${id}-last-number`}
1171
                            invalidText={mergedI18n.invalidNumberLabel}
1172
                            step={1}
1173
                            min={0}
1174
                            value={relativeValue ? relativeValue.lastNumber : 0}
357!
1175
                            onChange={onRelativeLastNumberChange}
1176
                            translateWithId={(messageId) =>
1177
                              messageId === 'increment.number'
590✔
1178
                                ? `${i18n.increment} ${i18n.number}`
1179
                                : messageId === 'decrement.number'
295!
1180
                                ? `${i18n.decrement} ${i18n.number}`
1181
                                : null
1182
                            }
1183
                            light
1184
                          />
1185
                          <Select
1186
                            {...others}
1187
                            id={`${id}-last-interval`}
1188
                            defaultValue={
1189
                              relativeValue ? relativeValue.lastInterval : INTERVAL_VALUES.MINUTES
357!
1190
                            }
1191
                            onChange={onRelativeLastIntervalChange}
1192
                            hideLabel
1193
                            light
1194
                          >
1195
                            {intervals.map((interval, i) => {
1196
                              return (
2,122✔
1197
                                <SelectItem
1198
                                  key={i}
1199
                                  value={interval.value}
1200
                                  text={mergedI18n.intervalLabels[i] || interval.label}
4,244✔
1201
                                />
1202
                              );
1203
                            })}
1204
                          </Select>
1205
                        </div>
1206
                      </FormGroup>
1207
                      <FormGroup
1208
                        legendText={mergedI18n.relativeToLabel}
1209
                        className={`${iotPrefix}--date-time-picker__menu-formgroup`}
1210
                      >
1211
                        <div className={`${iotPrefix}--date-time-picker__fields-wrapper`}>
1212
                          <Select
1213
                            {...others}
1214
                            ref={relativeSelect}
1215
                            id={`${id}-relative-to-when`}
1216
                            defaultValue={relativeValue ? relativeValue.relativeToWhen : ''}
357!
1217
                            onChange={onRelativeToWhenChange}
1218
                            hideLabel
1219
                            light
1220
                          >
1221
                            {relatives.map((relative, i) => {
1222
                              return (
707✔
1223
                                <SelectItem
1224
                                  key={i}
1225
                                  value={relative.value}
1226
                                  text={mergedI18n.relativeLabels[i] || relative.label}
1,414✔
1227
                                />
1228
                              );
1229
                            })}
1230
                          </Select>
1231
                          {hasTimeInput ? (
357✔
1232
                            <TimePickerSpinner
1233
                              id={`${id}-relative-to-time`}
1234
                              invalid={relativeToTimeInvalid}
1235
                              value={relativeValue ? relativeValue.relativeToTime : ''}
330!
1236
                              i18n={i18n}
1237
                              onChange={onRelativeToTimeChange}
1238
                              spinner
1239
                              autoComplete="off"
1240
                              light
1241
                            />
1242
                          ) : null}
1243
                        </div>
1244
                      </FormGroup>
1245
                    </>
1246
                  ) : (
1247
                    <div data-testid={`${testId}-datepicker`}>
1248
                      <div
1249
                        id={`${id}-${iotPrefix}--date-time-picker__datepicker`}
1250
                        className={`${iotPrefix}--date-time-picker__datepicker`}
1251
                      >
1252
                        <DatePicker
1253
                          datePickerType={datePickerType}
1254
                          dateFormat="m/d/Y"
1255
                          ref={handleDatePickerRef}
1256
                          onChange={
1257
                            datePickerType === 'single'
11,468✔
1258
                              ? onSingleDatePickerChange
1259
                              : onDatePickerChange
1260
                          }
1261
                          onClose={onDatePickerClose}
1262
                          value={
1263
                            absoluteValue && datePickerType === 'range'
33,584✔
1264
                              ? [absoluteValue.startDate, absoluteValue.endDate]
1265
                              : singleDateValue && datePickerType === 'single'
2,460✔
1266
                              ? [singleDateValue.startDate]
1267
                              : null
1268
                          }
1269
                          locale={locale}
1270
                        >
1271
                          <DatePickerInput
1272
                            labelText=""
1273
                            id={`${id}-date-picker-input-start`}
1274
                            hideLabel
1275
                          />
1276

1277
                          {datePickerType === 'range' ? (
11,468✔
1278
                            <DatePickerInput
1279
                              labelText=""
1280
                              id={`${id}-date-picker-input-end`}
1281
                              hideLabel
1282
                            />
1283
                          ) : null}
1284
                        </DatePicker>
1285
                      </div>
1286
                      {hasTimeInput ? (
11,468✔
1287
                        <TimePickerDropdown
1288
                          className={`${iotPrefix}--time-picker-dropdown`}
1289
                          id={id}
1290
                          key={defaultSingleDateValue}
1291
                          value={
1292
                            isSingleSelect
11,158✔
1293
                              ? getLocalizedTimeValue(singleTimeValue)
1294
                              : getLocalizedTimeValue(rangeStartTimeValue)
1295
                          }
1296
                          secondaryValue={getLocalizedTimeValue(rangeEndTimeValue)}
1297
                          hideLabel={!mergedI18n.startTimeLabel}
1298
                          hideSecondaryLabel={!mergedI18n.endTimeLabel}
1299
                          onChange={(startState, endState) =>
1300
                            isSingleSelect
8,314✔
1301
                              ? handleSingleTimeValueChange(startState)
1302
                              : handleRangeTimeValueChange(startState, endState)
1303
                          }
1304
                          type={isSingleSelect ? 'single' : 'range'}
11,158✔
1305
                          invalid={[invalidRangeStartTime, invalidRangeEndTime]}
1306
                          i18n={{
1307
                            labelText: mergedI18n.startTimeLabel,
1308
                            secondaryLabelText: mergedI18n.endTimeLabel,
1309
                            invalidText: mergedI18n.timePickerInvalidText,
1310
                            amString: mergedI18n.amString,
1311
                            pmString: mergedI18n.pmString,
1312
                          }}
1313
                          size="sm"
1314
                          testId={testId}
1315
                          style={{ zIndex: (style.zIndex ?? 0) + 6000 }}
17,063✔
1316
                          is24hours={is24hours}
1317
                        />
1318
                      ) : (
1319
                        <div className={`${iotPrefix}--date-time-picker__no-formgroup`} />
1320
                      )}
1321
                    </div>
1322
                  )}
1323
                </div>
1324
              )}
1325
            </div>
1326
          </FlyoutMenu>
1327
        </div>
1328
      </div>
1329
      {invalidState && !hasIconOnly ? (
26,843✔
1330
        <p
1331
          className={classnames(
1332
            `${prefix}--form__helper-text`,
1333
            `${iotPrefix}--date-time-picker__helper-text--invalid`
1334
          )}
1335
        >
1336
          {mergedI18n.invalidText}
1337
        </p>
1338
      ) : null}
1339
    </div>
1340
  );
1341
};
1342

1343
DateTimePicker.propTypes = propTypes;
47✔
1344
DateTimePicker.defaultProps = defaultProps;
47✔
1345

1346
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