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

IgniteUI / igniteui-webcomponents / 16984157745

15 Aug 2025 06:08AM UTC coverage: 98.223% (-0.06%) from 98.281%
16984157745

Pull #1733

github

web-flow
Merge 7ca2d82ce into a27bd79de
Pull Request #1733: Add editable date range input

5060 of 5328 branches covered (94.97%)

Branch coverage included in aggregate %.

933 of 957 new or added lines in 7 files covered. (97.49%)

8 existing lines in 2 files now uncovered.

32855 of 33273 relevant lines covered (98.74%)

1732.96 hits per line

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

97.57
/src/components/date-time-input/date-time-input.base.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
import { convertToDate } from '../calendar/helpers.js';
18✔
6
import {
18✔
7
  addKeybindings,
18✔
8
  arrowDown,
18✔
9
  arrowLeft,
18✔
10
  arrowRight,
18✔
11
  arrowUp,
18✔
12
  ctrlKey,
18✔
13
} from '../common/controllers/key-bindings.js';
18✔
14
import { watch } from '../common/decorators/watch.js';
18✔
15
import type { AbstractConstructor } from '../common/mixins/constructor.js';
18✔
16
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
18✔
17
import { partMap } from '../common/part-map.js';
18✔
18
import type { DateRangeValue } from '../date-range-picker/date-range-picker.js';
18✔
19
import type { IgcInputComponentEventMap } from '../input/input-base.js';
18✔
20
import {
18✔
21
  IgcMaskInputBaseComponent,
18✔
22
  type MaskRange,
18✔
23
} from '../mask-input/mask-input-base.js';
18✔
24
import type {
18✔
25
  DatePart,
18✔
26
  DatePartInfo,
18✔
27
  DateRangePart,
18✔
28
  DateRangePartInfo,
18✔
29
} from './date-util.js';
18✔
30
import { type DatePartDeltas, DateParts, DateTimeUtil } from './date-util.js';
18✔
31
import { dateTimeInputValidators } from './validators.js';
18✔
32

18✔
33
export interface IgcDateTimeInputComponentEventMap
18✔
34
  extends Omit<IgcInputComponentEventMap, 'igcChange'> {
18✔
35
  igcChange: CustomEvent<Date | DateRangeValue | null>;
18✔
36
}
18✔
37
export abstract class IgcDateTimeInputBaseComponent<
18✔
38
  TValue extends Date | DateRangeValue | string | null,
18✔
39
  TPart extends DatePart | DateRangePart,
18✔
40
  TPartInfo extends DatePartInfo | DateRangePartInfo,
18✔
41
> extends EventEmitterMixin<
18✔
42
  IgcDateTimeInputComponentEventMap,
18✔
43
  AbstractConstructor<IgcMaskInputBaseComponent>
18✔
44
>(IgcMaskInputBaseComponent) {
18✔
45
  // #region Internal state & properties
18✔
46

18✔
47
  protected override get __validators() {
18✔
48
    return dateTimeInputValidators;
18✔
49
  }
18✔
50
  private _min: Date | null = null;
18✔
51
  private _max: Date | null = null;
18✔
52
  protected _defaultMask!: string;
18✔
53
  protected _oldValue: TValue | null = null;
18✔
54
  protected _inputDateParts!: TPartInfo[];
18✔
55
  protected _inputFormat!: string;
18✔
56

18✔
57
  protected abstract _datePartDeltas: DatePartDeltas;
18✔
58
  protected abstract get targetDatePart(): TPart | undefined;
18✔
59

18✔
60
  protected get hasDateParts(): boolean {
18✔
61
    const parts =
97✔
62
      this._inputDateParts ||
97✔
63
      DateTimeUtil.parseDateTimeFormat(this.inputFormat);
9✔
64

97✔
65
    return parts.some(
97✔
66
      (p) =>
97✔
67
        p.type === DateParts.Date ||
97✔
68
        p.type === DateParts.Month ||
97!
NEW
69
        p.type === DateParts.Year
×
70
    );
97✔
71
  }
97✔
72

18✔
73
  protected get hasTimeParts(): boolean {
18✔
74
    const parts =
97✔
75
      this._inputDateParts ||
97✔
76
      DateTimeUtil.parseDateTimeFormat(this.inputFormat);
9✔
77
    return parts.some(
97✔
78
      (p) =>
97✔
79
        p.type === DateParts.Hours ||
485✔
80
        p.type === DateParts.Minutes ||
485✔
81
        p.type === DateParts.Seconds
485✔
82
    );
97✔
83
  }
97✔
84

18✔
85
  protected get datePartDeltas(): DatePartDeltas {
18✔
86
    return Object.assign({}, this._datePartDeltas, this.spinDelta);
45✔
87
  }
45✔
88

18✔
89
  // #endregion
18✔
90

18✔
91
  // #region Public properties
18✔
92

18✔
93
  public abstract override value: TValue | null;
18✔
94

18✔
95
  /**
18✔
96
   * The date format to apply on the input.
18✔
97
   * @attr input-format
18✔
98
   */
18✔
99
  @property({ attribute: 'input-format' })
18✔
100
  public get inputFormat(): string {
18✔
101
    return this._inputFormat || this._defaultMask;
1,551✔
102
  }
1,551✔
103

18✔
104
  public set inputFormat(val: string) {
18✔
105
    if (val) {
21✔
106
      this.setMask(val);
21✔
107
      this._inputFormat = val;
21✔
108
      if (this.value) {
21✔
109
        this.updateMask();
9✔
110
      }
9✔
111
    }
21✔
112
  }
21✔
113

18✔
114
  /**
18✔
115
   * The minimum value required for the input to remain valid.
18✔
116
   * @attr
18✔
117
   */
18✔
118
  @property({ converter: convertToDate })
18✔
119
  public set min(value: Date | string | null | undefined) {
18✔
120
    this._min = convertToDate(value);
313✔
121
    this._validate();
313✔
122
  }
313✔
123

18✔
124
  public get min(): Date | null {
18✔
125
    return this._min;
2,258✔
126
  }
2,258✔
127

18✔
128
  /**
18✔
129
   * The maximum value required for the input to remain valid.
18✔
130
   * @attr
18✔
131
   */
18✔
132
  @property({ converter: convertToDate })
18✔
133
  public set max(value: Date | string | null | undefined) {
18✔
134
    this._max = convertToDate(value);
313✔
135
    this._validate();
313✔
136
  }
313✔
137

18✔
138
  public get max(): Date | null {
18✔
139
    return this._max;
2,256✔
140
  }
2,256✔
141

18✔
142
  /**
18✔
143
   * Format to display the value in when not editing.
18✔
144
   * Defaults to the input format if not set.
18✔
145
   * @attr display-format
18✔
146
   */
18✔
147
  @property({ attribute: 'display-format' })
18✔
148
  public displayFormat!: string;
18✔
149

18✔
150
  /**
18✔
151
   * Delta values used to increment or decrement each date part on step actions.
18✔
152
   * All values default to `1`.
18✔
153
   */
18✔
154
  @property({ attribute: false })
18✔
155
  public spinDelta!: DatePartDeltas;
18✔
156

18✔
157
  /**
18✔
158
   * Sets whether to loop over the currently spun segment.
18✔
159
   * @attr spin-loop
18✔
160
   */
18✔
161
  @property({ type: Boolean, attribute: 'spin-loop' })
18✔
162
  public spinLoop = true;
18✔
163

18✔
164
  /**
18✔
165
   * The locale settings used to display the value.
18✔
166
   * @attr
18✔
167
   */
18✔
168
  @property()
18✔
169
  public locale = 'en';
18✔
170

18✔
171
  // #endregion
18✔
172

18✔
173
  // #region Lifecycle & observers
18✔
174

18✔
175
  constructor() {
18✔
176
    super();
495✔
177

495✔
178
    addKeybindings(this, {
495✔
179
      skip: () => this.readOnly,
495✔
180
      bindingDefaults: { triggers: ['keydownRepeat'] },
495✔
181
    })
495✔
182
      .set([ctrlKey, ';'], this.setToday)
495✔
183
      .set(arrowUp, this.keyboardSpin.bind(this, 'up'))
495✔
184
      .set(arrowDown, this.keyboardSpin.bind(this, 'down'))
495✔
185
      .set([ctrlKey, arrowLeft], this.navigateParts.bind(this, 0))
495✔
186
      .set([ctrlKey, arrowRight], this.navigateParts.bind(this, 1));
495✔
187
  }
495✔
188

18✔
189
  public override connectedCallback() {
18✔
190
    super.connectedCallback();
498✔
191
    this.updateDefaultMask();
498✔
192
    this.setMask(this.inputFormat);
498✔
193
    this._validate();
498✔
194
    if (this.value) {
498✔
195
      this.updateMask();
167✔
196
    }
167✔
197
  }
498✔
198

18✔
199
  @watch('locale', { waitUntilFirstUpdate: true })
18✔
200
  protected _setDefaultMask(): void {
18✔
201
    if (!this._inputFormat) {
4✔
202
      this.updateDefaultMask();
4✔
203
      this.setMask(this._defaultMask);
4✔
204
    }
4✔
205

4✔
206
    if (this.value) {
4✔
207
      this.updateMask();
1✔
208
    }
1✔
209
  }
4✔
210

18✔
211
  @watch('displayFormat', { waitUntilFirstUpdate: true })
18✔
212
  protected _setDisplayFormat(): void {
18✔
213
    if (this.value) {
47✔
214
      this.updateMask();
31✔
215
    }
31✔
216
  }
47✔
217

18✔
218
  @watch('prompt', { waitUntilFirstUpdate: true })
18✔
219
  protected _promptChange(): void {
18✔
220
    if (!this.prompt) {
1!
NEW
221
      this.prompt = this.parser.prompt;
×
222
    } else {
1✔
223
      this.parser.prompt = this.prompt;
1✔
224
    }
1✔
225
  }
1✔
226

18✔
227
  // #endregion
18✔
228

18✔
229
  // #region Methods
18✔
230

18✔
231
  /** Increments a date/time portion. */
18✔
232
  public stepUp(datePart?: TPart, delta?: number): void {
18✔
233
    const targetPart = datePart || this.targetDatePart;
25✔
234

25✔
235
    if (!targetPart) {
25!
NEW
236
      return;
×
NEW
237
    }
×
238

25✔
239
    const { start, end } = this.inputSelection;
25✔
240
    const newValue = this.trySpinValue(targetPart, delta);
25✔
241
    this.value = newValue as TValue;
25✔
242
    this.updateComplete.then(() => this.input.setSelectionRange(start, end));
25✔
243
  }
25✔
244

18✔
245
  /** Decrements a date/time portion. */
18✔
246
  public stepDown(datePart?: TPart, delta?: number): void {
18✔
247
    const targetPart = datePart || this.targetDatePart;
20✔
248

20✔
249
    if (!targetPart) {
20!
NEW
250
      return;
×
NEW
251
    }
×
252

20✔
253
    const { start, end } = this.inputSelection;
20✔
254
    const newValue = this.trySpinValue(targetPart, delta, true);
20✔
255
    this.value = newValue;
20✔
256
    this.updateComplete.then(() => this.input.setSelectionRange(start, end));
20✔
257
  }
20✔
258

18✔
259
  /** Clears the input element of user input. */
18✔
260
  public clear(): void {
18✔
261
    this.maskedValue = '';
12✔
262
    this.value = null;
12✔
263
  }
12✔
264

18✔
265
  protected setToday() {
18✔
266
    this.value = new Date() as TValue;
1✔
267
    this.handleInput();
1✔
268
  }
1✔
269

18✔
270
  protected handleDragLeave() {
18✔
271
    if (!this.focused) {
2✔
272
      this.updateMask();
1✔
273
    }
1✔
274
  }
2✔
275

18✔
276
  protected handleDragEnter() {
18✔
277
    if (!this.focused) {
1✔
278
      this.maskedValue = this.getMaskedValue();
1✔
279
    }
1✔
280
  }
1✔
281

18✔
282
  protected async updateInput(string: string, range: MaskRange) {
18✔
283
    const { value, end } = this.parser.replace(
16✔
284
      this.maskedValue,
16✔
285
      string,
16✔
286
      range.start,
16✔
287
      range.end
16✔
288
    );
16✔
289

16✔
290
    this.maskedValue = value;
16✔
291

16✔
292
    this.updateValue();
16✔
293
    this.requestUpdate();
16✔
294

16✔
295
    if (range.start !== this.inputFormat.length) {
16✔
296
      this.handleInput();
16✔
297
    }
16✔
298
    await this.updateComplete;
16✔
299
    this.input.setSelectionRange(end, end);
16✔
300
  }
16✔
301

18✔
302
  protected trySpinValue(
18✔
303
    datePart: TPart,
45✔
304
    delta?: number,
45✔
305
    negative = false
45✔
306
  ): TValue {
45✔
307
    // default to 1 if a delta is set to 0 or any other falsy value
45✔
308
    const _delta =
45✔
309
      delta || this.datePartDeltas[datePart as keyof DatePartDeltas] || 1;
45✔
310

45✔
311
    const spinValue = negative ? -Math.abs(_delta) : Math.abs(_delta);
45✔
312
    return this.spinValue(datePart, spinValue);
45✔
313
  }
45✔
314

18✔
315
  protected isComplete(): boolean {
18✔
316
    return !this.maskedValue.includes(this.prompt);
98✔
317
  }
98✔
318

18✔
319
  protected override _updateSetRangeTextValue() {
18✔
320
    this.updateValue();
5✔
321
  }
5✔
322

18✔
323
  protected navigateParts(delta: number) {
18✔
324
    const position = this.getNewPosition(this.input.value, delta);
6✔
325
    this.setSelectionRange(position, position);
6✔
326
  }
6✔
327

18✔
328
  protected async keyboardSpin(direction: 'up' | 'down') {
18✔
329
    direction === 'up' ? this.stepUp() : this.stepDown();
21✔
330
    this.handleInput();
21✔
331
    await this.updateComplete;
21✔
332
    this.setSelectionRange(this.selection.start, this.selection.end);
21✔
333
  }
21✔
334

18✔
335
  @eventOptions({ passive: false })
18✔
336
  private async onWheel(event: WheelEvent) {
18✔
337
    if (!this.focused || this.readOnly) {
5✔
338
      return;
2✔
339
    }
2✔
340

3✔
341
    event.preventDefault();
3✔
342
    event.stopPropagation();
3✔
343

3✔
344
    const { start, end } = this.inputSelection;
3✔
345
    event.deltaY > 0 ? this.stepDown() : this.stepUp();
5✔
346
    this.handleInput();
5✔
347

5✔
348
    await this.updateComplete;
5✔
349
    this.setSelectionRange(start, end);
3✔
350
  }
5✔
351

18✔
352
  protected updateDefaultMask(): void {
18✔
353
    this._defaultMask = DateTimeUtil.getDefaultMask(this.locale);
502✔
354
  }
502✔
355

18✔
356
  protected override renderInput() {
18✔
357
    return html`
1,758✔
358
      <input
1,758✔
359
        type="text"
1,758✔
360
        part=${partMap(this.resolvePartNames('input'))}
1,758✔
361
        name=${ifDefined(this.name)}
1,758✔
362
        .value=${live(this.maskedValue)}
1,758✔
363
        .placeholder=${live(this.placeholder || this.emptyMask)}
1,758!
364
        ?readonly=${this.readOnly}
1,758✔
365
        ?disabled=${this.disabled}
1,758✔
366
        @blur=${this.handleBlur}
1,758✔
367
        @focus=${this.handleFocus}
1,758✔
368
        @input=${super.handleInput}
1,758✔
369
        @wheel=${this.onWheel}
1,758✔
370
        @keydown=${super.handleKeydown}
1,758✔
371
        @click=${this.handleClick}
1,758✔
372
        @cut=${this.handleCut}
1,758✔
373
        @compositionstart=${this.handleCompositionStart}
1,758✔
374
        @compositionend=${this.handleCompositionEnd}
1,758✔
375
        @dragenter=${this.handleDragEnter}
1,758✔
376
        @dragleave=${this.handleDragLeave}
1,758✔
377
        @dragstart=${this.handleDragStart}
1,758✔
378
      />
1,758✔
379
    `;
1,758✔
380
  }
1,758✔
381

18✔
382
  protected abstract override handleInput(): void;
18✔
383
  protected abstract updateMask(): void;
18✔
384
  protected abstract updateValue(): void;
18✔
385
  protected abstract getNewPosition(value: string, direction: number): number;
18✔
386
  protected abstract spinValue(datePart: TPart, delta: number): TValue;
18✔
387
  protected abstract setMask(string: string): void;
18✔
388
  protected abstract getMaskedValue(): string;
18✔
389
  protected abstract handleBlur(): void;
18✔
390
  protected abstract handleFocus(): Promise<void>;
18✔
391

18✔
392
  // #endregion
18✔
393
}
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