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

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

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

push

github

carbon-bot
v2.153.0

7777 of 8122 branches covered (0.0%)

Branch coverage included in aggregate %.

9375 of 9480 relevant lines covered (98.89%)

2510.49 hits per line

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

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(' ');
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) => {
142✔
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) => {
142✔
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 }) => {
142✔
191
  const previousActiveElement = useRef(null);
34,264✔
192
  const [datePickerElem, setDatePickerElem] = useState(null);
34,264✔
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,264✔
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,264✔
209
    const timeout = setTimeout(() => {
4,197✔
210
      if (datePickerElem) {
3,021✔
211
        datePickerElem.cal.open();
952✔
212
        // while waiting for https://github.com/carbon-design-system/carbon/issues/5713
213
        // the only way to display the calendar inline is to re-parent its DOM to our component
214

215
        if (v2) {
952✔
216
          const dp = document.getElementById(`${id}-${iotPrefix}--date-time-picker__datepicker`);
815✔
217
          dp.appendChild(datePickerElem.cal.calendarContainer);
815✔
218
        } else {
219
          const wrapper = document.getElementById(`${id}-${iotPrefix}--date-time-picker__wrapper`);
137✔
220

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

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

238
    return () => {
4,197✔
239
      clearTimeout(timeout);
4,105✔
240
    };
241
  }, [datePickerElem, id, v2]);
242

243
  return [datePickerElem, handleDatePickerRef];
34,264✔
244
};
245

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

255
  useEffect(() => {
34,264✔
256
    if (datePickerElem && datePickerElem.inputField && datePickerElem.toInputField) {
3,512✔
257
      if (focusOnFirstField) {
1,279✔
258
        datePickerElem.inputField.click();
969✔
259
      } else {
260
        datePickerElem.toInputField.click();
310✔
261
      }
262
    }
263
  }, [datePickerElem, focusOnFirstField]);
264

265
  return [focusOnFirstField, setFocusOnFirstField];
34,264✔
266
};
267

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

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

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

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

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

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

356
  // Return invalid date if start time and end date not selected or if inputted time is not valid
357
  return true;
4,780✔
358
};
359

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

379
  // Util func to update the absolute value
380
  const changeAbsolutePropertyValue = (property, value) => {
34,264✔
381
    setAbsoluteValue((prev) => ({
3,526✔
382
      ...prev,
383
      [property]: value,
384
    }));
385
  };
386

387
  const resetAbsoluteValue = () => {
34,264✔
388
    setAbsoluteValue({
912✔
389
      startDate: '',
390
      startTime: null,
391
      endDate: '',
392
      endTime: null,
393
    });
394
  };
395

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

406
  const onAbsoluteEndTimeChange = (endTime, evt, meta) => {
34,264✔
407
    const { startTime } = absoluteValue;
1,945✔
408
    const invalidEnd = invalidEndDate(startTime, endTime, absoluteValue);
1,945✔
409
    const invalidStart = invalidStartDate(startTime, endTime, absoluteValue);
1,945✔
410
    setAbsoluteEndTimeInvalid(meta.invalid || invalidEnd);
1,945✔
411
    setAbsoluteStartTimeInvalid(invalidStart);
1,945✔
412
    changeAbsolutePropertyValue('endTime', endTime);
1,945✔
413
  };
414

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

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

454
  const resetRelativeValue = () => {
34,264✔
455
    setRelativeValue({
1,531✔
456
      lastNumber: 0,
457
      lastInterval: defaultInterval,
458
      relativeToWhen: defaultRelativeTo,
459
      relativeToTime: '',
460
    });
461
  };
462

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

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

490
  return {
34,264✔
491
    relativeValue,
492
    setRelativeValue,
493
    relativeToTimeInvalid,
494
    setRelativeToTimeInvalid,
495
    relativeLastNumberInvalid,
496
    setRelativeLastNumberInvalid,
497
    resetRelativeValue,
498
    onRelativeLastNumberChange,
499
    onRelativeLastIntervalChange,
500
    onRelativeToWhenChange,
501
    onRelativeToTimeChange,
502
  };
503
};
504

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

516
  const onCustomRangeChange = (kind) => {
34,264✔
517
    setCustomRangeKind(kind);
170✔
518
  };
519

520
  return [customRangeKind, setCustomRangeKind, onCustomRangeChange];
34,264✔
521
};
522

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

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

550
    return [];
×
551
  };
552

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

576
  const onFieldClick = (e) => {
34,264✔
577
    if (e.target.innerText !== 'Apply') setIsExpanded(!isExpanded);
675✔
578
  };
579

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

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

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

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

640
  return {
34,264✔
641
    presetListRef,
642
    isExpanded,
643
    setIsExpanded,
644
    getFocusableSiblings,
645
    onFieldInteraction,
646
    onNavigateRadioButton,
647
    onNavigatePresets,
648
    onFieldClick,
649
  };
650
};
651

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

673
  return '';
2,197✔
674
};
675

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

685
  /**
686
   * Shows and hides the tooltip with the humanValue (Relative) or full-range (Absolute) when
687
   * the user focuses or hovers on the input
688
   */
689
  const toggleTooltip = () => {
34,264✔
690
    if (isExpanded) {
7,747✔
691
      setIsTooltipOpen(false);
5,177✔
692
    } else {
693
      setIsTooltipOpen((prev) => !prev);
2,570✔
694
    }
695
  };
696

697
  return [isTooltipOpen, toggleTooltip, setIsTooltipOpen];
34,264✔
698
};
699

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

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

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

729
  closeDropdownCallback({ isEventOnField: false });
1,236✔
730
};
731

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

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

778
      if (!isEventOnField) {
252✔
779
        setIsExpanded(false);
209✔
780
      }
781

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

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

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

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

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

889
  const closeDropDown = () => {
28,244✔
890
    setIsExpanded(false);
5,932✔
891
  };
892

893
  useEffect(() => {
28,244✔
894
    const firstScrollableParent = getScrollParent(containerRef.current);
1,147✔
895
    if (firstScrollableParent) {
1,147✔
896
      firstScrollableParent.addEventListener('scroll', closeDropDown);
1,026✔
897
    }
898
    window.addEventListener('scroll', closeDropDown);
1,147✔
899
    return () => {
1,147✔
900
      if (firstScrollableParent) {
1,092✔
901
        firstScrollableParent.removeEventListener('scroll', closeDropDown);
978✔
902
      }
903
      window.removeEventListener('scroll', closeDropDown);
1,092✔
904
    };
905
    // eslint-disable-next-line react-hooks/exhaustive-deps
906
  }, []);
907

908
  // re-calculate window height when resize
909
  const [windowHeight, setWindowHeight] = useState(
28,244✔
910
    window.innerHeight || document.documentElement.clientHeight
28,244!
911
  );
912

913
  const handleWindowResize = debounce(() => {
28,244✔
914
    setWindowHeight(window.innerHeight || document.documentElement.clientHeight);
84!
915
  }, 50);
916

917
  useEffect(() => {
28,244✔
918
    window.addEventListener('resize', handleWindowResize);
24,289✔
919
    return () => window.removeEventListener('resize', handleWindowResize);
24,289✔
920
  }, [handleWindowResize]);
921

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

931
  const customHeight =
932
    offBottom && offTop ? (topGap > bottomGap ? topGap : bottomGap) - footerHeight : undefined;
28,244!
933

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