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

caleb531 / workday-time-calculator / 24477271159

15 Apr 2026 08:40PM UTC coverage: 86.925% (+1.7%) from 85.183%
24477271159

push

github

caleb531
Improve behavior of custom date inputs

More predictable and less quirky.

613 of 685 branches covered (89.49%)

Branch coverage included in aggregate %.

227 of 276 new or added lines in 2 files covered. (82.25%)

21 existing lines in 2 files now uncovered.

2578 of 2986 relevant lines covered (86.34%)

153.38 hits per line

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

77.16
/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;
52✔
12
    // Hold the delayed-close timer id used after selecting a date from the calendar.
13
    this.closeCalendarTimeoutId = null;
52✔
14
    // Keep references to the month/day/year inputs for keyboard-driven focus moves.
15
    this.segmentInputElements = {};
52✔
16
    // Remember which segment was focused most recently.
17
    this.activeSegment = 'month';
52✔
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 = '';
52✔
21
    this.segmentInputBufferSegment = null;
52✔
22
    this.segmentInputBufferUpdatedAt = 0;
52✔
23
    this.onbeforeupdate({ attrs });
52✔
24
  }
52✔
25

26
  onremove() {
2✔
27
    // Clean up async work so the component does not mutate state after unmount.
28
    this.clearCloseCalendarTimeout();
52✔
29
  }
52✔
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);
134✔
35
    if (parsedValue.isValid()) {
134✔
36
      const nextValue = parsedValue.format('YYYY-MM-DD');
134✔
37
      const currentValue = this.selectedDate
134✔
38
        ? this.selectedDate.format('YYYY-MM-DD')
82✔
39
        : null;
52✔
40
      if (nextValue !== currentValue) {
134✔
41
        this.selectedDate = parsedValue;
52✔
42
        this.syncSegmentInputsFromSelectedDate();
52✔
43
      }
52✔
44
    }
134✔
45
    this.value = value;
134✔
46
    this.onChange = onChange;
134✔
47
  }
134✔
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;
42✔
57
  }
42✔
58

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

63
  setSegmentInputValue(segment, value) {
2✔
64
    this[`${segment}InputValue`] = value;
4✔
65
  }
4✔
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);
2✔
71

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

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

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

92
  setSegmentInputElement(segment, dom) {
2✔
93
    // Keep a direct reference because keyboard navigation moves focus imperatively.
94
    this.segmentInputElements[segment] = dom;
402✔
95
  }
402✔
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];
38✔
101
    if (!inputElement || document.activeElement !== inputElement) {
38!
UNCOV
102
      return;
×
UNCOV
103
    }
×
104
    inputElement.select();
38✔
105
  }
38✔
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!
NEW
111
      return;
×
UNCOV
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 = '';
42✔
122
    this.segmentInputBufferSegment = null;
42✔
123
    this.segmentInputBufferUpdatedAt = 0;
42✔
124
  }
42✔
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();
4✔
130
    const shouldAppendToBuffer =
4✔
131
      this.segmentInputBufferSegment === segment &&
4✔
132
      now - this.segmentInputBufferUpdatedAt < 1500 &&
2✔
133
      this.segmentInputBuffer.length < this.getSegmentLength(segment);
2✔
134

135
    this.segmentInputBuffer = shouldAppendToBuffer
4✔
136
      ? `${this.segmentInputBuffer}${digit}`
2✔
137
      : digit;
2✔
138
    this.segmentInputBufferSegment = segment;
4✔
139
    this.segmentInputBufferUpdatedAt = now;
4✔
140
    return this.segmentInputBuffer;
4✔
141
  }
4✔
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
40✔
155
      .replace(/\D/g, '')
40✔
156
      .slice(0, this.getSegmentLength(segment));
40✔
157
  }
40✔
158

159
  getSegmentParts(date = this.selectedDate) {
2✔
160
    // Work with plain numeric parts when performing segment-level edits.
161
    return {
44✔
162
      month: date.month() + 1,
44✔
163
      day: date.date(),
44✔
164
      year: date.year()
44✔
165
    };
44✔
166
  }
44✔
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);
44✔
172
    const clampedMonth = Math.min(12, Math.max(1, month));
44✔
173
    const daysInMonth = moment([clampedYear, clampedMonth - 1]).daysInMonth();
44✔
174
    const clampedDay = Math.min(daysInMonth, Math.max(1, day));
44✔
175
    return moment([clampedYear, clampedMonth - 1, clampedDay]);
44✔
176
  }
44✔
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');
46✔
181
    const didValueChange = nextValue !== this.value;
46✔
182

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

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

192
  commitSegmentValue(segment, segmentValue) {
2✔
193
    // Apply an edited segment while preserving the other two segments.
194
    const parts = this.getSegmentParts();
42✔
195
    if (segment === 'month') {
42✔
196
      parts.month = Math.min(12, Math.max(1, segmentValue));
16✔
197
    } else if (segment === 'day') {
42✔
198
      const daysInMonth = moment([parts.year, parts.month - 1]).daysInMonth();
16✔
199
      parts.day = Math.min(daysInMonth, Math.max(1, segmentValue));
16✔
200
    } else {
26✔
201
      parts.year = Math.max(1, segmentValue);
10✔
202
    }
10✔
203
    this.commitSelectedDate(this.createDateFromParts(parts));
42✔
204
  }
42✔
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);
4✔
228
    return (
4✔
229
      normalizedValue.length === 1 &&
4✔
230
      (normalizedValue === '0' ||
2✔
231
        (segment === 'month' && numericValue <= 1) ||
2!
NEW
232
        (segment === 'day' && numericValue <= 3))
×
233
    );
234
  }
4✔
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(
38✔
240
      segment,
38✔
241
      this.getSegmentInputValue(segment)
38✔
242
    );
38✔
243

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

250
    this.commitSegmentValue(segment, parseInt(normalizedValue, 10));
38✔
251
    this.resetSegmentInputBuffer();
38✔
252
  }
38✔
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);
4✔
258

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

269
    if (bufferedValue.length === 1) {
4✔
270
      this.setSegmentDisplayValue(segment, bufferedValue.padStart(2, '0'));
2✔
271
      if (this.shouldWaitForSecondDigit(segment, bufferedValue)) {
2✔
272
        this.selectSegment(segment);
2✔
273
        return;
2✔
274
      }
2✔
275
    }
2✔
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
  }
4✔
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.
UNCOV
289
    const parsedValue = moment(
×
UNCOV
290
      inputValue,
×
UNCOV
291
      ['MM/DD/YYYY', 'M/D/YYYY', 'YYYY-MM-DD'],
×
UNCOV
292
      true
×
UNCOV
293
    );
×
UNCOV
294
    return parsedValue.isValid() ? parsedValue : null;
×
UNCOV
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) {
56✔
312
      window.clearTimeout(this.closeCalendarTimeoutId);
2✔
313
      this.closeCalendarTimeoutId = null;
2✔
314
    }
2✔
315
  }
56✔
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) {
36!
UNCOV
331
      this.resetSegmentInputBuffer();
×
UNCOV
332
    }
×
333
    this.activeSegment = segment;
36✔
334
    this.selectSegment(segment);
36✔
335
  }
36✔
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;
18✔
341
  }
18✔
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) {
18✔
347
      this.activeSegment = segment;
18✔
348
      event.preventDefault();
18✔
349
      event.target.select();
18✔
350
    }
18✔
351
  }
18✔
352

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

358
  handleSegmentInput(segment, event) {
2✔
359
    // Sanitize typed digits and progressively commit or auto-advance when the
360
    // segment has enough information to become a valid date part.
361
    this.resetSegmentInputBuffer();
2✔
362

363
    const normalizedValue = this.normalizeSegmentInputValue(
2✔
364
      segment,
2✔
365
      event.target.value
2✔
366
    );
2✔
367
    this.setSegmentInputValue(segment, normalizedValue);
2✔
368

369
    if (!normalizedValue) {
2!
NEW
370
      return;
×
NEW
371
    }
×
372

373
    if (segment === 'year') {
2!
NEW
374
      this.setSegmentDisplayValue(segment, normalizedValue);
×
NEW
375
      if (normalizedValue.length === 4) {
×
NEW
376
        this.commitSegmentValue('year', parseInt(normalizedValue, 10));
×
NEW
377
      }
×
NEW
378
      return;
×
NEW
379
    }
×
380

381
    if (normalizedValue.length === 1) {
2!
NEW
382
      this.setSegmentDisplayValue(segment, normalizedValue.padStart(2, '0'));
×
NEW
383
    }
×
384

385
    if (this.shouldWaitForSecondDigit(segment, normalizedValue)) {
2!
NEW
386
      this.segmentInputBuffer = normalizedValue;
×
NEW
387
      this.segmentInputBufferSegment = segment;
×
NEW
388
      this.segmentInputBufferUpdatedAt = Date.now();
×
NEW
389
      this.selectSegment(segment);
×
NEW
390
      return;
×
NEW
391
    }
×
392

393
    this.commitSegmentValue(segment, parseInt(normalizedValue, 10));
2✔
394
    const nextSegment = this.getAdjacentSegment(segment, 1);
2✔
395
    if (nextSegment) {
2✔
396
      this.focusSegment(nextSegment);
2✔
397
    }
2✔
398
  }
2✔
399

400
  handleSegmentKeyDown(segment, event) {
2✔
401
    // The field owns navigation semantics between segments while leaving normal
402
    // text editing to the focused segment input.
403
    if (!this.selectedDate) {
28!
404
      return;
×
405
    }
×
406

407
    if (/^[0-9]$/.test(event.key)) {
28✔
408
      event.preventDefault();
4✔
409
      this.handleSegmentDigit(segment, event.key);
4✔
410
      return;
4✔
411
    }
4✔
412

413
    if (event.key === 'ArrowLeft') {
28!
NEW
414
      const previousSegment = this.getAdjacentSegment(segment, -1);
×
NEW
415
      if (previousSegment) {
×
NEW
416
        event.preventDefault();
×
NEW
417
        this.finalizeSegment(segment);
×
NEW
418
        this.focusSegment(previousSegment);
×
NEW
419
      }
×
420
      return;
×
421
    }
✔
422

423
    if (event.key === 'ArrowRight') {
28!
NEW
424
      const nextSegment = this.getAdjacentSegment(segment, 1);
×
NEW
425
      if (nextSegment) {
×
NEW
426
        event.preventDefault();
×
NEW
427
        this.finalizeSegment(segment);
×
NEW
428
        this.focusSegment(nextSegment);
×
NEW
429
      }
×
430
      return;
×
431
    }
✔
432

433
    if (event.key === 'ArrowUp') {
28✔
434
      event.preventDefault();
2✔
435
      this.incrementSegment(segment, 1);
2✔
436
      return;
2✔
437
    }
2✔
438

439
    if (event.key === 'ArrowDown') {
28!
440
      event.preventDefault();
×
NEW
441
      this.incrementSegment(segment, -1);
×
442
      return;
×
443
    }
✔
444

445
    if (event.key === 'Home') {
28!
UNCOV
446
      event.preventDefault();
×
NEW
447
      this.focusSegment('month');
×
UNCOV
448
      return;
×
UNCOV
449
    }
✔
450

451
    if (event.key === 'End') {
28!
UNCOV
452
      event.preventDefault();
×
NEW
453
      this.focusSegment('year');
×
UNCOV
454
      return;
×
UNCOV
455
    }
✔
456

457
    if (event.key === 'Tab') {
28✔
458
      const nextSegment = this.getAdjacentSegment(
16✔
459
        segment,
16✔
460
        event.shiftKey ? -1 : 1
16✔
461
      );
16✔
462
      this.finalizeSegment(segment);
16✔
463
      if (nextSegment) {
16✔
464
        event.preventDefault();
12✔
465
        this.focusSegment(nextSegment);
12✔
466
      }
12✔
467
      return;
16✔
468
    }
16✔
469

470
    if (event.key === 'Backspace' || event.key === 'Delete') {
28!
471
      this.resetSegmentInputBuffer();
×
472
    }
×
473
  }
28✔
474

475
  handleSegmentPaste(event) {
2✔
476
    // Normalized paste support keeps the custom field usable for power users.
477
    const pastedText = event.clipboardData.getData('text');
×
478
    const parsedValue = this.parseAcceptedDateInput(pastedText);
×
479
    if (!parsedValue) {
×
480
      return;
×
481
    }
×
482

483
    event.preventDefault();
×
484
    this.commitSelectedDate(parsedValue);
×
485
  }
×
486

487
  setSelectedDate(selectedDate) {
2✔
488
    // Calendar selections flow through the same commit path as keyboard edits.
489
    this.commitSelectedDate(selectedDate);
2✔
490
    this.closeCalendarWithDelay();
2✔
491
  }
2✔
492

493
  view({ attrs }) {
2✔
494
    // The component exposes only an aria-label; styling and behavior are owned
495
    // internally so consumers cannot accidentally break the segmented UX.
496
    const ariaLabel = attrs['aria-label'];
134✔
497
    return (
134✔
498
      <div className="date-input" role="group" aria-label={ariaLabel}>
134✔
499
        <input
134✔
500
          type="text"
134✔
501
          inputmode="numeric"
134✔
502
          className="date-input-segment date-input-segment-month"
134✔
503
          aria-label={`${ariaLabel} Month`}
134✔
504
          maxlength="2"
134✔
505
          value={this.monthInputValue}
134✔
506
          onfocus={() => this.handleSegmentFocus('month')}
134✔
507
          onblur={() => this.handleSegmentBlur('month')}
134✔
508
          onmousedown={() => this.handleSegmentMouseDown('month')}
134✔
509
          onmouseup={(event) => this.handleSegmentMouseUp('month', event)}
134✔
510
          onkeydown={(event) => this.handleSegmentKeyDown('month', event)}
134✔
511
          oninput={(event) => this.handleSegmentInput('month', event)}
134✔
512
          onpaste={(event) => this.handleSegmentPaste(event)}
134✔
513
          oncreate={({ dom }) => this.setSegmentInputElement('month', dom)}
134✔
514
          onupdate={({ dom }) => this.setSegmentInputElement('month', dom)}
134✔
515
        />
134✔
516
        <span className="date-input-separator">/</span>
134✔
517
        <input
134✔
518
          type="text"
134✔
519
          inputmode="numeric"
134✔
520
          className="date-input-segment date-input-segment-day"
134✔
521
          aria-label={`${ariaLabel} Day`}
134✔
522
          maxlength="2"
134✔
523
          value={this.dayInputValue}
134✔
524
          onfocus={() => this.handleSegmentFocus('day')}
134✔
525
          onblur={() => this.handleSegmentBlur('day')}
134✔
526
          onmousedown={() => this.handleSegmentMouseDown('day')}
134✔
527
          onmouseup={(event) => this.handleSegmentMouseUp('day', event)}
134✔
528
          onkeydown={(event) => this.handleSegmentKeyDown('day', event)}
134✔
529
          oninput={(event) => this.handleSegmentInput('day', event)}
134✔
530
          onpaste={(event) => this.handleSegmentPaste(event)}
134✔
531
          oncreate={({ dom }) => this.setSegmentInputElement('day', dom)}
134✔
532
          onupdate={({ dom }) => this.setSegmentInputElement('day', dom)}
134✔
533
        />
134✔
534
        <span className="date-input-separator">/</span>
134✔
535
        <input
134✔
536
          type="text"
134✔
537
          inputmode="numeric"
134✔
538
          className="date-input-segment date-input-segment-year"
134✔
539
          aria-label={`${ariaLabel} Year`}
134✔
540
          maxlength="4"
134✔
541
          value={this.yearInputValue}
134✔
542
          onfocus={() => this.handleSegmentFocus('year')}
134✔
543
          onblur={() => this.handleSegmentBlur('year')}
134✔
544
          onmousedown={() => this.handleSegmentMouseDown('year')}
134✔
545
          onmouseup={(event) => this.handleSegmentMouseUp('year', event)}
134✔
546
          onkeydown={(event) => this.handleSegmentKeyDown('year', event)}
134✔
547
          oninput={(event) => this.handleSegmentInput('year', event)}
134✔
548
          onpaste={(event) => this.handleSegmentPaste(event)}
134✔
549
          oncreate={({ dom }) => this.setSegmentInputElement('year', dom)}
134✔
550
          onupdate={({ dom }) => this.setSegmentInputElement('year', dom)}
134✔
551
        />
134✔
552
        <button
134✔
553
          type="button"
134✔
554
          className="date-input-calendar-toggle"
134✔
555
          aria-label={`Open ${ariaLabel} Calendar`}
134✔
556
          aria-haspopup="dialog"
134✔
557
          aria-expanded={this.calendarOpen ? 'true' : 'false'}
134✔
558
          onclick={() => this.toggleCalendar()}
134✔
559
        >
560
          <CalendarIconComponent selectedDate={this.selectedDate} />
134✔
561
        </button>
134✔
562

563
        {this.selectedDate && this.calendarOpen ? (
134✔
564
          <CalendarComponent
4✔
565
            className="date-input-calendar"
4✔
566
            selectedDate={this.selectedDate}
4✔
567
            calendarOpen={this.calendarOpen}
4✔
568
            onShouldIgnoreOutsideClick={(target) => {
4✔
569
              // Clicking the calendar toggle button should not be treated as an
570
              // outside click by the popup close handler.
571
              let element = target;
×
572
              while (element && element !== document) {
×
573
                if (
×
574
                  element.classList &&
×
575
                  element.classList.contains('date-input-calendar-toggle')
×
576
                ) {
×
577
                  return true;
×
578
                }
×
579
                element = element.parentNode;
×
580
              }
×
581
              return false;
×
582
            }}
×
583
            onSetSelectedDate={(selectedDate) => {
4✔
584
              this.setSelectedDate(selectedDate);
2✔
585
            }}
2✔
586
            onCloseCalendar={() => {
4✔
587
              this.closeCalendar();
×
588
            }}
×
589
          />
4✔
590
        ) : null}
130✔
591
      </div>
134✔
592
    );
593
  }
134✔
594
}
2✔
595

596
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