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

IgniteUI / igniteui-webcomponents / 15581958502

11 Jun 2025 10:04AM UTC coverage: 98.308% (+0.01%) from 98.297%
15581958502

push

github

web-flow
refactor: Mask parser improvements (#1734)

* Reduced intermediate collections and allocations
while parsing and replacing parts of the mask
* Cleaned up some logic around applying/replacing
parts of the mask.
* Dropped some obsolete internal methods.
* Better API documentation.

4892 of 5132 branches covered (95.32%)

Branch coverage included in aggregate %.

279 of 279 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

31316 of 31699 relevant lines covered (98.79%)

1755.3 hits per line

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

97.82
/src/components/date-time-input/date-time-input.ts
1
import { html } from 'lit';
18✔
2
import { eventOptions, property } from 'lit/decorators.js';
18✔
3
import { ifDefined } from 'lit/directives/if-defined.js';
18✔
4
import { live } from 'lit/directives/live.js';
18✔
5

18✔
6
import { convertToDate } from '../calendar/helpers.js';
18✔
7
import {
18✔
8
  addKeybindings,
18✔
9
  arrowDown,
18✔
10
  arrowLeft,
18✔
11
  arrowRight,
18✔
12
  arrowUp,
18✔
13
  ctrlKey,
18✔
14
} from '../common/controllers/key-bindings.js';
18✔
15
import { watch } from '../common/decorators/watch.js';
18✔
16
import { registerComponent } from '../common/definitions/register.js';
18✔
17
import type { AbstractConstructor } from '../common/mixins/constructor.js';
18✔
18
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
18✔
19
import {
18✔
20
  type FormValueOf,
18✔
21
  createFormValueState,
18✔
22
  defaultDateTimeTransformers,
18✔
23
} from '../common/mixins/forms/form-value.js';
18✔
24
import { partMap } from '../common/part-map.js';
18✔
25
import type { IgcInputComponentEventMap } from '../input/input-base.js';
18✔
26
import {
18✔
27
  IgcMaskInputBaseComponent,
18✔
28
  type MaskRange,
18✔
29
} from '../mask-input/mask-input-base.js';
18✔
30
import IgcValidationContainerComponent from '../validation-container/validation-container.js';
18✔
31
import {
18✔
32
  DatePart,
18✔
33
  type DatePartDeltas,
18✔
34
  type DatePartInfo,
18✔
35
  DateParts,
18✔
36
  DateTimeUtil,
18✔
37
} from './date-util.js';
18✔
38
import { dateTimeInputValidators } from './validators.js';
18✔
39

18✔
40
export interface IgcDateTimeInputComponentEventMap
18✔
41
  extends Omit<IgcInputComponentEventMap, 'igcChange'> {
18✔
42
  igcChange: CustomEvent<Date | null>;
18✔
43
}
18✔
44

18✔
45
/**
18✔
46
 * A date time input is an input field that lets you set and edit the date and time in a chosen input element
18✔
47
 * using customizable display and input formats.
18✔
48
 *
18✔
49
 * @element igc-date-time-input
18✔
50
 *
18✔
51
 * @slot prefix - Renders content before the input.
18✔
52
 * @slot suffix - Renders content after input.
18✔
53
 * @slot helper-text - Renders content below the input.
18✔
54
 * @slot value-missing - Renders content when the required validation fails.
18✔
55
 * @slot range-overflow - Renders content when the max validation fails.
18✔
56
 * @slot range-underflow - Renders content when the min validation fails.
18✔
57
 * @slot custom-error - Renders content when setCustomValidity(message) is set.
18✔
58
 * @slot invalid - Renders content when the component is in invalid state (validity.valid = false).
18✔
59
 *
18✔
60
 * @fires igcInput - Emitted when the control input receives user input.
18✔
61
 * @fires igcChange - Emitted when the control's checked state changes.
18✔
62
 *
18✔
63
 * @csspart container - The main wrapper that holds all main input elements.
18✔
64
 * @csspart input - The native input element.
18✔
65
 * @csspart label - The native label element.
18✔
66
 * @csspart prefix - The prefix wrapper.
18✔
67
 * @csspart suffix - The suffix wrapper.
18✔
68
 * @csspart helper-text - The helper text wrapper.
18✔
69
 */
18✔
70
export default class IgcDateTimeInputComponent extends EventEmitterMixin<
18✔
71
  IgcDateTimeInputComponentEventMap,
18✔
72
  AbstractConstructor<IgcMaskInputBaseComponent>
18✔
73
>(IgcMaskInputBaseComponent) {
18✔
74
  public static readonly tagName = 'igc-date-time-input';
18✔
75

18✔
76
  /* blazorSuppress */
18✔
77
  public static register() {
18✔
78
    registerComponent(
8✔
79
      IgcDateTimeInputComponent,
8✔
80
      IgcValidationContainerComponent
8✔
81
    );
8✔
82
  }
8✔
83

18✔
84
  protected override get __validators() {
18✔
85
    return dateTimeInputValidators;
2,007✔
86
  }
2,007✔
87

18✔
88
  protected override readonly _formValue: FormValueOf<Date | null> =
18✔
89
    createFormValueState(this, {
18✔
90
      initialValue: null,
18✔
91
      transformers: defaultDateTimeTransformers,
18✔
92
    });
18✔
93

18✔
94
  protected _defaultMask!: string;
18✔
95
  private _oldValue: Date | null = null;
18✔
96
  private _min: Date | null = null;
18✔
97
  private _max: Date | null = null;
18✔
98

18✔
99
  private _inputDateParts!: DatePartInfo[];
18✔
100
  private _inputFormat!: string;
18✔
101
  private _datePartDeltas: DatePartDeltas = {
18✔
102
    date: 1,
18✔
103
    month: 1,
18✔
104
    year: 1,
18✔
105
    hours: 1,
18✔
106
    minutes: 1,
18✔
107
    seconds: 1,
18✔
108
  };
18✔
109

18✔
110
  /**
18✔
111
   * The date format to apply on the input.
18✔
112
   * @attr input-format
18✔
113
   */
18✔
114
  @property({ attribute: 'input-format' })
18✔
115
  public get inputFormat(): string {
18✔
116
    return this._inputFormat || this._defaultMask;
1,548✔
117
  }
1,548✔
118

18✔
119
  public set inputFormat(val: string) {
18✔
120
    if (val) {
21✔
121
      this.setMask(val);
21✔
122
      this._inputFormat = val;
21✔
123
      if (this.value) {
21✔
124
        this.updateMask();
9✔
125
      }
9✔
126
    }
21✔
127
  }
21✔
128

18✔
129
  public get value(): Date | null {
18✔
130
    return this._formValue.value;
12,431✔
131
  }
12,431✔
132

18✔
133
  /* @tsTwoWayProperty(true, "igcChange", "detail", false) */
18✔
134
  /**
18✔
135
   * The value of the input.
18✔
136
   * @attr
18✔
137
   */
18✔
138
  @property({ converter: convertToDate })
18✔
139
  public set value(value: Date | string | null | undefined) {
18✔
140
    this._formValue.setValueAndFormState(value as Date | null);
639✔
141
    this.updateMask();
639✔
142
    this._validate();
639✔
143
  }
639✔
144

18✔
145
  /**
18✔
146
   * The minimum value required for the input to remain valid.
18✔
147
   * @attr
18✔
148
   */
18✔
149
  @property({ converter: convertToDate })
18✔
150
  public set min(value: Date | string | null | undefined) {
18✔
151
    this._min = convertToDate(value);
313✔
152
    this._updateValidity();
313✔
153
  }
313✔
154

18✔
155
  public get min(): Date | null {
18✔
156
    return this._min;
1,534✔
157
  }
1,534✔
158

18✔
159
  /**
18✔
160
   * The maximum value required for the input to remain valid.
18✔
161
   * @attr
18✔
162
   */
18✔
163
  @property({ converter: convertToDate })
18✔
164
  public set max(value: Date | string | null | undefined) {
18✔
165
    this._max = convertToDate(value);
313✔
166
    this._updateValidity();
313✔
167
  }
313✔
168

18✔
169
  public get max(): Date | null {
18✔
170
    return this._max;
1,532✔
171
  }
1,532✔
172

18✔
173
  /**
18✔
174
   * Format to display the value in when not editing.
18✔
175
   * Defaults to the input format if not set.
18✔
176
   * @attr display-format
18✔
177
   */
18✔
178
  @property({ attribute: 'display-format' })
18✔
179
  public displayFormat!: string;
18✔
180

18✔
181
  /**
18✔
182
   * Delta values used to increment or decrement each date part on step actions.
18✔
183
   * All values default to `1`.
18✔
184
   */
18✔
185
  @property({ attribute: false })
18✔
186
  public spinDelta!: DatePartDeltas;
18✔
187

18✔
188
  /**
18✔
189
   * Sets whether to loop over the currently spun segment.
18✔
190
   * @attr spin-loop
18✔
191
   */
18✔
192
  @property({ type: Boolean, attribute: 'spin-loop' })
18✔
193
  public spinLoop = true;
18✔
194

18✔
195
  /**
18✔
196
   * The locale settings used to display the value.
18✔
197
   * @attr
18✔
198
   */
18✔
199
  @property()
18✔
200
  public locale = 'en';
18✔
201

18✔
202
  @watch('locale', { waitUntilFirstUpdate: true })
18✔
203
  protected setDefaultMask(): void {
18✔
204
    if (!this._inputFormat) {
4✔
205
      this.updateDefaultMask();
4✔
206
      this.setMask(this._defaultMask);
4✔
207
    }
4✔
208

4✔
209
    if (this.value) {
4✔
210
      this.updateMask();
1✔
211
    }
1✔
212
  }
4✔
213

18✔
214
  @watch('displayFormat', { waitUntilFirstUpdate: true })
18✔
215
  protected setDisplayFormat(): void {
18✔
216
    if (this.value) {
42✔
217
      this.updateMask();
26✔
218
    }
26✔
219
  }
42✔
220

18✔
221
  @watch('prompt', { waitUntilFirstUpdate: true })
18✔
222
  protected promptChange(): void {
18✔
223
    if (!this.prompt) {
1!
224
      this.prompt = this.parser.prompt;
×
225
    } else {
1✔
226
      this.parser.prompt = this.prompt;
1✔
227
    }
1✔
228
  }
1✔
229

18✔
230
  protected get hasDateParts(): boolean {
18✔
231
    const parts =
97✔
232
      this._inputDateParts ||
97✔
233
      DateTimeUtil.parseDateTimeFormat(this.inputFormat);
9✔
234

97✔
235
    return parts.some(
97✔
236
      (p) =>
97✔
237
        p.type === DateParts.Date ||
97✔
238
        p.type === DateParts.Month ||
97!
239
        p.type === DateParts.Year
×
240
    );
97✔
241
  }
97✔
242

18✔
243
  protected get hasTimeParts(): boolean {
18✔
244
    const parts =
97✔
245
      this._inputDateParts ||
97✔
246
      DateTimeUtil.parseDateTimeFormat(this.inputFormat);
9✔
247
    return parts.some(
97✔
248
      (p) =>
97✔
249
        p.type === DateParts.Hours ||
485✔
250
        p.type === DateParts.Minutes ||
485✔
251
        p.type === DateParts.Seconds
485✔
252
    );
97✔
253
  }
97✔
254

18✔
255
  private get targetDatePart(): DatePart | undefined {
18✔
256
    let result: DatePart | undefined;
33✔
257

33✔
258
    if (this.focused) {
33✔
259
      const partType = this._inputDateParts.find(
20✔
260
        (p) =>
20✔
261
          p.start <= this.inputSelection.start &&
34✔
262
          this.inputSelection.start <= p.end &&
34✔
263
          p.type !== DateParts.Literal
21✔
264
      )?.type as string as DatePart;
20✔
265

20✔
266
      if (partType) {
20✔
267
        result = partType;
20✔
268
      }
20✔
269
    } else if (this._inputDateParts.some((p) => p.type === DateParts.Date)) {
33✔
270
      result = DatePart.Date;
9✔
271
    } else if (this._inputDateParts.some((p) => p.type === DateParts.Hours)) {
13✔
272
      result = DatePart.Hours;
2✔
273
    } else {
2✔
274
      result = this._inputDateParts[0].type as string as DatePart;
2✔
275
    }
2✔
276

33✔
277
    return result;
33✔
278
  }
33✔
279

18✔
280
  private get datePartDeltas(): DatePartDeltas {
18✔
281
    return Object.assign({}, this._datePartDeltas, this.spinDelta);
41✔
282
  }
41✔
283

18✔
284
  constructor() {
18✔
285
    super();
354✔
286

354✔
287
    addKeybindings(this, {
354✔
288
      skip: () => this.readOnly,
354✔
289
      bindingDefaults: { preventDefault: true, triggers: ['keydownRepeat'] },
354✔
290
    })
354✔
291
      .set([ctrlKey, ';'], this.setToday)
354✔
292
      .set(arrowUp, this.keyboardSpin.bind(this, 'up'))
354✔
293
      .set(arrowDown, this.keyboardSpin.bind(this, 'down'))
354✔
294
      .set([ctrlKey, arrowLeft], this.navigateParts.bind(this, 0))
354✔
295
      .set([ctrlKey, arrowRight], this.navigateParts.bind(this, 1));
354✔
296
  }
354✔
297

18✔
298
  public override connectedCallback() {
18✔
299
    super.connectedCallback();
354✔
300
    this.updateDefaultMask();
354✔
301
    this.setMask(this.inputFormat);
354✔
302
    this._updateValidity();
354✔
303
    if (this.value) {
354✔
304
      this.updateMask();
26✔
305
    }
26✔
306
  }
354✔
307

18✔
308
  /** Increments a date/time portion. */
18✔
309
  public stepUp(datePart?: DatePart, delta?: number): void {
18✔
310
    const targetPart = datePart || this.targetDatePart;
20✔
311

20✔
312
    if (!targetPart) {
20!
313
      return;
×
314
    }
×
315

20✔
316
    const { start, end } = this.inputSelection;
20✔
317
    const newValue = this.trySpinValue(targetPart, delta);
20✔
318
    this.value = newValue;
20✔
319
    this.updateComplete.then(() => this.input.setSelectionRange(start, end));
20✔
320
  }
20✔
321

18✔
322
  /** Decrements a date/time portion. */
18✔
323
  public stepDown(datePart?: DatePart, delta?: number): void {
18✔
324
    const targetPart = datePart || this.targetDatePart;
21✔
325

21✔
326
    if (!targetPart) {
21!
327
      return;
×
328
    }
×
329

21✔
330
    const { start, end } = this.inputSelection;
21✔
331
    const newValue = this.trySpinValue(targetPart, delta, true);
21✔
332
    this.value = newValue;
21✔
333
    this.updateComplete.then(() => this.input.setSelectionRange(start, end));
21✔
334
  }
21✔
335

18✔
336
  /** Clears the input element of user input. */
18✔
337
  public clear(): void {
18✔
338
    this.maskedValue = '';
9✔
339
    this.value = null;
9✔
340
  }
9✔
341

18✔
342
  protected setToday() {
18✔
343
    this.value = new Date();
1✔
344
    this.handleInput();
1✔
345
  }
1✔
346

18✔
347
  protected updateMask() {
18✔
348
    if (this.focused) {
767✔
349
      this.maskedValue = this.getMaskedValue();
67✔
350
    } else {
767✔
351
      if (!DateTimeUtil.isValidDate(this.value)) {
700✔
352
        this.maskedValue = '';
316✔
353
        return;
316✔
354
      }
316✔
355

384✔
356
      const format = this.displayFormat || this.inputFormat;
398✔
357

700✔
358
      if (this.displayFormat) {
700✔
359
        this.maskedValue = DateTimeUtil.formatDate(
29✔
360
          this.value,
29✔
361
          this.locale,
29✔
362
          format,
29✔
363
          true
29✔
364
        );
29✔
365
      } else if (this.inputFormat) {
700✔
366
        this.maskedValue = DateTimeUtil.formatDate(
329✔
367
          this.value,
329✔
368
          this.locale,
329✔
369
          format
329✔
370
        );
329✔
371
      } else {
353✔
372
        this.maskedValue = this.value.toLocaleString();
26✔
373
      }
26✔
374
    }
700✔
375
  }
767✔
376

18✔
377
  protected override handleInput() {
18✔
378
    this.emitEvent('igcInput', { detail: this.value?.toString() });
33✔
379
  }
33✔
380

18✔
381
  protected handleDragLeave() {
18✔
382
    if (!this.focused) {
2✔
383
      this.updateMask();
1✔
384
    }
1✔
385
  }
2✔
386

18✔
387
  protected handleDragEnter() {
18✔
388
    if (!this.focused) {
1✔
389
      this.maskedValue = this.getMaskedValue();
1✔
390
    }
1✔
391
  }
1✔
392

18✔
393
  protected async updateInput(string: string, range: MaskRange) {
18✔
394
    const { value, end } = this.parser.replace(
12✔
395
      this.maskedValue,
12✔
396
      string,
12✔
397
      range.start,
12✔
398
      range.end
12✔
399
    );
12✔
400

12✔
401
    this.maskedValue = value;
12✔
402

12✔
403
    this.updateValue();
12✔
404
    this.requestUpdate();
12✔
405

12✔
406
    if (range.start !== this.inputFormat.length) {
12✔
407
      this.handleInput();
12✔
408
    }
12✔
409
    await this.updateComplete;
12✔
410
    this.input.setSelectionRange(end, end);
12✔
411
  }
12✔
412

18✔
413
  private trySpinValue(
18✔
414
    datePart: DatePart,
41✔
415
    delta?: number,
41✔
416
    negative = false
41✔
417
  ): Date {
41✔
418
    // default to 1 if a delta is set to 0 or any other falsy value
41✔
419
    const _delta =
41✔
420
      delta || this.datePartDeltas[datePart as keyof DatePartDeltas] || 1;
41✔
421

41✔
422
    const spinValue = negative ? -Math.abs(_delta) : Math.abs(_delta);
41✔
423
    return this.spinValue(datePart, spinValue);
41✔
424
  }
41✔
425

18✔
426
  private spinValue(datePart: DatePart, delta: number): Date {
18✔
427
    if (!(this.value && DateTimeUtil.isValidDate(this.value))) {
41✔
428
      return new Date();
11✔
429
    }
11✔
430

30✔
431
    const newDate = new Date(this.value.getTime());
30✔
432
    let formatPart: DatePartInfo | undefined;
30✔
433
    let amPmFromMask: string;
30✔
434

30✔
435
    switch (datePart) {
30✔
436
      case DatePart.Date:
39✔
437
        DateTimeUtil.spinDate(delta, newDate, this.spinLoop);
6✔
438
        break;
6✔
439
      case DatePart.Month:
41✔
440
        DateTimeUtil.spinMonth(delta, newDate, this.spinLoop);
10✔
441
        break;
10✔
442
      case DatePart.Year:
41✔
443
        DateTimeUtil.spinYear(delta, newDate);
7✔
444
        break;
7✔
445
      case DatePart.Hours:
41✔
446
        DateTimeUtil.spinHours(delta, newDate, this.spinLoop);
2✔
447
        break;
2✔
448
      case DatePart.Minutes:
41✔
449
        DateTimeUtil.spinMinutes(delta, newDate, this.spinLoop);
2✔
450
        break;
2✔
451
      case DatePart.Seconds:
41✔
452
        DateTimeUtil.spinSeconds(delta, newDate, this.spinLoop);
2✔
453
        break;
2✔
454
      case DatePart.AmPm:
41✔
455
        formatPart = this._inputDateParts.find(
1✔
456
          (dp) => dp.type === DateParts.AmPm
1✔
457
        );
1✔
458
        if (formatPart !== undefined) {
1✔
459
          amPmFromMask = this.maskedValue.substring(
1✔
460
            formatPart!.start,
1✔
461
            formatPart!.end
1✔
462
          );
1✔
463
          return DateTimeUtil.spinAmPm(newDate, this.value, amPmFromMask);
1✔
464
        }
1!
465
        break;
×
466
    }
41✔
467

29✔
468
    return newDate;
29✔
469
  }
41✔
470

18✔
471
  @eventOptions({ passive: false })
18✔
472
  private async onWheel(event: WheelEvent) {
18✔
473
    if (!this.focused || this.readOnly) {
5✔
474
      return;
2✔
475
    }
2✔
476

3✔
477
    event.preventDefault();
3✔
478
    event.stopPropagation();
3✔
479

3✔
480
    const { start, end } = this.inputSelection;
3✔
481
    event.deltaY > 0 ? this.stepDown() : this.stepUp();
5✔
482
    this.handleInput();
5✔
483

5✔
484
    await this.updateComplete;
5✔
485
    this.setSelectionRange(start, end);
3✔
486
  }
5✔
487

18✔
488
  private updateDefaultMask(): void {
18✔
489
    this._defaultMask = DateTimeUtil.getDefaultMask(this.locale);
358✔
490
  }
358✔
491

18✔
492
  private setMask(string: string): void {
18✔
493
    const oldFormat = this._inputDateParts?.map((p) => p.format).join('');
379✔
494
    this._inputDateParts = DateTimeUtil.parseDateTimeFormat(string);
379✔
495
    const value = this._inputDateParts.map((p) => p.format).join('');
379✔
496

379✔
497
    this._defaultMask = value;
379✔
498

379✔
499
    const newMask = (value || DateTimeUtil.DEFAULT_INPUT_FORMAT).replace(
379!
500
      new RegExp(/(?=[^t])[\w]/, 'g'),
379✔
501
      '0'
379✔
502
    );
379✔
503

379✔
504
    this._mask = newMask.includes('tt')
379✔
505
      ? newMask.replace(/tt/g, 'LL')
1✔
506
      : newMask;
378✔
507

379✔
508
    this.parser.mask = this._mask;
379✔
509
    this.parser.prompt = this.prompt;
379✔
510

379✔
511
    if (!this.placeholder || oldFormat === this.placeholder) {
379✔
512
      this.placeholder = value;
379✔
513
    }
379✔
514
  }
379✔
515

18✔
516
  private parseDate(val: string) {
18✔
517
    return val
14✔
518
      ? DateTimeUtil.parseValueFromMask(val, this._inputDateParts, this.prompt)
14!
UNCOV
519
      : null;
×
520
  }
14✔
521

18✔
522
  private getMaskedValue(): string {
18✔
523
    let mask = this.emptyMask;
68✔
524

68✔
525
    if (DateTimeUtil.isValidDate(this.value)) {
68✔
526
      for (const part of this._inputDateParts) {
58✔
527
        if (part.type === DateParts.Literal) {
290✔
528
          continue;
116✔
529
        }
116✔
530

174✔
531
        const targetValue = DateTimeUtil.getPartValue(
174✔
532
          part,
174✔
533
          part.format.length,
174✔
534
          this.value
174✔
535
        );
174✔
536

174✔
537
        mask = this.parser.replace(
174✔
538
          mask,
174✔
539
          targetValue,
174✔
540
          part.start,
174✔
541
          part.end
174✔
542
        ).value;
174✔
543
      }
174✔
544
      return mask;
58✔
545
    }
58✔
546

10✔
547
    return this.maskedValue === '' ? mask : this.maskedValue;
68✔
548
  }
68✔
549

18✔
550
  private isComplete(): boolean {
18✔
551
    return !this.maskedValue.includes(this.prompt);
70✔
552
  }
70✔
553

18✔
554
  private updateValue(): void {
18✔
555
    if (this.isComplete()) {
17✔
556
      const parsedDate = this.parseDate(this.maskedValue);
11✔
557
      this.value = DateTimeUtil.isValidDate(parsedDate) ? parsedDate : null;
11✔
558
    } else {
17✔
559
      this.value = null;
6✔
560
    }
6✔
561
  }
17✔
562

18✔
563
  protected override _updateSetRangeTextValue() {
18✔
564
    this.updateValue();
5✔
565
  }
5✔
566

18✔
567
  private getNewPosition(value: string, direction = 0): number {
18✔
568
    const cursorPos = this.selection.start;
2✔
569

2✔
570
    if (!direction) {
2✔
571
      // Last literal before the current cursor position or start of input value
1✔
572
      const part = this._inputDateParts.findLast(
1✔
573
        (part) => part.type === DateParts.Literal && part.end < cursorPos
1✔
574
      );
1✔
575
      return part?.end ?? 0;
1!
576
    }
1✔
577

1✔
578
    // First literal after the current cursor position or end of input value
1✔
579
    const part = this._inputDateParts.find(
1✔
580
      (part) => part.type === DateParts.Literal && part.start > cursorPos
1✔
581
    );
1✔
582
    return part?.start ?? value.length;
2!
583
  }
2✔
584

18✔
585
  protected async handleFocus() {
18✔
586
    this.focused = true;
53✔
587

53✔
588
    if (this.readOnly) {
53✔
589
      return;
16✔
590
    }
16✔
591

37✔
592
    this._oldValue = this.value;
37✔
593
    const areFormatsDifferent = this.displayFormat !== this.inputFormat;
37✔
594

37✔
595
    if (!this.value) {
53✔
596
      this.maskedValue = this.emptyMask;
22✔
597
      await this.updateComplete;
22✔
598
      this.select();
22✔
599
    } else if (areFormatsDifferent) {
53✔
600
      this.updateMask();
15✔
601
    }
15✔
602
  }
53✔
603

18✔
604
  protected handleBlur() {
18✔
605
    const isEmptyMask = this.maskedValue === this.emptyMask;
53✔
606

53✔
607
    this.focused = false;
53✔
608

53✔
609
    if (!(this.isComplete() || isEmptyMask)) {
53✔
610
      const parse = this.parseDate(this.maskedValue);
3✔
611

3✔
612
      if (parse) {
3✔
613
        this.value = parse;
2✔
614
      } else {
3✔
615
        this.value = null;
1✔
616
        this.maskedValue = '';
1✔
617
      }
1✔
618
    } else {
53✔
619
      this.updateMask();
50✔
620
    }
50✔
621

53✔
622
    const isSameValue = this._oldValue === this.value;
53✔
623

53✔
624
    if (!(this.readOnly || isSameValue)) {
53✔
625
      this.emitEvent('igcChange', { detail: this.value });
20✔
626
    }
20✔
627

53✔
628
    this.checkValidity();
53✔
629
  }
53✔
630

18✔
631
  protected navigateParts(delta: number) {
18✔
632
    const position = this.getNewPosition(this.input.value, delta);
2✔
633
    this.setSelectionRange(position, position);
2✔
634
  }
2✔
635

18✔
636
  protected async keyboardSpin(direction: 'up' | 'down') {
18✔
637
    direction === 'up' ? this.stepUp() : this.stepDown();
17✔
638
    this.handleInput();
17✔
639
    await this.updateComplete;
17✔
640
    this.setSelectionRange(this.selection.start, this.selection.end);
17✔
641
  }
17✔
642

18✔
643
  protected override renderInput() {
18✔
644
    return html`
1,226✔
645
      <input
1,226✔
646
        type="text"
1,226✔
647
        part=${partMap(this.resolvePartNames('input'))}
1,226✔
648
        name=${ifDefined(this.name)}
1,226✔
649
        .value=${live(this.maskedValue)}
1,226✔
650
        .placeholder=${live(this.placeholder || this.emptyMask)}
1,226!
651
        ?readonly=${this.readOnly}
1,226✔
652
        ?disabled=${this.disabled}
1,226✔
653
        @blur=${this.handleBlur}
1,226✔
654
        @focus=${this.handleFocus}
1,226✔
655
        @input=${super.handleInput}
1,226✔
656
        @wheel=${this.onWheel}
1,226✔
657
        @keydown=${super.handleKeydown}
1,226✔
658
        @click=${this.handleClick}
1,226✔
659
        @cut=${this.handleCut}
1,226✔
660
        @compositionstart=${this.handleCompositionStart}
1,226✔
661
        @compositionend=${this.handleCompositionEnd}
1,226✔
662
        @dragenter=${this.handleDragEnter}
1,226✔
663
        @dragleave=${this.handleDragLeave}
1,226✔
664
        @dragstart=${this.handleDragStart}
1,226✔
665
      />
1,226✔
666
    `;
1,226✔
667
  }
1,226✔
668
}
18✔
669

18✔
670
declare global {
18✔
671
  interface HTMLElementTagNameMap {
18✔
672
    'igc-date-time-input': IgcDateTimeInputComponent;
18✔
673
  }
18✔
674
}
18✔
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