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

caleb531 / workday-time-calculator / 24693894526

20 Apr 2026 10:35PM UTC coverage: 87.062% (-0.05%) from 87.112%
24693894526

push

github

caleb531
Allow date to be commited when change event fires

621 of 694 branches covered (89.48%)

Branch coverage included in aggregate %.

22 of 26 new or added lines in 1 file covered. (84.62%)

2 existing lines in 1 file now uncovered.

2609 of 3016 relevant lines covered (86.51%)

159.01 hits per line

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

78.43
/scripts/components/date-input.jsx
1
import m from 'mithril';
2✔
2
import moment from 'moment';
2✔
3
import CalendarIconComponent from './calendar-icon.jsx';
2✔
4
import CalendarComponent from './calendar.jsx';
2✔
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 {
2✔
9
  oninit({ attrs }) {
2✔
10
    // Track whether the popup calendar is currently visible.
11
    this.calendarOpen = false;
64✔
12
    // Hold the delayed-close timer id used after selecting a date from the calendar.
13
    this.closeCalendarTimeoutId = null;
64✔
14
    // Keep references to the month/day/year inputs for keyboard-driven focus moves.
15
    this.segmentInputElements = {};
64✔
16
    // Remember which segment was focused most recently.
17
    this.activeSegment = 'month';
64✔
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 = '';
64✔
21
    this.segmentInputBufferSegment = null;
64✔
22
    this.segmentInputBufferUpdatedAt = 0;
64✔
23
    this.onbeforeupdate({ attrs });
64✔
24
  }
64✔
25

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

31
  onbeforeupdate({ attrs: { value, onChange } }) {
2✔
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);
166✔
35
    if (parsedValue.isValid()) {
166✔
36
      const nextValue = parsedValue.format('YYYY-MM-DD');
166✔
37
      const currentValue = this.selectedDate
166✔
38
        ? this.selectedDate.format('YYYY-MM-DD')
102✔
39
        : null;
64✔
40
      if (nextValue !== currentValue) {
166✔
41
        this.selectedDate = parsedValue;
64✔
42
        this.syncSegmentInputsFromSelectedDate();
64✔
43
      }
64✔
44
    }
166✔
45
    this.value = value;
166✔
46
    this.onChange = onChange;
166✔
47
  }
166✔
48

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

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

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

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

67
  setSegmentDisplayValue(segment, value) {
2✔
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);
10✔
71

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

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

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

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

97
  selectSegment(segment) {
2✔
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];
56✔
101
    if (!inputElement || document.activeElement !== inputElement) {
56!
102
      return;
×
103
    }
×
104
    inputElement.select();
56✔
105
  }
56✔
106

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

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

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

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

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

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

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

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

168
  createDateFromParts({ month, day, year }) {
2✔
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);
47✔
172
    const clampedMonth = Math.min(12, Math.max(1, month));
47✔
173
    const daysInMonth = moment([clampedYear, clampedMonth - 1]).daysInMonth();
47✔
174
    const clampedDay = Math.min(daysInMonth, Math.max(1, day));
47✔
175
    return moment([clampedYear, clampedMonth - 1, clampedDay]);
47✔
176
  }
47✔
177

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

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

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

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

206
  incrementSegment(segment, delta) {
2✔
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();
2✔
210

211
    if (segment === 'month') {
2✔
212
      parts.month = ((parts.month - 1 + delta + 12) % 12) + 1;
2✔
213
    } else if (segment === 'day') {
2!
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));
2✔
221
    this.focusSegment(segment);
2✔
222
  }
2✔
223

224
  shouldWaitForSecondDigit(segment, normalizedValue) {
2✔
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);
8✔
228
    return (
8✔
229
      normalizedValue.length === 1 &&
8✔
230
      (normalizedValue === '0' ||
6✔
231
        (segment === 'month' && numericValue <= 1) ||
6!
232
        (segment === 'day' && numericValue <= 3))
×
233
    );
234
  }
8✔
235

236
  finalizeSegment(segment) {
2✔
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(
39✔
240
      segment,
39✔
241
      this.getSegmentInputValue(segment)
39✔
242
    );
39✔
243

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

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

254
  handleSegmentDigit(segment, digit) {
2✔
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);
12✔
258

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

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

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

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

286
  parseAcceptedDateInput(inputValue) {
2✔
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() {
2✔
298
    // Toggle from the calendar button without leaving stale delayed-close timers.
299
    this.clearCloseCalendarTimeout();
2✔
300
    this.calendarOpen = !this.calendarOpen;
2✔
301
  }
2✔
302

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

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

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

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

337
  handleSegmentMouseDown(segment) {
2✔
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;
24✔
341
  }
24✔
342

343
  handleSegmentMouseUp(segment, event) {
2✔
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) {
24✔
347
      this.activeSegment = segment;
24✔
348
      event.preventDefault();
24✔
349
      event.target.select();
24✔
350
    }
24✔
351
  }
24✔
352

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

358
  handleSegmentChange(segment, event) {
2✔
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(
2✔
362
      segment,
2✔
363
      event.target.value
2✔
364
    );
2✔
365

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

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

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

381
  handleSegmentInput(segment, event) {
2✔
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();
2✔
385

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

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

396
    if (segment === 'year') {
2!
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) {
2!
405
      this.setSegmentDisplayValue(segment, normalizedValue.padStart(2, '0'));
×
406
    }
×
407

408
    if (this.shouldWaitForSecondDigit(segment, normalizedValue)) {
2!
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));
2✔
417
    const nextSegment = this.getAdjacentSegment(segment, 1);
2✔
418
    if (nextSegment) {
2✔
419
      this.focusSegment(nextSegment);
2✔
420
    }
2✔
421
  }
2✔
422

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

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

436
    if (event.key === 'ArrowLeft') {
38!
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') {
38!
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') {
38✔
457
      event.preventDefault();
2✔
458
      this.incrementSegment(segment, 1);
2✔
459
      return;
2✔
460
    }
2✔
461

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

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

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

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

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

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

505
  handleSegmentPaste(event) {
2✔
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) {
2✔
518
    // Calendar selections flow through the same commit path as keyboard edits.
519
    this.commitSelectedDate(selectedDate);
2✔
520
    this.closeCalendarWithDelay();
2✔
521
  }
2✔
522

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

596
        {this.selectedDate && this.calendarOpen ? (
166✔
597
          <CalendarComponent
4✔
598
            className="date-input-calendar"
4✔
599
            selectedDate={this.selectedDate}
4✔
600
            calendarOpen={this.calendarOpen}
4✔
601
            onShouldIgnoreOutsideClick={(target) => {
4✔
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) => {
4✔
617
              this.setSelectedDate(selectedDate);
2✔
618
            }}
2✔
619
            onCloseCalendar={() => {
4✔
620
              this.closeCalendar();
×
621
            }}
×
622
          />
4✔
623
        ) : null}
162✔
624
      </div>
166✔
625
    );
626
  }
166✔
627
}
2✔
628

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