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

IgniteUI / igniteui-angular / 11501515279

24 Oct 2024 02:33PM UTC coverage: 91.583% (-0.008%) from 91.591%
11501515279

push

github

web-flow
fix(date-picker): KB nav and calendar focus handling (#14936)

* test(date-picker): update KB nav and interaction tests to check focus state

* fix(date-picker): move focus to calendar wrapper on open

* fix(calendar): select on click instead of mousedown

* fix(calendar): drop stopPropagation calls on kb nav events

* fix(date-range-picker): move focus to calendar wrapper on open

* fix(quick-filter): chip edit with date/time pickers

* fix(date-pickers): return focus to input on dropdown escape

---------

Co-authored-by: Stamen Stoychev <sstoychev@infragistics.com>

12803 of 15016 branches covered (85.26%)

20 of 20 new or added lines in 5 files covered. (100.0%)

3 existing lines in 3 files now uncovered.

26048 of 28442 relevant lines covered (91.58%)

33799.74 hits per line

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

91.19
/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts
1
import {
2
    AfterViewInit, booleanAttribute, ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef,
3
    EventEmitter, HostBinding, HostListener, Inject, Injector, Input, LOCALE_ID,
4
    OnChanges, OnDestroy, OnInit, Optional, Output, QueryList,
5
    SimpleChanges, TemplateRef, ViewChild, ViewContainerRef
6
} from '@angular/core';
7
import { NgTemplateOutlet, NgIf } from '@angular/common';
8
import {
9
    AbstractControl, ControlValueAccessor, NgControl,
10
    NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator
11
} from '@angular/forms';
12

13
import { fromEvent, merge, MonoTypeOperatorFunction, noop, Subscription } from 'rxjs';
14
import { filter, takeUntil } from 'rxjs/operators';
15

16
import { CalendarSelection, IgxCalendarComponent } from '../calendar/public_api';
17
import { DateRangeType } from '../core/dates';
18
import { DateRangePickerResourceStringsEN, IDateRangePickerResourceStrings } from '../core/i18n/date-range-picker-resources';
19
import { IBaseCancelableBrowserEventArgs, isDate, parseDate, PlatformUtil } from '../core/utils';
20
import { IgxCalendarContainerComponent } from '../date-common/calendar-container/calendar-container.component';
21
import { PickerBaseDirective } from '../date-common/picker-base.directive';
22
import { IgxPickerActionsDirective } from '../date-common/picker-icons.common';
23
import { DateTimeUtil } from '../date-common/util/date-time.util';
24
import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive';
25
import {
26
    IgxInputDirective, IgxInputGroupComponent, IgxInputGroupType, IgxInputState,
27
    IgxLabelDirective, IGX_INPUT_GROUP_TYPE
28
} from '../input-group/public_api';
29
import {
30
    AutoPositionStrategy, IgxOverlayService, OverlayCancelableEventArgs, OverlayEventArgs,
31
    OverlaySettings, PositionSettings
32
} from '../services/public_api';
33
import { DateRange, IgxDateRangeEndComponent, IgxDateRangeInputsBaseComponent, IgxDateRangeSeparatorDirective, IgxDateRangeStartComponent, DateRangePickerFormatPipe } from './date-range-picker-inputs.common';
34
import { IgxPrefixDirective } from '../directives/prefix/prefix.directive';
35
import { IgxIconComponent } from '../icon/icon.component';
36
import { getCurrentResourceStrings } from '../core/i18n/resources';
37
import { fadeIn, fadeOut } from 'igniteui-angular/animations';
38

39
const SingleInputDatesConcatenationString = ' - ';
2✔
40

41
/**
42
 * Provides the ability to select a range of dates from a calendar UI or editable inputs.
43
 *
44
 * @igxModule IgxDateRangeModule
45
 *
46
 * @igxTheme igx-input-group-theme, igx-calendar-theme, igx-date-range-picker-theme
47
 *
48
 * @igxKeywords date, range, date range, date picker
49
 *
50
 * @igxGroup scheduling
51
 *
52
 * @remarks
53
 * It displays the range selection in a single or two input fields.
54
 * The default template displays a single *readonly* input field
55
 * while projecting `igx-date-range-start` and `igx-date-range-end`
56
 * displays two *editable* input fields.
57
 *
58
 * @example
59
 * ```html
60
 * <igx-date-range-picker mode="dropdown"></igx-date-range-picker>
61
 * ```
62
 */
63
@Component({
64
    selector: 'igx-date-range-picker',
65
    templateUrl: './date-range-picker.component.html',
66
    providers: [
67
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxDateRangePickerComponent, multi: true },
68
        { provide: NG_VALIDATORS, useExisting: IgxDateRangePickerComponent, multi: true }
69
    ],
70
    standalone: true,
71
    imports: [
72
        NgIf,
73
        NgTemplateOutlet,
74
        IgxIconComponent,
75
        IgxInputGroupComponent,
76
        IgxInputDirective,
77
        IgxPrefixDirective,
78
        DateRangePickerFormatPipe
79
    ]
80
})
81
export class IgxDateRangePickerComponent extends PickerBaseDirective
2✔
82
    implements OnChanges, OnInit, AfterViewInit, OnDestroy, ControlValueAccessor, Validator {
83

84
    /**
85
     * The number of displayed month views.
86
     *
87
     * @remarks
88
     * Default is `2`.
89
     *
90
     * @example
91
     * ```html
92
     * <igx-date-range-picker [displayMonthsCount]="3"></igx-date-range-picker>
93
     * ```
94
     */
95
    @Input()
96
    public displayMonthsCount = 2;
69✔
97

98
    /**
99
     * Gets/Sets whether dates that are not part of the current month will be displayed.
100
     *
101
     * @remarks
102
     * Default value is `false`.
103
     *
104
     * @example
105
     * ```html
106
     * <igx-date-range-picker [hideOutsideDays]="true"></igx-date-range-picker>
107
     * ```
108
     */
109
    @Input({ transform: booleanAttribute })
110
    public hideOutsideDays: boolean;
111

112
    /**
113
     * A custom formatter function, applied on the selected or passed in date.
114
     *
115
     * @example
116
     * ```typescript
117
     * private dayFormatter = new Intl.DateTimeFormat("en", { weekday: "long" });
118
     * private monthFormatter = new Intl.DateTimeFormat("en", { month: "long" });
119
     *
120
     * public formatter(date: Date): string {
121
     *  return `${this.dayFormatter.format(date)} - ${this.monthFormatter.format(date)} - ${date.getFullYear()}`;
122
     * }
123
     * ```
124
     * ```html
125
     * <igx-date-range-picker [formatter]="formatter"></igx-date-range-picker>
126
     * ```
127
     */
128
    @Input()
129
    public formatter: (val: DateRange) => string;
130

131
    /**
132
     * Overrides the default text of the calendar dialog **Done** button.
133
     *
134
     * @remarks
135
     * Defaults to the value from resource strings, `"Done"` for the built-in EN.
136
     * The button will only show up in `dialog` mode.
137
     *
138
     * @example
139
     * ```html
140
     * <igx-date-range-picker doneButtonText="完了"></igx-date-range-picker>
141
     * ```
142
     */
143
    @Input()
144
    public set doneButtonText(value: string) {
145
        this._doneButtonText = value;
1✔
146
    }
147

148
    public get doneButtonText(): string {
149
        if (this._doneButtonText === null) {
6✔
150
            return this.resourceStrings.igx_date_range_picker_done_button;
5✔
151
        }
152
        return this._doneButtonText;
1✔
153
    }
154
    /**
155
     * Custom overlay settings that should be used to display the calendar.
156
     *
157
     * @example
158
     * ```html
159
     * <igx-date-range-picker [overlaySettings]="customOverlaySettings"></igx-date-range-picker>
160
     * ```
161
     */
162
    @Input()
163
    public override overlaySettings: OverlaySettings;
164

165
    /**
166
     * The format used when editable inputs are not focused.
167
     *
168
     * @remarks
169
     * Uses Angular's DatePipe.
170
     *
171
     * @example
172
     * ```html
173
     * <igx-date-range-picker displayFormat="EE/M/yy"></igx-date-range-picker>
174
     * ```
175
     *
176
     */
177
    @Input()
178
    public override displayFormat: string;
179

180
    /**
181
     * The expected user input format and placeholder.
182
     *
183
     * @remarks
184
     * Default is `"'MM/dd/yyyy'"`
185
     *
186
     * @example
187
     * ```html
188
     * <igx-date-range-picker inputFormat="dd/MM/yy"></igx-date-range-picker>
189
     * ```
190
     */
191
    @Input()
192
    public override inputFormat: string;
193

194
    /**
195
     * The minimum value in a valid range.
196
     *
197
     * @example
198
     * <igx-date-range-picker [minValue]="minDate"></igx-date-range-picker>
199
     */
200
    @Input()
201
    public set minValue(value: Date | string) {
202
        this._minValue = value;
27✔
203
        this.onValidatorChange();
27✔
204
    }
205

206
    public get minValue(): Date | string {
207
        return this._minValue;
97✔
208
    }
209

210
    /**
211
     * The maximum value in a valid range.
212
     *
213
     * @example
214
     * <igx-date-range-picker [maxValue]="maxDate"></igx-date-range-picker>
215
     */
216
    @Input()
217
    public set maxValue(value: Date | string) {
218
        this._maxValue = value;
27✔
219
        this.onValidatorChange();
27✔
220
    }
221

222
    public get maxValue(): Date | string {
223
        return this._maxValue;
97✔
224
    }
225

226
    /**
227
     * An accessor that sets the resource strings.
228
     * By default it uses EN resources.
229
     */
230
    @Input()
231
    public set resourceStrings(value: IDateRangePickerResourceStrings) {
232
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
×
233
    }
234

235
    /**
236
     * An accessor that returns the resource strings.
237
     */
238
    public get resourceStrings(): IDateRangePickerResourceStrings {
239
        return this._resourceStrings;
45✔
240
    }
241

242
    /**
243
     * Sets the `placeholder` for single-input `IgxDateRangePickerComponent`.
244
     *
245
     *   @example
246
     * ```html
247
     * <igx-date-range-picker [placeholder]="'Choose your dates'"></igx-date-range-picker>
248
     * ```
249
     */
250
    @Input()
251
    public override placeholder = '';
69✔
252

253
    /**
254
     * Gets/Sets the container used for the popup element.
255
     *
256
     * @remarks
257
     *  `outlet` is an instance of `IgxOverlayOutletDirective` or an `ElementRef`.
258
     * @example
259
     * ```html
260
     * <div igxOverlayOutlet #outlet="overlay-outlet"></div>
261
     * //..
262
     * <igx-date-range-picker [outlet]="outlet"></igx-date-range-picker>
263
     * //..
264
     * ```
265
     */
266
    @Input()
267
    public override outlet: IgxOverlayOutletDirective | ElementRef<any>;
268

269
    /**
270
     * Show/hide week numbers
271
     *
272
     * @remarks
273
     * Default is `false`.
274
     *
275
     * @example
276
     * ```html
277
     * <igx-date-range-picker [showWeekNumbers]="true"></igx-date-range-picker>
278
     * ``
279
     */
280
    @Input({ transform: booleanAttribute })
281
    public showWeekNumbers = false;
69✔
282

283
    /**
284
     * Emitted when the picker's value changes. Used for two-way binding.
285
     *
286
     * @example
287
     * ```html
288
     * <igx-date-range-picker [(value)]="date"></igx-date-range-picker>
289
     * ```
290
     */
291
    @Output()
292
    public valueChange = new EventEmitter<DateRange>();
69✔
293

294
    /** @hidden @internal */
295
    @HostBinding('class.igx-date-range-picker')
296
    public cssClass = 'igx-date-range-picker';
69✔
297

298
    @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef })
299
    private viewContainerRef: ViewContainerRef;
300

301
    /** @hidden @internal */
302
    @ViewChild(IgxInputDirective)
303
    public inputDirective: IgxInputDirective;
304

305
    /** @hidden @internal */
306
    @ContentChildren(IgxDateRangeInputsBaseComponent)
307
    public projectedInputs: QueryList<IgxDateRangeInputsBaseComponent>;
308

309
    @ContentChild(IgxLabelDirective)
310
    public label: IgxLabelDirective;
311

312
    @ContentChild(IgxPickerActionsDirective)
313
    public pickerActions: IgxPickerActionsDirective;
314

315
    /** @hidden @internal */
316
    @ContentChild(IgxDateRangeSeparatorDirective, { read: TemplateRef })
317
    public dateSeparatorTemplate: TemplateRef<any>;
318

319
    /** @hidden @internal */
320
    public get dateSeparator(): string {
321
        if (this._dateSeparator === null) {
40✔
322
            return this.resourceStrings.igx_date_range_picker_date_separator;
40✔
323
        }
324
        return this._dateSeparator;
×
325
    }
326

327
    /** @hidden @internal */
328
    public get appliedFormat(): string {
329
        return DateTimeUtil.getLocaleDateFormat(this.locale, this.displayFormat)
506✔
330
            || DateTimeUtil.DEFAULT_INPUT_FORMAT;
331
    }
332

333
    /** @hidden @internal */
334
    public get singleInputFormat(): string {
335
        if (this.placeholder !== '') {
236✔
336
            return this.placeholder;
2✔
337
        }
338

339
        const format = this.appliedFormat;
234✔
340
        return `${format}${SingleInputDatesConcatenationString}${format}`;
234✔
341
    }
342

343
    /**
344
     * Gets calendar state.
345
     *
346
     * ```typescript
347
     * let state = this.dateRange.collapsed;
348
     * ```
349
     */
350
    public override get collapsed(): boolean {
351
        return this._collapsed;
415✔
352
    }
353

354
    /**
355
     * The currently selected value / range from the calendar
356
     *
357
     * @remarks
358
     * The current value is of type `DateRange`
359
     *
360
     * @example
361
     * ```typescript
362
     * const newValue: DateRange = { start: new Date("2/2/2012"), end: new Date("3/3/2013")};
363
     * this.dateRangePicker.value = newValue;
364
     * ```
365
     */
366
    public get value(): DateRange | null {
367
        return this._value;
895✔
368
    }
369

370
    @Input()
371
    public set value(value: DateRange | null) {
372
        this.updateValue(value);
70✔
373
        this.onChangeCallback(value);
70✔
374
        this.valueChange.emit(value);
70✔
375
    }
376

377
    /** @hidden @internal */
378
    public get hasProjectedInputs(): boolean {
379
        return this.projectedInputs?.length > 0;
1,317✔
380
    }
381

382
    /** @hidden @internal */
383
    public get separatorClass(): string {
384
        return 'igx-date-range-picker__label';
240✔
385
    }
386

387
    protected override get toggleContainer(): HTMLElement | undefined {
388
        return this._calendarContainer;
7✔
389
    }
390

391
    private get required(): boolean {
392
        if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) {
135✔
393
            const error = this._ngControl.control.validator({} as AbstractControl);
98✔
394
            return (error && error.required) ? true : false;
98!
395
        }
396

397
        return false;
37✔
398
    }
399

400
    private get calendar(): IgxCalendarComponent {
401
        return this._calendar;
594✔
402
    }
403

404
    private get dropdownOverlaySettings(): OverlaySettings {
405
        return Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
26✔
406
    }
407

408
    private get dialogOverlaySettings(): OverlaySettings {
409
        return Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
6✔
410
    }
411

412
    private _resourceStrings = getCurrentResourceStrings(DateRangePickerResourceStringsEN);
69✔
413
    private _doneButtonText = null;
69✔
414
    private _dateSeparator = null;
69✔
415
    private _value: DateRange | null;
416
    private _overlayId: string;
417
    private _ngControl: NgControl;
418
    private _statusChanges$: Subscription;
419
    private _calendar: IgxCalendarComponent;
420
    private _calendarContainer?: HTMLElement;
421
    private _positionSettings: PositionSettings;
422
    private _focusedInput: IgxDateRangeInputsBaseComponent;
423
    private _overlaySubFilter:
69✔
424
        [MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs | OverlayCancelableEventArgs>] = [
425
            filter(x => x.id === this._overlayId),
102✔
426
            takeUntil(merge(this._destroy$, this.closed))
427
        ];
428
    private _dialogOverlaySettings: OverlaySettings = {
69✔
429
        closeOnOutsideClick: true,
430
        modal: true,
431
        closeOnEscape: true
432
    };
433
    private _dropDownOverlaySettings: OverlaySettings = {
69✔
434
        closeOnOutsideClick: true,
435
        modal: false,
436
        closeOnEscape: true
437
    };
438
    private onChangeCallback: (dateRange: DateRange) => void = noop;
69✔
439
    private onTouchCallback: () => void = noop;
69✔
440
    private onValidatorChange: () => void = noop;
69✔
441

442
    constructor(element: ElementRef,
443
        @Inject(LOCALE_ID) _localeId: string,
444
        protected platform: PlatformUtil,
69✔
445
        private _injector: Injector,
69✔
446
        private _cdr: ChangeDetectorRef,
69✔
447
        @Inject(IgxOverlayService) private _overlayService: IgxOverlayService,
69✔
448
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType?: IgxInputGroupType) {
449
        super(element, _localeId, _inputGroupType);
69✔
450
        this.locale = this.locale || this._localeId;
69!
451
    }
452

453
    /** @hidden @internal */
454
    @HostListener('keydown', ['$event'])
455
    /** @hidden @internal */
456
    public onKeyDown(event: KeyboardEvent): void {
457
        switch (event.key) {
4!
458
            case this.platform.KEYMAP.ARROW_UP:
459
                if (event.altKey) {
×
460
                    this.close();
×
461
                }
462
                break;
×
463
            case this.platform.KEYMAP.ARROW_DOWN:
464
                if (event.altKey) {
4✔
465
                    this.open();
4✔
466
                }
467
                break;
4✔
468
        }
469
    }
470

471
    /**
472
     * Opens the date range picker's dropdown or dialog.
473
     *
474
     * @example
475
     * ```html
476
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
477
     *
478
     * <button type="button" igxButton (click)="dateRange.open()">Open Dialog</button
479
     * ```
480
     */
481
    public open(overlaySettings?: OverlaySettings): void {
482
        if (!this.collapsed || this.disabled) {
36✔
483
            return;
4✔
484
        }
485

486
        const settings = Object.assign({}, this.isDropdown
32✔
487
            ? this.dropdownOverlaySettings
488
            : this.dialogOverlaySettings
489
            , overlaySettings);
490

491
        this._overlayId = this._overlayService
32✔
492
            .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings);
493
        this.subscribeToOverlayEvents();
32✔
494
        this._overlayService.show(this._overlayId);
32✔
495
    }
496

497
    /**
498
     * Closes the date range picker's dropdown or dialog.
499
     *
500
     * @example
501
     * ```html
502
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
503
     *
504
     * <button type="button" igxButton (click)="dateRange.close()">Close Dialog</button>
505
     * ```
506
     */
507
    public close(): void {
508
        if (!this.collapsed) {
46✔
509
            this._overlayService.hide(this._overlayId);
29✔
510
        }
511
    }
512

513
    /**
514
     * Toggles the date range picker's dropdown or dialog
515
     *
516
     * @example
517
     * ```html
518
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
519
     *
520
     * <button type="button" igxButton (click)="dateRange.toggle()">Toggle Dialog</button>
521
     * ```
522
     */
523
    public toggle(overlaySettings?: OverlaySettings): void {
524
        if (!this.collapsed) {
7✔
525
            this.close();
2✔
526
        } else {
527
            this.open(overlaySettings);
5✔
528
        }
529
    }
530

531
    /**
532
     * Selects a range of dates. If no `endDate` is passed, range is 1 day (only `startDate`)
533
     *
534
     * @example
535
     * ```typescript
536
     * public selectFiveDayRange() {
537
     *  const today = new Date();
538
     *  const inFiveDays = new Date(new Date().setDate(today.getDate() + 5));
539
     *  this.dateRange.select(today, inFiveDays);
540
     * }
541
     * ```
542
     */
543
    public select(startDate: Date, endDate?: Date): void {
544
        endDate = endDate ?? startDate;
19✔
545
        const dateRange = [startDate, endDate];
19✔
546
        this.handleSelection(dateRange);
19✔
547
    }
548

549
    /** @hidden @internal */
550
    public writeValue(value: DateRange): void {
551
        this.updateValue(value);
52✔
552
    }
553

554
    /** @hidden @internal */
555
    public registerOnChange(fn: any): void {
556
        this.onChangeCallback = fn;
36✔
557
    }
558

559
    /** @hidden @internal */
560
    public registerOnTouched(fn: any): void {
561
        this.onTouchCallback = fn;
35✔
562
    }
563

564
    /** @hidden @internal */
565
    public validate(control: AbstractControl): ValidationErrors | null {
566
        const value: DateRange = control.value;
181✔
567
        const errors = {};
181✔
568
        if (value) {
181✔
569
            if (this.hasProjectedInputs) {
41✔
570
                const startInput = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
38✔
571
                const endInput = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
76✔
572
                if (!startInput.dateTimeEditor.value) {
38!
573
                    Object.assign(errors, { startValue: true });
×
574
                }
575
                if (!endInput.dateTimeEditor.value) {
38✔
576
                    Object.assign(errors, { endValue: true });
1✔
577
                }
578
            }
579

580
            const min = parseDate(this.minValue);
41✔
581
            const max = parseDate(this.maxValue);
41✔
582
            const start = parseDate(value.start);
41✔
583
            const end = parseDate(value.end);
41✔
584
            if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false))
41✔
585
                || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) {
586
                Object.assign(errors, { minValue: true });
2✔
587
            }
588
            if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false))
41✔
589
                || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) {
590
                Object.assign(errors, { maxValue: true });
1✔
591
            }
592
        }
593

594
        return Object.keys(errors).length > 0 ? errors : null;
181✔
595
    }
596

597
    /** @hidden @internal */
598
    public registerOnValidatorChange?(fn: any): void {
599
        this.onValidatorChange = fn;
35✔
600
    }
601

602
    /** @hidden @internal */
603
    public setDisabledState?(isDisabled: boolean): void {
604
        this.disabled = isDisabled;
36✔
605
    }
606

607
    /** @hidden */
608
    public ngOnInit(): void {
609
        this._ngControl = this._injector.get<NgControl>(NgControl, null);
66✔
610

611
        this.locale = this.locale || this._localeId;
66!
612
    }
613

614
    /** @hidden */
615
    public override ngAfterViewInit(): void {
616
        super.ngAfterViewInit();
63✔
617
        this.subscribeToDateEditorEvents();
63✔
618
        this.configPositionStrategy();
63✔
619
        this.configOverlaySettings();
63✔
620
        this.cacheFocusedInput();
63✔
621
        this.attachOnTouched();
63✔
622

623
        this.setRequiredToInputs();
63✔
624

625
        if (this._ngControl) {
63✔
626
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
28✔
627
        }
628

629
        // delay invocations until the current change detection cycle has completed
630
        Promise.resolve().then(() => {
63✔
631
            this.updateDisabledState();
63✔
632
            this.initialSetValue();
63✔
633
            this.updateInputs();
63✔
634
            // B.P. 07 July 2021 - IgxDateRangePicker not showing initial disabled state with ChangeDetectionStrategy.OnPush #9776
635
            /**
636
             * if disabled is placed on the range picker element and there are projected inputs
637
             * run change detection since igxInput will initially set the projected inputs' disabled to false
638
             */
639
            if (this.hasProjectedInputs && this.disabled) {
63✔
640
                this._cdr.markForCheck();
3✔
641
            }
642
        });
643
        this.updateDisplayFormat();
63✔
644
        this.updateInputFormat();
63✔
645
    }
646

647
    /** @hidden @internal */
648
    public ngOnChanges(changes: SimpleChanges): void {
649
        if (changes['displayFormat'] && this.hasProjectedInputs) {
76✔
650
            this.updateDisplayFormat();
7✔
651
        }
652
        if (changes['inputFormat'] && this.hasProjectedInputs) {
76✔
653
            this.updateInputFormat();
4✔
654
        }
655
        if (changes['disabled']) {
76✔
656
            this.updateDisabledState();
57✔
657
        }
658
    }
659

660
    /** @hidden @internal */
661
    public override ngOnDestroy(): void {
662
        super.ngOnDestroy();
53✔
663
        if (this._statusChanges$) {
53✔
664
            this._statusChanges$.unsubscribe();
28✔
665
        }
666
        if (this._overlayId) {
53✔
667
            this._overlayService.detach(this._overlayId);
8✔
668
        }
669
    }
670

671
    /** @hidden @internal */
672
    public getEditElement() {
673
        return this.inputDirective.nativeElement;
×
674
    }
675

676
    protected onStatusChanged = () => {
69✔
677
        if (this.inputGroup) {
71✔
678
            this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
7✔
679
        } else if (this.hasProjectedInputs) {
64✔
680
            this.projectedInputs
64✔
681
                .forEach((i) => {
682
                    this.setValidityState(i.inputDirective, i.isFocused);
128✔
683
                });
684
        }
685
        this.setRequiredToInputs();
71✔
686
    };
687

688
    private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) {
689
        if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
135✔
690
            if (this.hasValidators && isFocused) {
125✔
691
                inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
2✔
692
            } else {
693
                inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
123✔
694
            }
695
        } else {
696
            inputDirective.valid = IgxInputState.INITIAL;
10✔
697
        }
698
    }
699

700
    private get isTouchedOrDirty(): boolean {
701
        return (this._ngControl.control.touched || this._ngControl.control.dirty);
125✔
702
    }
703

704
    private get hasValidators(): boolean {
705
        return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
125✔
706
    }
707

708
    private handleSelection(selectionData: Date[]): void {
709
        let newValue = this.extractRange(selectionData);
37✔
710
        if (!newValue.start && !newValue.end) {
37!
711
            newValue = null;
×
712
        }
713
        this.value = newValue;
37✔
714
        if (this.isDropdown && selectionData?.length > 1) {
37✔
715
            this.close();
26✔
716
        }
717
    }
718

719
    private handleClosing(e: IBaseCancelableBrowserEventArgs): void {
720
        const args = { owner: this, cancel: e?.cancel, event: e?.event };
32✔
721
        this.closing.emit(args);
32✔
722
        e.cancel = args.cancel;
32✔
723
        if (args.cancel) {
32✔
724
            return;
2✔
725
        }
726

727
        if (this.isDropdown && e?.event && !this.isFocused) {
30!
728
            // outside click
UNCOV
729
            this.updateValidityOnBlur();
×
730
        } else {
731
            this.onTouchCallback();
30✔
732
            // input click
733
            if (this.hasProjectedInputs && this._focusedInput) {
30✔
734
                this._focusedInput.setFocus();
5✔
735
                this._focusedInput = null;
5✔
736
            }
737
            if (this.inputDirective) {
30✔
738
                this.inputDirective.focus();
15✔
739
            }
740
        }
741
    }
742

743
    private subscribeToOverlayEvents() {
744
        this._overlayService.opening.pipe(...this._overlaySubFilter).subscribe((e) => {
32✔
745
            const overlayEvent = e as OverlayCancelableEventArgs;
32✔
746
            const args = { owner: this, cancel: overlayEvent?.cancel, event: e.event };
32✔
747
            this.opening.emit(args);
32✔
748
            if (args.cancel) {
32!
749
                this._overlayService.detach(this._overlayId);
×
750
                overlayEvent.cancel = true;
×
751
                return;
×
752
            }
753

754
            this._initializeCalendarContainer(e.componentRef.instance);
32✔
755
            this._calendarContainer = e.componentRef.location.nativeElement;
32✔
756
            this._collapsed = false;
32✔
757
            this.updateCalendar();
32✔
758
        });
759

760
        this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => {
32✔
761
            this.calendar.wrapper.nativeElement.focus();
24✔
762
            this.opened.emit({ owner: this });
24✔
763
        });
764

765
        this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e) => {
32✔
766
            this.handleClosing(e as OverlayCancelableEventArgs);
32✔
767
        });
768

769
        this._overlayService.closed.pipe(...this._overlaySubFilter).subscribe(() => {
32✔
770
            this._overlayService.detach(this._overlayId);
14✔
771
            this._collapsed = true;
14✔
772
            this._overlayId = null;
14✔
773
            this._calendar = null;
14✔
774
            this._calendarContainer = undefined;
14✔
775
            this.closed.emit({ owner: this });
14✔
776
        });
777
    }
778

779
    private updateValue(value: DateRange) {
780
        this._value = value ? value : null;
122✔
781
        this.updateInputs();
122✔
782
        this.updateCalendar();
122✔
783
    }
784

785
    private updateValidityOnBlur() {
786
        this.onTouchCallback();
2✔
787
        if (this._ngControl) {
2✔
788
            if (this.hasProjectedInputs) {
1✔
789
                this.projectedInputs.forEach(i => {
1✔
790
                    if (!this._ngControl.valid) {
2!
791
                        i.updateInputValidity(IgxInputState.INVALID);
2✔
792
                    } else {
793
                        i.updateInputValidity(IgxInputState.INITIAL);
×
794
                    }
795
                });
796
            }
797

798
            if (this.inputDirective) {
1!
799
                if (!this._ngControl.valid) {
×
800
                    this.inputDirective.valid = IgxInputState.INVALID;
×
801
                } else {
802
                    this.inputDirective.valid = IgxInputState.INITIAL;
×
803
                }
804
            }
805
        }
806
    }
807

808
    private updateDisabledState() {
809
        if (this.hasProjectedInputs) {
120✔
810
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
32✔
811
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
64✔
812
            start.inputDirective.disabled = this.disabled;
32✔
813
            end.inputDirective.disabled = this.disabled;
32✔
814
            return;
32✔
815
        }
816
    }
817

818
    private setRequiredToInputs(): void {
819
        // workaround for igxInput setting required
820
        Promise.resolve().then(() => {
134✔
821
            const isRequired = this.required;
134✔
822
            if (this.inputGroup && this.inputGroup.isRequired !== isRequired) {
134✔
823
                this.inputGroup.isRequired = isRequired;
4✔
824
            } else if (this.hasProjectedInputs && this._ngControl) {
130✔
825
                this.projectedInputs.forEach(i => i.isRequired = isRequired);
178✔
826
            }
827
        });
828
    }
829

830
    private parseMinValue(value: string | Date): Date | null {
831
        let minValue: Date = parseDate(value);
54✔
832
        if (!minValue && this.hasProjectedInputs) {
54✔
833
            const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0];
48✔
834
            if (start) {
24✔
835
                minValue = parseDate(start.dateTimeEditor.minValue);
24✔
836
            }
837
        }
838

839
        return minValue;
54✔
840
    }
841

842
    private parseMaxValue(value: string | Date): Date | null {
843
        let maxValue: Date = parseDate(value);
54✔
844
        if (!maxValue && this.projectedInputs) {
54✔
845
            const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0];
52✔
846
            if (end) {
52✔
847
                maxValue = parseDate(end.dateTimeEditor.maxValue);
24✔
848
            }
849
        }
850

851
        return maxValue;
54✔
852
    }
853

854
    private updateCalendar(): void {
855
        if (!this.calendar) {
156✔
856
            return;
102✔
857
        }
858
        this.calendar.disabledDates = [];
54✔
859
        const minValue = this.parseMinValue(this.minValue);
54✔
860
        if (minValue) {
54✔
861
            this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
2✔
862
        }
863
        const maxValue = this.parseMaxValue(this.maxValue);
54✔
864
        if (maxValue) {
54✔
865
            this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
2✔
866
        }
867

868
        const range: Date[] = [];
54✔
869
        if (this.value?.start && this.value?.end) {
54✔
870
            const _value = this.toRangeOfDates(this.value);
21✔
871
            if (DateTimeUtil.greaterThanMaxValue(_value.start, _value.end)) {
21!
872
                this.swapEditorDates();
×
873
            }
874
            if (this.valueInRange(this.value, minValue, maxValue)) {
21✔
875
                range.push(_value.start, _value.end);
21✔
876
            }
877
        }
878

879
        if (range.length > 0) {
54✔
880
            this.calendar.selectDate(range);
21✔
881
        } else if (range.length === 0 && this.calendar.monthViews) {
33!
882
            this.calendar.deselectDate();
×
883
        }
884
        this.calendar.viewDate = range[0] || new Date();
54✔
885
    }
886

887
    private swapEditorDates(): void {
888
        if (this.hasProjectedInputs) {
×
889
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
890
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
891
            [start.dateTimeEditor.value, end.dateTimeEditor.value] = [end.dateTimeEditor.value, start.dateTimeEditor.value];
×
892
            [this.value.start, this.value.end] = [this.value.end, this.value.start];
×
893
        }
894
    }
895

896
    private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean {
897
        const _value = this.toRangeOfDates(value);
21✔
898
        if (minValue && DateTimeUtil.lessThanMinValue(_value.start, minValue, false)) {
21!
899
            return false;
×
900
        }
901
        if (maxValue && DateTimeUtil.greaterThanMaxValue(_value.end, maxValue, false)) {
21!
902
            return false;
×
903
        }
904

905
        return true;
21✔
906
    }
907

908
    private extractRange(selection: Date[]): DateRange {
909
        return {
37✔
910
            start: selection[0] || null,
37!
911
            end: selection.length > 0 ? selection[selection.length - 1] : null
37!
912
        };
913
    }
914

915
    private toRangeOfDates(range: DateRange): { start: Date; end: Date } {
916
        let start;
917
        let end;
918
        if (!isDate(range.start)) {
82✔
919
            start = DateTimeUtil.parseIsoDate(range.start);
1✔
920
        }
921
        if (!isDate(range.end)) {
82✔
922
            end = DateTimeUtil.parseIsoDate(range.end);
2✔
923
        }
924

925
        if (start || end) {
82!
926
            return { start, end };
×
927
        }
928

929
        return { start: range.start as Date, end: range.end as Date };
82✔
930
    }
931

932
    private subscribeToDateEditorEvents(): void {
933
        if (this.hasProjectedInputs) {
63✔
934
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
27✔
935
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
54✔
936
            if (start && end) {
27✔
937
                start.dateTimeEditor.valueChange
27✔
938
                    .pipe(takeUntil(this._destroy$))
939
                    .subscribe(value => {
940
                        if (this.value) {
1!
941
                            this.value = { start: value, end: this.value.end };
×
942
                        } else {
943
                            this.value = { start: value, end: null };
1✔
944
                        }
945
                    });
946
                end.dateTimeEditor.valueChange
27✔
947
                    .pipe(takeUntil(this._destroy$))
948
                    .subscribe(value => {
949
                        if (this.value) {
1!
950
                            this.value = { start: this.value.start, end: value as Date };
1✔
951
                        } else {
952
                            this.value = { start: null, end: value as Date };
×
953
                        }
954
                    });
955
            }
956
        }
957
    }
958

959
    private attachOnTouched(): void {
960
        if (this.hasProjectedInputs) {
63✔
961
            this.projectedInputs.forEach(i => {
27✔
962
                fromEvent(i.dateTimeEditor.nativeElement, 'blur')
54✔
963
                    .pipe(takeUntil(this._destroy$))
964
                    .subscribe(() => {
965
                        if (this.collapsed) {
7✔
966
                            this.updateValidityOnBlur();
1✔
967
                        }
968
                    });
969
            });
970
        } else {
971
            fromEvent(this.inputDirective.nativeElement, 'blur')
36✔
972
                .pipe(takeUntil(this._destroy$))
973
                .subscribe(() => {
974
                    if (this.collapsed) {
7!
975
                        this.updateValidityOnBlur();
×
976
                    }
977
                });
978
        }
979
    }
980

981
    private cacheFocusedInput(): void {
982
        if (this.hasProjectedInputs) {
63✔
983
            this.projectedInputs.forEach(i => {
27✔
984
                fromEvent(i.dateTimeEditor.nativeElement, 'focus')
54✔
985
                    .pipe(takeUntil(this._destroy$))
986
                    .subscribe(() => this._focusedInput = i);
11✔
987
            });
988
        }
989
    }
990

991
    private configPositionStrategy(): void {
992
        this._positionSettings = {
63✔
993
            openAnimation: fadeIn,
994
            closeAnimation: fadeOut
995
        };
996
        this._dropDownOverlaySettings.positionStrategy = new AutoPositionStrategy(this._positionSettings);
63✔
997
        this._dropDownOverlaySettings.target = this.element.nativeElement;
63✔
998
    }
999

1000
    private configOverlaySettings(): void {
1001
        if (this.overlaySettings !== null) {
63✔
1002
            this._dropDownOverlaySettings = Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
63✔
1003
            this._dialogOverlaySettings = Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
63✔
1004
        }
1005
    }
1006

1007
    private initialSetValue() {
1008
        // if there is no value and no ngControl on the picker but we have inputs we may have value set through
1009
        // their ngModels - we should generate our initial control value
1010
        if ((!this.value || (!this.value.start && !this.value.end)) && this.hasProjectedInputs && !this._ngControl) {
63!
1011
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent);
2✔
1012
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent);
4✔
1013
            this._value = {
2✔
1014
                start: start.dateTimeEditor.value as Date,
1015
                end: end.dateTimeEditor.value as Date
1016
            };
1017
        }
1018
    }
1019

1020
    private updateInputs(): void {
1021
        const start = this.projectedInputs?.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
185✔
1022
        const end = this.projectedInputs?.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
185✔
1023
        if (start && end) {
185✔
1024
            const _value = this.value ? this.toRangeOfDates(this.value) : null;
87✔
1025
            start.updateInputValue(_value?.start || null);
87✔
1026
            end.updateInputValue(_value?.end || null);
87✔
1027
        }
1028
    }
1029

1030
    private updateDisplayFormat(): void {
1031
        this.projectedInputs.forEach(i => {
70✔
1032
            const input = i as IgxDateRangeInputsBaseComponent;
68✔
1033
            input.dateTimeEditor.displayFormat = this.displayFormat;
68✔
1034
        });
1035
    }
1036

1037
    private updateInputFormat(): void {
1038
        this.projectedInputs.forEach(i => {
67✔
1039
            const input = i as IgxDateRangeInputsBaseComponent;
62✔
1040
            if (input.dateTimeEditor.inputFormat !== this.inputFormat) {
62✔
1041
                input.dateTimeEditor.inputFormat = this.inputFormat;
62✔
1042
            }
1043
        });
1044
    }
1045

1046
    private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) {
1047
        this._calendar = componentInstance.calendar;
32✔
1048
        this.calendar.hasHeader = false;
32✔
1049
        this.calendar.locale = this.locale;
32✔
1050
        this.calendar.selection = CalendarSelection.RANGE;
32✔
1051
        this.calendar.weekStart = this.weekStart;
32✔
1052
        this.calendar.hideOutsideDays = this.hideOutsideDays;
32✔
1053
        this.calendar.monthsViewNumber = this.displayMonthsCount;
32✔
1054
        this.calendar.showWeekNumbers = this.showWeekNumbers;
32✔
1055
        this.calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev));
32✔
1056

1057
        componentInstance.mode = this.mode;
32✔
1058
        componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
32✔
1059
        componentInstance.pickerActions = this.pickerActions;
32✔
1060
        componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
32✔
1061
    }
1062
}
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