Skip to content

Commit afa63f2

Browse files
committed
fix(datepicker): not showing single-day comparison ranges
The logic that determines whether we show something as being a range doesn't allow for the start and end to be on the same date, because it increases the CSS complexity a lot. As a result, we aren't able to render ranges that start and end on the same day. This isn't a problem for the main range, because we style its start and end as selected which looks exactly the same as the start/end of a range, however it breaks down for the comparison range since it doesn't have the same selected styling for its start/end cells. These changes fix the issue by introducing a special class for the case where the comparison range is for a single day. While it's not ideal, it's preferrable to complicating the CSS for the range styling. Fixes #20100.
1 parent 89b5fa8 commit afa63f2

File tree

5 files changed

+83
-26
lines changed

5 files changed

+83
-26
lines changed

src/material/datepicker/_datepicker-theme.scss

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,18 @@ $mat-calendar-weekday-table-font-size: 11px !default;
4040
}
4141
}
4242

43+
// Utility mixin to target cells that aren't selected. Used to make selector easier to follow.
44+
@mixin _mat-datepicker-unselected-cell {
45+
&:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical) {
46+
@content;
47+
}
48+
}
49+
4350
@mixin mat-datepicker-color($config-or-theme) {
4451
$config: mat-get-color-config($config-or-theme);
4552
$foreground: map-get($config, foreground);
4653
$background: map-get($config, background);
54+
$disabled-color: mat-color($foreground, disabled-text);
4755

4856
.mat-calendar-arrow {
4957
border-top-color: mat-color($foreground, icon);
@@ -75,16 +83,23 @@ $mat-calendar-weekday-table-font-size: 11px !default;
7583
border-color: transparent;
7684
}
7785

78-
.mat-calendar-body-disabled > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected),
86+
.mat-calendar-body-disabled > .mat-calendar-body-cell-content {
87+
@include _mat-datepicker-unselected-cell {
88+
color: $disabled-color;
89+
}
90+
}
91+
7992
.mat-form-field-disabled .mat-date-range-input-separator {
80-
color: mat-color($foreground, disabled-text);
93+
color: $disabled-color;
8194
}
8295

8396
.mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover,
8497
.cdk-keyboard-focused .mat-calendar-body-active,
8598
.cdk-program-focused .mat-calendar-body-active {
86-
& > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected) {
87-
background-color: mat-color($background, hover);
99+
& > .mat-calendar-body-cell-content {
100+
@include _mat-datepicker-unselected-cell {
101+
background-color: mat-color($background, hover);
102+
}
88103
}
89104
}
90105

@@ -102,24 +117,28 @@ $mat-calendar-weekday-table-font-size: 11px !default;
102117
}
103118
}
104119

105-
.mat-calendar-body-today:not(.mat-calendar-body-selected) {
106-
// Note: though it's not text, the border is a hint about the fact that this is today's date,
107-
// so we use the hint color.
108-
border-color: mat-color($foreground, hint-text);
120+
.mat-calendar-body-today {
121+
@include _mat-datepicker-unselected-cell {
122+
// Note: though it's not text, the border is a hint about the fact that this is today's date,
123+
// so we use the hint color.
124+
border-color: mat-color($foreground, hint-text);
125+
}
109126
}
110127

111-
.mat-calendar-body-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected) {
112-
$color: mat-color($foreground, hint-text);
113-
114-
@if (type-of($color) == color) {
115-
border-color: fade-out($color, $mat-datepicker-today-fade-amount);
116-
}
117-
@else {
118-
// If the color didn't resolve to a color value, but something like a CSS variable, we can't
119-
// fade it out so we fall back to reducing the element opacity. Note that we don't use the
120-
// $mat-datepicker-today-fade-amount, because hint text usually has some opacity applied
121-
// to it already and we don't want them to stack on top of each other.
122-
opacity: 0.5;
128+
.mat-calendar-body-disabled > .mat-calendar-body-today {
129+
@include _mat-datepicker-unselected-cell {
130+
$color: mat-color($foreground, hint-text);
131+
132+
@if (type-of($color) == color) {
133+
border-color: fade-out($color, $mat-datepicker-today-fade-amount);
134+
}
135+
@else {
136+
// If the color didn't resolve to a color value, but something like a CSS variable, we can't
137+
// fade it out so we fall back to reducing the element opacity. Note that we don't use the
138+
// $mat-datepicker-today-fade-amount, because hint text usually has some opacity applied
139+
// to it already and we don't want them to stack on top of each other.
140+
opacity: 0.5;
141+
}
123142
}
124143
}
125144

@@ -196,6 +215,7 @@ $mat-calendar-weekday-table-font-size: 11px !default;
196215
background: $range-color;
197216
}
198217

218+
.mat-calendar-body-comparison-identical,
199219
.mat-calendar-body-in-comparison-range::before {
200220
background: $comparison-color;
201221
}
@@ -210,13 +230,14 @@ $mat-calendar-weekday-table-font-size: 11px !default;
210230
background: linear-gradient(to left, $range-color 50%, $comparison-color 50%);
211231
}
212232

233+
.mat-calendar-body-in-range > .mat-calendar-body-comparison-identical,
213234
.mat-calendar-body-in-comparison-range.mat-calendar-body-in-range::after {
214235
background: $overlap-color;
215236
}
216237

238+
.mat-calendar-body-comparison-identical.mat-calendar-body-selected,
217239
.mat-calendar-body-in-comparison-range > .mat-calendar-body-selected {
218240
background: $overlap-selected-color;
219-
220241
}
221242
}
222243

src/material/datepicker/calendar-body.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,14 @@
4949
[class.mat-calendar-body-in-preview]="_isInPreview(item.compareValue)"
5050
[attr.aria-label]="item.ariaLabel"
5151
[attr.aria-disabled]="!item.enabled || null"
52-
[attr.aria-selected]="_isSelected(item)"
52+
[attr.aria-selected]="_isSelected(item.compareValue)"
5353
(click)="_cellClicked(item, $event)"
5454
[style.width]="_cellWidth"
5555
[style.paddingTop]="_cellPadding"
5656
[style.paddingBottom]="_cellPadding">
5757
<div class="mat-calendar-body-cell-content mat-focus-indicator"
58-
[class.mat-calendar-body-selected]="_isSelected(item)"
58+
[class.mat-calendar-body-selected]="_isSelected(item.compareValue)"
59+
[class.mat-calendar-body-comparison-identical]="_isComparisonIdentical(item.compareValue)"
5960
[class.mat-calendar-body-today]="todayValue === item.compareValue">
6061
{{item.displayValue}}
6162
</div>

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,23 @@ describe('MatCalendarBody', () => {
566566
expect(cells[5].classList).toContain(previewEndClass);
567567
});
568568

569+
it('should mark a cell as being identical to the comparison range', () => {
570+
testComponent.comparisonStart = testComponent.comparisonEnd = 3;
571+
fixture.detectChanges();
572+
573+
const comparisonIdenticalCells: NodeListOf<HTMLElement> =
574+
fixture.nativeElement.querySelectorAll('.mat-calendar-body-comparison-identical');
575+
576+
expect(comparisonIdenticalCells.length).toBe(1);
577+
expect(cells[2].contains(comparisonIdenticalCells[0])).toBe(true);
578+
expect(cells.some(cell => {
579+
const classList = cell.classList;
580+
return classList.contains(startClass) || classList.contains(inRangeClass) ||
581+
classList.contains(endClass) || classList.contains(comparisonStartClass) ||
582+
classList.contains(inComparisonClass) || classList.contains(comparisonEndClass);
583+
})).toBe(false);
584+
});
585+
569586
});
570587

571588
});

src/material/datepicker/calendar-body.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
149149
}
150150

151151
/** Returns whether a cell should be marked as selected. */
152-
_isSelected(cell: MatCalendarCell) {
153-
return this.startValue === cell.compareValue || this.endValue === cell.compareValue;
152+
_isSelected(value: number) {
153+
return this.startValue === value || this.endValue === value;
154154
}
155155

156156
ngOnChanges(changes: SimpleChanges) {
@@ -270,6 +270,23 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
270270
return isInRange(value, this.comparisonStart, this.comparisonEnd, this.isRange);
271271
}
272272

273+
/**
274+
* Gets whether a value is the same as the start and end of the comparison range.
275+
* For context, the functions that we use to determine whether something is the start/end of
276+
* a range don't allow for the start and end to be on the same day, because we'd have to use
277+
* much more specific CSS selectors to style them correctly in all scenarios. This is fine for
278+
* the regular range, because when it happens, the selected styles take over and still show where
279+
* the range would've been, however we don't have these selected styles for a comparison range.
280+
* This function is used to apply a class that serves the same purpose as the one for selected
281+
* dates, but it only applies in the context of a comparison range.
282+
*/
283+
_isComparisonIdentical(value: number) {
284+
// Note that we don't need to null check the start/end
285+
// here, because the `value` will always be defined.
286+
return this.comparisonStart === this.comparisonEnd && value === this.comparisonStart &&
287+
value === this.comparisonEnd;
288+
}
289+
273290
/** Gets whether a value is the start of the preview range. */
274291
_isPreviewStart(value: number) {
275292
return isStart(value, this.previewStart, this.previewEnd);

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export declare class MatCalendarBody implements OnChanges, OnDestroy {
121121
_isComparisonBridgeEnd(value: number, rowIndex: number, colIndex: number): boolean;
122122
_isComparisonBridgeStart(value: number, rowIndex: number, colIndex: number): boolean;
123123
_isComparisonEnd(value: number): boolean;
124+
_isComparisonIdentical(value: number): boolean;
124125
_isComparisonStart(value: number): boolean;
125126
_isInComparisonRange(value: number): boolean;
126127
_isInPreview(value: number): boolean;
@@ -129,7 +130,7 @@ export declare class MatCalendarBody implements OnChanges, OnDestroy {
129130
_isPreviewStart(value: number): boolean;
130131
_isRangeEnd(value: number): boolean;
131132
_isRangeStart(value: number): boolean;
132-
_isSelected(cell: MatCalendarCell): boolean;
133+
_isSelected(value: number): boolean;
133134
ngOnChanges(changes: SimpleChanges): void;
134135
ngOnDestroy(): void;
135136
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatCalendarBody, "[mat-calendar-body]", ["matCalendarBody"], { "label": "label"; "rows": "rows"; "todayValue": "todayValue"; "startValue": "startValue"; "endValue": "endValue"; "labelMinRequiredCells": "labelMinRequiredCells"; "numCols": "numCols"; "activeCell": "activeCell"; "isRange": "isRange"; "cellAspectRatio": "cellAspectRatio"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; "previewStart": "previewStart"; "previewEnd": "previewEnd"; }, { "selectedValueChange": "selectedValueChange"; "previewChange": "previewChange"; }, never, never>;

0 commit comments

Comments
 (0)