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

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

pending completion
4821361778

push

github

carbon-bot
v2.153.0-next.15

7749 of 8086 branches covered (95.83%)

Branch coverage included in aggregate %.

9344 of 9448 relevant lines covered (98.9%)

2608.24 hits per line

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

97.75
/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;
142✔
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) => {
142✔
17
  const [, time] = dateTimeMask.split(' ');
15,713✔
18
  const hoursMask = time?.split(':')[0];
15,713✔
19
  return hoursMask ? hoursMask.includes('H') : false;
15,713!
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) => {
142✔
27
  if (time12hour === '' || !time12hour) {
61,255✔
28
    return '00:00';
3,104✔
29
  }
30
  const [time, modifier] = time12hour.split(' ');
58,151✔
31

32
  if (!modifier) {
58,151✔
33
    return time12hour;
36,864✔
34
  }
35

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

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

63
  const kind = timeRange.kind ?? timeRange.timeRangeKind;
12,303✔
64
  const value =
65
    kind === PICKER_KINDS.RELATIVE
12,303✔
66
      ? timeRange?.relative ?? timeRange.timeRangeValue
1,845✔
67
      : kind === PICKER_KINDS.ABSOLUTE
10,460✔
68
      ? timeRange?.absolute ?? timeRange.timeRangeValue
8,318✔
69
      : kind === PICKER_KINDS.SINGLE
2,143✔
70
      ? timeRange?.single ?? timeRange.timeSingleValue
349!
71
      : timeRange?.preset ?? timeRange.timeRangeValue;
1,795✔
72

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

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

80
  switch (kind) {
12,302✔
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);
8,317✔
113
      if (value.startTime) {
8,317✔
114
        const formatedStartTime = is24hours(dateTimeMask)
7,844✔
115
          ? value.startTime
116
          : format12hourTo24hour(value.startTime);
117
        startDate = startDate.hours(formatedStartTime.split(':')[0]);
7,844✔
118
        startDate = startDate.minutes(formatedStartTime.split(':')[1]);
7,844✔
119
      }
120
      if (!returnValue.absolute) {
8,317✔
121
        returnValue.absolute = {};
1✔
122
      }
123

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

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

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

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

179
  return { readableValue, ...returnValue };
12,302✔
180
};
181

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

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

203
    setDatePickerElem(node);
2,019✔
204
  }, []);
205

206
  useEffect(() => {
37,195✔
207
    const timeout = setTimeout(() => {
4,332✔
208
      if (datePickerElem) {
3,156✔
209
        datePickerElem.cal.open();
997✔
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) {
997✔
214
          const dp = document.getElementById(`${id}-${iotPrefix}--date-time-picker__datepicker`);
860✔
215
          dp.appendChild(datePickerElem.cal.calendarContainer);
860✔
216
        } else {
217
          const wrapper = document.getElementById(`${id}-${iotPrefix}--date-time-picker__wrapper`);
137✔
218

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

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

236
    return () => {
4,332✔
237
      clearTimeout(timeout);
4,239✔
238
    };
239
  }, [datePickerElem, id, v2]);
240

241
  return [datePickerElem, handleDatePickerRef];
37,195✔
242
};
243

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

253
  useEffect(() => {
37,195✔
254
    if (datePickerElem && datePickerElem.inputField && datePickerElem.toInputField) {
3,689✔
255
      if (focusOnFirstField) {
1,366✔
256
        datePickerElem.inputField.click();
1,015✔
257
      } else {
258
        datePickerElem.toInputField.click();
351✔
259
      }
260
    }
261
  }, [datePickerElem, focusOnFirstField]);
262

263
  return [focusOnFirstField, setFocusOnFirstField];
37,195✔
264
};
265

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

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

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

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

315
// Validates absolute start date
316
export const invalidStartDate = (startTime, endTime, absoluteValues) => {
142✔
317
  // If start and end date have been selected
318
  const formatedStartTime = format12hourTo24hour(startTime);
14,735✔
319
  const formatedEndTime = format12hourTo24hour(endTime);
14,735✔
320
  if (
14,735✔
321
    absoluteValues.hasOwnProperty('start') &&
44,017✔
322
    absoluteValues.hasOwnProperty('end') &&
323
    isValidDate(new Date(absoluteValues.start), formatedStartTime)
324
  ) {
325
    const startDate = new Date(`${absoluteValues.startDate} ${formatedStartTime}`);
10,856✔
326
    const endDate = new Date(`${absoluteValues.endDate} ${formatedEndTime}`);
10,856✔
327
    return startDate >= endDate;
10,856✔
328
  }
329
  // Return invalid date if start time and end date not selected or if inputted time is not valid
330
  return true;
3,879✔
331
};
332

333
/**
334
 *
335
 * @param {*} startTime
336
 * @param {*} endTime
337
 * @param {*} absoluteValues
338
 * @returns
339
 */
340
export const invalidEndDate = (startTime, endTime, absoluteValues) => {
142✔
341
  // If start and end date have been selected
342
  const formatedStartTime = format12hourTo24hour(startTime);
14,369✔
343
  const formatedEndTime = format12hourTo24hour(endTime);
14,369✔
344
  if (
14,369✔
345
    absoluteValues.hasOwnProperty('start') &&
42,959✔
346
    absoluteValues.hasOwnProperty('end') &&
347
    isValidDate(new Date(absoluteValues.end), formatedEndTime)
348
  ) {
349
    const startDate = new Date(`${absoluteValues.startDate} ${formatedStartTime}`);
8,637✔
350
    const endDate = new Date(`${absoluteValues.endDate} ${formatedEndTime}`);
8,637✔
351
    return startDate >= endDate;
8,637✔
352
  }
353

354
  // Return invalid date if start time and end date not selected or if inputted time is not valid
355
  return true;
5,732✔
356
};
357

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

377
  // Util func to update the absolute value
378
  const changeAbsolutePropertyValue = (property, value) => {
37,195✔
379
    setAbsoluteValue((prev) => ({
3,547✔
380
      ...prev,
381
      [property]: value,
382
    }));
383
  };
384

385
  const resetAbsoluteValue = () => {
37,195✔
386
    setAbsoluteValue({
912✔
387
      startDate: '',
388
      startTime: null,
389
      endDate: '',
390
      endTime: null,
391
    });
392
  };
393

394
  // on change functions that trigger a absolute value update
395
  const onAbsoluteStartTimeChange = (startTime, evt, meta) => {
37,195✔
396
    const { endTime } = absoluteValue;
1,582✔
397
    const invalidStart = invalidStartDate(startTime, endTime, absoluteValue);
1,582✔
398
    const invalidEnd = invalidEndDate(startTime, endTime, absoluteValue);
1,582✔
399
    setAbsoluteStartTimeInvalid(meta.invalid || invalidStart);
1,582✔
400
    setAbsoluteEndTimeInvalid(invalidEnd);
1,582✔
401
    changeAbsolutePropertyValue('startTime', startTime);
1,582✔
402
  };
403

404
  const onAbsoluteEndTimeChange = (endTime, evt, meta) => {
37,195✔
405
    const { startTime } = absoluteValue;
1,965✔
406
    const invalidEnd = invalidEndDate(startTime, endTime, absoluteValue);
1,965✔
407
    const invalidStart = invalidStartDate(startTime, endTime, absoluteValue);
1,965✔
408
    setAbsoluteEndTimeInvalid(meta.invalid || invalidEnd);
1,965✔
409
    setAbsoluteStartTimeInvalid(invalidStart);
1,965✔
410
    changeAbsolutePropertyValue('endTime', endTime);
1,965✔
411
  };
412

413
  return {
37,195✔
414
    absoluteValue,
415
    setAbsoluteValue,
416
    absoluteStartTimeInvalid,
417
    setAbsoluteStartTimeInvalid,
418
    absoluteEndTimeInvalid,
419
    setAbsoluteEndTimeInvalid,
420
    onAbsoluteStartTimeChange,
421
    onAbsoluteEndTimeChange,
422
    resetAbsoluteValue,
423
    changeAbsolutePropertyValue,
424
    format12hourTo24hour,
425
    isValid12HourTime,
426
    isValid24HourTime,
427
  };
428
};
429

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

452
  const resetRelativeValue = () => {
37,195✔
453
    setRelativeValue({
1,576✔
454
      lastNumber: 0,
455
      lastInterval: defaultInterval,
456
      relativeToWhen: defaultRelativeTo,
457
      relativeToTime: '',
458
    });
459
  };
460

461
  // Util func to update the relative value
462
  const changeRelativePropertyValue = (property, value) => {
37,195✔
463
    setRelativeValue((prev) => ({
1,543✔
464
      ...prev,
465
      [property]: value,
466
    }));
467
  };
468

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

488
  return {
37,195✔
489
    relativeValue,
490
    setRelativeValue,
491
    relativeToTimeInvalid,
492
    setRelativeToTimeInvalid,
493
    relativeLastNumberInvalid,
494
    setRelativeLastNumberInvalid,
495
    resetRelativeValue,
496
    onRelativeLastNumberChange,
497
    onRelativeLastIntervalChange,
498
    onRelativeToWhenChange,
499
    onRelativeToTimeChange,
500
  };
501
};
502

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

514
  const onCustomRangeChange = (kind) => {
37,195✔
515
    setCustomRangeKind(kind);
170✔
516
  };
517

518
  return [customRangeKind, setCustomRangeKind, onCustomRangeChange];
37,195✔
519
};
520

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

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

548
    return [];
×
549
  };
550

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

574
  const onFieldClick = (e) => {
37,195✔
575
    if (e.target.innerText !== 'Apply') setIsExpanded(!isExpanded);
755✔
576
  };
577

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

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

608
  const onNavigatePresets = ({ key }) => {
37,195✔
609
    switch (key) {
373!
610
      case 'ArrowUp':
611
        moveToPreviousElement();
125✔
612
        break;
125✔
613
      case 'ArrowDown':
614
        moveToNextElement();
248✔
615
        break;
248✔
616
      default:
617
        break;
×
618
    }
619
  };
620

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

638
  return {
37,195✔
639
    presetListRef,
640
    isExpanded,
641
    setIsExpanded,
642
    getFocusableSiblings,
643
    onFieldInteraction,
644
    onNavigateRadioButton,
645
    onNavigatePresets,
646
    onFieldClick,
647
  };
648
};
649

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

671
  return '';
2,287✔
672
};
673

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

683
  /**
684
   * Shows and hides the tooltip with the humanValue (Relative) or full-range (Absolute) when
685
   * the user focuses or hovers on the input
686
   */
687
  const toggleTooltip = () => {
37,195✔
688
    if (isExpanded) {
8,135✔
689
      setIsTooltipOpen(false);
5,475✔
690
    } else {
691
      setIsTooltipOpen((prev) => !prev);
2,660✔
692
    }
693
  };
694

695
  return [isTooltipOpen, toggleTooltip, setIsTooltipOpen];
37,195✔
696
};
697

698
/**
699
 * Hook to validate event and invoke callback
700
 * @param {function} closeDropdownCallback: function that will be called if validation passes
701
 * @returns void
702
 */
703
export const useDateTimePickerClickOutside = (closeDropdownCallback, containerRef) => (evt) => {
37,195✔
704
  if (
2,171✔
705
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__listitem--custom`) ||
10,195✔
706
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-back`) ||
707
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-reset`) ||
708
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-cancel`) ||
709
    evt?.target.classList?.contains(`${iotPrefix}--date-time-picker__menu-btn-apply`)
710
  ) {
711
    return;
657✔
712
  }
713

714
  if (containerRef.current?.firstChild.contains(evt.target)) {
1,514✔
715
    closeDropdownCallback({ isEventOnField: true });
271✔
716
    return;
271✔
717
  }
718

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

727
  closeDropdownCallback({ isEventOnField: false });
1,236✔
728
};
729

730
/**
731
 * Utility function to get time picker kind key
732
 * @param {Object} object: an object containing:
733
 *   kind: time picker kind
734
 *   timeRangeKind: time range kind
735
 * @returns
736
 */
737
const getTimeRangeKindKey = ({ kind, timeRangeKind }) => {
142✔
738
  if (kind === PICKER_KINDS.SINGLE || timeRangeKind === PICKER_KINDS.SINGLE) {
36✔
739
    return 'timeSingleValue';
5✔
740
  }
741
  return 'timeRangeValue';
31✔
742
};
743

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

776
      if (!isEventOnField) {
252✔
777
        setIsExpanded(false);
209✔
778
      }
779

780
      // memoized value at the time when dropdown was opened
781
      if (!isCustomRange) {
252✔
782
        setIsCustomRange(false);
168✔
783
      }
784

785
      if (
252✔
786
        (lastAppliedValue?.timeRangeKind === PICKER_KINDS.SINGLE ||
509✔
787
          lastAppliedValue?.kind === PICKER_KINDS.SINGLE) &&
788
        !singleTimeValue
789
      ) {
790
        setSingleDateValue({ start: null, startDate: null });
8✔
791
        setSingleTimeValue(null);
8✔
792
        return;
8✔
793
      }
794

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

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

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

887
  const closeDropDown = () => {
31,175✔
888
    setIsExpanded(false);
5,308✔
889
  };
890

891
  useEffect(() => {
31,175✔
892
    const firstScrollableParent = getScrollParent(containerRef.current);
1,192✔
893
    if (firstScrollableParent) {
1,192✔
894
      firstScrollableParent.addEventListener('scroll', closeDropDown);
1,071✔
895
    }
896
    window.addEventListener('scroll', closeDropDown);
1,192✔
897
    return () => {
1,192✔
898
      if (firstScrollableParent) {
1,136✔
899
        firstScrollableParent.removeEventListener('scroll', closeDropDown);
1,022✔
900
      }
901
      window.removeEventListener('scroll', closeDropDown);
1,136✔
902
    };
903
    // eslint-disable-next-line react-hooks/exhaustive-deps
904
  }, []);
905

906
  // re-calculate window height when resize
907
  const [windowHeight, setWindowHeight] = useState(
31,175✔
908
    window.innerHeight || document.documentElement.clientHeight
31,175!
909
  );
910

911
  const handleWindowResize = debounce(() => {
31,175✔
912
    setWindowHeight(window.innerHeight || document.documentElement.clientHeight);
384!
913
  }, 50);
914

915
  useEffect(() => {
31,175✔
916
    window.addEventListener('resize', handleWindowResize);
26,261✔
917
    return () => window.removeEventListener('resize', handleWindowResize);
26,261✔
918
  }, [handleWindowResize]);
919

920
  // calculate if flyout menu will be off top or bottom of the screen
921
  const inputBottom = containerRef?.current?.getBoundingClientRect().bottom;
31,175✔
922
  const inputTop = containerRef?.current?.getBoundingClientRect().top;
31,175✔
923
  const flyoutMenuHeight = maxHeight + footerHeight;
31,175✔
924
  const offBottom = windowHeight - inputBottom < flyoutMenuHeight;
31,175✔
925
  const offTop = inputTop < flyoutMenuHeight;
31,175✔
926
  const topGap = inputTop;
31,175✔
927
  const bottomGap = windowHeight - inputBottom;
31,175✔
928

929
  const customHeight =
930
    offBottom && offTop ? (topGap > bottomGap ? topGap : bottomGap) - footerHeight : undefined;
31,175!
931

932
  return [
31,175✔
933
    offTop,
934
    offBottom,
935
    inputTop,
936
    inputBottom,
937
    customHeight,
938
    maxHeight,
939
    invalidDateWarningHeight,
940
    invalidTimeWarningHeight,
941
    timeInputHeight,
942
  ];
943
};
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