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

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

11 Oct 2023 04:29PM UTC coverage: 97.443% (-0.06%) from 97.502%
6485381999

push

github

carbon-bot
v2.153.0

7777 of 8122 branches covered (0.0%)

Branch coverage included in aggregate %.

9375 of 9480 relevant lines covered (98.89%)

2510.49 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;
48✔
56

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

100
export const propTypes = {
48✔
101
  testId: PropTypes.string,
102
  /** default value for the picker */
103
  defaultValue: DateTimePickerDefaultValuePropTypes,
104
  /** the dayjs.js format for the human readable interval value */
105
  dateTimeMask: PropTypes.string,
106
  /** a list of options to for the default presets */
107
  presets: PropTypes.arrayOf(
108
    PropTypes.shape({
109
      id: PropTypes.string,
110
      label: PropTypes.string,
111
      offset: PropTypes.number,
112
    })
113
  ),
114
  /** a list of options to put on the 'Last' interval dropdown */
115
  intervals: PropTypes.arrayOf(
116
    PropTypes.shape({
117
      label: PropTypes.string,
118
      value: PropTypes.string,
119
    })
120
  ),
121
  /** a list of options to put on the 'Relative to' dropdown */
122
  relatives: PropTypes.arrayOf(
123
    PropTypes.shape({
124
      label: PropTypes.string,
125
      value: PropTypes.string,
126
    })
127
  ),
128
  /** show the picker in the expanded state */
129
  expanded: PropTypes.bool,
130
  /** 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 = {
48✔
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
    startAriaLabel: 'Date time start',
273
    endAriaLabel: 'Date time end',
274
    endTimeLabel: 'End time',
275
    applyBtnLabel: 'Apply',
276
    cancelBtnLabel: 'Cancel',
277
    backBtnLabel: 'Back',
278
    resetBtnLabel: 'Clear',
279
    increment: 'Increment',
280
    decrement: 'Decrement',
281
    hours: 'hours',
282
    minutes: 'minutes',
283
    number: 'number',
284
    timePickerInvalidText: undefined,
285
    invalidText: 'Time is required',
286
    invalidDateText: 'Date is required',
287
    amString: 'AM',
288
    pmString: 'PM',
289
  },
290
  light: false,
291
  locale: 'en',
292
  id: undefined,
293
  hasIconOnly: false,
294
  menuOffset: undefined,
295
  datePickerType: 'range',
296
  renderInPortal: true,
297
  useAutoPositioning: false,
298
  style: {},
299
};
300

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

339
  const langDir = useLangDirection();
14,235✔
340
  const mergedI18n = useMemo(
14,235✔
341
    () => ({
492✔
342
      ...defaultProps.i18n,
343
      ...i18n,
344
    }),
345
    [i18n]
346
  );
347

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

355
  // initialize the dayjs locale
356
  useEffect(() => {
14,235✔
357
    dayjs.locale(locale);
492✔
358
  }, [locale]);
359

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

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

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

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

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

450
  const translatedMeridian = {
14,235✔
451
    AM: mergedI18n.amString,
452
    am: mergedI18n.amString,
453
    PM: mergedI18n.pmString,
454
    pm: mergedI18n.pmString,
455
  };
456

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

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

473
    return is24hours ? timeValue : `${time} ${meridian}`;
15,927✔
474
  };
475

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

520
    return {
3,920✔
521
      ...value,
522
      ...parsedValue,
523
    };
524
  };
525

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

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

555
    const daysDidntChange =
556
      start &&
246✔
557
      end &&
558
      dayjs(absoluteValue.start).isSame(dayjs(start)) &&
559
      dayjs(absoluteValue.end).isSame(dayjs(end));
560

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

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

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

598
    setAbsoluteValue(newAbsolute);
144✔
599
    setInvalidRangeStartTime(
144✔
600
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
601
    );
602
    setInvalidRangeEndTime(
144✔
603
      invalidStartDate(newAbsolute.startTime, newAbsolute.endTime, newAbsolute)
604
    );
605
  };
606

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

612
    setSingleDateValue(newSingleDate);
×
613
    setInvalidRangeStartDate(!newSingleDate.startDate);
×
614
  };
615

616
  const onPresetClick = (preset) => {
14,235✔
617
    setSelectedPreset(preset.id ?? preset.offset);
217✔
618
    renderValue(preset);
217✔
619
  };
620

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

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

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

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

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

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

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

741
  const disableRelativeApply =
742
    isCustomRange &&
14,235✔
743
    customRangeKind === PICKER_KINDS.RELATIVE &&
744
    (relativeLastNumberInvalid || relativeToTimeInvalid);
745

746
  const disableApply = disableRelativeApply || disableAbsoluteApply;
14,235✔
747

748
  useEffect(() => setInvalidState(invalid), [invalid]);
14,235✔
749

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

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

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

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

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

806
    if (onApply && isValid) {
170✔
807
      setIsExpanded(false);
137✔
808
      onApply(returnValue);
137✔
809
    }
810
  };
811

812
  const onCancelClick = () => {
14,235✔
813
    parseDefaultValue(lastAppliedValue);
2✔
814
    setIsExpanded(false);
2✔
815

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

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

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

843
    onClear(returnValue);
43✔
844
  };
845

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

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

867
  const onClickOutside = useDateTimePickerClickOutside(closeDropdown, containerRef);
14,235✔
868

869
  useOnClickOutside(dropdownRef, onClickOutside);
14,235✔
870

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

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

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

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

963
  const menuOffsetTop = menuOffset?.top ? menuOffset.top : 0;
14,235!
964

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

984
  const direction = useAutoPositioning
14,235✔
985
    ? offTop
70✔
986
      ? FlyoutMenuDirection.BottomEnd
987
      : FlyoutMenuDirection.TopEnd
988
    : FlyoutMenuDirection.BottomEnd;
989

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

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

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

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

1408
DateTimePicker.propTypes = propTypes;
48✔
1409
DateTimePicker.defaultProps = defaultProps;
48✔
1410

1411
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