Skip to content

Commit 6f312e1

Browse files
committed
feat(datepicker): allow for date range selection logic to be customized
As discussed, adds a provider that allows the consumer to customize how the range is changed after the user has selected a value. For now it only has one method (`selectionFinished`), but the interface should allow us to add more later on.
1 parent ab3d915 commit 6f312e1

15 files changed

+192
-91
lines changed

goldens/ts-circular-deps.json

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
[
2-
[
3-
"src/cdk-experimental/dialog/dialog-config.ts",
4-
"src/cdk-experimental/dialog/dialog-container.ts"
5-
],
62
[
73
"src/cdk-experimental/popover-edit/edit-event-dispatcher.ts",
84
"src/cdk-experimental/popover-edit/edit-ref.ts"
95
],
6+
[
7+
"src/cdk/scrolling/scroll-dispatcher.ts",
8+
"src/cdk/scrolling/scrollable.ts"
9+
],
10+
[
11+
"src/cdk/scrolling/virtual-scroll-viewport.ts",
12+
"src/cdk/scrolling/virtual-for-of.ts"
13+
],
14+
[
15+
"src/cdk/scrolling/virtual-scroll-strategy.ts",
16+
"src/cdk/scrolling/virtual-scroll-viewport.ts"
17+
],
18+
[
19+
"src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts",
20+
"src/cdk/overlay/overlay-ref.ts"
21+
],
22+
[
23+
"src/cdk-experimental/dialog/dialog-config.ts",
24+
"src/cdk-experimental/dialog/dialog-container.ts"
25+
],
1026
[
1127
"src/cdk/drag-drop/drag-ref.ts",
1228
"src/cdk/drag-drop/drop-list-ref.ts"
@@ -25,29 +41,13 @@
2541
"src/cdk/drag-drop/directives/drag.ts"
2642
],
2743
[
28-
"src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts",
29-
"src/cdk/overlay/overlay-ref.ts"
44+
"src/cdk/testing/private/e2e/actions.ts",
45+
"src/cdk/testing/private/e2e/query.ts"
3046
],
3147
[
3248
"src/cdk/schematics/testing/index.ts",
3349
"src/cdk/schematics/testing/test-case-setup.ts"
3450
],
35-
[
36-
"src/cdk/scrolling/scroll-dispatcher.ts",
37-
"src/cdk/scrolling/scrollable.ts"
38-
],
39-
[
40-
"src/cdk/scrolling/virtual-scroll-viewport.ts",
41-
"src/cdk/scrolling/virtual-for-of.ts"
42-
],
43-
[
44-
"src/cdk/scrolling/virtual-scroll-strategy.ts",
45-
"src/cdk/scrolling/virtual-scroll-viewport.ts"
46-
],
47-
[
48-
"src/cdk/testing/private/e2e/actions.ts",
49-
"src/cdk/testing/private/e2e/query.ts"
50-
],
5151
[
5252
"src/material/core/ripple/ripple-ref.ts",
5353
"src/material/core/ripple/ripple-renderer.ts"
@@ -56,6 +56,10 @@
5656
"src/material/datepicker/datepicker.ts",
5757
"src/material/datepicker/datepicker-input.ts"
5858
],
59+
[
60+
"src/material/datepicker/date-range-input.ts",
61+
"src/material/datepicker/date-range-picker.ts"
62+
],
5963
[
6064
"src/material/grid-list/grid-list.ts",
6165
"src/material/grid-list/tile-styler.ts"

src/material/datepicker/calendar-body.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
[attr.aria-label]="item.ariaLabel"
5151
[attr.aria-disabled]="!item.enabled || null"
5252
[attr.aria-selected]="_isSelected(item)"
53-
(click)="_cellClicked(item)"
53+
(click)="_cellClicked(item, $event)"
5454
[style.width]="_cellWidth"
5555
[style.paddingTop]="_cellPadding"
5656
role="button"

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
22
import {Component} from '@angular/core';
3-
import {MatCalendarBody, MatCalendarCell} from './calendar-body';
3+
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';
44
import {By} from '@angular/platform-browser';
55
import {dispatchMouseEvent, dispatchFakeEvent} from '@angular/cdk/testing/private';
66

@@ -591,8 +591,8 @@ class StandardCalendarBody {
591591
labelMinRequiredCells = 3;
592592
numCols = 7;
593593

594-
onSelect(value: number) {
595-
this.selectedValue = value;
594+
onSelect(event: MatCalendarUserEvent<number>) {
595+
this.selectedValue = event.value;
596596
}
597597
}
598598

@@ -614,7 +614,8 @@ class RangeCalendarBody {
614614
comparisonStart: number | undefined;
615615
comparisonEnd: number | undefined;
616616

617-
onSelect(value: number) {
617+
onSelect(event: MatCalendarUserEvent<number>) {
618+
const value = event.value;
618619
if (!this.startValue) {
619620
this.startValue = value;
620621
} else if (!this.endValue) {

src/material/datepicker/calendar-body.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export class MatCalendarCell {
4040
public compareValue = value) {}
4141
}
4242

43+
/** Event emitted when a date inside the calendar is triggered as a result of a user action. */
44+
export interface MatCalendarUserEvent<D> {
45+
value: D;
46+
event: Event;
47+
}
4348

4449
/**
4550
* An internal component used to display calendar data in a table.
@@ -96,7 +101,8 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
96101
@Input() comparisonEnd: number | null;
97102

98103
/** Emits when a new value is selected. */
99-
@Output() readonly selectedValueChange: EventEmitter<number> = new EventEmitter<number>();
104+
@Output() readonly selectedValueChange: EventEmitter<MatCalendarUserEvent<number>> =
105+
new EventEmitter<MatCalendarUserEvent<number>>();
100106

101107
/** The number of blank cells to put at the beginning for the first row. */
102108
_firstRowOffset: number;
@@ -128,9 +134,9 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
128134
}
129135

130136
/** Called when a cell is clicked. */
131-
_cellClicked(cell: MatCalendarCell): void {
137+
_cellClicked(cell: MatCalendarCell, event: MouseEvent): void {
132138
if (cell.enabled) {
133-
this.selectedValueChange.emit(cell.value);
139+
this.selectedValueChange.emit({value: cell.value, event});
134140
}
135141
}
136142

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Injectable, InjectionToken} from '@angular/core';
10+
import {DateAdapter} from '@angular/material/core';
11+
import {DateRange} from './date-selection-model';
12+
13+
/** Injection token used to customize the date range selection behavior. */
14+
export const MAT_CALENDAR_RANGE_SELECTION_STRATEGY =
15+
new InjectionToken<MatCalendarRangeSelectionStrategy<any>>(
16+
'MAT_CALENDAR_RANGE_SELECTION_STRATEGY');
17+
18+
/** Object that can be provided in order to customize the date range selection behavior. */
19+
export interface MatCalendarRangeSelectionStrategy<D> {
20+
/** Called when the user has finished selecting a value. */
21+
selectionFinished(date: D | null, currentRange: DateRange<D>, event: Event): DateRange<D>;
22+
}
23+
24+
/** Provides the default date range selection behavior. */
25+
@Injectable()
26+
export class DefaultMatCalendarRangeStrategy<D> implements MatCalendarRangeSelectionStrategy<D> {
27+
constructor(private _dateAdapter: DateAdapter<D>) {}
28+
29+
selectionFinished(date: D, currentRange: DateRange<D>) {
30+
let {start, end} = currentRange;
31+
32+
if (start == null) {
33+
start = date;
34+
} else if (end == null && date && this._dateAdapter.compareDate(date, start) > 0) {
35+
end = date;
36+
} else {
37+
start = date;
38+
end = null;
39+
}
40+
41+
return new DateRange<D>(start, end);
42+
}
43+
}

src/material/datepicker/calendar.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
[dateClass]="dateClass"
1212
[comparisonStart]="comparisonStart"
1313
[comparisonEnd]="comparisonEnd"
14-
(selectedChange)="_dateSelected($event)"
15-
(_userSelection)="_userSelected()">
14+
(_userSelection)="_dateSelected($event)">
1615
</mat-month-view>
1716

1817
<mat-year-view

src/material/datepicker/calendar.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
MatDateFormats,
3232
} from '@angular/material/core';
3333
import {Subject, Subscription} from 'rxjs';
34-
import {MatCalendarCellCssClasses} from './calendar-body';
34+
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
3535
import {createMissingDateImplError} from './datepicker-errors';
3636
import {MatDatepickerIntl} from './datepicker-intl';
3737
import {MatMonthView} from './month-view';
@@ -47,6 +47,10 @@ import {
4747
DateRange,
4848
MatDateSelectionModel,
4949
} from './date-selection-model';
50+
import {
51+
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
52+
MatCalendarRangeSelectionStrategy
53+
} from './calendar-range-selection-strategy';
5054

5155
/**
5256
* Possible views for the calendar.
@@ -318,7 +322,9 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
318322
@Optional() private _dateAdapter: DateAdapter<D>,
319323
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
320324
private _changeDetectorRef: ChangeDetectorRef,
321-
private _model: MatDateSelectionModel<DateRange<D> | D | null>) {
325+
private _model: MatDateSelectionModel<DateRange<D> | D | null>,
326+
@Optional() @Inject(MAT_CALENDAR_RANGE_SELECTION_STRATEGY)
327+
private _rangeSelectionStrategy?: MatCalendarRangeSelectionStrategy<D>) {
322328

323329
if (!this._dateAdapter) {
324330
throw createMissingDateImplError('DateAdapter');
@@ -403,8 +409,25 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
403409
}
404410

405411
/** Handles date selection in the month view. */
406-
_dateSelected(date: D | null): void {
407-
this._model.add(date);
412+
_dateSelected(event: MatCalendarUserEvent<D | null>): void {
413+
const selection = this._model.selection;
414+
const value = event.value;
415+
const isRange = selection instanceof DateRange;
416+
417+
// If we're selecting a range and we have a selection strategy, always pass the value through
418+
// there. Otherwise don't assign null values to the model, unless we're selecting a range.
419+
// A null value when picking a range means that the user cancelled the selection (e.g. by
420+
// pressing escape), whereas when selecting a single value it means that the value didn't
421+
// change. This isn't very intuitive, but it's here for backwards-compatibility.
422+
if (isRange && this._rangeSelectionStrategy) {
423+
const newSelection = this._rangeSelectionStrategy.selectionFinished(value,
424+
selection as DateRange<D>, event.event);
425+
this._model.updateSelection(newSelection, this);
426+
} else if (value || isRange) {
427+
this._model.add(value);
428+
}
429+
430+
this._userSelection.emit();
408431
}
409432

410433
/** Handles year selection in the multiyear view. */
@@ -417,10 +440,6 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
417440
this.monthSelected.emit(normalizedMonth);
418441
}
419442

420-
_userSelected(): void {
421-
this._userSelection.emit();
422-
}
423-
424443
/** Handles year/month selection in the multi-year/year views. */
425444
_goToDateInView(date: D, view: 'month' | 'year' | 'multi-year'): void {
426445
this.activeDate = date;

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

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

99
import {FactoryProvider, Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core';
10-
import {DateAdapter} from '@angular/material/core';
1110
import {Observable, Subject} from 'rxjs';
1211

1312
/** A class representing a range of dates. */
@@ -50,8 +49,6 @@ export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<
5049
selectionChanged: Observable<DateSelectionModelChange<S>> = this._selectionChanged.asObservable();
5150

5251
protected constructor(
53-
/** Date adapter used when interacting with dates in the model. */
54-
protected readonly adapter: DateAdapter<D>,
5552
/** The current selection. */
5653
readonly selection: S) {
5754
this.selection = selection;
@@ -81,8 +78,8 @@ export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<
8178
/** A selection model that contains a single date. */
8279
@Injectable()
8380
export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | null, D> {
84-
constructor(adapter: DateAdapter<D>) {
85-
super(adapter, null);
81+
constructor() {
82+
super(null);
8683
}
8784

8885
/**
@@ -105,8 +102,8 @@ export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | nu
105102
/** A selection model that contains a date range. */
106103
@Injectable()
107104
export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRange<D>, D> {
108-
constructor(adapter: DateAdapter<D>) {
109-
super(adapter, new DateRange<D>(null, null));
105+
constructor() {
106+
super(new DateRange<D>(null, null));
110107
}
111108

112109
/**
@@ -119,7 +116,7 @@ export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRan
119116

120117
if (start == null) {
121118
start = date;
122-
} else if (end == null && date && this.adapter.compareDate(date, start) > 0) {
119+
} else if (end == null) {
123120
end = date;
124121
} else {
125122
start = date;
@@ -140,27 +137,27 @@ export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRan
140137

141138
/** @docs-private */
142139
export function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(
143-
parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>) {
144-
return parent || new MatSingleDateSelectionModel(adapter);
140+
parent: MatSingleDateSelectionModel<unknown>) {
141+
return parent || new MatSingleDateSelectionModel();
145142
}
146143

147144
/** Used to provide a single selection model to a component. */
148145
export const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
149146
provide: MatDateSelectionModel,
150-
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
147+
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel]],
151148
useFactory: MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY,
152149
};
153150

154151

155152
/** @docs-private */
156153
export function MAT_RANGE_DATE_SELECTION_MODEL_FACTORY(
157-
parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>) {
158-
return parent || new MatRangeDateSelectionModel(adapter);
154+
parent: MatSingleDateSelectionModel<unknown>) {
155+
return parent || new MatRangeDateSelectionModel();
159156
}
160157

161158
/** Used to provide a range selection model to a component. */
162159
export const MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
163160
provide: MatDateSelectionModel,
164-
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
161+
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel]],
165162
useFactory: MAT_RANGE_DATE_SELECTION_MODEL_FACTORY,
166163
};

src/material/datepicker/datepicker-module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import {MatYearView} from './year-view';
2929
import {MatDateRangeInput} from './date-range-input';
3030
import {MatStartDate, MatEndDate} from './date-range-input-parts';
3131
import {MatDateRangePicker} from './date-range-picker';
32+
import {
33+
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
34+
DefaultMatCalendarRangeStrategy
35+
} from './calendar-range-selection-strategy';
3236

3337

3438
@NgModule({
@@ -77,6 +81,10 @@ import {MatDateRangePicker} from './date-range-picker';
7781
providers: [
7882
MatDatepickerIntl,
7983
MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER,
84+
{
85+
provide: MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
86+
useClass: DefaultMatCalendarRangeStrategy
87+
}
8088
],
8189
entryComponents: [
8290
MatDatepickerContent,

0 commit comments

Comments
 (0)