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

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

pending completion
3950210882

push

github

carbon-bot
v2.152.0-next.20

7531 of 7855 branches covered (95.88%)

Branch coverage included in aggregate %.

9069 of 9168 relevant lines covered (98.92%)

2087.91 hits per line

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

94.7
/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
} from './dateTimePickerUtils';
53

54
const { iotPrefix, prefix } = settings;
47✔
55

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

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

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

285
const DateTimePicker = ({
47✔
286
  testId,
287
  defaultValue,
288
  dateTimeMask,
289
  presets,
290
  intervals,
291
  relatives,
292
  expanded,
293
  disabled,
294
  invalid,
295
  showRelativeOption,
296
  showCustomRangeLink,
297
  hasTimeInput,
298
  renderPresetTooltipText,
299
  onCancel,
300
  onApply,
301
  i18n,
302
  light,
303
  locale,
304
  id = uuidv4(),
578✔
305
  hasIconOnly,
306
  menuOffset,
307
  datePickerType,
308
  renderInPortal,
309
  useAutoPositioning,
310
  style,
311
  ...others
312
}) => {
313
  React.useEffect(() => {
18,550✔
314
    if (__DEV__) {
452✔
315
      warning(
394✔
316
        false,
317
        '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'
318
      );
319
    }
320
  }, []);
321

322
  const langDir = useLangDirection();
18,550✔
323
  const mergedI18n = useMemo(
18,550✔
324
    () => ({
452✔
325
      ...defaultProps.i18n,
326
      ...i18n,
327
    }),
328
    [i18n]
329
  );
330

331
  const is24hours = useMemo(() => {
18,550✔
332
    const [, time] = dateTimeMask.split(' ');
452✔
333
    const hoursMask = time?.split(':')[0];
452✔
334
    return hoursMask === 'HH';
452✔
335
  }, [dateTimeMask]);
336
  const isSingleSelect = useMemo(() => datePickerType === 'single', [datePickerType]);
18,550✔
337

338
  // initialize the dayjs locale
339
  useEffect(() => {
18,550✔
340
    dayjs.locale(locale);
452✔
341
  }, [locale]);
342

343
  // State
344
  const [customRangeKind, setCustomRangeKind, onCustomRangeChange] = useDateTimePickerRangeKind(
18,550✔
345
    showRelativeOption
346
  );
347
  const [isCustomRange, setIsCustomRange] = useState(false);
18,550✔
348
  const [selectedPreset, setSelectedPreset] = useState(null);
18,550✔
349
  const [currentValue, setCurrentValue] = useState(null);
18,550✔
350
  const [lastAppliedValue, setLastAppliedValue] = useState(null);
18,550✔
351
  const [humanValue, setHumanValue] = useState(null);
18,550✔
352
  const [defaultSingleDateValue, SetDefaultSingleDateValue] = useState(false);
18,550✔
353
  const [invalidState, setInvalidState] = useState(invalid);
18,550✔
354
  const [top, setTop] = useState(0);
18,550✔
355
  const [left, setLeft] = useState(0);
18,550✔
356
  const [datePickerElem, handleDatePickerRef] = useDateTimePickerRef({ id, v2: true });
18,550✔
357
  const [focusOnFirstField, setFocusOnFirstField] = useDateTimePickerFocus(datePickerElem);
18,550✔
358
  const relativeSelect = useRef(null);
18,550✔
359
  const containerRef = useRef();
18,550✔
360
  const dropdownRef = useRef();
18,550✔
361
  const updatedStyle = useMemo(
18,550✔
362
    () => ({
3,010✔
363
      ...style,
364
      '--zIndex': style.zIndex ?? 0,
5,917✔
365
      scrollTop: top,
366
      scrollLeft: left,
367
    }),
368
    [style, top, left]
369
  );
370
  const {
371
    absoluteValue,
372
    setAbsoluteValue,
373
    resetAbsoluteValue,
374
    isValid12HourTime,
375
    isValid24HourTime,
376
  } = useAbsoluteDateTimeValue();
18,550✔
377

378
  const {
379
    relativeValue,
380
    setRelativeValue,
381
    relativeToTimeInvalid,
382
    resetRelativeValue,
383
    relativeLastNumberInvalid,
384
    onRelativeLastNumberChange,
385
    onRelativeLastIntervalChange,
386
    onRelativeToWhenChange,
387
    onRelativeToTimeChange,
388
  } = useRelativeDateTimeValue({
18,550✔
389
    defaultInterval: intervals[0].value,
390
    defaultRelativeTo: relatives[0].value,
391
  });
392

393
  const {
394
    isExpanded,
395
    setIsExpanded,
396
    presetListRef,
397
    onFieldInteraction,
398
    onNavigateRadioButton,
399
    onNavigatePresets,
400
  } = useDateTimePickerKeyboardInteraction({ expanded, setCustomRangeKind });
18,550✔
401
  const [isTooltipOpen, toggleTooltip, setIsTooltipOpen] = useDateTimePickerTooltip({ isExpanded });
18,550✔
402

403
  const [singleDateValue, setSingleDateValue] = useState(null);
18,550✔
404
  const [singleTimeValue, setSingleTimeValue] = useState(null);
18,550✔
405
  const [rangeStartTimeValue, setRangeStartTimeValue] = useState(null);
18,550✔
406
  const [rangeEndTimeValue, setRangeEndTimeValue] = useState(null);
18,550✔
407
  const [invalidRangeStartTime, setInvalidRangeStartTime] = useState(false);
18,550✔
408
  const [invalidRangeEndTime, setInvalidRangeEndTime] = useState(false);
18,550✔
409

410
  const dateTimePickerBaseValue = {
18,550✔
411
    kind: '',
412
    preset: {
413
      id: presets[0].id,
414
      label: presets[0].label,
415
      offset: presets[0].offset,
416
    },
417
    relative: {
418
      lastNumber: null,
419
      lastInterval: intervals[0].value,
420
      relativeToWhen: relatives[0].value,
421
      relativeToTime: null,
422
    },
423
    absolute: {
424
      startDate: null,
425
      startTime: null,
426
      endDate: null,
427
      endTime: null,
428
    },
429
    single: {
430
      startDate: null,
431
      startTime: null,
432
    },
433
  };
434
  /**
435
   * Transforms a default or selected value into a full blown returnable object
436
   * @param {Object} [preset] clicked preset
437
   * @param {string} preset.label preset label
438
   * @param {number} preset.offset preset offset in minutes
439
   * @returns {Object} the augmented value itself and the human readable value
440
   */
441
  const renderValue = (clickedPreset = null) => {
18,550✔
442
    const value = { ...dateTimePickerBaseValue };
3,882✔
443
    if (isCustomRange) {
3,882✔
444
      if (customRangeKind === PICKER_KINDS.RELATIVE) {
3,412✔
445
        value.relative = relativeValue;
59✔
446
      } else if (customRangeKind === PICKER_KINDS.ABSOLUTE) {
3,353✔
447
        value.absolute = {
3,139✔
448
          ...absoluteValue,
449
          startTime: hasTimeInput ? rangeStartTimeValue : null,
3,139✔
450
          endTime: hasTimeInput ? rangeEndTimeValue : null,
3,139✔
451
        };
452
      } else {
453
        value.single = {
214✔
454
          ...singleDateValue,
455
          startTime: hasTimeInput && singleTimeValue !== '' ? singleTimeValue : null,
642✔
456
        };
457
      }
458
      value.kind = customRangeKind;
3,412✔
459
    } else {
460
      const preset = presets
470✔
461
        .filter((p) => {
462
          let filteredPreset;
463
          if (p.id) {
2,350✔
464
            filteredPreset = p.id === (clickedPreset ? clickedPreset.id : selectedPreset);
2,325✔
465
          } else {
466
            filteredPreset = p.offset === (clickedPreset ? clickedPreset.offset : selectedPreset);
25✔
467
          }
468
          return filteredPreset;
2,350✔
469
        })
470
        .pop();
471
      value.preset = preset;
470✔
472
      value.kind = PICKER_KINDS.PRESET;
470✔
473
    }
474
    setCurrentValue(value);
3,882✔
475
    const parsedValue = parseValue(value, dateTimeMask, mergedI18n.toLabel);
3,882✔
476
    setHumanValue(parsedValue.readableValue);
3,882✔
477

478
    return {
3,882✔
479
      ...value,
480
      ...parsedValue,
481
    };
482
  };
483

484
  useEffect(
18,550✔
485
    () => {
486
      if (
3,982✔
487
        absoluteValue ||
6,451✔
488
        relativeValue ||
489
        singleDateValue ||
490
        singleTimeValue ||
491
        rangeStartTimeValue ||
492
        rangeEndTimeValue
493
      ) {
494
        renderValue();
3,530✔
495
      }
496
    },
497
    // eslint-disable-next-line react-hooks/exhaustive-deps
498
    [
499
      absoluteValue,
500
      relativeValue,
501
      singleDateValue,
502
      singleTimeValue,
503
      rangeStartTimeValue,
504
      rangeEndTimeValue,
505
    ]
506
  );
507

508
  const onDatePickerChange = ([start, end], _, flatpickr) => {
18,550✔
509
    const calendarInFocus = document?.activeElement?.closest(
242✔
510
      `.${iotPrefix}--date-time-picker__datepicker`
511
    );
512

513
    const daysDidntChange =
514
      start &&
242✔
515
      end &&
516
      dayjs(absoluteValue.start).isSame(dayjs(start)) &&
517
      dayjs(absoluteValue.end).isSame(dayjs(end));
518

519
    if (daysDidntChange || !calendarInFocus) {
242✔
520
      // jump back to start to fix bug where flatpickr will change the month to the start
521
      // after it loses focus if you click outside the calendar
522
      if (focusOnFirstField) {
100!
523
        flatpickr.jumpToDate(start);
×
524
      } else {
525
        flatpickr.jumpToDate(end);
100✔
526
      }
527

528
      // In some situations, when the calendar loses focus flatpickr is firing the onChange event
529
      // again, but the dates reset to where both start and end are the same. This fixes that.
530
      if (!calendarInFocus && dayjs(start).isSame(dayjs(end))) {
100!
531
        flatpickr.setDate([absoluteValue.start, absoluteValue.end]);
×
532
      }
533
      return;
100✔
534
    }
535

536
    const newAbsolute = { ...absoluteValue };
142✔
537
    newAbsolute.start = start;
142✔
538
    newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
142✔
539
    const prevFocusOnFirstField = focusOnFirstField;
142✔
540
    if (end) {
142✔
541
      setFocusOnFirstField(!focusOnFirstField);
122✔
542
      newAbsolute.start = start;
122✔
543
      newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
122✔
544
      newAbsolute.end = end;
122✔
545
      newAbsolute.endDate = dayjs(newAbsolute.end).format('MM/DD/YYYY');
122✔
546
      if (prevFocusOnFirstField) {
122✔
547
        flatpickr.jumpToDate(newAbsolute.start, true);
101✔
548
      } else {
549
        flatpickr.jumpToDate(newAbsolute.end, true);
21✔
550
      }
551
    } else {
552
      setFocusOnFirstField(false);
20✔
553
      flatpickr.jumpToDate(newAbsolute.start, true);
20✔
554
    }
555

556
    setAbsoluteValue(newAbsolute);
142✔
557
    setInvalidRangeStartTime(
142✔
558
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
559
    );
560
    setInvalidRangeEndTime(
142✔
561
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
562
    );
563
  };
564

565
  const onSingleDatePickerChange = (start) => {
18,550✔
566
    const newSingleDate = { ...singleDateValue };
×
567
    newSingleDate.start = start;
×
568
    newSingleDate.startDate = dayjs(newSingleDate.start).format('MM/DD/YYYY');
×
569

570
    setSingleDateValue(newSingleDate);
×
571
  };
572

573
  const onPresetClick = (preset) => {
18,550✔
574
    setSelectedPreset(preset.id ?? preset.offset);
217✔
575
    renderValue(preset);
217✔
576
  };
577

578
  const parseDefaultValue = (parsableValue) => {
18,550✔
579
    const currentCustomRangeKind = showRelativeOption
546✔
580
      ? PICKER_KINDS.RELATIVE
581
      : datePickerType === 'range'
18✔
582
      ? PICKER_KINDS.ABSOLUTE
583
      : PICKER_KINDS.SINGLE;
584
    if (parsableValue !== null) {
546✔
585
      if (parsableValue.timeRangeKind === PICKER_KINDS.PRESET) {
358✔
586
        // preset
587
        resetAbsoluteValue();
5✔
588
        resetRelativeValue();
5✔
589
        setCustomRangeKind(currentCustomRangeKind);
5✔
590
        onPresetClick(parsableValue.timeRangeValue);
5✔
591
      }
592
      if (parsableValue.timeRangeKind === PICKER_KINDS.RELATIVE) {
358✔
593
        // relative
594
        resetAbsoluteValue();
6✔
595
        setIsCustomRange(true);
6✔
596
        setCustomRangeKind(currentCustomRangeKind);
6✔
597
        setRelativeValue(parsableValue.timeRangeValue);
6✔
598
      }
599
      if (parsableValue.timeRangeKind === PICKER_KINDS.ABSOLUTE) {
358✔
600
        // absolute
601
        // range
602
        const absolute = { ...parsableValue.timeRangeValue };
292✔
603
        resetRelativeValue();
292✔
604
        setIsCustomRange(true);
292✔
605
        setCustomRangeKind(PICKER_KINDS.ABSOLUTE);
292✔
606
        if (!absolute.hasOwnProperty('start')) {
292✔
607
          absolute.start = dayjs(`${absolute.startDate} ${absolute.startTime}`).valueOf();
41✔
608
        }
609
        if (!absolute.hasOwnProperty('end')) {
292✔
610
          absolute.end = dayjs(`${absolute.endDate} ${absolute.endTime}`).valueOf();
41✔
611
        }
612
        absolute.startDate = dayjs(absolute.start).format('MM/DD/YYYY');
292✔
613
        absolute.startTime = is24hours
292✔
614
          ? dayjs(absolute.start).format('HH:mm')
615
          : dayjs(absolute.start).format('hh:mm A');
616
        absolute.endDate = dayjs(absolute.end).format('MM/DD/YYYY');
292✔
617
        absolute.endTime = is24hours
292✔
618
          ? dayjs(absolute.end).format('HH:mm')
619
          : dayjs(absolute.end).format('hh:mm A');
620
        setAbsoluteValue(absolute);
292✔
621
        setRangeStartTimeValue(absolute.startTime);
292✔
622
        setRangeEndTimeValue(absolute.endTime);
292✔
623
      }
624

625
      if (parsableValue.timeRangeKind === PICKER_KINDS.SINGLE) {
358✔
626
        // single
627
        const single = { ...parsableValue.timeSingleValue };
55✔
628
        resetRelativeValue();
55✔
629
        setIsCustomRange(true);
55✔
630
        setCustomRangeKind(PICKER_KINDS.SINGLE);
55✔
631
        if (!single.hasOwnProperty('start') && single.startDate && single.startTime) {
55✔
632
          single.start = dayjs(`${single.startDate} ${single.startTime}`).valueOf();
49✔
633
        }
634
        single.startDate = single.start ? dayjs(single.start).format('MM/DD/YYYY') : null;
55✔
635
        single.startTime = single.start
55✔
636
          ? is24hours
54✔
637
            ? dayjs(single.start).format('HH:mm')
638
            : dayjs(single.start).format('hh:mm A')
639
          : null;
640
        setSingleDateValue(single);
55✔
641
        setSingleTimeValue(single.startTime);
55✔
642
      }
643
    } else {
644
      resetAbsoluteValue();
188✔
645
      resetRelativeValue();
188✔
646
      setCustomRangeKind(currentCustomRangeKind);
188✔
647
      onPresetClick(presets[0]);
188✔
648
    }
649
  };
650

651
  const toggleIsCustomRange = (event) => {
18,550✔
652
    // stop the event from bubbling
653
    event.stopPropagation();
60✔
654
    setIsCustomRange(!isCustomRange);
60✔
655

656
    // If value was changed reset when going back to Preset
657
    if (absoluteValue.startDate !== '' || relativeValue.lastNumber > 0) {
60✔
658
      if (selectedPreset) {
3✔
659
        onPresetClick(presets.filter((p) => p.id ?? p.offset === selectedPreset)[0]);
5!
660
        resetAbsoluteValue();
1✔
661
        resetRelativeValue();
1✔
662
      } else {
663
        onPresetClick(presets[0]);
2✔
664
        resetAbsoluteValue();
2✔
665
        resetRelativeValue();
2✔
666
      }
667
    }
668
  };
669

670
  useEffect(
18,550✔
671
    () => {
672
      /* istanbul ignore else */
673
      if (defaultValue || humanValue === null) {
453✔
674
        parseDefaultValue(defaultValue);
453✔
675
        setLastAppliedValue(defaultValue);
453✔
676
      }
677
    },
678
    // eslint-disable-next-line react-hooks/exhaustive-deps
679
    [defaultValue]
680
  );
681

682
  const tooltipValue = renderPresetTooltipText
18,550✔
683
    ? renderPresetTooltipText(currentValue)
684
    : datePickerType === 'range'
18,547✔
685
    ? getIntervalValue({ currentValue, mergedI18n, dateTimeMask, humanValue })
686
    : isSingleSelect
683!
687
    ? humanValue
688
    : dateTimeMask;
689

690
  const disableRelativeApply =
691
    isCustomRange &&
18,550✔
692
    customRangeKind === PICKER_KINDS.RELATIVE &&
693
    (relativeLastNumberInvalid || relativeToTimeInvalid);
694

695
  const disableAbsoluteApply =
696
    isCustomRange &&
18,550✔
697
    customRangeKind === PICKER_KINDS.ABSOLUTE &&
698
    (invalidRangeStartTime ||
699
      invalidRangeEndTime ||
700
      (absoluteValue?.startDate === '' && absoluteValue?.endDate === '') ||
701
      (hasTimeInput ? !rangeStartTimeValue || !rangeEndTimeValue : false));
26,954✔
702

703
  const disableSingleApply =
704
    isCustomRange &&
18,550✔
705
    customRangeKind === PICKER_KINDS.SINGLE &&
706
    (invalidRangeStartTime ||
707
      (!singleDateValue.start && !singleDateValue.startDate) ||
708
      (hasTimeInput ? !singleTimeValue : false));
507!
709

710
  const disableApply = disableRelativeApply || disableAbsoluteApply || disableSingleApply;
18,550✔
711

712
  useEffect(() => setInvalidState(invalid), [invalid]);
18,550✔
713

714
  const onApplyClick = () => {
18,550✔
715
    setIsExpanded(false);
135✔
716
    const value = renderValue();
135✔
717
    setLastAppliedValue(value);
135✔
718
    const returnValue = {
135✔
719
      timeRangeKind: value.kind,
720
      timeRangeValue: null,
721
      timeSingleValue: null,
722
    };
723
    switch (value.kind) {
135✔
724
      case PICKER_KINDS.ABSOLUTE:
725
        returnValue.timeRangeValue = {
128✔
726
          ...value.absolute,
727
          humanValue,
728
          tooltipValue,
729
        };
730
        break;
128✔
731
      case PICKER_KINDS.SINGLE:
732
        returnValue.timeSingleValue = {
5✔
733
          ...value.single,
734
          humanValue,
735
          tooltipValue,
736
        };
737
        break;
5✔
738
      case PICKER_KINDS.RELATIVE:
739
        returnValue.timeRangeValue = {
1✔
740
          ...value.relative,
741
          humanValue,
742
          tooltipValue,
743
        };
744
        break;
1✔
745
      default:
746
        returnValue.timeRangeValue = {
1✔
747
          ...value.preset,
748
          tooltipValue,
749
        };
750
        break;
1✔
751
    }
752

753
    if (onApply) {
135!
754
      onApply(returnValue);
135✔
755
    }
756
  };
757

758
  const onCancelClick = () => {
18,550✔
759
    parseDefaultValue(lastAppliedValue);
2✔
760
    setIsExpanded(false);
2✔
761

762
    /* istanbul ignore else */
763
    if (onCancel) {
2✔
764
      onCancel();
2✔
765
    }
766
  };
767

768
  const onClearClick = () => {
18,550✔
769
    setSingleDateValue({ start: null, startDate: null });
9✔
770
    setSingleTimeValue(null);
9✔
771
    SetDefaultSingleDateValue(true);
9✔
772
    setIsExpanded(false);
9✔
773
  };
774

775
  // Close tooltip if dropdown was closed by click outside
776
  const onFieldBlur = (evt) => {
18,550✔
777
    if (evt.target !== evt.currentTarget) {
1,322✔
778
      setIsTooltipOpen(false);
1,128✔
779
    }
780
  };
781

782
  const closeDropdown = useCloseDropdown({
18,550✔
783
    isExpanded,
784
    isCustomRange,
785
    setIsCustomRange,
786
    setIsExpanded,
787
    parseDefaultValue,
788
    defaultValue,
789
    setCustomRangeKind,
790
    lastAppliedValue,
791
    singleTimeValue,
792
    setSingleDateValue,
793
    setSingleTimeValue,
794
  });
795

796
  const onClickOutside = useDateTimePickerClickOutside(closeDropdown, containerRef);
18,550✔
797

798
  useOnClickOutside(dropdownRef, onClickOutside);
18,550✔
799

800
  // eslint-disable-next-line react/prop-types
801
  const CustomFooter = () => {
18,550✔
802
    return (
9,572✔
803
      <div className={`${iotPrefix}--date-time-picker__menu-btn-set`}>
804
        {isCustomRange && !isSingleSelect ? (
28,446✔
805
          <Button
806
            kind="secondary"
807
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-back`}
808
            size="field"
809
            {...others}
810
            onClick={toggleIsCustomRange}
811
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], toggleIsCustomRange)}
812
          >
813
            {mergedI18n.backBtnLabel}
814
          </Button>
815
        ) : isSingleSelect ? (
554✔
816
          <Button
817
            kind="secondary"
818
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-reset`}
819
            size="field"
820
            {...others}
821
            onClick={onClearClick}
822
            onMouseDown={(e) => e.preventDefault()}
9✔
823
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], onClearClick)}
824
          >
825
            {mergedI18n.resetBtnLabel}
826
          </Button>
827
        ) : (
828
          <Button
829
            kind="secondary"
830
            className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-cancel`}
831
            onClick={onCancelClick}
832
            size="field"
833
            {...others}
834
            onKeyUp={handleSpecificKeyDown(['Enter', ' '], onCancelClick)}
835
          >
836
            {mergedI18n.cancelBtnLabel}
837
          </Button>
838
        )}
839
        <Button
840
          kind="primary"
841
          className={`${iotPrefix}--date-time-picker__menu-btn ${iotPrefix}--date-time-picker__menu-btn-apply`}
842
          {...others}
843
          onClick={onApplyClick}
844
          onKeyUp={handleSpecificKeyDown(['Enter', ' '], onApplyClick)}
845
          onMouseDown={(e) => e.preventDefault()}
108✔
846
          size="field"
847
          disabled={disableApply}
848
        >
849
          {mergedI18n.applyBtnLabel}
850
        </Button>
851
      </div>
852
    );
853
  };
854

855
  const menuOffsetLeft = menuOffset?.left
18,550!
856
    ? menuOffset.left
857
    : langDir === 'ltr'
18,550!
858
    ? 0
859
    : hasIconOnly
×
860
    ? -15
861
    : 288;
862
  const menuOffsetTop = menuOffset?.top ? menuOffset.top : 0;
18,550!
863

864
  const handleRangeTimeValueChange = (startState, endState) => {
18,550✔
865
    setRangeStartTimeValue(startState);
10,714✔
866
    setRangeEndTimeValue(endState);
10,714✔
867
    setInvalidRangeStartTime(
10,714✔
868
      (absoluteValue && invalidStartDate(startState, endState, absoluteValue)) ||
29,668✔
869
        (is24hours ? !isValid24HourTime(startState) : !isValid12HourTime(startState))
8,273✔
870
    );
871
    setInvalidRangeEndTime(
10,714✔
872
      (absoluteValue && invalidEndDate(startState, endState, absoluteValue)) ||
28,762✔
873
        (is24hours ? !isValid24HourTime(endState) : !isValid12HourTime(endState))
7,367✔
874
    );
875
  };
876

877
  const handleSingleTimeValueChange = (startState) => {
18,550✔
878
    setSingleTimeValue(startState);
330✔
879
    setInvalidRangeStartTime(
330✔
880
      is24hours ? !isValid24HourTime(startState) : !isValid12HourTime(startState)
330✔
881
    );
882
  };
883

884
  const windowHeight = window.innerHeight || document.documentElement.clientHeight;
18,550!
885
  const inputBottom = containerRef.current?.getBoundingClientRect().bottom;
18,550✔
886
  const flyoutMenuHeight = 482;
18,550✔
887
  const offBottom = windowHeight - inputBottom < flyoutMenuHeight;
18,550✔
888

889
  const getPosition = (event) => {
18,550✔
890
    setLeft(event.target.scrollLeft);
1,740✔
891
    setTop(event.target.scrollTop);
1,740✔
892
  };
893

894
  // Re-calculate X and Y when parents scrolled
895
  useEffect(() => {
18,550✔
896
    let currentNode = containerRef.current?.parentNode;
452✔
897
    const parentNodes = [];
452✔
898
    while (currentNode) {
452✔
899
      parentNodes.push(currentNode);
2,068✔
900
      currentNode.addEventListener('scroll', getPosition);
2,068✔
901
      currentNode = currentNode.parentNode;
2,068✔
902
    }
903

904
    return () => {
452✔
905
      parentNodes.map((node) => node.removeEventListener('scroll', getPosition));
1,979✔
906
    };
907
  }, []);
908

909
  return (
18,550✔
910
    <div className={`${iotPrefix}--date-time-pickerv2`} ref={containerRef}>
911
      <div
912
        data-testid={testId}
913
        id={`${id}-${iotPrefix}--date-time-pickerv2__wrapper`}
914
        className={classnames(`${iotPrefix}--date-time-pickerv2__wrapper`, {
915
          [`${iotPrefix}--date-time-pickerv2__wrapper--disabled`]: disabled,
916
          [`${iotPrefix}--date-time-pickerv2__wrapper--invalid`]: invalidState,
917
        })}
918
        style={{ '--wrapper-width': hasIconOnly ? '3rem' : '20rem' }}
18,550✔
919
        role="button"
920
        onClick={onFieldInteraction}
921
        onKeyDown={handleSpecificKeyDown(['Enter', ' ', 'Escape', 'ArrowDown'], (event) => {
922
          // the onApplyClick event gets blocked when called via the keyboard from the flyout menu's
923
          // custom footer. This is a catch to ensure the onApplyCLick is called correctly for preset
924
          // ranges via the keyboard.
925
          if (
79!
926
            (event.key === 'Enter' || event.key === ' ') &&
158!
927
            event.target.classList.contains(`${iotPrefix}--date-time-picker__menu-btn-apply`) &&
928
            !isCustomRange
929
          ) {
930
            onApplyClick();
×
931
          }
932

933
          onFieldInteraction(event);
79✔
934
        })}
935
        onFocus={toggleTooltip}
936
        onBlur={onFieldBlur}
937
        onMouseEnter={toggleTooltip}
938
        onMouseLeave={toggleTooltip}
939
        tabIndex={0}
940
      >
941
        <div
942
          className={classnames({
943
            [`${iotPrefix}--date-time-picker__box--full`]: !hasIconOnly,
944
            [`${iotPrefix}--date-time-picker__box--light`]: light,
945
            [`${iotPrefix}--date-time-picker__box--disabled`]: disabled,
946
            [`${iotPrefix}--date-time-picker__box--invalid`]: invalidState,
947
          })}
948
        >
949
          {!hasIconOnly ? (
18,550✔
950
            <div
951
              data-testid={`${testId}__field`}
952
              className={`${iotPrefix}--date-time-picker__field`}
953
            >
954
              {isExpanded || (currentValue && currentValue.kind !== PICKER_KINDS.PRESET) ? (
42,475✔
955
                <span
956
                  className={classnames({
957
                    [`${iotPrefix}--date-time-picker__disabled`]:
958
                      disabled || (isSingleSelect && !singleDateValue.startDate),
34,389✔
959
                  })}
960
                  title={humanValue}
961
                >
962
                  {humanValue}
963
                </span>
964
              ) : humanValue ? (
1,480✔
965
                <TooltipDefinition
966
                  align="start"
967
                  direction="bottom"
968
                  tooltipText={tooltipValue}
969
                  triggerClassName={disabled ? `${iotPrefix}--date-time-picker__disabled` : ''}
773✔
970
                >
971
                  {humanValue}
972
                </TooltipDefinition>
973
              ) : null}
974
              {!isExpanded && isTooltipOpen && !isSingleSelect ? (
40,398✔
975
                <Tooltip
976
                  open={isTooltipOpen}
977
                  showIcon={false}
978
                  focusTrap={false}
979
                  menuOffset={{ top: 16, left: 16 }}
980
                  triggerClassName={`${iotPrefix}--date-time-picker__tooltip-trigger`}
981
                  className={`${iotPrefix}--date-time-picker__tooltip`}
982
                >
983
                  {tooltipValue}
984
                </Tooltip>
985
              ) : null}
986
            </div>
987
          ) : null}
988

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

1235
                          {datePickerType === 'range' ? (
16,598✔
1236
                            <DatePickerInput
1237
                              labelText=""
1238
                              id={`${id}-date-picker-input-end`}
1239
                              hideLabel
1240
                            />
1241
                          ) : null}
1242
                        </DatePicker>
1243
                      </div>
1244
                      {hasTimeInput ? (
16,598✔
1245
                        <TimePickerDropdown
1246
                          className={`${iotPrefix}--time-picker-dropdown`}
1247
                          id={id}
1248
                          key={defaultSingleDateValue}
1249
                          value={isSingleSelect ? singleTimeValue : rangeStartTimeValue}
16,288✔
1250
                          secondaryValue={rangeEndTimeValue}
1251
                          hideLabel={!mergedI18n.startTimeLabel}
1252
                          hideSecondaryLabel={!mergedI18n.endTimeLabel}
1253
                          onChange={(startState, endState) =>
1254
                            isSingleSelect
11,044✔
1255
                              ? handleSingleTimeValueChange(startState)
1256
                              : handleRangeTimeValueChange(startState, endState)
1257
                          }
1258
                          type={isSingleSelect ? 'single' : 'range'}
16,288✔
1259
                          invalid={[invalidRangeStartTime, invalidRangeEndTime]}
1260
                          i18n={{
1261
                            labelText: mergedI18n.startTimeLabel,
1262
                            secondaryLabelText: mergedI18n.endTimeLabel,
1263
                            invalidText: mergedI18n.timePickerInvalidText,
1264
                          }}
1265
                          size="sm"
1266
                          testId={testId}
1267
                          style={{ zIndex: (style.zIndex ?? 0) + 6000 }}
26,891✔
1268
                          is24hours={is24hours}
1269
                        />
1270
                      ) : (
1271
                        <div className={`${iotPrefix}--date-time-picker__no-formgroup`} />
1272
                      )}
1273
                    </div>
1274
                  )}
1275
                </div>
1276
              )}
1277
            </div>
1278
          </FlyoutMenu>
1279
        </div>
1280
      </div>
1281
      {invalidState && !hasIconOnly ? (
37,103✔
1282
        <p
1283
          className={classnames(
1284
            `${prefix}--form__helper-text`,
1285
            `${iotPrefix}--date-time-picker__helper-text--invalid`
1286
          )}
1287
        >
1288
          {mergedI18n.invalidText}
1289
        </p>
1290
      ) : null}
1291
    </div>
1292
  );
1293
};
1294

1295
DateTimePicker.propTypes = propTypes;
47✔
1296
DateTimePicker.defaultProps = defaultProps;
47✔
1297

1298
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