Skip to content

feat(datepicker): implement comparison and overlap ranges #18753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/dev-app/datepicker/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ ng_module(
sass_binary(
name = "datepicker_demo_scss",
src = "datepicker-demo.scss",
deps = [
"//src/material/datepicker:datepicker_scss_lib",
],
)

sass_binary(
Expand Down
7 changes: 7 additions & 0 deletions src/dev-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ <h2>Range picker</h2>
[min]="minDate"
[max]="maxDate"
[disabled]="inputDisabled"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd"
[dateFilter]="filterOdd ? dateFilter : undefined">
<input matStartDate formControlName="start" placeholder="Start date"/>
<input matEndDate formControlName="end" placeholder="End date"/>
Expand All @@ -203,6 +205,8 @@ <h2>Range picker</h2>
[min]="minDate"
[max]="maxDate"
[disabled]="inputDisabled"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd"
[dateFilter]="filterOdd ? dateFilter : undefined">
<input matStartDate formControlName="start" placeholder="Start date"/>
<input matEndDate formControlName="end" placeholder="End date"/>
Expand All @@ -211,6 +215,7 @@ <h2>Range picker</h2>
<mat-date-range-picker
[touchUi]="touch"
[disabled]="datepickerDisabled"
panelClass="demo-custom-range"
#range2Picker></mat-date-range-picker>
</mat-form-field>
<div>{{range2.value | json}}</div>
Expand All @@ -225,6 +230,8 @@ <h2>Range picker</h2>
[min]="minDate"
[max]="maxDate"
[disabled]="inputDisabled"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd"
[dateFilter]="filterOdd ? dateFilter : undefined">
<input matStartDate formControlName="start" placeholder="Start date"/>
<input matEndDate formControlName="end" placeholder="End date"/>
Expand Down
6 changes: 6 additions & 0 deletions src/dev-app/datepicker/datepicker-demo.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
@import '../../material/datepicker/datepicker-theme';

mat-calendar {
width: 300px;
}

.demo-range-group {
margin-bottom: 30px;
}

.demo-custom-range {
@include mat-datepicker-range-colors(hotpink, teal, yellow, purple);
}
14 changes: 13 additions & 1 deletion src/dev-app/datepicker/datepicker-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
Inject,
OnDestroy,
Optional,
ViewChild
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {FormControl, FormGroup} from '@angular/forms';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats, ThemePalette} from '@angular/material/core';
Expand All @@ -30,6 +31,7 @@ import {takeUntil} from 'rxjs/operators';
selector: 'datepicker-demo',
templateUrl: 'datepicker-demo.html',
styleUrls: ['datepicker-demo.css'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DatepickerDemo {
Expand All @@ -50,6 +52,16 @@ export class DatepickerDemo {
range1 = new FormGroup({start: new FormControl(), end: new FormControl()});
range2 = new FormGroup({start: new FormControl(), end: new FormControl()});
range3 = new FormGroup({start: new FormControl(), end: new FormControl()});
comparisonStart: Date;
comparisonEnd: Date;

constructor() {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
this.comparisonStart = new Date(year, month, 9);
this.comparisonEnd = new Date(year, month, 13);
}

dateFilter: (date: Date | null) => boolean =
(date: Date | null) => {
Expand Down
40 changes: 36 additions & 4 deletions src/material/datepicker/_datepicker-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ $mat-calendar-body-font-size: 13px !default;
$mat-calendar-weekday-table-font-size: 11px !default;

@mixin _mat-datepicker-color($palette) {
@include mat-datepicker-range-colors(
mat-color($palette, default, $mat-datepicker-range-fade-amount));

.mat-calendar-body-selected {
background-color: mat-color($palette);
color: mat-color($palette, default-contrast);
}

.mat-calendar-body-in-range::before {
background-color: mat-color($palette, default, $mat-datepicker-range-fade-amount);
}

.mat-calendar-body-disabled > .mat-calendar-body-selected {
$background: mat-color($palette);

Expand Down Expand Up @@ -167,3 +166,36 @@ $mat-calendar-weekday-table-font-size: 11px !default;
}
}
}

@mixin mat-datepicker-range-colors(
$range-color,
$comparison-color: rgba(#f9ab00, $mat-datepicker-range-fade-amount),
$overlap-color: #a8dab5,
$overlap-selected-color: darken($overlap-color, 30%)) {

.mat-calendar-body-in-range::before {
background: $range-color;
}

.mat-calendar-body-in-comparison-range::before {
background: $comparison-color;
}

.mat-calendar-body-comparison-bridge-start::before,
[dir='rtl'] .mat-calendar-body-comparison-bridge-end::before {
background: linear-gradient(to right, $range-color 50%, $comparison-color 50%);
}

.mat-calendar-body-comparison-bridge-end::before,
[dir='rtl'] .mat-calendar-body-comparison-bridge-start::before {
background: linear-gradient(to left, $range-color 50%, $comparison-color 50%);
}

.mat-calendar-body-in-comparison-range.mat-calendar-body-in-range::after {
background: $overlap-color;
}

.mat-calendar-body-in-comparison-range > .mat-calendar-body-selected {
background: $overlap-selected-color;
}
}
9 changes: 7 additions & 2 deletions src/material/datepicker/calendar-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@
[attr.data-mat-col]="colIndex"
[class.mat-calendar-body-disabled]="!item.enabled"
[class.mat-calendar-body-active]="_isActiveCell(rowIndex, colIndex)"
[class.mat-calendar-body-range-start]="_isRangeStart(item.compareValue)"
[class.mat-calendar-body-range-end]="_isRangeEnd(item.compareValue)"
[class.mat-calendar-body-in-range]="_isInRange(item.compareValue)"
[class.mat-calendar-body-range-start]="item.compareValue === startValue"
[class.mat-calendar-body-range-end]="item.compareValue === endValue || item.compareValue === _hoveredValue"
[class.mat-calendar-body-comparison-bridge-start]="_isComparisonBridgeStart(item.compareValue, rowIndex, colIndex)"
[class.mat-calendar-body-comparison-bridge-end]="_isComparisonBridgeEnd(item.compareValue, rowIndex, colIndex)"
[class.mat-calendar-body-comparison-start]="_isComparisonStart(item.compareValue)"
[class.mat-calendar-body-comparison-end]="_isComparisonEnd(item.compareValue)"
[class.mat-calendar-body-in-comparison-range]="_isInComparisonRange(item.compareValue)"
[attr.aria-label]="item.ariaLabel"
[attr.aria-disabled]="!item.enabled || null"
[attr.aria-selected]="_isSelected(item)"
Expand Down
47 changes: 35 additions & 12 deletions src/material/datepicker/calendar-body.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,32 @@ $mat-calendar-range-end-body-cell-size:
text-align: center;
outline: none;
cursor: pointer;
}

.mat-calendar-body-cell::before,
.mat-calendar-body-cell::after,
.mat-calendar-body-comparison-bridge-start::before {
// We use ::before to apply a background to the body cell, because we need to apply a border
// radius to the start/end which means that part of the element will be cut off, making
// hovering through all the cells look glitchy. We can't do it on the cell itself, because
// it's the one that has the event listener and it can't be on the cell content, because
// it always has a border radius.
&::before {
content: '';
position: absolute;
top: $mat-calendar-body-cell-content-margin;
left: 0;
content: '';
position: absolute;
top: $mat-calendar-body-cell-content-margin;
left: 0;
z-index: 0;

// We want the range background to be slightly shorter than the cell so
// that there's a gap when the range goes across multiple rows.
height: $mat-calendar-body-cell-content-size;
width: 100%;
}
// We want the range background to be slightly shorter than the cell so
// that there's a gap when the range goes across multiple rows.
height: $mat-calendar-body-cell-content-size;
width: 100%;
}

.mat-calendar-body-range-start::before {
.mat-calendar-body-range-start:not(.mat-calendar-body-in-comparison-range)::before,
.mat-calendar-body-range-start::after,
.mat-calendar-body-comparison-start:not(.mat-calendar-body-comparison-bridge-start)::before,
.mat-calendar-body-comparison-start::after {
// Since the range background isn't a perfect circle, we need to size
// and offset the start so that it aligns with the main circle.
left: $mat-calendar-body-cell-content-margin;
Expand All @@ -71,12 +77,19 @@ $mat-calendar-range-end-body-cell-size:
}
}

.mat-calendar-body-range-end::before {
@mixin _mat-calendar-body-range-right-radius {
// Since the range background isn't a perfect circle, we need to
// resize the end so that it aligns with the main circle.
width: $mat-calendar-range-end-body-cell-size;
border-top-right-radius: $mat-calendar-body-cell-radius;
border-bottom-right-radius: $mat-calendar-body-cell-radius;
}

.mat-calendar-body-range-end:not(.mat-calendar-body-in-comparison-range)::before,
.mat-calendar-body-range-end::after,
.mat-calendar-body-comparison-end:not(.mat-calendar-body-comparison-bridge-end)::before,
.mat-calendar-body-comparison-end::after {
@include _mat-calendar-body-range-right-radius;

[dir='rtl'] & {
left: $mat-calendar-body-cell-content-margin;
Expand All @@ -86,6 +99,15 @@ $mat-calendar-range-end-body-cell-size:
}
}

[dir='rtl'] {
.mat-calendar-body-comparison-bridge-start.mat-calendar-body-range-end::after,
.mat-calendar-body-comparison-bridge-end.mat-calendar-body-range-start::after,
.mat-calendar-body-range-start.mat-calendar-body-range-end::before,
.mat-calendar-body-range-start.mat-calendar-body-range-end::after {
@include _mat-calendar-body-range-right-radius;
}
}

.mat-calendar-body-disabled {
cursor: default;
}
Expand All @@ -94,6 +116,7 @@ $mat-calendar-range-end-body-cell-size:
position: absolute;
top: $mat-calendar-body-cell-content-margin;
left: $mat-calendar-body-cell-content-margin;
z-index: 1;

display: flex;
align-items: center;
Expand Down
86 changes: 75 additions & 11 deletions src/material/datepicker/calendar-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
*/
@Input() cellAspectRatio: number = 1;

/** Start of the comparison range. */
@Input() comparisonStart: number | null;

/** End of the comparison range. */
@Input() comparisonEnd: number | null;

/** Emits when a new value is selected. */
@Output() readonly selectedValueChange: EventEmitter<number> = new EventEmitter<number>();

Expand All @@ -105,7 +111,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
* Value that the user is either currently hovering over or is focusing
* using the keyboard. Only applies when selecting the end of a date range.
*/
_hoveredValue: number;
_hoveredValue = -1;

constructor(
private _elementRef: ElementRef<HTMLElement>,
Expand Down Expand Up @@ -189,17 +195,73 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
}

/** Gets whether the calendar is currently selecting a range. */
_isRange(): boolean {
_isSelectingRange(): boolean {
return this.startValue !== this.endValue;
}

/** Gets whether a value is the start of the main range. */
_isRangeStart(value: number) {
return value === this.startValue;
}

/** Gets whether a value is the end of the main range. */
_isRangeEnd(value: number) {
return value === this.endValue || (value === this._hoveredValue && value >= this.startValue);
}

/** Gets whether a value is within the currently-selected range. */
_isInRange(value: number): boolean {
if (!this._isRange() || value < this.startValue) {
return this._isSelectingRange() && value >= this.startValue &&
(value <= this.endValue || value <= this._hoveredValue);
}

/** Gets whether a value is the start of the comparison range. */
_isComparisonStart(value: number) {
return value === this.comparisonStart;
}

/** Whether the cell is a start bridge cell between the main and comparison ranges. */
_isComparisonBridgeStart(value: number, rowIndex: number, colIndex: number) {
if (!this._isComparisonStart(value) || this._isRangeStart(value) || !this._isInRange(value)) {
return false;
}

let previousCell: MatCalendarCell | undefined = this.rows[rowIndex][colIndex - 1];

if (!previousCell) {
const previousRow = this.rows[rowIndex - 1];
previousCell = previousRow && previousRow[previousRow.length - 1];
}

return previousCell && !this._isRangeEnd(previousCell.compareValue);
}

/** Whether the cell is an end bridge cell between the main and comparison ranges. */
_isComparisonBridgeEnd(value: number, rowIndex: number, colIndex: number) {
if (!this._isComparisonEnd(value) || this._isRangeEnd(value) || !this._isInRange(value)) {
return false;
}

return value <= this.endValue || value <= this._hoveredValue;
let nextCell: MatCalendarCell | undefined = this.rows[rowIndex][colIndex + 1];

if (!nextCell) {
const nextRow = this.rows[rowIndex + 1];
nextCell = nextRow && nextRow[0];
}

return nextCell && !this._isRangeStart(nextCell.compareValue);
}

/** Gets whether a value is the end of the comparison range. */
_isComparisonEnd(value: number) {
return value === this.comparisonEnd;
}

/** Gets whether a value is within the current comparison range. */
_isInComparisonRange(value: number) {
return this.comparisonStart && this.comparisonEnd &&
value >= this.comparisonStart &&
value <= this.comparisonEnd;
}

/**
Expand All @@ -209,18 +271,20 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
private _enterHandler = (event: Event) => {
// We only need to hit the zone when we're selecting a range, we
// have a start value without an end value and we've hovered over a date cell.
if (!event.target || !this.startValue || this.endValue || !this._isRange()) {
if (!event.target || !this.startValue || this.endValue || !this._isSelectingRange()) {
return;
}

const cell = this._getCellFromElement(event.target as HTMLElement);

if (cell) {
this._ngZone.run(() => {
this._hoveredValue =
cell.enabled && cell.compareValue !== this.startValue ? cell.compareValue : -1;
this._changeDetectorRef.markForCheck();
});
const value = cell.compareValue;
const hoveredValue = cell.enabled ? value : -1;

if (hoveredValue !== this._hoveredValue) {
this._hoveredValue = hoveredValue;
this._ngZone.run(() => this._changeDetectorRef.markForCheck());
}
}
}

Expand All @@ -230,7 +294,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
*/
private _leaveHandler = (event: Event) => {
// We only need to hit the zone when we're selecting a range.
if (this._hoveredValue !== -1 && this._isRange()) {
if (this._hoveredValue !== -1 && this._isSelectingRange()) {
// Only reset the hovered value when leaving cells. This looks better, because
// we have a gap between the cells and the rows and we don't want to remove the
// range just for it to show up again when the user moves a few pixels to the side.
Expand Down
Loading