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

IgniteUI / igniteui-webcomponents / 15702329392

17 Jun 2025 08:36AM UTC coverage: 98.184% (-0.06%) from 98.243%
15702329392

Pull #1733

github

web-flow
Merge 773dc529e into 43a4c38cb
Pull Request #1733: Add editable date range input

4975 of 5234 branches covered (95.05%)

Branch coverage included in aggregate %.

942 of 966 new or added lines in 7 files covered. (97.52%)

1 existing line in 1 file now uncovered.

31847 of 32269 relevant lines covered (98.69%)

1796.03 hits per line

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

97.58
/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
  arrowDown,
18✔
7
  arrowLeft,
18✔
8
  arrowRight,
18✔
9
  arrowUp,
18✔
10
  ctrlKey,
18✔
11
} from '../common/controllers/key-bindings.js';
18✔
12
import { partMap } from '../common/part-map.js';
18✔
13
import {
18✔
14
  IgcMaskInputBaseComponent,
18✔
15
  type MaskRange,
18✔
16
} from '../mask-input/mask-input-base.js';
18✔
17

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

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

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

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

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

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

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

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

18✔
90
  // #endregion
18✔
91

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

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

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

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

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

18✔
125
  public get min(): Date | null {
18✔
126
    return this._min;
1,975✔
127
  }
1,975✔
128

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

18✔
139
  public get max(): Date | null {
18✔
140
    return this._max;
1,973✔
141
  }
1,973✔
142

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

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

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

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

18✔
172
  // #endregion
18✔
173

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

18✔
176
  constructor() {
18✔
177
    super();
498✔
178

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

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

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

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

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

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

18✔
228
  // #endregion
18✔
229

18✔
230
  // #region Methods
18✔
231

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

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

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

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

21✔
250
    if (!targetPart) {
21!
NEW
251
      return;
×
NEW
252
    }
×
253

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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