Skip to content

Commit 0964234

Browse files
PlamenaMitevaIvayloG
authored andcommitted
fix(date-picker): add validation when disabledDates is set #7800
1 parent 6c2bbc8 commit 0964234

File tree

2 files changed

+126
-89
lines changed

2 files changed

+126
-89
lines changed

projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts

Lines changed: 108 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, ViewChild, ElementRef, EventEmitter } from '@angular/core';
22
import { async, fakeAsync, TestBed, tick, flush, ComponentFixture } from '@angular/core/testing';
3-
import { FormsModule, FormGroup, FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3+
import { FormsModule, FormGroup, FormBuilder, ReactiveFormsModule, Validators, NgControl } from '@angular/forms';
44
import { By } from '@angular/platform-browser';
55
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
66
import { IgxDatePickerComponent, IgxDatePickerModule } from './date-picker.component';
@@ -329,97 +329,97 @@ describe('IgxDatePicker', () => {
329329
});
330330

331331
it('When datepicker in "dropdown" mode is closed via outside click, the input should not receive focus',
332-
fakeAsync(() => {
333-
const fixture = TestBed.createComponent(IgxDatePickerDropdownButtonsComponent);
334-
fixture.detectChanges();
332+
fakeAsync(() => {
333+
const fixture = TestBed.createComponent(IgxDatePickerDropdownButtonsComponent);
334+
fixture.detectChanges();
335335

336-
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
337-
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
338-
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
336+
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
337+
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
338+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
339339

340-
expect(overlayToggle.length).toEqual(0);
340+
expect(overlayToggle.length).toEqual(0);
341341

342-
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
343-
flush();
344-
fixture.detectChanges();
342+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
343+
flush();
344+
fixture.detectChanges();
345345

346-
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
347-
expect(overlayToggle[0]).not.toBeNull();
348-
expect(overlayToggle[0]).not.toBeUndefined();
346+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
347+
expect(overlayToggle[0]).not.toBeNull();
348+
expect(overlayToggle[0]).not.toBeUndefined();
349349

350-
const dummyInput = fixture.componentInstance.dummyInput.nativeElement;
351-
dummyInput.focus();
352-
dummyInput.click();
353-
tick();
354-
fixture.detectChanges();
350+
const dummyInput = fixture.componentInstance.dummyInput.nativeElement;
351+
dummyInput.focus();
352+
dummyInput.click();
353+
tick();
354+
fixture.detectChanges();
355355

356-
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
357-
expect(overlayToggle[0]).toEqual(undefined);
358-
expect(input).not.toEqual(document.activeElement);
359-
expect(dummyInput).toEqual(document.activeElement);
360-
}));
356+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
357+
expect(overlayToggle[0]).toEqual(undefined);
358+
expect(input).not.toEqual(document.activeElement);
359+
expect(dummyInput).toEqual(document.activeElement);
360+
}));
361361

362362
it('When datepicker in "dropdown" mode, should focus input on user interaction with Today btn, Cancel btn, Enter Key, Escape key',
363-
fakeAsync(() => {
364-
const fixture = TestBed.createComponent(IgxDatePickerDropdownButtonsComponent);
365-
fixture.detectChanges();
366-
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
367-
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
368-
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
369-
expect(overlayToggle.length).toEqual(0);
363+
fakeAsync(() => {
364+
const fixture = TestBed.createComponent(IgxDatePickerDropdownButtonsComponent);
365+
fixture.detectChanges();
366+
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
367+
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
368+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
369+
expect(overlayToggle.length).toEqual(0);
370370

371-
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
372-
flush();
373-
fixture.detectChanges();
374-
const buttons = document.getElementsByClassName('igx-button--flat');
375-
expect(buttons.length).toEqual(2);
371+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
372+
flush();
373+
fixture.detectChanges();
374+
const buttons = document.getElementsByClassName('igx-button--flat');
375+
expect(buttons.length).toEqual(2);
376376

377-
// Today btn
378-
const todayBtn = buttons[1] as HTMLElement;
379-
expect(todayBtn.innerText).toBe('Today');
380-
todayBtn.click();
381-
tick();
382-
fixture.detectChanges();
383-
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
384-
expect(overlayToggle[0]).toEqual(undefined);
385-
expect(input).toEqual(document.activeElement);
377+
// Today btn
378+
const todayBtn = buttons[1] as HTMLElement;
379+
expect(todayBtn.innerText).toBe('Today');
380+
todayBtn.click();
381+
tick();
382+
fixture.detectChanges();
383+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
384+
expect(overlayToggle[0]).toEqual(undefined);
385+
expect(input).toEqual(document.activeElement);
386386

387-
// Cancel btn
388-
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
389-
flush();
390-
fixture.detectChanges();
391-
const cancelBtn = buttons[0] as HTMLElement;
392-
expect(cancelBtn.innerText).toBe('Cancel');
393-
cancelBtn.click();
394-
tick();
395-
fixture.detectChanges();
396-
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
397-
expect(overlayToggle[0]).toEqual(undefined);
398-
expect(input).toEqual(document.activeElement);
387+
// Cancel btn
388+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
389+
flush();
390+
fixture.detectChanges();
391+
const cancelBtn = buttons[0] as HTMLElement;
392+
expect(cancelBtn.innerText).toBe('Cancel');
393+
cancelBtn.click();
394+
tick();
395+
fixture.detectChanges();
396+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
397+
expect(overlayToggle[0]).toEqual(undefined);
398+
expect(input).toEqual(document.activeElement);
399399

400-
// Enter key
401-
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
402-
flush();
403-
fixture.detectChanges();
404-
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
405-
tick();
406-
fixture.detectChanges();
407-
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
408-
expect(overlayToggle[0]).toEqual(undefined);
409-
expect(input).toEqual(document.activeElement);
400+
// Enter key
401+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
402+
flush();
403+
fixture.detectChanges();
404+
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
405+
tick();
406+
fixture.detectChanges();
407+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
408+
expect(overlayToggle[0]).toEqual(undefined);
409+
expect(input).toEqual(document.activeElement);
410410

411-
// Esc key
412-
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
413-
flush();
414-
fixture.detectChanges();
415-
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
416-
tick();
417-
fixture.detectChanges();
411+
// Esc key
412+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
413+
flush();
414+
fixture.detectChanges();
415+
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
416+
tick();
417+
fixture.detectChanges();
418418

419-
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
420-
expect(overlayToggle[0]).toEqual(undefined);
421-
expect(input).toEqual(document.activeElement);
422-
}));
419+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
420+
expect(overlayToggle[0]).toEqual(undefined);
421+
expect(input).toEqual(document.activeElement);
422+
}));
423423

424424
it('Datepicker week start day (Monday)', () => {
425425
const fixture = TestBed.createComponent(IgxDatePickerWithWeekStartComponent);
@@ -1332,12 +1332,33 @@ describe('IgxDatePicker', () => {
13321332
fixture.detectChanges();
13331333
expect(inputDirective.valid).toEqual(IgxInputState.INITIAL);
13341334

1335-
inputDirectiveElement.triggerEventHandler('blur', { target: { value: ''}});
1335+
inputDirectiveElement.triggerEventHandler('blur', { target: { value: '' } });
13361336
fixture.detectChanges();
13371337

13381338
expect(inputDirective.valid).toEqual(IgxInputState.INVALID);
13391339
}));
13401340

1341+
it('Should set date picker status to invalid when date is disabled', fakeAsync(() => {
1342+
datePickerOnChangeComponent.disabledDates = [{ type: DateRangeType.Before, dateRange: [new Date()] }];
1343+
const inputGroupsElements = fixture.debugElement.queryAll(By.directive(IgxInputDirective));
1344+
const inputGroupElement = inputGroupsElements.find(d => d.componentInstance === datePickerOnChangeComponent);
1345+
const inputDirective = inputGroupElement.injector.get(IgxInputDirective) as IgxInputDirective;
1346+
1347+
const today = new Date();
1348+
datePickerOnChangeComponent.value = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
1349+
fixture.detectChanges();
1350+
expect(inputDirective.valid).toEqual(IgxInputState.INVALID);
1351+
1352+
datePickerOnChangeComponent.value = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
1353+
fixture.detectChanges();
1354+
expect(inputDirective.valid).toEqual(IgxInputState.INITIAL);
1355+
1356+
datePickerOnChangeComponent.disabledDates = [{ type: DateRangeType.Before,
1357+
dateRange: [new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2)] }];
1358+
fixture.detectChanges();
1359+
expect(inputDirective.valid).toEqual(IgxInputState.INVALID);
1360+
}));
1361+
13411362
// Bug #6025 Date picker does not disable in reactive form
13421363
it('Should disable when form is disabled', () => {
13431364
const formGroup: FormGroup = fixture.componentInstance.reactiveForm;
@@ -1375,10 +1396,10 @@ describe('IgxDatePicker', () => {
13751396
};
13761397
element = {};
13771398
cdr = {
1378-
markForCheck: () => {},
1379-
detectChanges: () => {},
1380-
detach: () => {},
1381-
reattach: () => {}
1399+
markForCheck: () => { },
1400+
detectChanges: () => { },
1401+
detach: () => { },
1402+
reattach: () => { }
13821403
};
13831404
moduleRef = {};
13841405
injector = { get: () => ngModel };
@@ -1388,7 +1409,7 @@ describe('IgxDatePicker', () => {
13881409
it('should initialize date picker with required correctly', () => {
13891410
const datePicker = new IgxDatePickerComponent(overlay, element, cdr, moduleRef, injector);
13901411
datePicker['inputGroup'] = inputGroup;
1391-
ngModel.control.validator = () => ({ required: true});
1412+
ngModel.control.validator = () => ({ required: true });
13921413
datePicker.ngOnInit();
13931414
datePicker.ngAfterViewInit();
13941415
datePicker.ngAfterViewChecked();
@@ -1407,11 +1428,11 @@ describe('IgxDatePicker', () => {
14071428
expect(datePicker).toBeDefined();
14081429
expect(inputGroup.isRequired).toBeFalsy();
14091430

1410-
ngModel.control.validator = () => ({ required: true});
1431+
ngModel.control.validator = () => ({ required: true });
14111432
ngModel.statusChanges.emit();
14121433
expect(inputGroup.isRequired).toBeTruthy();
14131434

1414-
ngModel.control.validator = () => ({ required: false});
1435+
ngModel.control.validator = () => ({ required: false });
14151436
ngModel.statusChanges.emit();
14161437
expect(inputGroup.isRequired).toBeFalsy();
14171438
});
@@ -1583,7 +1604,7 @@ class IgxDatePickerReactiveFormComponent {
15831604
const date = new Date(2000, 10, 15);
15841605
this.reactiveForm = fb.group({
15851606
datePickerOnChange: [date, Validators.required],
1586-
datePickerOnBlur: [date, { updateOn: 'blur', validators: Validators.required}]
1607+
datePickerOnBlur: [date, { updateOn: 'blur', validators: Validators.required }]
15871608
});
15881609
}
15891610
}

projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
Injector,
2121
AfterViewChecked
2222
} from '@angular/core';
23-
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, AbstractControl } from '@angular/forms';
23+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, AbstractControl, NG_VALIDATORS, FormControl, ValidationErrors } from '@angular/forms';
2424
import {
2525
IgxCalendarComponent,
2626
IgxCalendarHeaderTemplateDirective,
@@ -133,6 +133,11 @@ const noop = () => { };
133133
provide: NG_VALUE_ACCESSOR,
134134
useExisting: IgxDatePickerComponent,
135135
multi: true
136+
},
137+
{
138+
provide: NG_VALIDATORS,
139+
useExisting: IgxDatePickerComponent,
140+
multi: true
136141
}],
137142
// tslint:disable-next-line:component-selector
138143
selector: 'igx-date-picker',
@@ -308,6 +313,10 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor
308313
}
309314
public set disabledDates(value: DateRangeDescriptor[]) {
310315
this._disabledDates = value;
316+
if (this._ngControl && this._value && this._disabledDates) {
317+
this._ngControl.control.validator({} as AbstractControl);
318+
this._onChangeCallback(this._value);
319+
}
311320
}
312321

313322
/**
@@ -741,6 +750,13 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor
741750
/** @hidden @internal */
742751
public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; }
743752

753+
/** @hidden @internal */
754+
public validate(control: AbstractControl): ValidationErrors | null {
755+
if (this.value && this.disabledDates && isDateInRanges(this.value, this.disabledDates)) {
756+
return { dateIsDisabled: true };
757+
}
758+
return null;
759+
}
744760
//#endregion
745761

746762
/**
@@ -874,7 +890,7 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor
874890

875891
protected onStatusChanged() {
876892
if ((this._ngControl.control.touched || this._ngControl.control.dirty) &&
877-
(this._ngControl.control.validator || this._ngControl.control.asyncValidator)) {
893+
(this._ngControl.control.validator || this._ngControl.control.asyncValidator) && this.inputGroup) {
878894
if (this.inputGroup.isFocused) {
879895
this._inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
880896
} else {

0 commit comments

Comments
 (0)