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

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

04 Sep 2025 10:12AM UTC coverage: 97.401%. Remained the same
17460591691

push

github

carbon-bot
v2.155.12

7839 of 8192 branches covered (95.69%)

Branch coverage included in aggregate %.

9513 of 9623 relevant lines covered (98.86%)

2469.37 hits per line

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

97.37
/packages/react/src/components/DateTimePicker/dateTimePickerUtils.js
1
import { cloneDeep, debounce } from 'lodash-es';
2
import { useCallback, useEffect, useRef, useState } from 'react';
3
import customParseFormat from 'dayjs/plugin/customParseFormat';
4

5
import { settings } from '../../constants/Settings';
6
import { PICKER_KINDS, INTERVAL_VALUES, RELATIVE_VALUES } from '../../constants/DateConstants';
7
import dayjs from '../../utils/dayjs';
8

9
const { iotPrefix } = settings;
141✔
10

11
/** check if current time is 24 hours
12
 *
13
 * @param {string} dateTimeMask like YYYY-MM-DD HH:MM
14
 * @returns true or false
15
 */
16
const is24hours = (dateTimeMask) => {
141✔
17
  const [, time] = dateTimeMask.split(' ');
14,098✔
18
  const hoursMask = time?.split(':')[0];
14,098✔
19
  return hoursMask ? hoursMask.includes('H') : false;
14,098!
20
};
21

22
/** convert time from 12 hours to 24 hours, if time12hour is 24 hours format, return immediately
23
 * @param {Object} object hh:mm A time oject
24
 * @returns HH:mm time object
25
 */
26
export const format12hourTo24hour = (time12hour) => {
141✔
27
  if (time12hour === '' || !time12hour) {
49,867✔
28
    return '00:00';
2,638✔
29
  }
30
  const [time, modifier] = time12hour.split(' ');
47,229✔
31

32
  if (!modifier) {
47,229✔
33
    return time12hour;
36,582✔
34
  }
35

36
  // eslint-disable-next-line prefer-const
37
  let [hours, minutes] = time.split(':');
10,647✔
38
  if (hours === '12') {
10,647✔
39
    hours = '00';
2,047✔
40
  }
41
  if (modifier === 'PM') {
10,647✔
42
    hours = parseInt(hours, 10) + 12;
5,681✔
43
  }
44
  return `${hours}:${minutes}`;
10,647✔
45
};
46

47
/**
48
 * Parses a value object into a human readable value
49
 * @param {Object} value - the currently selected value
50
 * @param {string} value.kind - preset/relative/absolute
51
 * @param {Object} value.preset - the preset selection
52
 * @param {Object} value - the relative time selection
53
 * @param {Object} value - the absolute time selection
54
 * @returns {Object} a human readable value and a furtherly augmented value object
55
 */
56
export const parseValue = (timeRange, dateTimeMask, toLabel, hasTimeInput) => {
141✔
57
  let readableValue = '';
11,478✔
58

59
  if (!timeRange) {
11,478✔
60
    return { readableValue };
22✔
61
  }
62

63
  const kind = timeRange.kind ?? timeRange.timeRangeKind;
11,456✔
64
  const value =
65
    kind === PICKER_KINDS.RELATIVE
11,456✔
66
      ? timeRange?.relative ?? timeRange.timeRangeValue
1,845✔
67
      : kind === PICKER_KINDS.ABSOLUTE
9,613✔
68
      ? timeRange?.absolute ?? timeRange.timeRangeValue
7,471✔
69
      : kind === PICKER_KINDS.SINGLE
2,143✔
70
      ? timeRange?.single ?? timeRange.timeSingleValue
349!
71
      : timeRange?.preset ?? timeRange.timeRangeValue;
1,795✔
72

73
  if (!value) {
11,456✔
74
    return { readableValue };
1✔
75
  }
76

77
  const returnValue = cloneDeep(timeRange);
11,455✔
78
  dayjs.extend(customParseFormat);
11,455✔
79

80
  switch (kind) {
11,455✔
81
    case PICKER_KINDS.RELATIVE: {
82
      let endDate = dayjs();
1,843✔
83
      if (value.relativeToWhen !== '') {
1,843✔
84
        endDate =
1,507✔
85
          value.relativeToWhen === RELATIVE_VALUES.YESTERDAY
1,507✔
86
            ? dayjs().add(-1, INTERVAL_VALUES.DAYS)
87
            : dayjs();
88
        // wait to parse it until fully typed
89
        if (value.relativeToTime.length === 5) {
1,507✔
90
          endDate = endDate.hour(Number(value.relativeToTime.split(':')[0]));
346✔
91
          endDate = endDate.minute(Number(value.relativeToTime.split(':')[1]));
346✔
92
        }
93

94
        const startDate = endDate
1,507✔
95
          .clone()
96
          .subtract(
97
            value.lastNumber,
98
            value.lastInterval ? value.lastInterval : INTERVAL_VALUES.MINUTES
1,507✔
99
          );
100
        if (!returnValue.relative) {
1,507✔
101
          returnValue.relative = {};
2✔
102
        }
103
        returnValue.relative.start = new Date(startDate.valueOf());
1,507✔
104
        returnValue.relative.end = new Date(endDate.valueOf());
1,507✔
105
        readableValue = `${dayjs(startDate).format(dateTimeMask)} ${toLabel} ${dayjs(
1,507✔
106
          endDate
107
        ).format(dateTimeMask)}`;
108
      }
109
      break;
1,843✔
110
    }
111
    case PICKER_KINDS.ABSOLUTE: {
112
      let startDate = dayjs(value.start ?? value.startDate);
7,470✔
113
      if (value.startTime) {
7,470✔
114
        const formatedStartTime = is24hours(dateTimeMask)
7,035✔
115
          ? value.startTime
116
          : format12hourTo24hour(value.startTime);
117
        startDate = startDate.hours(formatedStartTime.split(':')[0]);
7,035✔
118
        startDate = startDate.minutes(formatedStartTime.split(':')[1]);
7,035✔
119
      }
120
      if (!returnValue.absolute) {
7,470✔
121
        returnValue.absolute = {};
1✔
122
      }
123

124
      returnValue.absolute.start = new Date(startDate.valueOf());
7,470✔
125

126
      const startTimeValue = value.startTime
7,470✔
127
        ? `${dayjs(startDate).format(dateTimeMask)}`
128
        : `${dayjs(startDate).format(dateTimeMask)}`.split(' ')[0];
129
      if (value.end ?? value.endDate) {
7,470✔
130
        let endDate = dayjs(value.end ?? value.endDate);
7,389✔
131
        if (value.endTime) {
7,389✔
132
          const formatedEndTime = is24hours(dateTimeMask)
6,917✔
133
            ? value.endTime
134
            : format12hourTo24hour(value.endTime);
135
          endDate = endDate.hours(formatedEndTime.split(':')[0]);
6,917✔
136
          endDate = endDate.minutes(formatedEndTime.split(':')[1]);
6,917✔
137
        }
138

139
        const endTimeValue = value.endTime
7,389✔
140
          ? `${dayjs(endDate).format(dateTimeMask)}`
141
          : `${dayjs(endDate).format(dateTimeMask)}`.split(' ')[0];
142

143
        returnValue.absolute.end = new Date(endDate.valueOf());
7,389✔
144
        readableValue = `${startTimeValue} ${toLabel} ${endTimeValue}`;
7,389✔
145
      } else {
146
        readableValue = `${startTimeValue} ${toLabel} ${startTimeValue}`;
81✔
147
      }
148
      break;
7,470✔
149
    }
150
    case PICKER_KINDS.SINGLE: {
151
      // replace 'a' or 'A' in dateTimeMask to be consistent with time picker placeholder text
152
      const updatedDateTimeMask = dateTimeMask.replace(/a|A/, 'XM');
349✔
153
      if (!value.start && !value.startDate) {
349✔
154
        readableValue = updatedDateTimeMask;
183✔
155
        returnValue.single.start = null;
183✔
156
        break;
183✔
157
      }
158
      let startDate = dayjs(value.start ?? value.startDate);
166!
159
      if (value.startTime) {
166✔
160
        const formatedStartTime = is24hours(dateTimeMask)
146✔
161
          ? value.startTime
162
          : format12hourTo24hour(value.startTime);
163
        startDate = startDate.hours(formatedStartTime.split(':')[0]);
146✔
164
        startDate = startDate.minutes(formatedStartTime.split(':')[1]);
146✔
165
      } else if (hasTimeInput) {
20!
166
        returnValue.absolute.startTime = null;
20✔
167
        readableValue = updatedDateTimeMask;
20✔
168
        break;
20✔
169
      }
170
      returnValue.single.start = new Date(startDate.valueOf());
146✔
171
      readableValue = value.startTime
146!
172
        ? `${dayjs(startDate).format(dateTimeMask)}`
173
        : `${dayjs(startDate).format(dateTimeMask)}`.split(' ')[0];
174
      break;
146✔
175
    }
176
    default:
177
      readableValue = value.label;
1,793✔
178
      break;
1,793✔
179
  }
180

181
  return { readableValue, ...returnValue };
11,455✔
182
};
183

184
/**
185
 * A hook to set the ref to the current date time pick and re-parent it based on V1/V2.
186
 *
187
 * @param {Object} object contains the ID of the current picker, and a bool if it's v2
188
 * @returns An array containing: [dateTimePickerRef (object ref to the picker), function (a callback for setting the element)]
189
 */
190
export const useDateTimePickerRef = ({ id, v2 = false }) => {
141✔
191
  const previousActiveElement = useRef(null);
34,269✔
192
  const [datePickerElem, setDatePickerElem] = useState(null);
34,269✔
193

194
  /**
195
   * A callback ref to capture the DateTime node. When a user changes from Relative to Absolute
196
   * the calendar would capture focus and move the users position adding confusion to where they
197
   * are on the page. This also checks if they're currently focused on the Absolute radio button
198
   * and captures it so focus can be restored after the calendar has been re-parented below.
199
   */
200
  const handleDatePickerRef = useCallback((node) => {
34,269✔
201
    if (document.activeElement?.getAttribute('value') === PICKER_KINDS.ABSOLUTE) {
1,929✔
202
      previousActiveElement.current = document.activeElement;
213✔
203
    }
204

205
    setDatePickerElem(node);
1,929✔
206
  }, []);
207

208
  useEffect(() => {
34,269✔
209
    if (datePickerElem) {
3,099✔
210
      // while waiting for https://github.com/carbon-design-system/carbon/issues/5713
211
      // the only way to display the calendar inline is to re-parent its DOM to our component
212

213
      if (v2) {
972✔
214
        datePickerElem.cal.open();
834✔
215
        const dp = document.getElementById(`${id}-${iotPrefix}--date-time-picker__datepicker`);
834✔
216
        dp.appendChild(datePickerElem.cal.calendarContainer);
834✔
217
      } else {
218
        const wrapper = document.getElementById(`${id}-${iotPrefix}--date-time-picker__wrapper`);
138✔
219

220
        if (typeof wrapper !== 'undefined' && wrapper !== null) {
138✔
221
          const dp = document
132✔
222
            .getElementById(`${id}-${iotPrefix}--date-time-picker__wrapper`)
223
            .getElementsByClassName(`${iotPrefix}--date-time-picker__datepicker`)[0];
224
          dp.appendChild(datePickerElem.cal.calendarContainer);
132✔
225
        }
226
      }
227

228
      // if we were focused on the Absolute radio button previously, restore focus to it.
229
      /* istanbul ignore if */
230
      if (previousActiveElement.current) {
972✔
231
        previousActiveElement.current.focus();
232
        previousActiveElement.current = null;
233
      }
234
    }
235
  }, [datePickerElem, id, v2]);
236

237
  return [datePickerElem, handleDatePickerRef];
34,269✔
238
};
239

240
/**
241
 * A helper to switch focus between start and end times when choosing absolute date/times in the calendar
242
 *
243
 * @param {Object} datePickerElem ref to current dateTimePicker element
244
 * @returns An array containing [bool (is the start date in focus), function (set the focus on the start field)]
245
 */
246
export const useDateTimePickerFocus = (datePickerElem) => {
141✔
247
  const [focusOnFirstField, setFocusOnFirstField] = useState(true);
34,269✔
248

249
  useEffect(() => {
34,269✔
250
    if (datePickerElem && datePickerElem.inputField && datePickerElem.toInputField) {
3,512✔
251
      if (focusOnFirstField) {
1,279✔
252
        datePickerElem.inputField.click();
969✔
253
      } else {
254
        datePickerElem.toInputField.click();
310✔
255
      }
256
    }
257
  }, [datePickerElem, focusOnFirstField]);
258

259
  return [focusOnFirstField, setFocusOnFirstField];
34,269✔
260
};
261

262
/**
263
 * Simple date/time validator
264
 *
265
 * @param {Date} date The date to check
266
 * @param {string} time The time string to check
267
 * @returns bool
268
 */
269
export const isValidDate = (date, time) => {
141✔
270
  const isValid24HoursRegex = /^([01][0-9]|2[0-3]):([0-5][0-9])$/;
23,940✔
271
  return date instanceof Date && !Number.isNaN(date) && isValid24HoursRegex.test(time);
23,940✔
272
};
273

274
/**
275
 * 12 hour time validator
276
 *
277
 * @param {string} time The time string to check
278
 * @returns bool
279
 */
280
export const isValid12HourTime = (time) => {
141✔
281
  const isValid12HoursRegex = /^((0[1-9])?|(1[0-2])?)*:[0-5][0-9] (AM|PM)$/;
3,508✔
282
  return isValid12HoursRegex.test(time) || time === '';
3,508✔
283
};
284

285
/**
286
 * 24 hour time validator
287
 *
288
 * @param {string} time The time string to check
289
 * @returns bool
290
 */
291
export const isValid24HourTime = (time) => {
141✔
292
  const isValid24HoursRegex = /^(2[0-3]|[01]?[0-9]):([0-5]?[0-9])$/;
7,635✔
293
  return isValid24HoursRegex.test(time) || time === '';
7,635✔
294
};
295

296
/**
297
 * Simple function to handle keeping flatpickr open when it would normally close
298
 *
299
 * @param {*} range unused
300
 * @param {*} single unused
301
 * @param {class} flatpickr The flatpickr instance
302
 */
303
export const onDatePickerClose = (range, single, flatpickr) => {
141✔
304
  // force it to stay open
305
  /* istanbul ignore else */
306
  if (flatpickr) {
3,791✔
307
    flatpickr.open();
3,791✔
308
  }
309
};
310

311
// Validates absolute start date
312
export const invalidStartDate = (startTime, endTime, absoluteValues) => {
141✔
313
  // If start and end date have been selected
314
  const formatedStartTime = format12hourTo24hour(startTime);
12,229✔
315
  const formatedEndTime = format12hourTo24hour(endTime);
12,229✔
316
  if (
12,229✔
317
    absoluteValues.hasOwnProperty('start') &&
36,499✔
318
    absoluteValues.hasOwnProperty('end') &&
319
    isValidDate(new Date(absoluteValues.start), formatedStartTime)
320
  ) {
321
    const startDate = new Date(`${absoluteValues.startDate} ${formatedStartTime}`);
8,473✔
322
    const endDate = new Date(`${absoluteValues.endDate} ${formatedEndTime}`);
8,473✔
323
    return startDate >= endDate;
8,473✔
324
  }
325
  // Return invalid date if start time and end date not selected or if inputted time is not valid
326
  return true;
3,756✔
327
};
328

329
/**
330
 *
331
 * @param {*} startTime
332
 * @param {*} endTime
333
 * @param {*} absoluteValues
334
 * @returns
335
 */
336
export const invalidEndDate = (startTime, endTime, absoluteValues) => {
141✔
337
  // If start and end date have been selected
338
  const formatedStartTime = format12hourTo24hour(startTime);
11,941✔
339
  const formatedEndTime = format12hourTo24hour(endTime);
11,941✔
340
  if (
11,941✔
341
    absoluteValues.hasOwnProperty('start') &&
35,675✔
342
    absoluteValues.hasOwnProperty('end') &&
343
    isValidDate(new Date(absoluteValues.end), formatedEndTime)
344
  ) {
345
    const startDate = new Date(`${absoluteValues.startDate} ${formatedStartTime}`);
7,161✔
346
    const endDate = new Date(`${absoluteValues.endDate} ${formatedEndTime}`);
7,161✔
347
    return startDate >= endDate;
7,161✔
348
  }
349

350
  // Return invalid date if start time and end date not selected or if inputted time is not valid
351
  return true;
4,780✔
352
};
353

354
/**
355
 * A DateTimePicker hook for handling all absolute time values
356
 *
357
 * @returns Object an object containing:
358
 *    absoluteValue (object): The currently selected absolute value
359
 *    setAbsoluteValue (function): Set the current absolute value
360
 *    absoluteStartTimeInvalid (bool): Is the start time invalid
361
 *    setAbsoluteStartTimeInvalid (function): Set the start time invalid
362
 *    absoluteEndTimeInvalid (bool): Is the end time invalid
363
 *    setAbsoluteEndTimeInvalid (function): Set the end time invalid
364
 *    onAbsoluteStartTimeChange (function): handles changes to start time
365
 *    onAbsoluteEndTimeChange (function): handles changes to end time
366
 *    resetAbsoluteValue (function): reset absolute value to empty defaults
367
 */
368
export const useAbsoluteDateTimeValue = () => {
141✔
369
  const [absoluteValue, setAbsoluteValue] = useState(null);
34,269✔
370
  const [absoluteStartTimeInvalid, setAbsoluteStartTimeInvalid] = useState(false);
34,269✔
371
  const [absoluteEndTimeInvalid, setAbsoluteEndTimeInvalid] = useState(false);
34,269✔
372

373
  // Util func to update the absolute value
374
  const changeAbsolutePropertyValue = (property, value) => {
34,269✔
375
    setAbsoluteValue((prev) => ({
3,526✔
376
      ...prev,
377
      [property]: value,
378
    }));
379
  };
380

381
  const resetAbsoluteValue = () => {
34,269✔
382
    setAbsoluteValue({
912✔
383
      startDate: '',
384
      startTime: null,
385
      endDate: '',
386
      endTime: null,
387
    });
388
  };
389

390
  // on change functions that trigger a absolute value update
391
  const onAbsoluteStartTimeChange = (startTime, evt, meta) => {
34,269✔
392
    const { endTime } = absoluteValue;
1,581✔
393
    const invalidStart = invalidStartDate(startTime, endTime, absoluteValue);
1,581✔
394
    const invalidEnd = invalidEndDate(startTime, endTime, absoluteValue);
1,581✔
395
    setAbsoluteStartTimeInvalid(meta.invalid || invalidStart);
1,581✔
396
    setAbsoluteEndTimeInvalid(invalidEnd);
1,581✔
397
    changeAbsolutePropertyValue('startTime', startTime);
1,581✔
398
  };
399

400
  const onAbsoluteEndTimeChange = (endTime, evt, meta) => {
34,269✔
401
    const { startTime } = absoluteValue;
1,945✔
402
    const invalidEnd = invalidEndDate(startTime, endTime, absoluteValue);
1,945✔
403
    const invalidStart = invalidStartDate(startTime, endTime, absoluteValue);
1,945✔
404
    setAbsoluteEndTimeInvalid(meta.invalid || invalidEnd);
1,945✔
405
    setAbsoluteStartTimeInvalid(invalidStart);
1,945✔
406
    changeAbsolutePropertyValue('endTime', endTime);
1,945✔
407
  };
408

409
  return {
34,269✔
410
    absoluteValue,
411
    setAbsoluteValue,
412
    absoluteStartTimeInvalid,
413
    setAbsoluteStartTimeInvalid,
414
    absoluteEndTimeInvalid,
415
    setAbsoluteEndTimeInvalid,
416
    onAbsoluteStartTimeChange,
417
    onAbsoluteEndTimeChange,
418
    resetAbsoluteValue,
419
    changeAbsolutePropertyValue,
420
    format12hourTo24hour,
421
    isValid12HourTime,
422
    isValid24HourTime,
423
  };
424
};
425

426
/**
427
 * A DateTimePicker hook for handling all relative time values
428
 *
429
 * @param {object} object an object containing the interval and default relativeTo values for relative times
430
 * @returns Object an object containing:
431
 *    relativeValue (object): The currently set relative value object
432
 *    setRelativeValue (function): Set the current relative value
433
 *    relativeToTimeInvalid (bool): Is the current relative time invalid
434
 *    setRelativeToTimeInvalid (function): Set the current relative time invalid
435
 *    relativeLastNumberInvalid (bool): Is the relative last number invalid
436
 *    setRelativeLastNumberInvalid (function): Set the relative last number
437
 *    resetRelativeValue (function): Resets the relative value to empty defaults
438
 *    onRelativeLastNumberChange (function): handles changes to last number
439
 *    onRelativeLastIntervalChange (function): handles changes to interval
440
 *    onRelativeToWhenChange (function): handles changes to relative to when
441
 *    onRelativeToTimeChange (function): handles changes to relative to time
442
 */
443
export const useRelativeDateTimeValue = ({ defaultInterval, defaultRelativeTo }) => {
141✔
444
  const [relativeValue, setRelativeValue] = useState(null);
34,269✔
445
  const [relativeToTimeInvalid, setRelativeToTimeInvalid] = useState(false);
34,269✔
446
  const [relativeLastNumberInvalid, setRelativeLastNumberInvalid] = useState(false);
34,269✔
447

448
  const resetRelativeValue = () => {
34,269✔
449
    setRelativeValue({
1,531✔
450
      lastNumber: 0,
451
      lastInterval: defaultInterval,
452
      relativeToWhen: defaultRelativeTo,
453
      relativeToTime: '',
454
    });
455
  };
456

457
  // Util func to update the relative value
458
  const changeRelativePropertyValue = (property, value) => {
34,269✔
459
    setRelativeValue((prev) => ({
1,543✔
460
      ...prev,
461
      [property]: value,
462
    }));
463
  };
464

465
  // on change functions that trigger a relative value update
466
  const onRelativeLastNumberChange = (event) => {
34,269✔
467
    const valid = !event.imaginaryTarget.getAttribute('data-invalid');
105✔
468
    setRelativeLastNumberInvalid(!valid);
105✔
469
    if (valid) {
105✔
470
      changeRelativePropertyValue('lastNumber', Number(event.imaginaryTarget.value));
96✔
471
    }
472
  };
473
  const onRelativeLastIntervalChange = (event) => {
34,269✔
474
    changeRelativePropertyValue('lastInterval', event.currentTarget.value);
3✔
475
  };
476
  const onRelativeToWhenChange = (event) => {
34,269✔
477
    changeRelativePropertyValue('relativeToWhen', event.currentTarget.value);
3✔
478
  };
479
  const onRelativeToTimeChange = (pickerValue, evt, meta) => {
34,269✔
480
    setRelativeToTimeInvalid(meta.invalid);
1,441✔
481
    changeRelativePropertyValue('relativeToTime', pickerValue);
1,441✔
482
  };
483

484
  return {
34,269✔
485
    relativeValue,
486
    setRelativeValue,
487
    relativeToTimeInvalid,
488
    setRelativeToTimeInvalid,
489
    relativeLastNumberInvalid,
490
    setRelativeLastNumberInvalid,
491
    resetRelativeValue,
492
    onRelativeLastNumberChange,
493
    onRelativeLastIntervalChange,
494
    onRelativeToWhenChange,
495
    onRelativeToTimeChange,
496
  };
497
};
498

499
/**
500
 * Simple hook to change the type of DateTimePicker we're working with (relative or absolute)
501
 *
502
 * @param {boolean} showRelativeOption Are the relative options shown by default
503
 * @returns an array containing [string (current kind), function (set the current range type), function (handles changing the range kind)]
504
 */
505
export const useDateTimePickerRangeKind = (showRelativeOption) => {
141✔
506
  const [customRangeKind, setCustomRangeKind] = useState(
34,269✔
507
    showRelativeOption ? PICKER_KINDS.RELATIVE : PICKER_KINDS.ABSOLUTE
34,269✔
508
  );
509

510
  const onCustomRangeChange = (kind) => {
34,269✔
511
    setCustomRangeKind(kind);
170✔
512
  };
513

514
  return [customRangeKind, setCustomRangeKind, onCustomRangeChange];
34,269✔
515
};
516

517
/**
518
 * A hook handling interactions related to keyboard navigation and changing of the currently selected
519
 * DateTimePicker kind (relative/absolute)
520
 *
521
 * @param {boolean} showRelativeOption boolean determining if relative options are shown
522
 * @returns Object An object containing:
523
 *    presetListRef (Object): the ref to the preset div element
524
 *    isExpanded (boolean): is the DateTimePicker expanded
525
 *    setIsExpanded (function): set the isExpanded state
526
 *    getFocusableSiblings (function): return other focusable siblings from the presetListRef
527
 *    onFieldInteraction (function): handles changing expanded or focus state on different key presses
528
 *    onNavigateRadioButton (function): handles changing the focus state of the current radio button (relative/absolute)
529
 *    onNavigatePresets (function): handles changing the currently focuses preset as keyboard navigates
530
 */
531
export const useDateTimePickerKeyboardInteraction = ({ expanded, setCustomRangeKind }) => {
141✔
532
  const [isExpanded, setIsExpanded] = useState(expanded);
34,269✔
533
  const presetListRef = useRef(null);
34,269✔
534

535
  const getFocusableSiblings = () => {
34,269✔
536
    /* istanbul ignore else */
537
    if (presetListRef?.current) {
428✔
538
      const siblings = presetListRef.current.querySelectorAll('[tabindex]');
428✔
539
      return Array.from(siblings).filter(
428✔
540
        (sibling) => parseInt(sibling.getAttribute('tabindex'), 10) !== -1
2,568✔
541
      );
542
    }
543

544
    return [];
×
545
  };
546

547
  const onFieldInteraction = ({ key }) => {
34,269✔
548
    switch (key) {
1,344✔
549
      case 'Escape':
550
        setIsExpanded(false);
64✔
551
        break;
64✔
552
      // if the input box is focused and a down arrow is pressed this
553
      // moves focus to the first item in the preset list that has a tabindex
554
      case 'ArrowDown':
555
        /* istanbul ignore else */
556
        if (presetListRef?.current) {
112✔
557
          const listItems = getFocusableSiblings();
55✔
558
          /* istanbul ignore else */
559
          if (listItems?.[0]?.focus) {
55✔
560
            listItems[0].focus();
55✔
561
          }
562
        }
563
        break;
112✔
564
      default:
565
        setIsExpanded(!isExpanded);
1,168✔
566
        break;
1,168✔
567
    }
568
  };
569

570
  const onFieldClick = (e) => {
34,269✔
571
    if (e.target.innerText !== 'Apply') setIsExpanded(!isExpanded);
675✔
572
  };
573

574
  /**
575
   * Moves up the preset list to the previous focusable element or wraps around to the bottom
576
   * if already at the top.
577
   */
578
  const moveToPreviousElement = () => {
34,269✔
579
    const siblings = getFocusableSiblings();
125✔
580
    const index = siblings.findIndex((elem) => elem === document.activeElement);
495✔
581
    const previous = siblings[index - 1];
125✔
582
    if (previous) {
125✔
583
      previous.focus();
83✔
584
    } else {
585
      siblings[siblings.length - 1].focus();
42✔
586
    }
587
  };
588

589
  /**
590
   * Moves down the preset list to the next focusable element or wraps around to the top
591
   * if already at the bottom
592
   */
593
  const moveToNextElement = () => {
34,269✔
594
    const siblings = getFocusableSiblings();
248✔
595
    const index = siblings.findIndex((elem) => elem === document.activeElement);
868✔
596
    const next = siblings[index + 1];
248✔
597
    if (next) {
248✔
598
      next.focus();
206✔
599
    } else {
600
      siblings[0].focus();
42✔
601
    }
602
  };
603

604
  const onNavigatePresets = ({ key }) => {
34,269✔
605
    switch (key) {
373!
606
      case 'ArrowUp':
607
        moveToPreviousElement();
125✔
608
        break;
125✔
609
      case 'ArrowDown':
610
        moveToNextElement();
248✔
611
        break;
248✔
612
      default:
613
        break;
×
614
    }
615
  };
616

617
  /**
618
   * Allows navigation back and forth between the radio buttons for Relative/Absolute
619
   *
620
   * @param {KeyboardEvent} e
621
   */
622
  const onNavigateRadioButton = (e) => {
34,269✔
623
    if (e.target.getAttribute('id').includes('absolute')) {
146✔
624
      setCustomRangeKind(PICKER_KINDS.RELATIVE);
73✔
625
      document.activeElement.parentNode.previousSibling
73✔
626
        .querySelector('input[type="radio"]')
627
        .focus();
628
    } else {
629
      setCustomRangeKind(PICKER_KINDS.ABSOLUTE);
73✔
630
      document.activeElement.parentNode.nextSibling.querySelector('input[type="radio"]').focus();
73✔
631
    }
632
  };
633

634
  return {
34,269✔
635
    presetListRef,
636
    isExpanded,
637
    setIsExpanded,
638
    getFocusableSiblings,
639
    onFieldInteraction,
640
    onNavigateRadioButton,
641
    onNavigatePresets,
642
    onFieldClick,
643
  };
644
};
645

646
/**
647
 * Get an alternative human readable value for a preset to show in tooltips and dropdown
648
 * ie. 'Last 30 minutes' displays '2020-04-01 11:30 to Now' on the tooltip
649
 * @param {Object} object an object containing:
650
 *    currentValue: the current picker value
651
 *    strings: i18n translation strings
652
 *    dateTimeMask: the current date/time string mask
653
 *    humanValue: the human readable string value for the current time
654
 *
655
 * @returns {string} an interval string, starting point in time to now
656
 */
657
export const getIntervalValue = ({ currentValue, mergedI18n, dateTimeMask, humanValue }) => {
141✔
658
  if (currentValue) {
32,999✔
659
    if (currentValue.kind === PICKER_KINDS.PRESET) {
30,802✔
660
      return `${dayjs().subtract(currentValue.preset.offset, 'minutes').format(dateTimeMask)} ${
4,843✔
661
        mergedI18n.toNowLabel
662
      }`;
663
    }
664
    return humanValue;
25,959✔
665
  }
666

667
  return '';
2,197✔
668
};
669

670
/**
671
 * Helper hook to open and close the tooltip as the DateTimePicker is opened and closed
672
 *
673
 * @param {Object} object An object telling the current state of the DateTimePicker being open
674
 * @returns Array an array containing [bool (is the tooltip open), func (function to toggle tooltip state)]
675
 */
676
export const useDateTimePickerTooltip = ({ isExpanded }) => {
141✔
677
  const [isTooltipOpen, setIsTooltipOpen] = useState(false);
34,269✔
678

679
  /**
680
   * Shows and hides the tooltip with the humanValue (Relative) or full-range (Absolute) when
681
   * the user focuses or hovers on the input
682
   */
683
  const toggleTooltip = () => {
34,269✔
684
    if (isExpanded) {
7,751✔
685
      setIsTooltipOpen(false);
5,181✔
686
    } else {
687
      setIsTooltipOpen((prev) => !prev);
2,570✔
688
    }
689
  };
690

691
  return [isTooltipOpen, toggleTooltip, setIsTooltipOpen];
34,269✔
692
};
693

694
/**
695
 * Hook to validate event and invoke callback
696
 * @param {function} closeDropdownCallback: function that will be called if validation passes
697
 * @returns void
698
 */
699
export const useDateTimePickerClickOutside = (closeDropdownCallback, containerRef) => (evt) => {
34,269✔
700
  if (
2,130✔
701
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__listitem--custom`) ||
9,990✔
702
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-back`) ||
703
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-reset`) ||
704
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-cancel`) ||
705
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-apply`)
706
  ) {
707
    return;
616✔
708
  }
709

710
  if (containerRef.current?.firstChild.contains(evt.target)) {
1,514✔
711
    closeDropdownCallback({ isEventOnField: true });
271✔
712
    return;
271✔
713
  }
714

715
  // Composed path is needed in order to detect if event is bubbled from TimePickerSpinner which is a React Portal
716
  if (
1,243✔
717
    evt.composed &&
2,486✔
718
    evt.composedPath().some((el) => el.classList?.contains(`${iotPrefix}--time-picker-spinner`))
13,310✔
719
  ) {
720
    return;
7✔
721
  }
722

723
  closeDropdownCallback({ isEventOnField: false });
1,236✔
724
};
725

726
/**
727
 * Utility function to get time picker kind key
728
 * @param {Object} object: an object containing:
729
 *   kind: time picker kind
730
 *   timeRangeKind: time range kind
731
 * @returns
732
 */
733
const getTimeRangeKindKey = ({ kind, timeRangeKind }) => {
141✔
734
  if (kind === PICKER_KINDS.SINGLE || timeRangeKind === PICKER_KINDS.SINGLE) {
26!
735
    return 'timeSingleValue';
×
736
  }
737
  return 'timeRangeValue';
26✔
738
};
739

740
/**
741
 * Hook to close time picker dropdown and reset default value
742
 * @param {Object} object: an object containing:
743
 *   isExpanded: current state of the dropdown
744
 *   setIsExpanded: useState callback
745
 *   isCustomRange: if dropdown was opened in custom range
746
 *   setIsCustomRange: useState callback
747
 *   defaultValue: props value for time picker
748
 *   parseDefaultValue: parses value from string to time picker format
749
 *   setCustomRangeKind: useState callback
750
 *   lastAppliedValue: last saved value
751
 * @returns {function}
752
 */
753
export const useCloseDropdown = ({
141✔
754
  isExpanded,
755
  setIsExpanded,
756
  isCustomRange,
757
  setIsCustomRange,
758
  defaultValue,
759
  parseDefaultValue,
760
  setCustomRangeKind,
761
  lastAppliedValue,
762
  singleTimeValue,
763
  setSingleDateValue,
764
  setSingleTimeValue,
765
}) =>
766
  useCallback(
34,269✔
767
    ({ isEventOnField }) => {
768
      if (!isExpanded) {
1,507✔
769
        return;
1,255✔
770
      }
771

772
      if (!isEventOnField) {
252✔
773
        setIsExpanded(false);
209✔
774
      }
775

776
      // memoized value at the time when dropdown was opened
777
      if (!isCustomRange) {
252✔
778
        setIsCustomRange(false);
168✔
779
      }
780

781
      if (
252✔
782
        (lastAppliedValue?.timeRangeKind === PICKER_KINDS.SINGLE ||
504✔
783
          lastAppliedValue?.kind === PICKER_KINDS.SINGLE) &&
784
        !singleTimeValue
785
      ) {
786
        setSingleDateValue({ start: null, startDate: null });
8✔
787
        setSingleTimeValue(null);
8✔
788
        return;
8✔
789
      }
790

791
      if (lastAppliedValue) {
244✔
792
        setCustomRangeKind(lastAppliedValue.kind || lastAppliedValue.timeRangeKind);
83✔
793
        parseDefaultValue({
83✔
794
          ...lastAppliedValue,
795
          ...(!lastAppliedValue.timeRangeKind && {
109✔
796
            timeRangeKind: lastAppliedValue?.kind,
797
            [getTimeRangeKindKey(lastAppliedValue)]:
798
              lastAppliedValue[lastAppliedValue?.kind.toLowerCase()],
799
          }),
800
        });
801
      } else {
802
        setCustomRangeKind(defaultValue ? defaultValue.timeRangeKind : PICKER_KINDS.RELATIVE);
161!
803
        parseDefaultValue(defaultValue);
161✔
804
      }
805
    },
806
    // eslint-disable-next-line react-hooks/exhaustive-deps
807
    [defaultValue, isExpanded, setCustomRangeKind, setIsExpanded, lastAppliedValue]
808
  );
809

810
/**
811
 * For a given element, walk up the dom to find scroll container.
812
 * Only gets first as modals should prevent scrolling in elements above.
813
 * @param{element} element
814
 */
815
export const getScrollParent = (element) => {
141✔
816
  try {
4,482✔
817
    /* istanbul ignore next */
818
    if (
819
      element.scrollHeight > parseInt(element.clientHeight, 10) + 10 ||
820
      element.scrollWidth > parseInt(element.clientWidth, 10) + 10
821
    ) {
822
      const computedStyle = window.getComputedStyle(element);
823
      if (
824
        ['scroll', 'auto'].includes(computedStyle.overflowY) ||
825
        ['scroll', 'auto'].includes(computedStyle.overflow)
826
      ) {
827
        return element;
828
      }
829
    }
830
    if (element.parentElement) {
4,312✔
831
      return getScrollParent(element.parentElement);
3,335✔
832
    }
833
    return document.scrollingElement;
977✔
834
  } catch (error) {
835
    /* istanbul ignore next */
836
    return window;
837
  }
838
};
839

840
/**
841
 * A hook handling the height of the drop down menu
842
 *
843
 * @param {object} containerRef the ref to the container div of the drop down menu
844
 * @param {boolean} isSingleSelect if it is single select calendar
845
 * @param {boolean} isCustomRange if dropdown was opened in custom range
846
 * @param {boolean} showRelativeOption are the relative options shown by default
847
 * @param {string} customRangeKind custom range kind is either relative or absolute
848
 * @param {function} setIsExpanded set the isExpanded state
849
 * @returns Object An object containing:
850
 *    offTop (boolean): if the menu is off top
851
 *    offBottom (boolean): if the menu is off bottom
852
 *    inputTop (string) : the top position of the date time input
853
 *    inputBottom (string): the bottom position of the date time input
854
 *    customHeight (string): the adjusted height of the drop down menu if both offTop and offBottom are true
855
 *    maxHeight (string) : maximum height of the drop down menu
856
 */
857
export const useCustomHeight = ({
141✔
858
  containerRef,
859
  isSingleSelect,
860
  isCustomRange,
861
  showRelativeOption,
862
  customRangeKind,
863
  setIsExpanded,
864
}) => {
865
  // calculate max height for varies dropdown
866
  const presetMaxHeight = 315;
28,249✔
867
  const relativeMaxHeight = 200;
28,249✔
868
  const absoluteMaxHeight = 446;
28,249✔
869
  const singleMaxHeight = 442;
28,249✔
870
  const footerHeight = 40;
28,249✔
871
  const invalidDateWarningHeight = 38;
28,249✔
872
  const invalidTimeWarningHeight = 22;
28,249✔
873
  const relativeOptionHeight = 69;
28,249✔
874
  const timeInputHeight = 64;
28,249✔
875
  const maxHeight = isSingleSelect
28,249✔
876
    ? singleMaxHeight
877
    : isCustomRange
26,988✔
878
    ? (customRangeKind === PICKER_KINDS.ABSOLUTE ? absoluteMaxHeight : relativeMaxHeight) +
22,845✔
879
      (showRelativeOption ? relativeOptionHeight : 0)
22,845✔
880
    : presetMaxHeight;
881

882
  const closeDropDown = () => {
28,249✔
883
    setIsExpanded(false);
7,989✔
884
  };
885

886
  useEffect(() => {
28,249✔
887
    const firstScrollableParent = getScrollParent(containerRef.current);
1,147✔
888
    if (firstScrollableParent) {
1,147✔
889
      firstScrollableParent.addEventListener('scroll', closeDropDown);
1,026✔
890
    }
891
    window.addEventListener('scroll', closeDropDown);
1,147✔
892
    return () => {
1,147✔
893
      if (firstScrollableParent) {
1,092✔
894
        firstScrollableParent.removeEventListener('scroll', closeDropDown);
978✔
895
      }
896
      window.removeEventListener('scroll', closeDropDown);
1,092✔
897
    };
898
    // eslint-disable-next-line react-hooks/exhaustive-deps
899
  }, []);
900

901
  // re-calculate window height when resize
902
  const [windowHeight, setWindowHeight] = useState(
28,249✔
903
    window.innerHeight || document.documentElement.clientHeight
28,249!
904
  );
905

906
  const handleWindowResize = debounce(() => {
28,249✔
907
    setWindowHeight(window.innerHeight || document.documentElement.clientHeight);
288!
908
  }, 50);
909

910
  useEffect(() => {
28,249✔
911
    window.addEventListener('resize', handleWindowResize);
24,279✔
912
    return () => window.removeEventListener('resize', handleWindowResize);
24,279✔
913
  }, [handleWindowResize]);
914

915
  // calculate if flyout menu will be off top or bottom of the screen
916
  const inputBottom = containerRef?.current?.getBoundingClientRect().bottom;
28,249✔
917
  const inputTop = containerRef?.current?.getBoundingClientRect().top;
28,249✔
918
  const flyoutMenuHeight = maxHeight + footerHeight;
28,249✔
919
  const offBottom = windowHeight - inputBottom < flyoutMenuHeight;
28,249✔
920
  const offTop = inputTop < flyoutMenuHeight;
28,249✔
921
  const topGap = inputTop;
28,249✔
922
  const bottomGap = windowHeight - inputBottom;
28,249✔
923

924
  const customHeight =
925
    offBottom && offTop ? (topGap > bottomGap ? topGap : bottomGap) - footerHeight : undefined;
28,249!
926

927
  return [
28,249✔
928
    offTop,
929
    offBottom,
930
    inputTop,
931
    inputBottom,
932
    customHeight,
933
    maxHeight,
934
    invalidDateWarningHeight,
935
    invalidTimeWarningHeight,
936
    timeInputHeight,
937
  ];
938
};
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