Skip to content

Commit 9afa74e

Browse files
authored
fix(datepicker): don't render invalid ranges and clean up range display logic (#19111)
No longer shows dates on the calendar if the range is invalid. Also moves out some common range logic that was being repeated in multiple places.
1 parent 9ccaa65 commit 9afa74e

File tree

6 files changed

+108
-52
lines changed

6 files changed

+108
-52
lines changed

src/material/datepicker/calendar-body.spec.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -336,22 +336,24 @@ describe('MatCalendarBody', () => {
336336
expect(cells[27].classList).toContain(inRangeClass);
337337
});
338338

339-
it('should be able to mark a date both as the range start and end', () => {
339+
it('should not to mark a date as both the start and end', () => {
340340
testComponent.startValue = 1;
341341
testComponent.endValue = 1;
342342
fixture.detectChanges();
343343

344-
expect(cells[0].classList).toContain(startClass);
345-
expect(cells[0].classList).toContain(endClass);
344+
expect(cells[0].classList).not.toContain(startClass);
345+
expect(cells[0].classList).not.toContain(inRangeClass);
346+
expect(cells[0].classList).not.toContain(endClass);
346347
});
347348

348-
it('should be able to mark a date both as the comparison range start and end', () => {
349+
it('should not mark a date as both the comparison start and end', () => {
349350
testComponent.comparisonStart = 1;
350351
testComponent.comparisonEnd = 1;
351352
fixture.detectChanges();
352353

353-
expect(cells[0].classList).toContain(comparisonStartClass);
354-
expect(cells[0].classList).toContain(comparisonEndClass);
354+
expect(cells[0].classList).not.toContain(comparisonStartClass);
355+
expect(cells[0].classList).not.toContain(inComparisonClass);
356+
expect(cells[0].classList).not.toContain(comparisonEndClass);
355357
});
356358

357359
it('should not mark a date as the range end if it comes before the start', () => {
@@ -361,7 +363,7 @@ describe('MatCalendarBody', () => {
361363

362364
expect(cells[0].classList).not.toContain(endClass);
363365
expect(cells[0].classList).not.toContain(inRangeClass);
364-
expect(cells[1].classList).toContain(startClass);
366+
expect(cells[1].classList).not.toContain(startClass);
365367
});
366368

367369
it('should not mark a date as the comparison range end if it comes before the start', () => {
@@ -371,7 +373,7 @@ describe('MatCalendarBody', () => {
371373

372374
expect(cells[0].classList).not.toContain(comparisonEndClass);
373375
expect(cells[0].classList).not.toContain(inComparisonClass);
374-
expect(cells[1].classList).toContain(comparisonStartClass);
376+
expect(cells[1].classList).not.toContain(comparisonStartClass);
375377
});
376378

377379
it('should not show a range if there is no start', () => {
@@ -473,7 +475,7 @@ describe('MatCalendarBody', () => {
473475
dispatchMouseEvent(cells[2], 'mouseenter');
474476
fixture.detectChanges();
475477

476-
expect(cells[5].classList).toContain(startClass);
478+
expect(cells[5].classList).not.toContain(startClass);
477479
expect(cells[5].classList).not.toContain(previewStartClass);
478480
expect(cells.some(cell => cell.classList.contains(inPreviewClass))).toBe(false);
479481
});

src/material/datepicker/calendar-body.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -215,23 +215,22 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
215215

216216
/** Gets whether a value is the start of the main range. */
217217
_isRangeStart(value: number) {
218-
return value === this.startValue;
218+
return isStart(value, this.startValue, this.endValue);
219219
}
220220

221221
/** Gets whether a value is the end of the main range. */
222222
_isRangeEnd(value: number) {
223-
return this.startValue && value >= this.startValue && value === this.endValue;
223+
return isEnd(value, this.startValue, this.endValue);
224224
}
225225

226226
/** Gets whether a value is within the currently-selected range. */
227227
_isInRange(value: number): boolean {
228-
return this.isRange && this.startValue !== null && this.endValue !== null &&
229-
value >= this.startValue && value <= this.endValue;
228+
return isInRange(value, this.startValue, this.endValue, this.isRange);
230229
}
231230

232231
/** Gets whether a value is the start of the comparison range. */
233232
_isComparisonStart(value: number) {
234-
return value === this.comparisonStart;
233+
return isStart(value, this.comparisonStart, this.comparisonEnd);
235234
}
236235

237236
/** Whether the cell is a start bridge cell between the main and comparison ranges. */
@@ -268,36 +267,27 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
268267

269268
/** Gets whether a value is the end of the comparison range. */
270269
_isComparisonEnd(value: number) {
271-
return this.comparisonStart && value >= this.comparisonStart &&
272-
value === this.comparisonEnd;
270+
return isEnd(value, this.comparisonStart, this.comparisonEnd);
273271
}
274272

275273
/** Gets whether a value is within the current comparison range. */
276274
_isInComparisonRange(value: number) {
277-
return this.comparisonStart && this.comparisonEnd &&
278-
value >= this.comparisonStart &&
279-
value <= this.comparisonEnd;
275+
return isInRange(value, this.comparisonStart, this.comparisonEnd, this.isRange);
280276
}
281277

282278
/** Gets whether a value is the start of the preview range. */
283279
_isPreviewStart(value: number) {
284-
return value === this.previewStart && this.previewEnd && value < this.previewEnd;
280+
return isStart(value, this.previewStart, this.previewEnd);
285281
}
286282

287283
/** Gets whether a value is the end of the preview range. */
288284
_isPreviewEnd(value: number) {
289-
return value === this.previewEnd && this.previewStart && value > this.previewStart;
285+
return isEnd(value, this.previewStart, this.previewEnd);
290286
}
291287

292288
/** Gets whether a value is inside the preview range. */
293289
_isInPreview(value: number) {
294-
if (!this.isRange) {
295-
return false;
296-
}
297-
298-
const {previewStart, previewEnd} = this;
299-
return previewStart !== null && previewEnd !== null && previewStart !== previewEnd &&
300-
value >= previewStart && value <= previewEnd;
290+
return isInRange(value, this.previewStart, this.previewEnd, this.isRange);
301291
}
302292

303293
/**
@@ -372,3 +362,22 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
372362
function isTableCell(node: Node): node is HTMLTableCellElement {
373363
return node.nodeName === 'TD';
374364
}
365+
366+
/** Checks whether a value is the start of a range. */
367+
function isStart(value: number, start: number | null, end: number | null): boolean {
368+
return end !== null && start !== end && value < end && value === start;
369+
}
370+
371+
/** Checks whether a value is the end of a range. */
372+
function isEnd(value: number, start: number | null, end: number | null): boolean {
373+
return start !== null && start !== end && value >= start && value === end;
374+
}
375+
376+
/** Checks whether a value is inside of a range. */
377+
function isInRange(value: number,
378+
start: number | null,
379+
end: number | null,
380+
rangeEnabled: boolean): boolean {
381+
return rangeEnabled && start !== null && end !== null && start !== end &&
382+
value >= start && value <= end;
383+
}

src/material/datepicker/calendar.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<mat-month-view
55
*ngSwitchCase="'month'"
66
[(activeDate)]="activeDate"
7-
[selected]="selected"
7+
[selected]="_getDisplaySelection()"
88
[dateFilter]="dateFilter"
99
[maxDate]="maxDate"
1010
[minDate]="minDate"
@@ -17,7 +17,7 @@
1717
<mat-year-view
1818
*ngSwitchCase="'year'"
1919
[(activeDate)]="activeDate"
20-
[selected]="selected"
20+
[selected]="_getDisplaySelection()"
2121
[dateFilter]="dateFilter"
2222
[maxDate]="maxDate"
2323
[minDate]="minDate"
@@ -28,7 +28,7 @@
2828
<mat-multi-year-view
2929
*ngSwitchCase="'multi-year'"
3030
[(activeDate)]="activeDate"
31-
[selected]="selected"
31+
[selected]="_getDisplaySelection()"
3232
[dateFilter]="dateFilter"
3333
[maxDate]="maxDate"
3434
[minDate]="minDate"

src/material/datepicker/calendar.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,11 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
446446
this.currentView = view;
447447
}
448448

449+
/** Gets the selection that should be displayed to the user. */
450+
_getDisplaySelection(): DateRange<D> | D | null {
451+
return this._model.isValid() ? this._model.selection : null;
452+
}
453+
449454
/**
450455
* @param obj The object to check.
451456
* @returns The given object if it is both a date instance and valid, otherwise null.

src/material/datepicker/date-selection-model.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {FactoryProvider, Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core';
10+
import {DateAdapter} from '@angular/material/core';
1011
import {Observable, Subject} from 'rxjs';
1112

1213
/** A class representing a range of dates. */
@@ -50,7 +51,8 @@ export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<
5051

5152
protected constructor(
5253
/** The current selection. */
53-
readonly selection: S) {
54+
readonly selection: S,
55+
protected _adapter: DateAdapter<D>) {
5456
this.selection = selection;
5557
}
5658

@@ -68,18 +70,25 @@ export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<
6870
this._selectionChanged.complete();
6971
}
7072

73+
protected _isValidDateInstance(date: D): boolean {
74+
return this._adapter.isDateInstance(date) && this._adapter.isValid(date);
75+
}
76+
7177
/** Adds a date to the current selection. */
7278
abstract add(date: D | null): void;
7379

80+
/** Checks whether the current selection is valid. */
81+
abstract isValid(): boolean;
82+
7483
/** Checks whether the current selection is complete. */
7584
abstract isComplete(): boolean;
7685
}
7786

7887
/** A selection model that contains a single date. */
7988
@Injectable()
8089
export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | null, D> {
81-
constructor() {
82-
super(null);
90+
constructor(adapter: DateAdapter<D>) {
91+
super(null, adapter);
8392
}
8493

8594
/**
@@ -90,6 +99,11 @@ export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | nu
9099
super.updateSelection(date, this);
91100
}
92101

102+
/** Checks whether the current selection is valid. */
103+
isValid(): boolean {
104+
return this.selection != null && this._isValidDateInstance(this.selection);
105+
}
106+
93107
/**
94108
* Checks whether the current selection is complete. In the case of a single date selection, this
95109
* is true if the current selection is not null.
@@ -102,8 +116,8 @@ export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | nu
102116
/** A selection model that contains a date range. */
103117
@Injectable()
104118
export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRange<D>, D> {
105-
constructor() {
106-
super(new DateRange<D>(null, null));
119+
constructor(adapter: DateAdapter<D>) {
120+
super(new DateRange<D>(null, null), adapter);
107121
}
108122

109123
/**
@@ -126,6 +140,26 @@ export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRan
126140
super.updateSelection(new DateRange<D>(start, end), this);
127141
}
128142

143+
/** Checks whether the current selection is valid. */
144+
isValid(): boolean {
145+
const {start, end} = this.selection;
146+
147+
// Empty ranges are valid.
148+
if (start == null && end == null) {
149+
return true;
150+
}
151+
152+
// Complete ranges are only valid if both dates are valid and the start is before the end.
153+
if (start != null && end != null) {
154+
return this._isValidDateInstance(start) && this._isValidDateInstance(end) &&
155+
this._adapter.compareDate(start, end) <= 0;
156+
}
157+
158+
// Partial ranges are valid if the start/end is valid.
159+
return (start == null || this._isValidDateInstance(start)) &&
160+
(end == null || this._isValidDateInstance(end));
161+
}
162+
129163
/**
130164
* Checks whether the current selection is complete. In the case of a date range selection, this
131165
* is true if the current selection has a non-null `start` and `end`.
@@ -137,27 +171,27 @@ export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRan
137171

138172
/** @docs-private */
139173
export function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(
140-
parent: MatSingleDateSelectionModel<unknown>) {
141-
return parent || new MatSingleDateSelectionModel();
174+
parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>) {
175+
return parent || new MatSingleDateSelectionModel(adapter);
142176
}
143177

144178
/** Used to provide a single selection model to a component. */
145179
export const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
146180
provide: MatDateSelectionModel,
147-
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel]],
181+
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
148182
useFactory: MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY,
149183
};
150184

151185

152186
/** @docs-private */
153187
export function MAT_RANGE_DATE_SELECTION_MODEL_FACTORY(
154-
parent: MatSingleDateSelectionModel<unknown>) {
155-
return parent || new MatRangeDateSelectionModel();
188+
parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>) {
189+
return parent || new MatRangeDateSelectionModel(adapter);
156190
}
157191

158192
/** Used to provide a range selection model to a component. */
159193
export const MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
160194
provide: MatDateSelectionModel,
161-
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel]],
195+
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
162196
useFactory: MAT_RANGE_DATE_SELECTION_MODEL_FACTORY,
163197
};

0 commit comments

Comments
 (0)