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

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

pending completion
4821361778

push

github

carbon-bot
v2.153.0-next.15

7749 of 8086 branches covered (95.83%)

Branch coverage included in aggregate %.

9344 of 9448 relevant lines covered (98.9%)

2608.24 hits per line

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

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

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

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

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

57
export const DateTimePickerDefaultValuePropTypes = PropTypes.oneOfType([
49✔
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 = {
49✔
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
  /** call back function for clear values in single select */
150
  onClear: PropTypes.func,
151
  /** All the labels that need translation */
152
  i18n: PropTypes.shape({
153
    toLabel: PropTypes.string,
154
    toNowLabel: PropTypes.string,
155
    calendarLabel: PropTypes.string,
156
    presetLabels: PropTypes.arrayOf(PropTypes.string),
157
    intervalLabels: PropTypes.arrayOf(PropTypes.string),
158
    relativeLabels: PropTypes.arrayOf(PropTypes.string),
159
    customRangeLinkLabel: PropTypes.string,
160
    customRangeLabel: PropTypes.string,
161
    relativeLabel: PropTypes.string,
162
    lastLabel: PropTypes.string,
163
    invalidNumberLabel: PropTypes.string,
164
    relativeToLabel: PropTypes.string,
165
    absoluteLabel: PropTypes.string,
166
    startTimeLabel: PropTypes.string,
167
    endTimeLabel: PropTypes.string,
168
    applyBtnLabel: PropTypes.string,
169
    cancelBtnLabel: PropTypes.string,
170
    backBtnLabel: PropTypes.string,
171
    resetBtnLabel: PropTypes.string,
172
    increment: PropTypes.string,
173
    decrement: PropTypes.string,
174
    hours: PropTypes.string,
175
    minutes: PropTypes.string,
176
    number: PropTypes.string,
177
    timePickerInvalidText: PropTypes.string,
178
    invalidText: PropTypes.string,
179
    amString: PropTypes.string,
180
    pmString: PropTypes.string,
181
  }),
182
  /** Light version  */
183
  light: PropTypes.bool,
184
  /** The language locale used to format the days of the week, months, and numbers. */
185
  locale: PropTypes.string,
186
  /** Unique id of the component */
187
  id: PropTypes.string,
188
  /** Optionally renders only an icon rather than displaying the current selected time */
189
  hasIconOnly: PropTypes.bool,
190
  /** Allow repositioning the flyout menu */
191
  menuOffset: PropTypes.shape({
192
    left: PropTypes.number,
193
    top: PropTypes.number,
194
    inputTop: PropTypes.number,
195
    inputBottom: PropTypes.number,
196
  }),
197
  /** Date picker types are single and range, default is range */
198
  datePickerType: PropTypes.string,
199
  /** If set to true it will render outside of the current DOM in a portal, otherwise render as a child */
200
  renderInPortal: PropTypes.bool,
201
  /** Auto reposition if flyout menu offscreen */
202
  useAutoPositioning: PropTypes.bool,
203
  style: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
204
};
205

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

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

337
  const langDir = useLangDirection();
17,031✔
338
  const mergedI18n = useMemo(
17,031✔
339
    () => ({
533✔
340
      ...defaultProps.i18n,
341
      ...i18n,
342
    }),
343
    [i18n]
344
  );
345

346
  const is24hours = useMemo(() => {
17,031✔
347
    const [, time] = dateTimeMask.split(' ');
533✔
348
    const hoursMask = time?.split(':')[0];
533✔
349
    return hoursMask ? hoursMask.includes('H') : false;
533!
350
  }, [dateTimeMask]);
351
  const isSingleSelect = useMemo(() => datePickerType === 'single', [datePickerType]);
17,031✔
352

353
  // initialize the dayjs locale
354
  useEffect(() => {
17,031✔
355
    dayjs.locale(locale);
533✔
356
  }, [locale]);
357

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

389
  const {
390
    relativeValue,
391
    setRelativeValue,
392
    relativeToTimeInvalid,
393
    resetRelativeValue,
394
    relativeLastNumberInvalid,
395
    onRelativeLastNumberChange,
396
    onRelativeLastIntervalChange,
397
    onRelativeToWhenChange,
398
    onRelativeToTimeChange,
399
  } = useRelativeDateTimeValue({
17,031✔
400
    defaultInterval: intervals[0].value,
401
    defaultRelativeTo: relatives[0].value,
402
  });
403

404
  const {
405
    isExpanded,
406
    setIsExpanded,
407
    presetListRef,
408
    onFieldInteraction,
409
    onNavigateRadioButton,
410
    onNavigatePresets,
411
    onFieldClick,
412
  } = useDateTimePickerKeyboardInteraction({ expanded, setCustomRangeKind });
17,031✔
413
  const [isTooltipOpen, toggleTooltip, setIsTooltipOpen] = useDateTimePickerTooltip({ isExpanded });
17,031✔
414

415
  const [singleDateValue, setSingleDateValue] = useState(null);
17,031✔
416
  const [singleTimeValue, setSingleTimeValue] = useState(null);
17,031✔
417
  const [rangeStartTimeValue, setRangeStartTimeValue] = useState(null);
17,031✔
418
  const [rangeEndTimeValue, setRangeEndTimeValue] = useState(null);
17,031✔
419
  const [invalidRangeStartTime, setInvalidRangeStartTime] = useState(false);
17,031✔
420
  const [invalidRangeEndTime, setInvalidRangeEndTime] = useState(false);
17,031✔
421
  const [invalidRangeStartDate, setInvalidRangeStartDate] = useState(false);
17,031✔
422

423
  const dateTimePickerBaseValue = {
17,031✔
424
    kind: '',
425
    preset: {
426
      id: presets[0].id,
427
      label: presets[0].label,
428
      offset: presets[0].offset,
429
    },
430
    relative: {
431
      lastNumber: null,
432
      lastInterval: intervals[0].value,
433
      relativeToWhen: relatives[0].value,
434
      relativeToTime: null,
435
    },
436
    absolute: {
437
      startDate: null,
438
      startTime: null,
439
      endDate: null,
440
      endTime: null,
441
    },
442
    single: {
443
      startDate: null,
444
      startTime: null,
445
    },
446
  };
447

448
  const translatedMeridian = {
17,031✔
449
    AM: mergedI18n.amString,
450
    am: mergedI18n.amString,
451
    PM: mergedI18n.pmString,
452
    pm: mergedI18n.pmString,
453
  };
454

455
  const getLocalizedTimeValue = (timeValue) =>
17,031✔
456
    !is24hours && timeValue
34,457✔
457
      ? timeValue?.replace(/am|AM|pm|PM/g, (matched) => translatedMeridian[matched])
10,417✔
458
      : timeValue;
459

460
  const getTranslatedTimeValue = (timeValue) => {
17,031✔
461
    if (!timeValue) {
21,628✔
462
      return timeValue;
1,124✔
463
    }
464
    const localizedMeridian = {
20,504✔
465
      [mergedI18n.amString]: 'AM',
466
      [mergedI18n.pmString]: 'PM',
467
    };
468
    const time = timeValue.split(' ')[0];
20,504✔
469
    const meridian = localizedMeridian[timeValue.split(' ')[1]];
20,504✔
470

471
    return is24hours ? timeValue : `${time} ${meridian}`;
20,504✔
472
  };
473

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

518
    return {
4,737✔
519
      ...value,
520
      ...parsedValue,
521
    };
522
  };
523

524
  useEffect(
17,031✔
525
    () => {
526
      if (
4,844✔
527
        absoluteValue ||
7,820✔
528
        relativeValue ||
529
        singleDateValue ||
530
        singleTimeValue ||
531
        rangeStartTimeValue ||
532
        rangeEndTimeValue
533
      ) {
534
        renderValue();
4,311✔
535
      }
536
    },
537
    // eslint-disable-next-line react-hooks/exhaustive-deps
538
    [
539
      absoluteValue,
540
      relativeValue,
541
      singleDateValue,
542
      singleTimeValue,
543
      rangeStartTimeValue,
544
      rangeEndTimeValue,
545
    ]
546
  );
547

548
  const onDatePickerChange = ([start, end], _, flatpickr) => {
17,031✔
549
    const calendarInFocus = document?.activeElement?.closest(
286✔
550
      `.${iotPrefix}--date-time-picker__datepicker`
551
    );
552

553
    const daysDidntChange =
554
      start &&
286✔
555
      end &&
556
      dayjs(absoluteValue.start).isSame(dayjs(start)) &&
557
      dayjs(absoluteValue.end).isSame(dayjs(end));
558

559
    if (daysDidntChange || !calendarInFocus) {
286✔
560
      // jump back to start to fix bug where flatpickr will change the month to the start
561
      // after it loses focus if you click outside the calendar
562
      if (focusOnFirstField) {
103!
563
        flatpickr.jumpToDate(start);
×
564
      } else {
565
        flatpickr.jumpToDate(end);
103✔
566
      }
567

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

576
    const newAbsolute = { ...absoluteValue };
183✔
577
    newAbsolute.start = start;
183✔
578
    newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
183✔
579
    const prevFocusOnFirstField = focusOnFirstField;
183✔
580
    if (end) {
183✔
581
      setFocusOnFirstField(!focusOnFirstField);
163✔
582
      newAbsolute.start = start;
163✔
583
      newAbsolute.startDate = dayjs(newAbsolute.start).format('MM/DD/YYYY');
163✔
584
      newAbsolute.end = end;
163✔
585
      newAbsolute.endDate = dayjs(newAbsolute.end).format('MM/DD/YYYY');
163✔
586
      if (prevFocusOnFirstField) {
163✔
587
        flatpickr.jumpToDate(newAbsolute.start, true);
142✔
588
      } else {
589
        flatpickr.jumpToDate(newAbsolute.end, true);
21✔
590
      }
591
    } else {
592
      setFocusOnFirstField(false);
20✔
593
      flatpickr.jumpToDate(newAbsolute.start, true);
20✔
594
    }
595

596
    setAbsoluteValue(newAbsolute);
183✔
597
    setInvalidRangeStartTime(
183✔
598
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
599
    );
600
    setInvalidRangeEndTime(
183✔
601
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
602
    );
603
  };
604

605
  const onSingleDatePickerChange = (start) => {
17,031✔
606
    const newSingleDate = { ...singleDateValue };
×
607
    newSingleDate.start = start;
×
608
    newSingleDate.startDate = dayjs(newSingleDate.start).format('MM/DD/YYYY');
×
609

610
    setSingleDateValue(newSingleDate);
×
611
    setInvalidRangeStartDate(!newSingleDate.startDate);
×
612
  };
613

614
  const onPresetClick = (preset) => {
17,031✔
615
    setSelectedPreset(preset.id ?? preset.offset);
217✔
616
    renderValue(preset);
217✔
617
  };
618

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

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

692
  const toggleIsCustomRange = (event) => {
17,031✔
693
    // stop the event from bubbling
694
    event.stopPropagation();
60✔
695
    setIsCustomRange(!isCustomRange);
60✔
696

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

711
  useEffect(
17,031✔
712
    () => {
713
      /* istanbul ignore else */
714
      if (defaultValue || humanValue === null) {
534✔
715
        parseDefaultValue(defaultValue);
534✔
716
        setLastAppliedValue(defaultValue);
534✔
717
      }
718
    },
719
    // eslint-disable-next-line react-hooks/exhaustive-deps
720
    [defaultValue]
721
  );
722

723
  const tooltipValue = renderPresetTooltipText
17,031✔
724
    ? renderPresetTooltipText(currentValue)
725
    : datePickerType === 'range'
17,028✔
726
    ? getIntervalValue({ currentValue, mergedI18n, dateTimeMask, humanValue })
727
    : isSingleSelect
1,256!
728
    ? humanValue
729
    : dateTimeMask;
730

731
  const disableAbsoluteApply =
732
    isCustomRange &&
17,031✔
733
    customRangeKind === PICKER_KINDS.ABSOLUTE &&
734
    (invalidRangeStartTime ||
735
      invalidRangeEndTime ||
736
      (absoluteValue?.startDate === '' && absoluteValue?.endDate === '') ||
737
      (hasTimeInput ? !rangeStartTimeValue || !rangeEndTimeValue : false));
16,871✔
738

739
  const disableRelativeApply =
740
    isCustomRange &&
17,031✔
741
    customRangeKind === PICKER_KINDS.RELATIVE &&
742
    (relativeLastNumberInvalid || relativeToTimeInvalid);
743

744
  const disableApply = disableRelativeApply || disableAbsoluteApply;
17,031✔
745

746
  useEffect(() => setInvalidState(invalid), [invalid]);
17,031✔
747

748
  const onApplyClick = () => {
17,031✔
749
    const value = renderValue();
209✔
750
    setLastAppliedValue(value);
209✔
751
    const returnValue = {
209✔
752
      timeRangeKind: value.kind,
753
      timeRangeValue: null,
754
      timeSingleValue: null,
755
    };
756

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

776
        setInvalidRangeStartTime(hasTimeInput ? !value.single.startTime : false);
38!
777
        setInvalidRangeStartDate(!value.single.startDate);
38✔
778

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

789
      case PICKER_KINDS.RELATIVE:
790
        returnValue.timeRangeValue = {
1✔
791
          ...value.relative,
792
          humanValue,
793
          tooltipValue,
794
        };
795
        break;
1✔
796
      default:
797
        returnValue.timeRangeValue = {
1✔
798
          ...value.preset,
799
          tooltipValue,
800
        };
801
        break;
1✔
802
    }
803

804
    if (onApply && isValid) {
209✔
805
      setIsExpanded(false);
176✔
806
      onApply(returnValue);
176✔
807
    }
808
  };
809

810
  const onCancelClick = () => {
17,031✔
811
    parseDefaultValue(lastAppliedValue);
2✔
812
    setIsExpanded(false);
2✔
813

814
    /* istanbul ignore else */
815
    if (onCancel) {
2✔
816
      onCancel();
2✔
817
    }
818
  };
819

820
  const onClearClick = () => {
17,031✔
821
    setSingleDateValue({ start: null, startDate: null });
43✔
822
    setSingleTimeValue(null);
43✔
823
    setDefaultTimeValueUpdate(!defaultTimeValueUpdate);
43✔
824
    setInvalidRangeStartDate(false);
43✔
825
    setIsExpanded(false);
43✔
826
    const returnValue = {
43✔
827
      timeRangeKind: PICKER_KINDS.SINGLE,
828
      timeRangeValue: null,
829
      timeSingleValue: null,
830
    };
831

832
    returnValue.timeSingleValue = {
43✔
833
      ISOStart: null,
834
      humanValue: dateTimeMask,
835
      start: null,
836
      startDate: null,
837
      startTime: null,
838
      tooltipValue: dateTimeMask,
839
    };
840

841
    onClear(returnValue);
43✔
842
  };
843

844
  // Close tooltip if dropdown was closed by click outside
845
  const onFieldBlur = (evt) => {
17,031✔
846
    if (evt.target !== evt.currentTarget) {
1,497✔
847
      setIsTooltipOpen(false);
1,261✔
848
    }
849
  };
850

851
  const closeDropdown = useCloseDropdown({
17,031✔
852
    isExpanded,
853
    isCustomRange,
854
    setIsCustomRange,
855
    setIsExpanded,
856
    parseDefaultValue,
857
    defaultValue,
858
    setCustomRangeKind,
859
    lastAppliedValue,
860
    singleTimeValue,
861
    setSingleDateValue,
862
    setSingleTimeValue,
863
  });
864

865
  const onClickOutside = useDateTimePickerClickOutside(closeDropdown, containerRef);
17,031✔
866

867
  useOnClickOutside(dropdownRef, onClickOutside);
17,031✔
868

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

924
  const handleRangeTimeValueChange = (startState, endState) => {
17,031✔
925
    const translatedStartTimeValue = getTranslatedTimeValue(startState);
10,583✔
926
    const translatedEndTimeValue = getTranslatedTimeValue(endState);
10,583✔
927
    setRangeStartTimeValue(translatedStartTimeValue);
10,583✔
928
    setRangeEndTimeValue(translatedEndTimeValue);
10,583✔
929
    setInvalidRangeStartTime(
10,583✔
930
      (absoluteValue &&
29,107✔
931
        invalidStartDate(translatedStartTimeValue, translatedEndTimeValue, absoluteValue)) ||
932
        (is24hours
7,974✔
933
          ? !isValid24HourTime(translatedStartTimeValue)
934
          : !isValid12HourTime(translatedStartTimeValue))
935
    );
936
    setInvalidRangeEndTime(
10,583✔
937
      (absoluteValue &&
27,589✔
938
        invalidEndDate(translatedStartTimeValue, translatedEndTimeValue, absoluteValue)) ||
939
        (is24hours
6,456✔
940
          ? !isValid24HourTime(translatedEndTimeValue)
941
          : !isValid12HourTime(translatedEndTimeValue))
942
    );
943
  };
944

945
  const handleSingleTimeValueChange = (startState) => {
17,031✔
946
    const translatedTimeValue = getTranslatedTimeValue(startState);
462✔
947
    setSingleTimeValue(translatedTimeValue);
462✔
948
    setInvalidRangeStartTime(
462✔
949
      is24hours ? !isValid24HourTime(translatedTimeValue) : !isValid12HourTime(translatedTimeValue)
462✔
950
    );
951
  };
952

953
  const menuOffsetLeft = menuOffset?.left
17,031!
954
    ? menuOffset.left
955
    : langDir === 'ltr'
17,031!
956
    ? 0
957
    : hasIconOnly
×
958
    ? -15
959
    : 288;
960

961
  const menuOffsetTop = menuOffset?.top ? menuOffset.top : 0;
17,031!
962

963
  const [
964
    offTop,
965
    ,
966
    inputTop,
967
    inputBottom,
968
    customHeight,
969
    maxHeight,
970
    invalidDateWarningHeight,
971
    invalidTimeWarningHeight,
972
    timeInputHeight,
973
  ] = useCustomHeight({
17,031✔
974
    containerRef,
975
    isSingleSelect,
976
    isCustomRange,
977
    showRelativeOption,
978
    customRangeKind,
979
    setIsExpanded,
980
  });
981

982
  const direction = useAutoPositioning
17,031✔
983
    ? offTop
70✔
984
      ? FlyoutMenuDirection.BottomEnd
985
      : FlyoutMenuDirection.TopEnd
986
    : FlyoutMenuDirection.BottomEnd;
987

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

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

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

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

1404
DateTimePicker.propTypes = propTypes;
49✔
1405
DateTimePicker.defaultProps = defaultProps;
49✔
1406

1407
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