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

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

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

Pull #3825

github

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

7624 of 7956 branches covered (0.0%)

Branch coverage included in aggregate %.

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

9141 of 9241 relevant lines covered (98.92%)

2104.61 hits per line

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

98.08
/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;
139✔
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) => {
139✔
17
  const [, time] = dateTimeMask.split(' ');
13,801✔
18
  const hoursMask = time?.split(':')[0];
13,801✔
19
  return hoursMask ? hoursMask.includes('H') : false;
13,801!
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) => {
139✔
27
  if (time12hour === '' || !time12hour) {
48,808✔
28
    return '00:00';
2,598✔
29
  }
30
  const [time, modifier] = time12hour.split(' ');
46,210✔
31

32
  if (!modifier) {
46,210✔
33
    return time12hour;
35,824✔
34
  }
35

36
  // eslint-disable-next-line prefer-const
37
  let [hours, minutes] = time.split(':');
10,386✔
38
  if (hours === '12') {
10,386✔
39
    hours = '00';
1,993✔
40
  }
41
  if (modifier === 'PM') {
10,386✔
42
    hours = parseInt(hours, 10) + 12;
5,540✔
43
  }
44
  return `${hours}:${minutes}`;
10,386✔
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) => {
139✔
57
  let readableValue = '';
11,190✔
58

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

63
  const kind = timeRange.kind ?? timeRange.timeRangeKind;
11,168✔
64
  const value =
65
    kind === PICKER_KINDS.RELATIVE
11,168✔
66
      ? timeRange?.relative ?? timeRange.timeRangeValue
1,830✔
67
      : kind === PICKER_KINDS.ABSOLUTE
9,340✔
68
      ? timeRange?.absolute ?? timeRange.timeRangeValue
7,335✔
69
      : kind === PICKER_KINDS.SINGLE
2,006✔
70
      ? timeRange?.single ?? timeRange.timeSingleValue
214!
71
      : timeRange?.preset ?? timeRange.timeRangeValue;
1,793✔
72

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

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

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

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

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

126
      const startTimeValue = value.startTime
7,334✔
127
        ? `${dayjs(startDate).format(dateTimeMask)}`
128
        : `${dayjs(startDate).format(dateTimeMask)}`.split(' ')[0];
129

130
      if (value.end ?? value.endDate) {
7,334✔
131
        let endDate = dayjs(value.end ?? value.endDate);
7,253✔
132
        if (value.endTime) {
7,253✔
133
          const formatedEndTime = is24hours(dateTimeMask)
6,787✔
134
            ? value.endTime
135
            : format12hourTo24hour(value.endTime);
136
          endDate = endDate.hours(formatedEndTime.split(':')[0]);
6,787✔
137
          endDate = endDate.minutes(formatedEndTime.split(':')[1]);
6,787✔
138
        }
139

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

144
        returnValue.absolute.end = new Date(endDate.valueOf());
7,253✔
145
        readableValue = `${startTimeValue} ${toLabel} ${endTimeValue}`;
7,253✔
146
      } else {
147
        readableValue = `${startTimeValue} ${toLabel} ${startTimeValue}`;
81✔
148
      }
149
      break;
7,334✔
150
    }
151
    case PICKER_KINDS.SINGLE: {
152
      if (!value.start && !value.startDate) {
214✔
153
        readableValue = dateTimeMask;
82✔
154
        returnValue.single.start = null;
82✔
155
        break;
82✔
156
      }
157
      let startDate = dayjs(value.start ?? value.startDate);
132!
158
      if (value.startTime) {
132✔
159
        const formatedStartTime = is24hours(dateTimeMask)
111✔
160
          ? value.startTime
161
          : format12hourTo24hour(value.startTime);
162
        startDate = startDate.hours(formatedStartTime.split(':')[0]);
111✔
163
        startDate = startDate.minutes(formatedStartTime.split(':')[1]);
111✔
164
      }
165
      returnValue.single.start = new Date(startDate.valueOf());
132✔
166
      readableValue = value.startTime
132✔
167
        ? `${dayjs(startDate).format(dateTimeMask)}`
168
        : `${dayjs(startDate).format(dateTimeMask)}`.split(' ')[0];
169
      break;
132✔
170
    }
171
    default:
172
      readableValue = value.label;
1,791✔
173
      break;
1,791✔
174
  }
175

176
  return { readableValue, ...returnValue };
11,167✔
177
};
178

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

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

200
    setDatePickerElem(node);
1,772✔
201
  }, []);
202

203
  useEffect(() => {
33,335✔
204
    const timeout = setTimeout(() => {
4,018✔
205
      if (datePickerElem) {
2,856✔
206
        datePickerElem.cal.open();
874✔
207
        // while waiting for https://github.com/carbon-design-system/carbon/issues/5713
208
        // the only way to display the calendar inline is to re-parent its DOM to our component
209

210
        if (v2) {
874✔
211
          const dp = document.getElementById(`${id}-${iotPrefix}--date-time-picker__datepicker`);
739✔
212
          dp.appendChild(datePickerElem.cal.calendarContainer);
739✔
213
        } else {
214
          const wrapper = document.getElementById(`${id}-${iotPrefix}--date-time-picker__wrapper`);
135✔
215

216
          if (typeof wrapper !== 'undefined' && wrapper !== null) {
135✔
217
            const dp = document
125✔
218
              .getElementById(`${id}-${iotPrefix}--date-time-picker__wrapper`)
219
              .getElementsByClassName(`${iotPrefix}--date-time-picker__datepicker`)[0];
220
            dp.appendChild(datePickerElem.cal.calendarContainer);
125✔
221
          }
222
        }
223

224
        // if we were focused on the Absolute radio button previously, restore focus to it.
225
        /* istanbul ignore if */
226
        if (previousActiveElement.current) {
874✔
227
          previousActiveElement.current.focus();
228
          previousActiveElement.current = null;
229
        }
230
      }
231
    }, 0);
232

233
    return () => {
4,018✔
234
      clearTimeout(timeout);
3,927✔
235
    };
236
  }, [datePickerElem, id, v2]);
237

238
  return [datePickerElem, handleDatePickerRef];
33,335✔
239
};
240

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

250
  useEffect(() => {
33,335✔
251
    if (datePickerElem && datePickerElem.inputField && datePickerElem.toInputField) {
3,338✔
252
      if (focusOnFirstField) {
1,262✔
253
        datePickerElem.inputField.click();
956✔
254
      } else {
255
        datePickerElem.toInputField.click();
306✔
256
      }
257
    }
258
  }, [datePickerElem, focusOnFirstField]);
259

260
  return [focusOnFirstField, setFocusOnFirstField];
33,335✔
261
};
262

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

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

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

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

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

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

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

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

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

382
  const resetAbsoluteValue = () => {
33,335✔
383
    setAbsoluteValue({
911✔
384
      startDate: '',
385
      startTime: null,
386
      endDate: '',
387
      endTime: null,
388
    });
389
  };
390

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

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

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

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

449
  const resetRelativeValue = () => {
33,335✔
450
    setRelativeValue({
1,483✔
451
      lastNumber: 0,
452
      lastInterval: defaultInterval,
453
      relativeToWhen: defaultRelativeTo,
454
      relativeToTime: '',
455
    });
456
  };
457

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

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

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

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

511
  const onCustomRangeChange = (kind) => {
33,335✔
512
    setCustomRangeKind(kind);
170✔
513
  };
514

515
  return [customRangeKind, setCustomRangeKind, onCustomRangeChange];
33,335✔
516
};
517

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

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

545
    return [];
×
546
  };
547

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

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

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

601
  const onNavigatePresets = ({ key }) => {
33,335✔
602
    switch (key) {
373!
603
      case 'ArrowUp':
604
        moveToPreviousElement();
125✔
605
        break;
125✔
606
      case 'ArrowDown':
607
        moveToNextElement();
248✔
608
        break;
248✔
609
      default:
610
        break;
×
611
    }
612
  };
613

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

631
  return {
33,335✔
632
    presetListRef,
633
    isExpanded,
634
    setIsExpanded,
635
    getFocusableSiblings,
636
    onFieldInteraction,
637
    onNavigateRadioButton,
638
    onNavigatePresets,
639
  };
640
};
641

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

663
  return '';
2,172✔
664
};
665

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

675
  /**
676
   * Shows and hides the tooltip with the humanValue (Relative) or full-range (Absolute) when
677
   * the user focuses or hovers on the input
678
   */
679
  const toggleTooltip = () => {
33,335✔
680
    if (isExpanded) {
7,538✔
681
      setIsTooltipOpen(false);
5,091✔
682
    } else {
683
      setIsTooltipOpen((prev) => !prev);
2,447✔
684
    }
685
  };
686

687
  return [isTooltipOpen, toggleTooltip, setIsTooltipOpen];
33,335✔
688
};
689

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

706
  if (containerRef.current?.firstChild.contains(evt.target)) {
1,514✔
707
    closeDropdownCallback({ isEventOnField: true });
271✔
708
    return;
271✔
709
  }
710

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

719
  closeDropdownCallback({ isEventOnField: false });
1,236✔
720
};
721

722
/**
723
 * Utility function to get time picker kind key
724
 * @param {Object} object: an object containing:
725
 *   kind: time picker kind
726
 *   timeRangeKind: time range kind
727
 * @returns
728
 */
729
const getTimeRangeKindKey = ({ kind, timeRangeKind }) => {
139✔
730
  if (kind === PICKER_KINDS.SINGLE || timeRangeKind === PICKER_KINDS.SINGLE) {
36✔
731
    return 'timeSingleValue';
5✔
732
  }
733
  return 'timeRangeValue';
31✔
734
};
735

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

768
      if (!isEventOnField) {
252✔
769
        setIsExpanded(false);
209✔
770
      }
771

772
      // memoized value at the time when dropdown was opened
773
      if (!isCustomRange) {
252✔
774
        setIsCustomRange(false);
168✔
775
      }
776

777
      if (
252✔
778
        (lastAppliedValue?.timeRangeKind === PICKER_KINDS.SINGLE ||
509✔
779
          lastAppliedValue?.kind === PICKER_KINDS.SINGLE) &&
780
        !singleTimeValue
781
      ) {
782
        setSingleDateValue({ start: null, startDate: null });
8✔
783
        setSingleTimeValue(null);
8✔
784
        return;
8✔
785
      }
786

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

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

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

879
  const closeDropDown = () => {
27,321✔
880
    setIsExpanded(false);
6,070✔
881
  };
882

883
  useEffect(() => {
27,321✔
884
    const firstScrollableParent = getScrollParent(containerRef.current);
1,101✔
885
    if (firstScrollableParent) {
1,101✔
886
      firstScrollableParent.addEventListener('scroll', closeDropDown);
982✔
887
    }
888
    window.addEventListener('scroll', closeDropDown);
1,101✔
889
    return () => {
1,101✔
890
      if (firstScrollableParent) {
1,047✔
891
        firstScrollableParent.removeEventListener('scroll', closeDropDown);
935✔
892
      }
893
      window.removeEventListener('scroll', closeDropDown);
1,047✔
894
    };
895
    // eslint-disable-next-line react-hooks/exhaustive-deps
896
  }, []);
897

898
  // re-calculate window height when resize
899
  const [windowHeight, setWindowHeight] = useState(
27,321✔
900
    window.innerHeight || document.documentElement.clientHeight
27,321!
901
  );
902

903
  const handleWindowResize = debounce(() => {
27,321✔
904
    setWindowHeight(window.innerHeight || document.documentElement.clientHeight);
375!
905
  }, 50);
906

907
  useEffect(() => {
27,321✔
908
    window.addEventListener('resize', handleWindowResize);
23,401✔
909
    return () => window.removeEventListener('resize', handleWindowResize);
23,401✔
910
  }, [handleWindowResize]);
911

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

921
  const customHeight =
922
    offBottom && offTop ? (topGap > bottomGap ? topGap : bottomGap) - footerHeight : undefined;
27,321!
923

924
  return [offTop, offBottom, inputTop, inputBottom, customHeight, maxHeight];
27,321✔
925
};
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