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

IgniteUI / igniteui-webcomponents / 15535432632

09 Jun 2025 01:14PM UTC coverage: 98.239% (-0.06%) from 98.299%
15535432632

Pull #1733

github

web-flow
Merge 18aef5866 into c01572b11
Pull Request #1733: Add editable date range input

4985 of 5242 branches covered (95.1%)

Branch coverage included in aggregate %.

947 of 971 new or added lines in 7 files covered. (97.53%)

1 existing line in 1 file now uncovered.

31665 of 32065 relevant lines covered (98.75%)

1803.53 hits per line

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

97.61
/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 { live } from 'lit/directives/live.js';
18✔
4
import {
18✔
5
  addKeybindings,
18✔
6
  altKey,
18✔
7
  arrowDown,
18✔
8
  arrowLeft,
18✔
9
  arrowRight,
18✔
10
  arrowUp,
18✔
11
  ctrlKey,
18✔
12
} from '../common/controllers/key-bindings.js';
18✔
13
import { partMap } from '../common/part-map.js';
18✔
14
import { noop } from '../common/util.js';
18✔
15
import {
18✔
16
  IgcMaskInputBaseComponent,
18✔
17
  type MaskRange,
18✔
18
} from '../mask-input/mask-input-base.js';
18✔
19

18✔
20
import { ifDefined } from 'lit/directives/if-defined.js';
18✔
21
import { convertToDate } from '../calendar/helpers.js';
18✔
22
import { watch } from '../common/decorators/watch.js';
18✔
23
import type { AbstractConstructor } from '../common/mixins/constructor.js';
18✔
24
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
18✔
25
import type { DateRangeValue } from '../date-range-picker/date-range-picker.js';
18✔
26
import type { IgcInputComponentEventMap } from '../input/input-base.js';
18✔
27
import { type DatePartDeltas, DateParts, DateTimeUtil } from './date-util.js';
18✔
28
import type {
18✔
29
  DatePart,
18✔
30
  DatePartInfo,
18✔
31
  DateRangePart,
18✔
32
  DateRangePartInfo,
18✔
33
} from './date-util.js';
18✔
34
import { dateTimeInputValidators } from './validators.js';
18✔
35

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

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

18✔
60
  protected abstract _datePartDeltas: DatePartDeltas;
18✔
61
  protected abstract get targetDatePart(): TPart | undefined;
18✔
62

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

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

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

18✔
88
  protected get datePartDeltas(): DatePartDeltas {
18✔
89
    return Object.assign({}, this._datePartDeltas, this.spinDelta);
46✔
90
  }
46✔
91

18✔
92
  // #endregion
18✔
93

18✔
94
  // #region Public properties
18✔
95

18✔
96
  public abstract override value: TValue | null;
18✔
97

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

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

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

18✔
127
  public get min(): Date | null {
18✔
128
    return this._min;
1,965✔
129
  }
1,965✔
130

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

18✔
141
  public get max(): Date | null {
18✔
142
    return this._max;
1,963✔
143
  }
1,963✔
144

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

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

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

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

18✔
174
  // #endregion
18✔
175

18✔
176
  // #region Lifecycle & observers
18✔
177

18✔
178
  constructor() {
18✔
179
    super();
495✔
180

495✔
181
    addKeybindings(this, {
495✔
182
      skip: () => this.readOnly,
495✔
183
      bindingDefaults: { preventDefault: true, triggers: ['keydownRepeat'] },
495✔
184
    })
495✔
185
      // Skip default spin when in the context of a date picker
495✔
186
      .set([altKey, arrowUp], noop)
495✔
187
      .set([altKey, arrowDown], noop)
495✔
188

495✔
189
      .set([ctrlKey, ';'], this.setToday)
495✔
190
      .set(arrowUp, this.keyboardSpin.bind(this, 'up'))
495✔
191
      .set(arrowDown, this.keyboardSpin.bind(this, 'down'))
495✔
192
      .set([ctrlKey, arrowLeft], this.navigateParts.bind(this, 0))
495✔
193
      .set([ctrlKey, arrowRight], this.navigateParts.bind(this, 1));
495✔
194
  }
495✔
195

18✔
196
  public override connectedCallback() {
18✔
197
    super.connectedCallback();
495✔
198
    this.updateDefaultMask();
495✔
199
    this.setMask(this.inputFormat);
495✔
200
    this._updateValidity();
495✔
201
    if (this.value) {
495✔
202
      this.updateMask();
167✔
203
    }
167✔
204
  }
495✔
205

18✔
206
  @watch('locale', { waitUntilFirstUpdate: true })
18✔
207
  protected _setDefaultMask(): void {
18✔
208
    if (!this._inputFormat) {
4✔
209
      this.updateDefaultMask();
4✔
210
      this.setMask(this._defaultMask);
4✔
211
    }
4✔
212

4✔
213
    if (this.value) {
4✔
214
      this.updateMask();
1✔
215
    }
1✔
216
  }
4✔
217

18✔
218
  @watch('displayFormat', { waitUntilFirstUpdate: true })
18✔
219
  protected _setDisplayFormat(): void {
18✔
220
    if (this.value) {
47✔
221
      this.updateMask();
31✔
222
    }
31✔
223
  }
47✔
224

18✔
225
  @watch('prompt', { waitUntilFirstUpdate: true })
18✔
226
  protected _promptChange(): void {
18✔
227
    if (!this.prompt) {
1!
NEW
228
      this.prompt = this.parser.prompt;
×
229
    } else {
1✔
230
      this.parser.prompt = this.prompt;
1✔
231
    }
1✔
232
  }
1✔
233

18✔
234
  // #endregion
18✔
235

18✔
236
  // #region Methods
18✔
237

18✔
238
  /** Increments a date/time portion. */
18✔
239
  public stepUp(datePart?: TPart, delta?: number): void {
18✔
240
    const targetPart = datePart || this.targetDatePart;
25✔
241

25✔
242
    if (!targetPart) {
25!
NEW
243
      return;
×
NEW
244
    }
×
245

25✔
246
    const { start, end } = this.inputSelection;
25✔
247
    const newValue = this.trySpinValue(targetPart, delta);
25✔
248
    this.value = newValue as TValue;
25✔
249
    this.updateComplete.then(() => this.input.setSelectionRange(start, end));
25✔
250
  }
25✔
251

18✔
252
  /** Decrements a date/time portion. */
18✔
253
  public stepDown(datePart?: TPart, delta?: number): void {
18✔
254
    const targetPart = datePart || this.targetDatePart;
21✔
255

21✔
256
    if (!targetPart) {
21!
NEW
257
      return;
×
NEW
258
    }
×
259

21✔
260
    const { start, end } = this.inputSelection;
21✔
261
    const newValue = this.trySpinValue(targetPart, delta, true);
21✔
262
    this.value = newValue;
21✔
263
    this.updateComplete.then(() => this.input.setSelectionRange(start, end));
21✔
264
  }
21✔
265

18✔
266
  /** Clears the input element of user input. */
18✔
267
  public clear(): void {
18✔
268
    this.maskedValue = '';
12✔
269
    this.value = null;
12✔
270
  }
12✔
271

18✔
272
  protected setToday() {
18✔
273
    this.value = new Date() as TValue;
1✔
274
    this.handleInput();
1✔
275
  }
1✔
276

18✔
277
  protected handleDragLeave() {
18✔
278
    if (!this.focused) {
2✔
279
      this.updateMask();
1✔
280
    }
1✔
281
  }
2✔
282

18✔
283
  protected handleDragEnter() {
18✔
284
    if (!this.focused) {
1✔
285
      this.maskedValue = this.getMaskedValue();
1✔
286
    }
1✔
287
  }
1✔
288

18✔
289
  protected async updateInput(string: string, range: MaskRange) {
18✔
290
    const { value, end } = this.parser.replace(
16✔
291
      this.maskedValue,
16✔
292
      string,
16✔
293
      range.start,
16✔
294
      range.end
16✔
295
    );
16✔
296

16✔
297
    this.maskedValue = value;
16✔
298

16✔
299
    this.updateValue();
16✔
300
    this.requestUpdate();
16✔
301

16✔
302
    if (range.start !== this.inputFormat.length) {
16✔
303
      this.handleInput();
16✔
304
    }
16✔
305
    await this.updateComplete;
16✔
306
    this.input.setSelectionRange(end, end);
16✔
307
  }
16✔
308

18✔
309
  protected trySpinValue(
18✔
310
    datePart: TPart,
46✔
311
    delta?: number,
46✔
312
    negative = false
46✔
313
  ): TValue {
46✔
314
    // default to 1 if a delta is set to 0 or any other falsy value
46✔
315
    const _delta =
46✔
316
      delta || this.datePartDeltas[datePart as keyof DatePartDeltas] || 1;
46✔
317

46✔
318
    const spinValue = negative ? -Math.abs(_delta) : Math.abs(_delta);
46✔
319
    return this.spinValue(datePart, spinValue);
46✔
320
  }
46✔
321

18✔
322
  protected isComplete(): boolean {
18✔
323
    return !this.maskedValue.includes(this.prompt);
90✔
324
  }
90✔
325

18✔
326
  protected override _updateSetRangeTextValue() {
18✔
327
    this.updateValue();
5✔
328
  }
5✔
329

18✔
330
  protected navigateParts(delta: number) {
18✔
331
    const position = this.getNewPosition(this.input.value, delta);
6✔
332
    this.setSelectionRange(position, position);
6✔
333
  }
6✔
334

18✔
335
  protected async keyboardSpin(direction: 'up' | 'down') {
18✔
336
    direction === 'up' ? this.stepUp() : this.stepDown();
22✔
337
    this.handleInput();
22✔
338
    await this.updateComplete;
22✔
339
    this.setSelectionRange(this.selection.start, this.selection.end);
22✔
340
  }
22✔
341

18✔
342
  @eventOptions({ passive: false })
18✔
343
  private async onWheel(event: WheelEvent) {
18✔
344
    if (!this.focused || this.readOnly) {
5✔
345
      return;
2✔
346
    }
2✔
347

3✔
348
    event.preventDefault();
3✔
349
    event.stopPropagation();
3✔
350

3✔
351
    const { start, end } = this.inputSelection;
3✔
352
    event.deltaY > 0 ? this.stepDown() : this.stepUp();
5✔
353
    this.handleInput();
5✔
354

5✔
355
    await this.updateComplete;
5✔
356
    this.setSelectionRange(start, end);
3✔
357
  }
5✔
358

18✔
359
  protected updateDefaultMask(): void {
18✔
360
    this._defaultMask = DateTimeUtil.getDefaultMask(this.locale);
499✔
361
  }
499✔
362

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

18✔
389
  protected abstract override handleInput(): void;
18✔
390
  protected abstract updateMask(): void;
18✔
391
  protected abstract updateValue(): void;
18✔
392
  protected abstract getNewPosition(value: string, direction: number): number;
18✔
393
  protected abstract spinValue(datePart: TPart, delta: number): TValue;
18✔
394
  protected abstract setMask(string: string): void;
18✔
395
  protected abstract getMaskedValue(): string;
18✔
396
  protected abstract handleBlur(): void;
18✔
397
  protected abstract handleFocus(): Promise<void>;
18✔
398

18✔
399
  // #endregion
18✔
400
}
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