Skip to content

Commit 93cb0b0

Browse files
committed
feat(drp): add disabledDates prop + test
1 parent 062203d commit 93cb0b0

File tree

6 files changed

+175
-31
lines changed

6 files changed

+175
-31
lines changed

projects/igniteui-angular/src/lib/calendar/common/helpers.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,34 @@ export function getClosestActiveDate(
8888
* @remarks
8989
* By default, `unit` is set to 'day'.
9090
*/
91-
export function* calendarRange(options: CalendarRangeParams) {
92-
let low = toCalendarDay(options.start);
93-
const unit = options.unit ?? "day";
94-
const high =
95-
typeof options.end === "number"
96-
? low.add(unit, options.end)
97-
: toCalendarDay(options.end);
98-
99-
const reverse = high.lessThan(low);
100-
const step = reverse ? -1 : 1;
101-
102-
while (!reverse ? low.lessThan(high) : low.greaterThan(high)) {
103-
yield low;
104-
low = low.add(unit, step);
91+
export function* calendarRange(
92+
options: CalendarRangeParams
93+
): Generator<CalendarDay, void, unknown> {
94+
const { start, end, unit = 'day', inclusive = false } = options;
95+
96+
let currentDate = toCalendarDay(start);
97+
const endDate =
98+
typeof end === 'number'
99+
? toCalendarDay(start).add(unit, end)
100+
: toCalendarDay(end);
101+
102+
const isReversed = endDate.lessThan(currentDate);
103+
const step = isReversed ? -1 : 1;
104+
105+
const shouldContinue = () => {
106+
if (inclusive) {
107+
return isReversed
108+
? currentDate.greaterThanOrEqual(endDate)
109+
: currentDate.lessThanOrEqual(endDate);
110+
}
111+
return isReversed
112+
? currentDate.greaterThan(endDate)
113+
: currentDate.lessThan(endDate);
114+
};
115+
116+
while (shouldContinue()) {
117+
yield currentDate;
118+
currentDate = currentDate.add(unit, step);
105119
}
106120
}
107121

projects/igniteui-angular/src/lib/calendar/common/model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type CalendarRangeParams = {
77
start: DayParameter;
88
end: DayParameter | number;
99
unit?: DayInterval;
10+
inclusive?: boolean;
1011
};
1112

1213
type CalendarDayParams = {
@@ -237,11 +238,18 @@ export class CalendarDay {
237238
public greaterThan(value: DayParameter) {
238239
return this.timestamp > toCalendarDay(value).timestamp;
239240
}
241+
public greaterThanOrEqual(value: DayParameter) {
242+
return this.timestamp >= toCalendarDay(value).timestamp;
243+
}
240244

241245
public lessThan(value: DayParameter) {
242246
return this.timestamp < toCalendarDay(value).timestamp;
243247
}
244248

249+
public lessThanOrEqual(value: DayParameter) {
250+
return this.timestamp <= toCalendarDay(value).timestamp;
251+
}
252+
245253
public toString() {
246254
return `${this.native}`;
247255
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,42 @@ describe('IgxDateRangePicker', () => {
275275
expect((dateRange as any).calendar.disabledDates[1].type).toEqual(DateRangeType.After);
276276
expect((dateRange as any).calendar.disabledDates[1].dateRange[0]).toEqual(new Date(dateRange.maxValue));
277277
}));
278+
279+
it('should validate correctly when disabledDates are set', () => {
280+
const dateRange = new IgxDateRangePickerComponent(elementRef, 'en-US', platform, mockInjector, null, null, null);
281+
dateRange.ngOnInit();
282+
283+
dateRange.registerOnChange(mockNgControl.registerOnChangeCb);
284+
dateRange.registerOnValidatorChange(mockNgControl.registerOnValidatorChangeCb);
285+
mockNgControl.registerOnValidatorChangeCb.calls.reset();
286+
spyOnProperty((dateRange as any), 'calendar').and.returnValue(mockCalendar);
287+
288+
const start = new Date(new Date().getFullYear(), new Date().getMonth(), 10);
289+
const end = new Date(new Date().getFullYear(), new Date().getMonth(), 18);
290+
291+
const disabledDates = [{
292+
type: DateRangeType.Between, dateRange: [ start, end ]
293+
}];
294+
dateRange.disabledDates = disabledDates;
295+
expect(mockNgControl.registerOnValidatorChangeCb).toHaveBeenCalledTimes(1);
296+
297+
298+
const validRange = {
299+
start: new Date(new Date().getFullYear(), new Date().getMonth(), 2),
300+
end: new Date(new Date().getFullYear(), new Date().getMonth(), 5),
301+
};
302+
dateRange.writeValue(validRange);
303+
const mockFormControl = new UntypedFormControl(dateRange.value);
304+
expect(dateRange.validate(mockFormControl)).toBeNull();
305+
306+
(dateRange as any).updateCalendar();
307+
expect((dateRange as any).calendar.disabledDates.length).toEqual(1);
308+
expect((dateRange as any).calendar.disabledDates[0].type).toEqual(DateRangeType.Between);
309+
310+
start.setDate(start.getDate() - 2);
311+
dateRange.writeValue({ start, end });
312+
expect(dateRange.validate(mockFormControl)).toEqual({ dateIsDisabled: true });
313+
});
278314
});
279315

280316
describe('Integration tests', () => {
@@ -1684,6 +1720,34 @@ describe('IgxDateRangePicker', () => {
16841720
expect(dateRange['_calendar'].specialDates).toEqual(specialDates);
16851721
}));
16861722

1723+
it('should set the disabledDates of the calendar', fakeAsync(() => {
1724+
fixture = TestBed.createComponent(DateRangeDefaultComponent);
1725+
fixture.detectChanges();
1726+
dateRange = fixture.componentInstance.dateRange;
1727+
1728+
const disabledDates = [{
1729+
type: DateRangeType.Between, dateRange: [
1730+
new Date(new Date().getFullYear(), new Date().getMonth(), 3),
1731+
new Date(new Date().getFullYear(), new Date().getMonth(), 8)
1732+
]
1733+
}];
1734+
dateRange.disabledDates = disabledDates;
1735+
fixture.detectChanges();
1736+
1737+
dateRange.open();
1738+
tick();
1739+
fixture.detectChanges();
1740+
1741+
expect(dateRange['_calendar'].disabledDates).toEqual(disabledDates);
1742+
1743+
// should not allow to select a date from the disabled dates
1744+
startDate = new Date(new Date().getFullYear(), new Date().getMonth(), 4);
1745+
endDate = new Date(new Date().getFullYear(), new Date().getMonth(), 6);
1746+
1747+
selectDateRangeFromCalendar(startDate, endDate);
1748+
expect(dateRange.value).toBeNull();
1749+
}));
1750+
16871751
describe('Templated Calendar Header', () => {
16881752
let dateRangeDebugEl: DebugElement;
16891753
beforeEach(fakeAsync(() => {

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

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { IgxIconComponent } from '../icon/icon.component';
3636
import { getCurrentResourceStrings } from '../core/i18n/resources';
3737
import { fadeIn, fadeOut } from 'igniteui-angular/animations';
3838
import { PickerCalendarOrientation } from '../date-common/types';
39+
import { calendarRange, isDateInRanges } from '../calendar/common/helpers';
3940

4041
const SingleInputDatesConcatenationString = ' - ';
4142

@@ -234,6 +235,24 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective
234235
return this._maxValue;
235236
}
236237

238+
/**
239+
* Gets/Sets the disabled dates descriptors.
240+
*
241+
* @example
242+
* ```typescript
243+
* let disabledDates = this.dateRangePicker.disabledDates;
244+
* this.dateRangePicker.disabledDates = [ {type: DateRangeType.Weekends}, ...];
245+
* ```
246+
*/
247+
@Input()
248+
public get disabledDates(): DateRangeDescriptor[] {
249+
return this._disabledDates;
250+
}
251+
public set disabledDates(value: DateRangeDescriptor[]) {
252+
this._disabledDates = value;
253+
this.onValidatorChange();
254+
}
255+
237256
/**
238257
* Gets/Sets the special dates descriptors.
239258
*
@@ -493,6 +512,7 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective
493512
private _focusedInput: IgxDateRangeInputsBaseComponent;
494513
private _displayMonthsCount = 2;
495514
private _specialDates: DateRangeDescriptor[] = null;
515+
private _disabledDates: DateRangeDescriptor[] = null;
496516
private _overlaySubFilter:
497517
[MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs | OverlayCancelableEventArgs>] = [
498518
filter(x => x.id === this._overlayId),
@@ -650,16 +670,19 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective
650670
}
651671
}
652672

653-
const min = parseDate(this.minValue);
654-
const max = parseDate(this.maxValue);
673+
if (this._isValueInDisabledRange(value)) {
674+
Object.assign(errors, { dateIsDisabled: true });
675+
}
676+
677+
const { minValue, maxValue } = this._getMinMaxDates();
655678
const start = parseDate(value.start);
656679
const end = parseDate(value.end);
657-
if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false))
658-
|| (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) {
680+
if ((minValue && start && DateTimeUtil.lessThanMinValue(start, minValue, false))
681+
|| (minValue && end && DateTimeUtil.lessThanMinValue(end, minValue, false))) {
659682
Object.assign(errors, { minValue: true });
660683
}
661-
if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false))
662-
|| (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) {
684+
if ((maxValue && start && DateTimeUtil.greaterThanMaxValue(start, maxValue, false))
685+
|| (maxValue && end && DateTimeUtil.greaterThanMaxValue(end, maxValue, false))) {
663686
Object.assign(errors, { maxValue: true });
664687
}
665688
}
@@ -928,15 +951,8 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective
928951
if (!this.calendar) {
929952
return;
930953
}
931-
this.calendar.disabledDates = [];
932-
const minValue = this.parseMinValue(this.minValue);
933-
if (minValue) {
934-
this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
935-
}
936-
const maxValue = this.parseMaxValue(this.maxValue);
937-
if (maxValue) {
938-
this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
939-
}
954+
this._setDisabledDates();
955+
const { minValue, maxValue } = this._getMinMaxDates();
940956

941957
const range: Date[] = [];
942958
if (this.value?.start && this.value?.end) {
@@ -1140,9 +1156,44 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective
11401156
this._calendar.specialDates = this.specialDates;
11411157
this._calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev));
11421158

1159+
this._setDisabledDates();
1160+
11431161
componentInstance.mode = this.mode;
11441162
componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
11451163
componentInstance.pickerActions = this.pickerActions;
11461164
componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
11471165
}
1166+
1167+
private _setDisabledDates(): void {
1168+
if (!this.calendar) {
1169+
return;
1170+
}
1171+
this.calendar.disabledDates = this.disabledDates ? [...this.disabledDates] : [];
1172+
const { minValue, maxValue } = this._getMinMaxDates();
1173+
if (minValue) {
1174+
this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
1175+
}
1176+
if (maxValue) {
1177+
this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
1178+
}
1179+
}
1180+
1181+
private _getMinMaxDates() {
1182+
const minValue = this.parseMinValue(this.minValue);
1183+
const maxValue = this.parseMaxValue(this.maxValue);
1184+
return { minValue, maxValue };
1185+
}
1186+
1187+
private _isValueInDisabledRange(value: DateRange) {
1188+
if (value && this.disabledDates) {
1189+
const isOutsideDisabledRange = Array.from(
1190+
calendarRange({
1191+
start: parseDate(this.value.start),
1192+
end: parseDate(this.value.end),
1193+
inclusive: true
1194+
})).every((date) => !isDateInRanges(date, this.disabledDates));
1195+
return !isOutsideDisabledRange;
1196+
}
1197+
return false;
1198+
}
11481199
}

src/app/date-range/date-range.sample.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ <h5>Without forms</h5>
1010
<button igxButton="contained" igxRipple (click)="changeWeekStart(5)">Change weekStart to Friday</button>
1111
<button igxButton="contained" igxRipple (click)="changeWeekStart(0)">Change weekStart to Sunday</button>
1212
</div>
13-
<igx-date-range-picker #dr1 mode="dialog"></igx-date-range-picker>
13+
<igx-date-range-picker #dr1 mode="dialog" [disabledDates]="disabledDates"></igx-date-range-picker>
1414

1515
<p>Drop down single input with custom suffix</p>
1616
<igx-date-range-picker>

src/app/date-range/date-range.sample.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, ViewChild } from '@angular/core';
22
import { JsonPipe } from '@angular/common';
33
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl, ValidatorFn, AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
4-
import { DateRange, IgxButtonDirective, IgxDateRangeEndComponent, IgxDateRangePickerComponent, IgxDateRangeStartComponent, IgxDateTimeEditorDirective, IgxIconComponent, IgxInputDirective, IgxLabelDirective, IgxPickerToggleComponent, IgxPrefixDirective, IgxRadioComponent, IgxRippleDirective, IgxSuffixDirective, IGX_INPUT_GROUP_TYPE, IChangeCheckboxEventArgs } from 'igniteui-angular';
4+
import { DateRangeType } from 'igniteui-angular/src/lib/calendar/common/types';
55

66

77
@Component({
@@ -37,6 +37,13 @@ export class DateRangeSampleComponent {
3737
public updateOnOptions: string[] = ['change', 'blur', 'submit'];
3838
public updateOn = 'blur';
3939

40+
public disabledDates = [{
41+
type: DateRangeType.Between, dateRange: [
42+
new Date(new Date().getFullYear(), new Date().getMonth(), 20),
43+
new Date(new Date().getFullYear(), new Date().getMonth(), 25)
44+
]
45+
}];
46+
4047
constructor(fb: UntypedFormBuilder) {
4148
const today = new Date();
4249
const in5days = new Date();

0 commit comments

Comments
 (0)