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

caleb531 / workday-time-calculator / 26660516246

29 May 2026 08:28PM UTC coverage: 80.912% (-6.2%) from 87.062%
26660516246

push

github

caleb531
Drop Node 20 from CI; add Node 24

415 of 533 branches covered (77.86%)

Branch coverage included in aggregate %.

1005 of 1222 relevant lines covered (82.24%)

141.3 hits per line

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

67.75
/scripts/components/date-input.jsx
1
import m from 'mithril';
2
import moment from 'moment';
3
import CalendarIconComponent from './calendar-icon.jsx';
4
import CalendarComponent from './calendar.jsx';
5

6
// Analytics-only custom date field built from three text inputs so each segment
7
// can receive native focus independently while still presenting as one control.
8
class DateInputComponent {
9
  oninit({ attrs }) {
10
    // Track whether the popup calendar is currently visible.
11
    this.calendarOpen = false;
32✔
12
    // Hold the delayed-close timer id used after selecting a date from the calendar.
13
    this.closeCalendarTimeoutId = null;
32✔
14
    // Keep references to the month/day/year inputs for keyboard-driven focus moves.
15
    this.segmentInputElements = {};
32✔
16
    // Remember which segment was focused most recently.
17
    this.activeSegment = 'month';
32✔
18
    // Buffer in-progress digit entry so month/day can show leading zeroes while
19
    // still allowing a second digit to replace the placeholder zero.
20
    this.segmentInputBuffer = '';
32✔
21
    this.segmentInputBufferSegment = null;
32✔
22
    this.segmentInputBufferUpdatedAt = 0;
32✔
23
    this.onbeforeupdate({ attrs });
32✔
24
  }
25

26
  onremove() {
27
    // Clean up async work so the component does not mutate state after unmount.
28
    this.clearCloseCalendarTimeout();
32✔
29
  }
30

31
  onbeforeupdate({ attrs: { value, onChange } }) {
32
    // The public value stays normalized as YYYY-MM-DD even though the field is
33
    // rendered and edited as segmented MM/DD/YYYY inputs.
34
    const parsedValue = moment(value, 'YYYY-MM-DD', true);
88✔
35
    if (parsedValue.isValid()) {
88!
36
      const nextValue = parsedValue.format('YYYY-MM-DD');
88✔
37
      const currentValue = this.selectedDate
88✔
38
        ? this.selectedDate.format('YYYY-MM-DD')
39
        : null;
40
      if (nextValue !== currentValue) {
88✔
41
        this.selectedDate = parsedValue;
32✔
42
        this.syncSegmentInputsFromSelectedDate();
32✔
43
      }
44
    }
45
    this.value = value;
88✔
46
    this.onChange = onChange;
88✔
47
  }
48

49
  get segmentOrder() {
50
    // Canonical navigation order for keyboard movement across the three inputs.
51
    return ['month', 'day', 'year'];
27✔
52
  }
53

54
  getSegmentLength(segment) {
55
    // Month and day are two digits, while year is four digits.
56
    return segment === 'year' ? 4 : 2;
24✔
57
  }
58

59
  getSegmentInputValue(segment) {
60
    return this[`${segment}InputValue`] || '';
20!
61
  }
62

63
  setSegmentInputValue(segment, value) {
64
    this[`${segment}InputValue`] = value;
7✔
65
  }
66

67
  setSegmentDisplayValue(segment, value) {
68
    // Update both component state and the live DOM input so the segment display
69
    // changes immediately even before the next redraw cycle completes.
70
    this.setSegmentInputValue(segment, value);
5✔
71

72
    const inputElement = this.segmentInputElements[segment];
5✔
73
    if (inputElement) {
5!
74
      inputElement.value = value;
5✔
75
    }
76
  }
77

78
  syncSegmentInputsFromSelectedDate() {
79
    // Keep the visible segment strings in sync with the committed date.
80
    if (!this.selectedDate) {
57!
81
      this.monthInputValue = '';
×
82
      this.dayInputValue = '';
×
83
      this.yearInputValue = '';
×
84
      return;
×
85
    }
86

87
    this.monthInputValue = this.selectedDate.format('MM');
57✔
88
    this.dayInputValue = this.selectedDate.format('DD');
57✔
89
    this.yearInputValue = this.selectedDate.format('YYYY');
57✔
90
  }
91

92
  setSegmentInputElement(segment, dom) {
93
    // Keep a direct reference because keyboard navigation moves focus imperatively.
94
    this.segmentInputElements[segment] = dom;
264✔
95
  }
96

97
  selectSegment(segment) {
98
    // Native date inputs highlight the whole active segment, so mirror that
99
    // behavior by selecting the entire segment input value.
100
    const inputElement = this.segmentInputElements[segment];
28✔
101
    if (!inputElement || document.activeElement !== inputElement) {
28!
102
      return;
×
103
    }
104
    inputElement.select();
28✔
105
  }
106

107
  focusSegment(segment) {
108
    // Move focus to a sibling segment and immediately highlight it.
109
    const inputElement = this.segmentInputElements[segment];
9✔
110
    if (!inputElement) {
9!
111
      return;
×
112
    }
113

114
    this.activeSegment = segment;
9✔
115
    inputElement.focus();
9✔
116
    inputElement.select();
9✔
117
  }
118

119
  resetSegmentInputBuffer() {
120
    // Discard any partially typed digits for the current segment.
121
    this.segmentInputBuffer = '';
23✔
122
    this.segmentInputBufferSegment = null;
23✔
123
    this.segmentInputBufferUpdatedAt = 0;
23✔
124
  }
125

126
  updateSegmentInputBuffer(segment, digit) {
127
    // Append digits typed in quick succession to the same segment; otherwise,
128
    // start a new segment edit.
129
    const now = Date.now();
6✔
130
    const shouldAppendToBuffer =
131
      this.segmentInputBufferSegment === segment &&
6✔
132
      now - this.segmentInputBufferUpdatedAt < 1500 &&
133
      this.segmentInputBuffer.length < this.getSegmentLength(segment);
134

135
    this.segmentInputBuffer = shouldAppendToBuffer
6✔
136
      ? `${this.segmentInputBuffer}${digit}`
137
      : digit;
138
    this.segmentInputBufferSegment = segment;
6✔
139
    this.segmentInputBufferUpdatedAt = now;
6✔
140
    return this.segmentInputBuffer;
6✔
141
  }
142

143
  getAdjacentSegment(segment, offset) {
144
    const currentIndex = this.segmentOrder.indexOf(segment);
10✔
145
    const nextIndex = currentIndex + offset;
10✔
146
    if (nextIndex < 0 || nextIndex >= this.segmentOrder.length) {
10✔
147
      return null;
2✔
148
    }
149
    return this.segmentOrder[nextIndex];
8✔
150
  }
151

152
  normalizeSegmentInputValue(segment, inputValue) {
153
    // Strip any non-digit characters and clamp to the segment's maximum length.
154
    return inputValue
22✔
155
      .replace(/\D/g, '')
156
      .slice(0, this.getSegmentLength(segment));
157
  }
158

159
  getSegmentParts(date = this.selectedDate) {
24✔
160
    // Work with plain numeric parts when performing segment-level edits.
161
    return {
24✔
162
      month: date.month() + 1,
163
      day: date.date(),
164
      year: date.year()
165
    };
166
  }
167

168
  createDateFromParts({ month, day, year }) {
169
    // Clamp the resulting parts into a valid Gregorian date so edits like moving
170
    // from January 31 to February produce a valid last day of the month.
171
    const clampedYear = Math.max(1, year);
24✔
172
    const clampedMonth = Math.min(12, Math.max(1, month));
24✔
173
    const daysInMonth = moment([clampedYear, clampedMonth - 1]).daysInMonth();
24✔
174
    const clampedDay = Math.min(daysInMonth, Math.max(1, day));
24✔
175
    return moment([clampedYear, clampedMonth - 1, clampedDay]);
24✔
176
  }
177

178
  commitSelectedDate(selectedDate) {
179
    // Persist the canonical date back to the parent component in normalized form.
180
    const nextValue = selectedDate.format('YYYY-MM-DD');
25✔
181
    const didValueChange = nextValue !== this.value;
25✔
182

183
    this.selectedDate = selectedDate.clone();
25✔
184
    this.syncSegmentInputsFromSelectedDate();
25✔
185
    this.value = nextValue;
25✔
186

187
    if (didValueChange) {
25✔
188
      this.onChange(this.value);
6✔
189
    }
190
  }
191

192
  commitSegmentValue(segment, segmentValue) {
193
    // Apply an edited segment while preserving the other two segments.
194
    const parts = this.getSegmentParts();
23✔
195
    if (segment === 'month') {
23✔
196
      parts.month = Math.min(12, Math.max(1, segmentValue));
10✔
197
    } else if (segment === 'day') {
13✔
198
      const daysInMonth = moment([parts.year, parts.month - 1]).daysInMonth();
8✔
199
      parts.day = Math.min(daysInMonth, Math.max(1, segmentValue));
8✔
200
    } else {
201
      parts.year = Math.max(1, segmentValue);
5✔
202
    }
203
    this.commitSelectedDate(this.createDateFromParts(parts));
23✔
204
  }
205

206
  incrementSegment(segment, delta) {
207
    // Arrow keys step the active segment and wrap month/day like native desktop
208
    // date fields while clamping year at 1.
209
    const parts = this.getSegmentParts();
1✔
210

211
    if (segment === 'month') {
1!
212
      parts.month = ((parts.month - 1 + delta + 12) % 12) + 1;
1✔
213
    } else if (segment === 'day') {
×
214
      const daysInMonth = moment([parts.year, parts.month - 1]).daysInMonth();
×
215
      parts.day = ((parts.day - 1 + delta + daysInMonth) % daysInMonth) + 1;
×
216
    } else {
217
      parts.year = Math.max(1, parts.year + delta);
×
218
    }
219

220
    this.commitSelectedDate(this.createDateFromParts(parts));
1✔
221
    this.focusSegment(segment);
1✔
222
  }
223

224
  shouldWaitForSecondDigit(segment, normalizedValue) {
225
    // Month/day allow a brief pause after digits that could still lead to a
226
    // valid two-digit value, such as 0_, 1_, 2_, or 3_.
227
    const numericValue = parseInt(normalizedValue, 10);
4✔
228
    return (
4✔
229
      normalizedValue.length === 1 &&
13!
230
      (normalizedValue === '0' ||
231
        (segment === 'month' && numericValue <= 1) ||
232
        (segment === 'day' && numericValue <= 3))
233
    );
234
  }
235

236
  finalizeSegment(segment) {
237
    // Commit a partially edited segment when focus leaves it, or revert to the
238
    // last valid committed date if the segment is left empty.
239
    const normalizedValue = this.normalizeSegmentInputValue(
20✔
240
      segment,
241
      this.getSegmentInputValue(segment)
242
    );
243

244
    if (!normalizedValue) {
20!
245
      this.resetSegmentInputBuffer();
×
246
      this.syncSegmentInputsFromSelectedDate();
×
247
      return;
×
248
    }
249

250
    this.commitSegmentValue(segment, parseInt(normalizedValue, 10));
20✔
251
    this.resetSegmentInputBuffer();
20✔
252
  }
253

254
  handleSegmentDigit(segment, digit) {
255
    // Numeric typing is handled explicitly so month/day can display a padded
256
    // leading zero while still waiting for a possible second digit.
257
    const bufferedValue = this.updateSegmentInputBuffer(segment, digit);
6✔
258

259
    if (segment === 'year') {
6✔
260
      this.setSegmentDisplayValue(segment, bufferedValue.padStart(4, '0'));
2✔
261
      if (bufferedValue.length === 4) {
2!
262
        this.commitSegmentValue('year', parseInt(bufferedValue, 10));
×
263
        this.resetSegmentInputBuffer();
×
264
      }
265
      this.selectSegment(segment);
2✔
266
      return;
2✔
267
    }
268

269
    if (bufferedValue.length === 1) {
4✔
270
      this.setSegmentDisplayValue(segment, bufferedValue.padStart(2, '0'));
3✔
271
      if (this.shouldWaitForSecondDigit(segment, bufferedValue)) {
3!
272
        this.selectSegment(segment);
3✔
273
        return;
3✔
274
      }
275
    }
276

277
    this.commitSegmentValue(segment, parseInt(bufferedValue, 10));
1✔
278
    this.resetSegmentInputBuffer();
1✔
279

280
    const nextSegment = this.getAdjacentSegment(segment, 1);
1✔
281
    if (nextSegment) {
1!
282
      this.focusSegment(nextSegment);
1✔
283
    }
284
  }
285

286
  parseAcceptedDateInput(inputValue) {
287
    // Paste still accepts a few common date formats and normalizes them back
288
    // into the field's canonical display format.
289
    const parsedValue = moment(
×
290
      inputValue,
291
      ['MM/DD/YYYY', 'M/D/YYYY', 'YYYY-MM-DD'],
292
      true
293
    );
294
    return parsedValue.isValid() ? parsedValue : null;
×
295
  }
296

297
  toggleCalendar() {
298
    // Toggle from the calendar button without leaving stale delayed-close timers.
299
    this.clearCloseCalendarTimeout();
1✔
300
    this.calendarOpen = !this.calendarOpen;
1✔
301
  }
302

303
  closeCalendar() {
304
    // Close immediately, typically after outside clicks.
305
    this.clearCloseCalendarTimeout();
×
306
    this.calendarOpen = false;
×
307
  }
308

309
  clearCloseCalendarTimeout() {
310
    // Ensure only one delayed-close timer exists at a time.
311
    if (this.closeCalendarTimeoutId) {
34✔
312
      window.clearTimeout(this.closeCalendarTimeoutId);
1✔
313
      this.closeCalendarTimeoutId = null;
1✔
314
    }
315
  }
316

317
  closeCalendarWithDelay() {
318
    // Leave the calendar visible briefly after selecting a date so the click
319
    // feels acknowledged before the popup disappears.
320
    this.clearCloseCalendarTimeout();
1✔
321
    this.closeCalendarTimeoutId = window.setTimeout(() => {
1✔
322
      this.calendarOpen = false;
×
323
      this.closeCalendarTimeoutId = null;
×
324
      m.redraw();
×
325
    }, 250);
326
  }
327

328
  handleSegmentFocus(segment) {
329
    // Focusing any segment should highlight its full value.
330
    if (this.activeSegment !== segment) {
21!
331
      this.resetSegmentInputBuffer();
×
332
    }
333
    this.activeSegment = segment;
21✔
334
    this.selectSegment(segment);
21✔
335
  }
336

337
  handleSegmentMouseDown(segment) {
338
    // Remember the clicked segment as soon as the pointer goes down so the
339
    // browser's natural focus target and the component's active segment match.
340
    this.activeSegment = segment;
12✔
341
  }
342

343
  handleSegmentMouseUp(segment, event) {
344
    // Re-select the whole segment after click placement so the field still feels
345
    // like one segmented date input instead of three freeform text fields.
346
    if (document.activeElement === event.target) {
12!
347
      this.activeSegment = segment;
12✔
348
      event.preventDefault();
12✔
349
      event.target.select();
12✔
350
    }
351
  }
352

353
  handleSegmentBlur(segment) {
354
    // Normalize or revert partial edits whenever a segment loses focus.
355
    this.finalizeSegment(segment);
11✔
356
  }
357

358
  handleSegmentChange(segment, event) {
359
    // Native change events should commit from the input's current visible value
360
    // so padded single-digit edits become real dates immediately.
361
    const normalizedValue = this.normalizeSegmentInputValue(
1✔
362
      segment,
363
      event.target.value
364
    );
365

366
    if (!normalizedValue) {
1!
367
      this.resetSegmentInputBuffer();
×
368
      this.syncSegmentInputsFromSelectedDate();
×
369
      return;
×
370
    }
371

372
    this.setSegmentInputValue(segment, normalizedValue);
1✔
373
    this.commitSegmentValue(segment, parseInt(normalizedValue, 10));
1✔
374
    this.resetSegmentInputBuffer();
1✔
375

376
    if (document.activeElement === event.target) {
1!
377
      this.selectSegment(segment);
1✔
378
    }
379
  }
380

381
  handleSegmentInput(segment, event) {
382
    // Sanitize typed digits and progressively commit or auto-advance when the
383
    // segment has enough information to become a valid date part.
384
    this.resetSegmentInputBuffer();
1✔
385

386
    const normalizedValue = this.normalizeSegmentInputValue(
1✔
387
      segment,
388
      event.target.value
389
    );
390
    this.setSegmentInputValue(segment, normalizedValue);
1✔
391

392
    if (!normalizedValue) {
1!
393
      return;
×
394
    }
395

396
    if (segment === 'year') {
1!
397
      this.setSegmentDisplayValue(segment, normalizedValue.padStart(4, '0'));
×
398
      if (normalizedValue.length === 4) {
×
399
        this.commitSegmentValue('year', parseInt(normalizedValue, 10));
×
400
      }
401
      return;
×
402
    }
403

404
    if (normalizedValue.length === 1) {
1!
405
      this.setSegmentDisplayValue(segment, normalizedValue.padStart(2, '0'));
×
406
    }
407

408
    if (this.shouldWaitForSecondDigit(segment, normalizedValue)) {
1!
409
      this.segmentInputBuffer = normalizedValue;
×
410
      this.segmentInputBufferSegment = segment;
×
411
      this.segmentInputBufferUpdatedAt = Date.now();
×
412
      this.selectSegment(segment);
×
413
      return;
×
414
    }
415

416
    this.commitSegmentValue(segment, parseInt(normalizedValue, 10));
1✔
417
    const nextSegment = this.getAdjacentSegment(segment, 1);
1✔
418
    if (nextSegment) {
1!
419
      this.focusSegment(nextSegment);
1✔
420
    }
421
  }
422

423
  handleSegmentKeyDown(segment, event) {
424
    // The field owns navigation semantics between segments while leaving normal
425
    // text editing to the focused segment input.
426
    if (!this.selectedDate) {
19!
427
      return;
×
428
    }
429

430
    if (/^[0-9]$/.test(event.key)) {
19✔
431
      event.preventDefault();
6✔
432
      this.handleSegmentDigit(segment, event.key);
6✔
433
      return;
6✔
434
    }
435

436
    if (event.key === 'ArrowLeft') {
13!
437
      const previousSegment = this.getAdjacentSegment(segment, -1);
×
438
      if (previousSegment) {
×
439
        event.preventDefault();
×
440
        this.finalizeSegment(segment);
×
441
        this.focusSegment(previousSegment);
×
442
      }
443
      return;
×
444
    }
445

446
    if (event.key === 'ArrowRight') {
13!
447
      const nextSegment = this.getAdjacentSegment(segment, 1);
×
448
      if (nextSegment) {
×
449
        event.preventDefault();
×
450
        this.finalizeSegment(segment);
×
451
        this.focusSegment(nextSegment);
×
452
      }
453
      return;
×
454
    }
455

456
    if (event.key === 'ArrowUp') {
13✔
457
      event.preventDefault();
1✔
458
      this.incrementSegment(segment, 1);
1✔
459
      return;
1✔
460
    }
461

462
    if (event.key === 'ArrowDown') {
12!
463
      event.preventDefault();
×
464
      this.incrementSegment(segment, -1);
×
465
      return;
×
466
    }
467

468
    if (event.key === 'Home') {
12!
469
      event.preventDefault();
×
470
      this.focusSegment('month');
×
471
      return;
×
472
    }
473

474
    if (event.key === 'End') {
12!
475
      event.preventDefault();
×
476
      this.focusSegment('year');
×
477
      return;
×
478
    }
479

480
    if (event.key === 'Tab') {
12✔
481
      const nextSegment = this.getAdjacentSegment(
8✔
482
        segment,
483
        event.shiftKey ? -1 : 1
8✔
484
      );
485
      this.finalizeSegment(segment);
8✔
486
      if (nextSegment) {
8✔
487
        event.preventDefault();
6✔
488
        this.focusSegment(nextSegment);
6✔
489
      }
490
      return;
8✔
491
    }
492

493
    if (event.key === 'Enter') {
4✔
494
      event.preventDefault();
1✔
495
      this.finalizeSegment(segment);
1✔
496
      this.selectSegment(segment);
1✔
497
      return;
1✔
498
    }
499

500
    if (event.key === 'Backspace' || event.key === 'Delete') {
3!
501
      this.resetSegmentInputBuffer();
×
502
    }
503
  }
504

505
  handleSegmentPaste(event) {
506
    // Normalized paste support keeps the custom field usable for power users.
507
    const pastedText = event.clipboardData.getData('text');
×
508
    const parsedValue = this.parseAcceptedDateInput(pastedText);
×
509
    if (!parsedValue) {
×
510
      return;
×
511
    }
512

513
    event.preventDefault();
×
514
    this.commitSelectedDate(parsedValue);
×
515
  }
516

517
  setSelectedDate(selectedDate) {
518
    // Calendar selections flow through the same commit path as keyboard edits.
519
    this.commitSelectedDate(selectedDate);
1✔
520
    this.closeCalendarWithDelay();
1✔
521
  }
522

523
  view({ attrs }) {
524
    // The component exposes only an aria-label; styling and behavior are owned
525
    // internally so consumers cannot accidentally break the segmented UX.
526
    const ariaLabel = attrs['aria-label'];
88✔
527
    return (
88✔
528
      <div className="date-input" role="group" aria-label={ariaLabel}>
529
        <input
530
          type="text"
531
          inputmode="numeric"
532
          className="date-input-segment date-input-segment-month"
533
          aria-label={`${ariaLabel} Month`}
534
          maxlength="2"
535
          value={this.monthInputValue}
536
          onfocus={() => this.handleSegmentFocus('month')}
10✔
537
          onblur={() => this.handleSegmentBlur('month')}
4✔
538
          onchange={(event) => this.handleSegmentChange('month', event)}
1✔
539
          onmousedown={() => this.handleSegmentMouseDown('month')}
8✔
540
          onmouseup={(event) => this.handleSegmentMouseUp('month', event)}
8✔
541
          onkeydown={(event) => this.handleSegmentKeyDown('month', event)}
10✔
542
          oninput={(event) => this.handleSegmentInput('month', event)}
×
543
          onpaste={(event) => this.handleSegmentPaste(event)}
×
544
          oncreate={({ dom }) => this.setSegmentInputElement('month', dom)}
32✔
545
          onupdate={({ dom }) => this.setSegmentInputElement('month', dom)}
56✔
546
        />
547
        <span className="date-input-separator">/</span>
548
        <input
549
          type="text"
550
          inputmode="numeric"
551
          className="date-input-segment date-input-segment-day"
552
          aria-label={`${ariaLabel} Day`}
553
          maxlength="2"
554
          value={this.dayInputValue}
555
          onfocus={() => this.handleSegmentFocus('day')}
4✔
556
          onblur={() => this.handleSegmentBlur('day')}
4✔
557
          onchange={(event) => this.handleSegmentChange('day', event)}
×
558
          onmousedown={() => this.handleSegmentMouseDown('day')}
×
559
          onmouseup={(event) => this.handleSegmentMouseUp('day', event)}
×
560
          onkeydown={(event) => this.handleSegmentKeyDown('day', event)}
4✔
561
          oninput={(event) => this.handleSegmentInput('day', event)}
1✔
562
          onpaste={(event) => this.handleSegmentPaste(event)}
×
563
          oncreate={({ dom }) => this.setSegmentInputElement('day', dom)}
32✔
564
          onupdate={({ dom }) => this.setSegmentInputElement('day', dom)}
56✔
565
        />
566
        <span className="date-input-separator">/</span>
567
        <input
568
          type="text"
569
          inputmode="numeric"
570
          className="date-input-segment date-input-segment-year"
571
          aria-label={`${ariaLabel} Year`}
572
          maxlength="4"
573
          value={this.yearInputValue}
574
          onfocus={() => this.handleSegmentFocus('year')}
7✔
575
          onblur={() => this.handleSegmentBlur('year')}
3✔
576
          onchange={(event) => this.handleSegmentChange('year', event)}
×
577
          onmousedown={() => this.handleSegmentMouseDown('year')}
4✔
578
          onmouseup={(event) => this.handleSegmentMouseUp('year', event)}
4✔
579
          onkeydown={(event) => this.handleSegmentKeyDown('year', event)}
5✔
580
          oninput={(event) => this.handleSegmentInput('year', event)}
×
581
          onpaste={(event) => this.handleSegmentPaste(event)}
×
582
          oncreate={({ dom }) => this.setSegmentInputElement('year', dom)}
32✔
583
          onupdate={({ dom }) => this.setSegmentInputElement('year', dom)}
56✔
584
        />
585
        <button
586
          type="button"
587
          className="date-input-calendar-toggle"
588
          aria-label={`Open ${ariaLabel} Calendar`}
589
          aria-haspopup="dialog"
590
          aria-expanded={this.calendarOpen ? 'true' : 'false'}
88✔
591
          onclick={() => this.toggleCalendar()}
1✔
592
        >
593
          <CalendarIconComponent selectedDate={this.selectedDate} />
594
        </button>
595

596
        {this.selectedDate && this.calendarOpen ? (
264✔
597
          <CalendarComponent
598
            className="date-input-calendar"
599
            selectedDate={this.selectedDate}
600
            calendarOpen={this.calendarOpen}
601
            onShouldIgnoreOutsideClick={(target) => {
602
              // Clicking the calendar toggle button should not be treated as an
603
              // outside click by the popup close handler.
604
              let element = target;
×
605
              while (element && element !== document) {
×
606
                if (
×
607
                  element.classList &&
×
608
                  element.classList.contains('date-input-calendar-toggle')
609
                ) {
610
                  return true;
×
611
                }
612
                element = element.parentNode;
×
613
              }
614
              return false;
×
615
            }}
616
            onSetSelectedDate={(selectedDate) => {
617
              this.setSelectedDate(selectedDate);
1✔
618
            }}
619
            onCloseCalendar={() => {
620
              this.closeCalendar();
×
621
            }}
622
          />
623
        ) : null}
624
      </div>
625
    );
626
  }
627
}
628

629
export default DateInputComponent;
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