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

IgniteUI / igniteui-angular / 16800667852

07 Aug 2025 09:31AM UTC coverage: 91.511% (+0.07%) from 91.44%
16800667852

Pull #16096

github

web-flow
Merge 4feeffc08 into 6d3657091
Pull Request #16096: Address grids column headers accessibility issues - active descendant, what is announced by SRs - master

13510 of 15839 branches covered (85.3%)

55 of 59 new or added lines in 9 files covered. (93.22%)

2 existing lines in 1 file now uncovered.

27240 of 29767 relevant lines covered (91.51%)

34666.81 hits per line

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

91.24
/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, getLocaleFirstDayOfWeek } 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, IgxPickerClearComponent } from '../date-common/public_api';
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, IgxSuffixDirective
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 = ' - ';
3✔
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
    imports: [
71
        NgTemplateOutlet,
72
        IgxIconComponent,
73
        IgxInputGroupComponent,
74
        IgxInputDirective,
75
        IgxPrefixDirective,
76
        IgxSuffixDirective,
77
        DateRangePickerFormatPipe,
78
    ]
79
})
80
export class IgxDateRangePickerComponent extends PickerBaseDirective
3✔
81
    implements OnChanges, OnInit, AfterViewInit, OnDestroy, ControlValueAccessor, Validator {
82

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

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

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

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

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

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

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

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

202
    public get minValue(): Date | string {
203
        return this._minValue;
102✔
204
    }
205

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

218
    public get maxValue(): Date | string {
219
        return this._maxValue;
102✔
220
    }
221

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

231
    /**
232
     * An accessor that returns the resource strings.
233
     */
234
    public get resourceStrings(): IDateRangePickerResourceStrings {
235
        return this._resourceStrings;
53✔
236
    }
237

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

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

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

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

290
    /** @hidden @internal */
291
    @HostBinding('class.igx-date-range-picker')
292
    public cssClass = 'igx-date-range-picker';
83✔
293

294
    @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef })
295
    private viewContainerRef: ViewContainerRef;
296

297
    /** @hidden @internal */
298
    @ViewChild(IgxInputDirective)
299
    public inputDirective: IgxInputDirective;
300

301
    /** @hidden @internal */
302
    @ContentChildren(IgxDateRangeInputsBaseComponent)
303
    public projectedInputs: QueryList<IgxDateRangeInputsBaseComponent>;
304

305
    /** @hidden @internal */
306
    @ContentChildren(IgxPickerClearComponent, { descendants: true })
307
    public clearComponents: QueryList<IgxPickerClearComponent>;
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) {
48✔
322
            return this.resourceStrings.igx_date_range_picker_date_separator;
48✔
323
        }
324
        return this._dateSeparator;
×
325
    }
326

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

333
    /**
334
     * @example
335
     * ```html
336
     * <igx-date-range-picker locale="jp"></igx-date-range-picker>
337
     * ```
338
     */
339
    /**
340
     * Gets the `locale` of the date-range-picker.
341
     * If not set, defaults to application's locale.
342
     */
343
    @Input()
344
    public override get locale(): string {
345
        return this._locale;
1,227✔
346
    }
347

348
    /**
349
     * Sets the `locale` of the date-picker.
350
     * Expects a valid BCP 47 language tag.
351
     */
352
    public override set locale(value: string) {
353
        this._locale = value;
253✔
354
        // if value is invalid, set it back to _localeId
355
        try {
253✔
356
            getLocaleFirstDayOfWeek(this._locale);
253✔
357
        } catch (e) {
358
            this._locale = this._localeId;
1✔
359
        }
360
        if (this.hasProjectedInputs) {
253✔
361
            this.updateInputLocale();
3✔
362
            this.updateDisplayFormat();
3✔
363
        }
364
    }
365

366
    /** @hidden @internal */
367
    public get singleInputFormat(): string {
368
        if (this.placeholder !== '') {
284✔
369
            return this.placeholder;
2✔
370
        }
371

372
        const format = this.appliedFormat;
282✔
373
        return `${format}${SingleInputDatesConcatenationString}${format}`;
282✔
374
    }
375

376
    /**
377
     * Gets calendar state.
378
     *
379
     * ```typescript
380
     * let state = this.dateRange.collapsed;
381
     * ```
382
     */
383
    public override get collapsed(): boolean {
384
        return this._collapsed;
479✔
385
    }
386

387
    /**
388
     * The currently selected value / range from the calendar
389
     *
390
     * @remarks
391
     * The current value is of type `DateRange`
392
     *
393
     * @example
394
     * ```typescript
395
     * const newValue: DateRange = { start: new Date("2/2/2012"), end: new Date("3/3/2013")};
396
     * this.dateRangePicker.value = newValue;
397
     * ```
398
     */
399
    public get value(): DateRange | null {
400
        return this._value;
1,361✔
401
    }
402

403
    @Input()
404
    public set value(value: DateRange | null) {
405
        this.updateValue(value);
79✔
406
        this.onChangeCallback(value);
79✔
407
        this.valueChange.emit(value);
79✔
408
    }
409

410
    /** @hidden @internal */
411
    public get hasProjectedInputs(): boolean {
412
        return this.projectedInputs?.length > 0;
1,806✔
413
    }
414

415
    /** @hidden @internal */
416
    public get separatorClass(): string {
417
        return 'igx-date-range-picker__label';
282✔
418
    }
419

420
    protected override get toggleContainer(): HTMLElement | undefined {
421
        return this._calendarContainer;
7✔
422
    }
423

424
    private get required(): boolean {
425
        if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) {
156✔
426
            const error = this._ngControl.control.validator({} as AbstractControl);
109✔
427
            return (error && error.required) ? true : false;
109!
428
        }
429

430
        return false;
47✔
431
    }
432

433
    private get calendar(): IgxCalendarComponent {
434
        return this._calendar;
637✔
435
    }
436

437
    private get dropdownOverlaySettings(): OverlaySettings {
438
        return Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
28✔
439
    }
440

441
    private get dialogOverlaySettings(): OverlaySettings {
442
        return Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
6✔
443
    }
444

445
    private _resourceStrings = getCurrentResourceStrings(DateRangePickerResourceStringsEN);
83✔
446
    private _doneButtonText = null;
83✔
447
    private _dateSeparator = null;
83✔
448
    private _value: DateRange | null;
449
    private _overlayId: string;
450
    private _ngControl: NgControl;
451
    private _statusChanges$: Subscription;
452
    private _calendar: IgxCalendarComponent;
453
    private _calendarContainer?: HTMLElement;
454
    private _positionSettings: PositionSettings;
455
    private _focusedInput: IgxDateRangeInputsBaseComponent;
456
    private _overlaySubFilter:
83✔
457
        [MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs | OverlayCancelableEventArgs>] = [
458
            filter(x => x.id === this._overlayId),
110✔
459
            takeUntil(merge(this._destroy$, this.closed))
460
        ];
461
    private _dialogOverlaySettings: OverlaySettings = {
83✔
462
        closeOnOutsideClick: true,
463
        modal: true,
464
        closeOnEscape: true
465
    };
466
    private _dropDownOverlaySettings: OverlaySettings = {
83✔
467
        closeOnOutsideClick: true,
468
        modal: false,
469
        closeOnEscape: true
470
    };
471
    private onChangeCallback: (dateRange: DateRange) => void = noop;
83✔
472
    private onTouchCallback: () => void = noop;
83✔
473
    private onValidatorChange: () => void = noop;
83✔
474

475
    constructor(element: ElementRef,
476
        @Inject(LOCALE_ID) _localeId: string,
477
        protected platform: PlatformUtil,
83✔
478
        private _injector: Injector,
83✔
479
        private _cdr: ChangeDetectorRef,
83✔
480
        @Inject(IgxOverlayService) private _overlayService: IgxOverlayService,
83✔
481
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType?: IgxInputGroupType) {
482
        super(element, _localeId, _inputGroupType);
83✔
483
        this.locale = this.locale || this._localeId;
83!
484
    }
485

486
    /** @hidden @internal */
487
    @HostListener('keydown', ['$event'])
488
    /** @hidden @internal */
489
    public onKeyDown(event: KeyboardEvent): void {
490
        switch (event.key) {
5!
491
            case this.platform.KEYMAP.ARROW_UP:
492
                if (event.altKey) {
×
493
                    this.close();
×
494
                }
495
                break;
×
496
            case this.platform.KEYMAP.ARROW_DOWN:
497
                if (event.altKey) {
5✔
498
                    this.open();
5✔
499
                }
500
                break;
5✔
501
        }
502
    }
503

504
    /**
505
     * Opens the date range picker's dropdown or dialog.
506
     *
507
     * @example
508
     * ```html
509
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
510
     *
511
     * <button type="button" igxButton (click)="dateRange.open()">Open Dialog</button
512
     * ```
513
     */
514
    public open(overlaySettings?: OverlaySettings): void {
515
        if (!this.collapsed || this.disabled) {
37✔
516
            return;
3✔
517
        }
518

519
        const settings = Object.assign({}, this.isDropdown
34✔
520
            ? this.dropdownOverlaySettings
521
            : this.dialogOverlaySettings
522
            , overlaySettings);
523

524
        this._overlayId = this._overlayService
34✔
525
            .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings);
526
        this.subscribeToOverlayEvents();
34✔
527
        this._overlayService.show(this._overlayId);
34✔
528
    }
529

530
    /**
531
     * Closes the date range picker's dropdown or dialog.
532
     *
533
     * @example
534
     * ```html
535
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
536
     *
537
     * <button type="button" igxButton (click)="dateRange.close()">Close Dialog</button>
538
     * ```
539
     */
540
    public close(): void {
541
        if (!this.collapsed) {
48✔
542
            this._overlayService.hide(this._overlayId);
31✔
543
        }
544
    }
545

546
    /**
547
     * Toggles the date range picker's dropdown or dialog
548
     *
549
     * @example
550
     * ```html
551
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
552
     *
553
     * <button type="button" igxButton (click)="dateRange.toggle()">Toggle Dialog</button>
554
     * ```
555
     */
556
    public toggle(overlaySettings?: OverlaySettings): void {
557
        if (!this.collapsed) {
8✔
558
            this.close();
2✔
559
        } else {
560
            this.open(overlaySettings);
6✔
561
        }
562
    }
563

564
    /**
565
     * Selects a range of dates. If no `endDate` is passed, range is 1 day (only `startDate`)
566
     *
567
     * @example
568
     * ```typescript
569
     * public selectFiveDayRange() {
570
     *  const today = new Date();
571
     *  const inFiveDays = new Date(new Date().setDate(today.getDate() + 5));
572
     *  this.dateRange.select(today, inFiveDays);
573
     * }
574
     * ```
575
     */
576
    public select(startDate: Date, endDate?: Date): void {
577
        endDate = endDate ?? startDate;
19✔
578
        const dateRange = [startDate, endDate];
19✔
579
        this.handleSelection(dateRange);
19✔
580
    }
581

582
    /**
583
     * Clears the input field(s) and the picker's value.
584
     *
585
     * @example
586
     * ```typescript
587
     * this.dateRangePicker.clear();
588
     * ```
589
     */
590
    public clear(): void {
591
        // TODO clear the projected inputs
592
        if (!this.disabled) {
2✔
593
            this.value = null;
2✔
594
            this._calendar?.deselectDate();
2✔
595
            this.inputDirective.clear();
2✔
596
        }
597
    }
598

599
    /** @hidden @internal */
600
    public writeValue(value: DateRange): void {
601
        this.updateValue(value);
60✔
602
    }
603

604
    /** @hidden @internal */
605
    public registerOnChange(fn: any): void {
606
        this.onChangeCallback = fn;
40✔
607
    }
608

609
    /** @hidden @internal */
610
    public registerOnTouched(fn: any): void {
611
        this.onTouchCallback = fn;
39✔
612
    }
613

614
    /** @hidden @internal */
615
    public validate(control: AbstractControl): ValidationErrors | null {
616
        const value: DateRange = control.value;
203✔
617
        const errors = {};
203✔
618
        if (value) {
203✔
619
            if (this.hasProjectedInputs) {
44✔
620
                const startInput = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
41✔
621
                const endInput = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
82✔
622
                if (!startInput.dateTimeEditor.value) {
41!
623
                    Object.assign(errors, { startValue: true });
×
624
                }
625
                if (!endInput.dateTimeEditor.value) {
41✔
626
                    Object.assign(errors, { endValue: true });
1✔
627
                }
628
            }
629

630
            const min = parseDate(this.minValue);
44✔
631
            const max = parseDate(this.maxValue);
44✔
632
            const start = parseDate(value.start);
44✔
633
            const end = parseDate(value.end);
44✔
634
            if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false))
44✔
635
                || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) {
636
                Object.assign(errors, { minValue: true });
2✔
637
            }
638
            if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false))
44✔
639
                || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) {
640
                Object.assign(errors, { maxValue: true });
1✔
641
            }
642
        }
643

644
        return Object.keys(errors).length > 0 ? errors : null;
203✔
645
    }
646

647
    /** @hidden @internal */
648
    public registerOnValidatorChange?(fn: any): void {
649
        this.onValidatorChange = fn;
39✔
650
    }
651

652
    /** @hidden @internal */
653
    public setDisabledState?(isDisabled: boolean): void {
654
        this.disabled = isDisabled;
40✔
655
    }
656

657
    /** @hidden */
658
    public ngOnInit(): void {
659
        this._ngControl = this._injector.get<NgControl>(NgControl, null);
80✔
660

661
        this.locale = this.locale || this._localeId;
80!
662
    }
663

664
    /** @hidden */
665
    public override ngAfterViewInit(): void {
666
        super.ngAfterViewInit();
77✔
667
        this.subscribeToDateEditorEvents();
77✔
668
        this.subscribeToClick();
77✔
669
        this.subToIconsClicked(this.clearComponents, () => this.clear());
77✔
670
        this.clearComponents.changes.pipe(takeUntil(this._destroy$))
77✔
NEW
671
            .subscribe(() => this.subToIconsClicked(this.clearComponents, () => this.clear()));
×
672
        this.configPositionStrategy();
77✔
673
        this.configOverlaySettings();
77✔
674
        this.cacheFocusedInput();
77✔
675
        this.attachOnTouched();
77✔
676

677
        this.setRequiredToInputs();
77✔
678

679
        if (this._ngControl) {
77✔
680
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
32✔
681
        }
682

683
        // delay invocations until the current change detection cycle has completed
684
        Promise.resolve().then(() => {
77✔
685
            this.updateDisabledState();
77✔
686
            this.initialSetValue();
77✔
687
            this.updateInputs();
77✔
688
            // B.P. 07 July 2021 - IgxDateRangePicker not showing initial disabled state with ChangeDetectionStrategy.OnPush #9776
689
            /**
690
             * if disabled is placed on the range picker element and there are projected inputs
691
             * run change detection since igxInput will initially set the projected inputs' disabled to false
692
             */
693
            if (this.hasProjectedInputs && this.disabled) {
77✔
694
                this._cdr.markForCheck();
3✔
695
            }
696
        });
697
        this.updateDisplayFormat();
77✔
698
        this.updateInputFormat();
77✔
699
    }
700

701
    /** @hidden @internal */
702
    public ngOnChanges(changes: SimpleChanges): void {
703
        if (changes['displayFormat'] && this.hasProjectedInputs) {
90✔
704
            this.updateDisplayFormat();
11✔
705
        }
706
        if (changes['inputFormat'] && this.hasProjectedInputs) {
90✔
707
            this.updateInputFormat();
4✔
708
        }
709
        if (changes['disabled']) {
90✔
710
            this.updateDisabledState();
67✔
711
        }
712
    }
713

714
    /** @hidden @internal */
715
    public override ngOnDestroy(): void {
716
        super.ngOnDestroy();
67✔
717
        if (this._statusChanges$) {
67✔
718
            this._statusChanges$.unsubscribe();
32✔
719
        }
720
        if (this._overlayId) {
67✔
721
            this._overlayService.detach(this._overlayId);
8✔
722
        }
723
    }
724

725
    /** @hidden @internal */
726
    public getEditElement(): HTMLInputElement | undefined {
727
        return this.inputDirective?.nativeElement;
77✔
728
    }
729

730
    protected onStatusChanged = () => {
83✔
731
        if (this.inputGroup) {
78✔
732
            this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
7✔
733
        } else if (this.hasProjectedInputs) {
71✔
734
            this.projectedInputs
71✔
735
                .forEach((i) => {
736
                    this.setValidityState(i.inputDirective, i.isFocused);
142✔
737
                });
738
        }
739
        this.setRequiredToInputs();
78✔
740
    };
741

742
    private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) {
743
        if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
149✔
744
            if (this.hasValidators && isFocused) {
137✔
745
                inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
2✔
746
            } else {
747
                inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
135✔
748
            }
749
        } else {
750
            inputDirective.valid = IgxInputState.INITIAL;
12✔
751
        }
752
    }
753

754
    private get isTouchedOrDirty(): boolean {
755
        return (this._ngControl.control.touched || this._ngControl.control.dirty);
139✔
756
    }
757

758
    private get hasValidators(): boolean {
759
        return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
137✔
760
    }
761

762
    private handleSelection(selectionData: Date[]): void {
763
        let newValue = this.extractRange(selectionData);
37✔
764
        if (!newValue.start && !newValue.end) {
37!
765
            newValue = null;
×
766
        }
767
        this.value = newValue;
37✔
768
        if (this.isDropdown && selectionData?.length > 1) {
37✔
769
            this.close();
26✔
770
        }
771
    }
772

773
    private handleClosing(e: IBaseCancelableBrowserEventArgs): void {
774
        const args = { owner: this, cancel: e?.cancel, event: e?.event };
34✔
775
        this.closing.emit(args);
34✔
776
        e.cancel = args.cancel;
34✔
777
        if (args.cancel) {
34✔
778
            return;
2✔
779
        }
780

781
        if (this.isDropdown && e?.event && !this.isFocused) {
32!
782
            // outside click
783
            this.updateValidityOnBlur();
×
784
        } else {
785
            this.onTouchCallback();
32✔
786
            // input click
787
            if (this.hasProjectedInputs && this._focusedInput) {
32✔
788
                this._focusedInput.setFocus();
7✔
789
            }
790
            if (this.inputDirective) {
32✔
791
                this.inputDirective.focus();
15✔
792
            }
793
        }
794
    }
795

796
    private subscribeToOverlayEvents() {
797
        this._overlayService.opening.pipe(...this._overlaySubFilter).subscribe((e) => {
34✔
798
            const overlayEvent = e as OverlayCancelableEventArgs;
34✔
799
            const args = { owner: this, cancel: overlayEvent?.cancel, event: e.event };
34✔
800
            this.opening.emit(args);
34✔
801
            if (args.cancel) {
34!
802
                this._overlayService.detach(this._overlayId);
×
803
                overlayEvent.cancel = true;
×
804
                return;
×
805
            }
806

807
            this._initializeCalendarContainer(e.componentRef.instance);
34✔
808
            this._calendarContainer = e.componentRef.location.nativeElement;
34✔
809
            this._collapsed = false;
34✔
810
            this.updateCalendar();
34✔
811
        });
812

813
        this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => {
34✔
814
            this.calendar.wrapper.nativeElement.focus();
26✔
815
            this.opened.emit({ owner: this });
26✔
816
        });
817

818
        this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e) => {
34✔
819
            this.handleClosing(e as OverlayCancelableEventArgs);
34✔
820
        });
821

822
        this._overlayService.closed.pipe(...this._overlaySubFilter).subscribe(() => {
34✔
823
            this._overlayService.detach(this._overlayId);
16✔
824
            this._collapsed = true;
16✔
825
            this._overlayId = null;
16✔
826
            this._calendar = null;
16✔
827
            this._calendarContainer = undefined;
16✔
828
            this.closed.emit({ owner: this });
16✔
829
        });
830
    }
831

832
    private updateValue(value: DateRange) {
833
        this._value = value ? value : null;
139✔
834
        this.updateInputs();
139✔
835
        this.updateCalendar();
139✔
836
    }
837

838
    private updateValidityOnBlur() {
839
        this._focusedInput = null;
2✔
840
        this.onTouchCallback();
2✔
841
        if (this._ngControl) {
2✔
842
            if (this.hasProjectedInputs) {
1✔
843
                this.projectedInputs.forEach(i => {
1✔
844
                    if (!this._ngControl.valid) {
2!
845
                        i.updateInputValidity(IgxInputState.INVALID);
2✔
846
                    } else {
847
                        i.updateInputValidity(IgxInputState.INITIAL);
×
848
                    }
849
                });
850
            }
851

852
            if (this.inputDirective) {
1!
853
                if (!this._ngControl.valid) {
×
854
                    this.inputDirective.valid = IgxInputState.INVALID;
×
855
                } else {
856
                    this.inputDirective.valid = IgxInputState.INITIAL;
×
857
                }
858
            }
859
        }
860
    }
861

862
    private updateDisabledState() {
863
        if (this.hasProjectedInputs) {
144✔
864
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
36✔
865
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
72✔
866
            start.inputDirective.disabled = this.disabled;
36✔
867
            end.inputDirective.disabled = this.disabled;
36✔
868
            return;
36✔
869
        }
870
    }
871

872
    private setRequiredToInputs(): void {
873
        // workaround for igxInput setting required
874
        Promise.resolve().then(() => {
155✔
875
            const isRequired = this.required;
155✔
876
            if (this.inputGroup && this.inputGroup.isRequired !== isRequired) {
155✔
877
                this.inputGroup.isRequired = isRequired;
4✔
878
            } else if (this.hasProjectedInputs && this._ngControl) {
151✔
879
                this.projectedInputs.forEach(i => i.isRequired = isRequired);
200✔
880
            }
881
        });
882
    }
883

884
    private parseMinValue(value: string | Date): Date | null {
885
        let minValue: Date = parseDate(value);
56✔
886
        if (!minValue && this.hasProjectedInputs) {
56✔
887
            const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0];
52✔
888
            if (start) {
26✔
889
                minValue = parseDate(start.dateTimeEditor.minValue);
26✔
890
            }
891
        }
892

893
        return minValue;
56✔
894
    }
895

896
    private parseMaxValue(value: string | Date): Date | null {
897
        let maxValue: Date = parseDate(value);
56✔
898
        if (!maxValue && this.projectedInputs) {
56✔
899
            const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0];
54✔
900
            if (end) {
54✔
901
                maxValue = parseDate(end.dateTimeEditor.maxValue);
26✔
902
            }
903
        }
904

905
        return maxValue;
56✔
906
    }
907

908
    private updateCalendar(): void {
909
        if (!this.calendar) {
175✔
910
            return;
119✔
911
        }
912
        this.calendar.disabledDates = [];
56✔
913
        const minValue = this.parseMinValue(this.minValue);
56✔
914
        if (minValue) {
56✔
915
            this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
2✔
916
        }
917
        const maxValue = this.parseMaxValue(this.maxValue);
56✔
918
        if (maxValue) {
56✔
919
            this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
2✔
920
        }
921

922
        const range: Date[] = [];
56✔
923
        if (this.value?.start && this.value?.end) {
56✔
924
            const _value = this.toRangeOfDates(this.value);
21✔
925
            if (DateTimeUtil.greaterThanMaxValue(_value.start, _value.end)) {
21!
926
                this.swapEditorDates();
×
927
            }
928
            if (this.valueInRange(this.value, minValue, maxValue)) {
21✔
929
                range.push(_value.start, _value.end);
21✔
930
            }
931
        }
932

933
        if (range.length > 0) {
56✔
934
            this.calendar.selectDate(range);
21✔
935
        } else if (range.length === 0 && this.calendar.monthViews) {
35!
936
            this.calendar.deselectDate();
×
937
        }
938
        this.calendar.viewDate = range[0] || new Date();
56✔
939
    }
940

941
    private swapEditorDates(): void {
942
        if (this.hasProjectedInputs) {
×
943
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
944
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
945
            [start.dateTimeEditor.value, end.dateTimeEditor.value] = [end.dateTimeEditor.value, start.dateTimeEditor.value];
×
946
            [this.value.start, this.value.end] = [this.value.end, this.value.start];
×
947
        }
948
    }
949

950
    private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean {
951
        const _value = this.toRangeOfDates(value);
21✔
952
        if (minValue && DateTimeUtil.lessThanMinValue(_value.start, minValue, false)) {
21!
953
            return false;
×
954
        }
955
        if (maxValue && DateTimeUtil.greaterThanMaxValue(_value.end, maxValue, false)) {
21!
956
            return false;
×
957
        }
958

959
        return true;
21✔
960
    }
961

962
    private extractRange(selection: Date[]): DateRange {
963
        return {
37✔
964
            start: selection[0] || null,
37!
965
            end: selection.length > 0 ? selection[selection.length - 1] : null
37!
966
        };
967
    }
968

969
    private toRangeOfDates(range: DateRange): { start: Date; end: Date } {
970
        let start;
971
        let end;
972
        if (!isDate(range.start)) {
85✔
973
            start = DateTimeUtil.parseIsoDate(range.start);
1✔
974
        }
975
        if (!isDate(range.end)) {
85✔
976
            end = DateTimeUtil.parseIsoDate(range.end);
2✔
977
        }
978

979
        if (start || end) {
85!
980
            return { start, end };
×
981
        }
982

983
        return { start: range.start as Date, end: range.end as Date };
85✔
984
    }
985

986
    private subscribeToClick() {
987
        const editElement = this.getEditElement();
77✔
988
        if (!editElement) {
77✔
989
            return;
31✔
990
        }
991
        fromEvent(editElement, 'click')
46✔
992
            .pipe(takeUntil(this._destroy$))
993
            .subscribe(() => {
NEW
994
                if (!this.isDropdown) {
×
NEW
995
                    this.toggle();
×
996
                }
997
            });
998
    }
999

1000
    private subscribeToDateEditorEvents(): void {
1001
        if (this.hasProjectedInputs) {
77✔
1002
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
31✔
1003
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
62✔
1004
            if (start && end) {
31✔
1005
                start.dateTimeEditor.valueChange
31✔
1006
                    .pipe(takeUntil(this._destroy$))
1007
                    .subscribe(value => {
1008
                        if (this.value) {
1!
1009
                            this.value = { start: value, end: this.value.end };
×
1010
                        } else {
1011
                            this.value = { start: value, end: null };
1✔
1012
                        }
1013
                    });
1014
                end.dateTimeEditor.valueChange
31✔
1015
                    .pipe(takeUntil(this._destroy$))
1016
                    .subscribe(value => {
1017
                        if (this.value) {
1!
1018
                            this.value = { start: this.value.start, end: value as Date };
1✔
1019
                        } else {
1020
                            this.value = { start: null, end: value as Date };
×
1021
                        }
1022
                    });
1023
            }
1024
        }
1025
    }
1026

1027
    private attachOnTouched(): void {
1028
        if (this.hasProjectedInputs) {
77✔
1029
            this.projectedInputs.forEach(i => {
31✔
1030
                fromEvent(i.dateTimeEditor.nativeElement, 'blur')
62✔
1031
                    .pipe(takeUntil(this._destroy$))
1032
                    .subscribe(() => {
1033
                        if (this.collapsed) {
9✔
1034
                            this.updateValidityOnBlur();
1✔
1035
                        }
1036
                    });
1037
            });
1038
        } else {
1039
            fromEvent(this.inputDirective.nativeElement, 'blur')
46✔
1040
                .pipe(takeUntil(this._destroy$))
1041
                .subscribe(() => {
1042
                    if (this.collapsed) {
6!
1043
                        this.updateValidityOnBlur();
×
1044
                    }
1045
                });
1046
        }
1047
    }
1048

1049
    private cacheFocusedInput(): void {
1050
        if (this.hasProjectedInputs) {
77✔
1051
            this.projectedInputs.forEach(i => {
31✔
1052
                fromEvent(i.dateTimeEditor.nativeElement, 'focus')
62✔
1053
                    .pipe(takeUntil(this._destroy$))
1054
                    .subscribe(() => this._focusedInput = i);
13✔
1055
            });
1056
        }
1057
    }
1058

1059
    private configPositionStrategy(): void {
1060
        this._positionSettings = {
77✔
1061
            openAnimation: fadeIn,
1062
            closeAnimation: fadeOut
1063
        };
1064
        this._dropDownOverlaySettings.positionStrategy = new AutoPositionStrategy(this._positionSettings);
77✔
1065
        this._dropDownOverlaySettings.target = this.element.nativeElement;
77✔
1066
    }
1067

1068
    private configOverlaySettings(): void {
1069
        if (this.overlaySettings !== null) {
77✔
1070
            this._dropDownOverlaySettings = Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
77✔
1071
            this._dialogOverlaySettings = Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
77✔
1072
        }
1073
    }
1074

1075
    private initialSetValue() {
1076
        // if there is no value and no ngControl on the picker but we have inputs we may have value set through
1077
        // their ngModels - we should generate our initial control value
1078
        if ((!this.value || (!this.value.start && !this.value.end)) && this.hasProjectedInputs && !this._ngControl) {
77!
1079
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent);
2✔
1080
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent);
4✔
1081
            this._value = {
2✔
1082
                start: start.dateTimeEditor.value as Date,
1083
                end: end.dateTimeEditor.value as Date
1084
            };
1085
        }
1086
    }
1087

1088
    private updateInputs(): void {
1089
        const start = this.projectedInputs?.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
216✔
1090
        const end = this.projectedInputs?.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
216✔
1091
        if (start && end) {
216✔
1092
            const _value = this.value ? this.toRangeOfDates(this.value) : null;
98✔
1093
            start.updateInputValue(_value?.start || null);
98✔
1094
            end.updateInputValue(_value?.end || null);
98✔
1095
        }
1096
    }
1097

1098
    private updateDisplayFormat(): void {
1099
        this.projectedInputs.forEach(i => {
91✔
1100
            const input = i as IgxDateRangeInputsBaseComponent;
90✔
1101
            input.dateTimeEditor.displayFormat = this.displayFormat;
90✔
1102
        });
1103
    }
1104

1105
    private updateInputFormat(): void {
1106
        this.projectedInputs.forEach(i => {
81✔
1107
            const input = i as IgxDateRangeInputsBaseComponent;
70✔
1108
            if (input.dateTimeEditor.inputFormat !== this.inputFormat) {
70✔
1109
                input.dateTimeEditor.inputFormat = this.inputFormat;
70✔
1110
            }
1111
        });
1112
    }
1113

1114
    private updateInputLocale(): void {
1115
        this.projectedInputs.forEach(i => {
3✔
1116
            const input = i as IgxDateRangeInputsBaseComponent;
6✔
1117
            input.dateTimeEditor.locale = this.locale;
6✔
1118
        });
1119
    }
1120

1121
    private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) {
1122
        this._calendar = componentInstance.calendar;
34✔
1123
        this.calendar.hasHeader = false;
34✔
1124
        this.calendar.locale = this.locale;
34✔
1125
        this.calendar.selection = CalendarSelection.RANGE;
34✔
1126
        this.calendar.weekStart = this.weekStart;
34✔
1127
        this.calendar.hideOutsideDays = this.hideOutsideDays;
34✔
1128
        this.calendar.monthsViewNumber = this.displayMonthsCount;
34✔
1129
        this.calendar.showWeekNumbers = this.showWeekNumbers;
34✔
1130
        this.calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev));
34✔
1131

1132
        componentInstance.mode = this.mode;
34✔
1133
        componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
34✔
1134
        componentInstance.pickerActions = this.pickerActions;
34✔
1135
        componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
34✔
1136
    }
1137
}
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