Skip to content

Commit 43e938a

Browse files
committed
refactor(datepicker): align preview range appearance with Material Design spec (#18936)
Our current approach to styling the preview range in the same way as the selected range is discouraged by the Material Design spec and may make it hard in the future if we made it possible to have both a preview and selected range at the same time. These changes align with the spec by using a dashed outline instead.
1 parent d0941d7 commit 43e938a

File tree

8 files changed

+168
-102
lines changed

8 files changed

+168
-102
lines changed

src/material/datepicker/_datepicker-theme.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ $mat-calendar-weekday-table-font-size: 11px !default;
8484
}
8585
}
8686

87+
.mat-calendar-body-in-preview {
88+
$divider-color: mat-color($foreground, divider);
89+
90+
@if type-of($divider-color) == color {
91+
// The divider color is set under the assumption that it'll be used
92+
// for a solid border, but because we're using a dashed border for the
93+
// preview range, we need to bump its opacity to ensure that it's visible.
94+
color: rgba($divider-color, min(opacity($divider-color) * 2, 1));
95+
}
96+
@else {
97+
color: $divider-color;
98+
}
99+
}
100+
87101
.mat-calendar-body-today:not(.mat-calendar-body-selected) {
88102
// Note: though it's not text, the border is a hint about the fact that this is today's date,
89103
// so we use the hint color.

src/material/datepicker/calendar-body.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
[class.mat-calendar-body-comparison-start]="_isComparisonStart(item.compareValue)"
4545
[class.mat-calendar-body-comparison-end]="_isComparisonEnd(item.compareValue)"
4646
[class.mat-calendar-body-in-comparison-range]="_isInComparisonRange(item.compareValue)"
47+
[class.mat-calendar-body-preview-start]="_isPreviewStart(item.compareValue)"
48+
[class.mat-calendar-body-preview-end]="_isPreviewEnd(item.compareValue)"
49+
[class.mat-calendar-body-in-preview]="_isInPreview(item.compareValue)"
4750
[attr.aria-label]="item.ariaLabel"
4851
[attr.aria-disabled]="!item.enabled || null"
4952
[attr.aria-selected]="_isSelected(item)"
@@ -57,5 +60,6 @@
5760
[class.mat-calendar-body-today]="todayValue === item.compareValue">
5861
{{item.displayValue}}
5962
</div>
63+
<div class="mat-calendar-body-cell-preview"></div>
6064
</td>
6165
</tr>

src/material/datepicker/calendar-body.scss

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ $mat-calendar-body-cell-min-size: 32px !default;
1010
$mat-calendar-body-cell-content-margin: 5% !default;
1111
$mat-calendar-body-cell-content-border-width: 1px !default;
1212
$mat-calendar-body-cell-radius: 999px !default;
13+
$mat-calendar-body-preview-cell-border: dashed 1px;
1314

1415
$mat-calendar-body-min-size: 7 * $mat-calendar-body-cell-min-size !default;
1516
$mat-calendar-body-cell-content-size: 100% - $mat-calendar-body-cell-content-margin * 2 !default;
@@ -38,30 +39,36 @@ $mat-calendar-range-end-body-cell-size:
3839
cursor: pointer;
3940
}
4041

41-
.mat-calendar-body-cell {
42-
// We use ::before to apply a background to the body cell, because we need to apply a border
43-
// radius to the start/end which means that part of the element will be cut off, making
44-
// hovering through all the cells look glitchy. We can't do it on the cell itself, because
45-
// it's the one that has the event listener and it can't be on the cell content, because
46-
// it always has a border radius.
47-
&::before, &::after {
48-
content: '';
49-
position: absolute;
50-
top: $mat-calendar-body-cell-content-margin;
51-
left: 0;
52-
z-index: 0;
42+
// We use ::before to apply a background to the body cell, because we need to apply a border
43+
// radius to the start/end which means that part of the element will be cut off, making hovering
44+
// through all the cells look glitchy. We can't do it on the cell itself, because it's the one
45+
// that has the event listener and it can't be on the cell content, because it always has a
46+
// border radius. Note that this and the selectors below can be much simpler if we were to use
47+
// two separate elements for the main and comparison ranges, like we're doing for the preview
48+
// range. We don't follow the simpler approach, because the range colors usually have some
49+
// kind of opacity, which means that they'll start mixing when they're layered on top of each
50+
// other, making the calendar look messy.
51+
.mat-calendar-body-cell::before,
52+
.mat-calendar-body-cell::after,
53+
.mat-calendar-body-cell-preview {
54+
content: '';
55+
position: absolute;
56+
top: $mat-calendar-body-cell-content-margin;
57+
left: 0;
58+
z-index: 0;
59+
box-sizing: border-box;
5360

54-
// We want the range background to be slightly shorter than the cell so
55-
// that there's a gap when the range goes across multiple rows.
56-
height: $mat-calendar-body-cell-content-size;
57-
width: 100%;
58-
}
61+
// We want the range background to be slightly shorter than the cell so
62+
// that there's a gap when the range goes across multiple rows.
63+
height: $mat-calendar-body-cell-content-size;
64+
width: 100%;
5965
}
6066

6167
.mat-calendar-body-range-start:not(.mat-calendar-body-in-comparison-range)::before,
6268
.mat-calendar-body-range-start::after,
6369
.mat-calendar-body-comparison-start:not(.mat-calendar-body-comparison-bridge-start)::before,
64-
.mat-calendar-body-comparison-start::after {
70+
.mat-calendar-body-comparison-start::after,
71+
.mat-calendar-body-preview-start .mat-calendar-body-cell-preview {
6572
// Since the range background isn't a perfect circle, we need to size
6673
// and offset the start so that it aligns with the main circle.
6774
left: $mat-calendar-body-cell-content-margin;
@@ -88,7 +95,8 @@ $mat-calendar-range-end-body-cell-size:
8895
.mat-calendar-body-range-end:not(.mat-calendar-body-in-comparison-range)::before,
8996
.mat-calendar-body-range-end::after,
9097
.mat-calendar-body-comparison-end:not(.mat-calendar-body-comparison-bridge-end)::before,
91-
.mat-calendar-body-comparison-end::after {
98+
.mat-calendar-body-comparison-end::after,
99+
.mat-calendar-body-preview-end .mat-calendar-body-cell-preview {
92100
@include _mat-calendar-body-range-right-radius;
93101

94102
[dir='rtl'] & {
@@ -117,6 +125,29 @@ $mat-calendar-range-end-body-cell-size:
117125
}
118126
}
119127

128+
.mat-calendar-body-in-preview .mat-calendar-body-cell-preview {
129+
border-top: $mat-calendar-body-preview-cell-border;
130+
border-bottom: $mat-calendar-body-preview-cell-border;
131+
}
132+
133+
.mat-calendar-body-preview-start .mat-calendar-body-cell-preview {
134+
border-left: $mat-calendar-body-preview-cell-border;
135+
136+
[dir='rtl'] & {
137+
border-left: 0;
138+
border-right: $mat-calendar-body-preview-cell-border;
139+
}
140+
}
141+
142+
.mat-calendar-body-preview-end .mat-calendar-body-cell-preview {
143+
border-right: $mat-calendar-body-preview-cell-border;
144+
145+
[dir='rtl'] & {
146+
border-right: 0;
147+
border-left: $mat-calendar-body-preview-cell-border;
148+
}
149+
}
150+
120151
.mat-calendar-body-disabled {
121152
cursor: default;
122153
}

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

Lines changed: 59 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ describe('MatCalendarBody', () => {
122122
const comparisonEndClass = 'mat-calendar-body-comparison-end';
123123
const bridgeStart = 'mat-calendar-body-comparison-bridge-start';
124124
const bridgeEnd = 'mat-calendar-body-comparison-bridge-end';
125+
const previewStartClass = 'mat-calendar-body-preview-start';
126+
const inPreviewClass = 'mat-calendar-body-in-preview';
127+
const previewEndClass = 'mat-calendar-body-preview-end';
125128
let fixture: ComponentFixture<RangeCalendarBody>;
126129
let testComponent: RangeCalendarBody;
127130
let cells: HTMLElement[];
@@ -405,29 +408,29 @@ describe('MatCalendarBody', () => {
405408
dispatchMouseEvent(cells[5], 'mouseenter');
406409
fixture.detectChanges();
407410

408-
expect(cells[2].classList).toContain(startClass);
409-
expect(cells[3].classList).toContain(inRangeClass);
410-
expect(cells[4].classList).toContain(inRangeClass);
411-
expect(cells[5].classList).toContain(endClass);
411+
expect(cells[2].classList).toContain(previewStartClass);
412+
expect(cells[3].classList).toContain(inPreviewClass);
413+
expect(cells[4].classList).toContain(inPreviewClass);
414+
expect(cells[5].classList).toContain(previewEndClass);
412415

413416
// Go a few cells ahead.
414417
dispatchMouseEvent(cells[7], 'mouseenter');
415418
fixture.detectChanges();
416419

417-
expect(cells[5].classList).not.toContain(endClass);
418-
expect(cells[5].classList).toContain(inRangeClass);
419-
expect(cells[6].classList).toContain(inRangeClass);
420-
expect(cells[7].classList).toContain(endClass);
420+
expect(cells[5].classList).not.toContain(previewEndClass);
421+
expect(cells[5].classList).toContain(inPreviewClass);
422+
expect(cells[6].classList).toContain(inPreviewClass);
423+
expect(cells[7].classList).toContain(previewEndClass);
421424

422425
// Go back a few cells.
423426
dispatchMouseEvent(cells[4], 'mouseenter');
424427
fixture.detectChanges();
425428

426-
expect(cells[5].classList).not.toContain(inRangeClass);
427-
expect(cells[6].classList).not.toContain(inRangeClass);
428-
expect(cells[7].classList).not.toContain(endClass);
429-
expect(cells[3].classList).toContain(inRangeClass);
430-
expect(cells[4].classList).toContain(endClass);
429+
expect(cells[5].classList).not.toContain(inPreviewClass);
430+
expect(cells[6].classList).not.toContain(inPreviewClass);
431+
expect(cells[7].classList).not.toContain(previewEndClass);
432+
expect(cells[3].classList).toContain(inPreviewClass);
433+
expect(cells[4].classList).toContain(previewEndClass);
431434
});
432435

433436
it('should preview the selected range after the user selects a start and moves focus away',
@@ -438,29 +441,29 @@ describe('MatCalendarBody', () => {
438441
dispatchFakeEvent(cells[5], 'focus');
439442
fixture.detectChanges();
440443

441-
expect(cells[2].classList).toContain(startClass);
442-
expect(cells[3].classList).toContain(inRangeClass);
443-
expect(cells[4].classList).toContain(inRangeClass);
444-
expect(cells[5].classList).toContain(endClass);
444+
expect(cells[2].classList).toContain(previewStartClass);
445+
expect(cells[3].classList).toContain(inPreviewClass);
446+
expect(cells[4].classList).toContain(inPreviewClass);
447+
expect(cells[5].classList).toContain(previewEndClass);
445448

446449
// Go a few cells ahead.
447450
dispatchFakeEvent(cells[7], 'focus');
448451
fixture.detectChanges();
449452

450-
expect(cells[5].classList).not.toContain(endClass);
451-
expect(cells[5].classList).toContain(inRangeClass);
452-
expect(cells[6].classList).toContain(inRangeClass);
453-
expect(cells[7].classList).toContain(endClass);
453+
expect(cells[5].classList).not.toContain(previewEndClass);
454+
expect(cells[5].classList).toContain(inPreviewClass);
455+
expect(cells[6].classList).toContain(inPreviewClass);
456+
expect(cells[7].classList).toContain(previewEndClass);
454457

455458
// Go back a few cells.
456459
dispatchFakeEvent(cells[4], 'focus');
457460
fixture.detectChanges();
458461

459-
expect(cells[5].classList).not.toContain(inRangeClass);
460-
expect(cells[6].classList).not.toContain(inRangeClass);
461-
expect(cells[7].classList).not.toContain(endClass);
462-
expect(cells[3].classList).toContain(inRangeClass);
463-
expect(cells[4].classList).toContain(endClass);
462+
expect(cells[5].classList).not.toContain(inPreviewClass);
463+
expect(cells[6].classList).not.toContain(inPreviewClass);
464+
expect(cells[7].classList).not.toContain(previewEndClass);
465+
expect(cells[3].classList).toContain(inPreviewClass);
466+
expect(cells[4].classList).toContain(previewEndClass);
464467
});
465468

466469
it('should not be able to extend the range before the start', () => {
@@ -471,7 +474,8 @@ describe('MatCalendarBody', () => {
471474
fixture.detectChanges();
472475

473476
expect(cells[5].classList).toContain(startClass);
474-
expect(cells.some(cell => cell.classList.contains(inRangeClass))).toBe(false);
477+
expect(cells[5].classList).not.toContain(previewStartClass);
478+
expect(cells.some(cell => cell.classList.contains(inPreviewClass))).toBe(false);
475479
});
476480

477481
it('should be able to show a range, starting before the beginning of the calendar, ' +
@@ -482,10 +486,10 @@ describe('MatCalendarBody', () => {
482486
dispatchMouseEvent(cells[2], 'mouseenter');
483487
fixture.detectChanges();
484488

485-
expect(cells.some(cell => cell.classList.contains(startClass))).toBe(false);
486-
expect(cells[0].classList).toContain(inRangeClass);
487-
expect(cells[1].classList).toContain(inRangeClass);
488-
expect(cells[2].classList).toContain(endClass);
489+
expect(cells.some(cell => cell.classList.contains(previewStartClass))).toBe(false);
490+
expect(cells[0].classList).toContain(inPreviewClass);
491+
expect(cells[1].classList).toContain(inPreviewClass);
492+
expect(cells[2].classList).toContain(previewEndClass);
489493
});
490494

491495
it('should be able to show a range, starting before the beginning of the calendar, ' +
@@ -496,10 +500,10 @@ describe('MatCalendarBody', () => {
496500
dispatchMouseEvent(cells[2], 'focus');
497501
fixture.detectChanges();
498502

499-
expect(cells.some(cell => cell.classList.contains(startClass))).toBe(false);
500-
expect(cells[0].classList).toContain(inRangeClass);
501-
expect(cells[1].classList).toContain(inRangeClass);
502-
expect(cells[2].classList).toContain(endClass);
503+
expect(cells.some(cell => cell.classList.contains(previewStartClass))).toBe(false);
504+
expect(cells[0].classList).toContain(inPreviewClass);
505+
expect(cells[1].classList).toContain(inPreviewClass);
506+
expect(cells[2].classList).toContain(previewEndClass);
503507
});
504508

505509
it('should remove the preview if the user moves their pointer away', () => {
@@ -509,26 +513,26 @@ describe('MatCalendarBody', () => {
509513
dispatchMouseEvent(cells[4], 'mouseenter');
510514
fixture.detectChanges();
511515

512-
expect(cells[2].classList).toContain(startClass);
513-
expect(cells[3].classList).toContain(inRangeClass);
514-
expect(cells[4].classList).toContain(endClass);
516+
expect(cells[2].classList).toContain(previewStartClass);
517+
expect(cells[3].classList).toContain(inPreviewClass);
518+
expect(cells[4].classList).toContain(previewEndClass);
515519

516520
// Move the pointer away.
517521
dispatchMouseEvent(cells[4], 'mouseleave');
518522
fixture.detectChanges();
519523

520-
expect(cells[2].classList).toContain(startClass);
521-
expect(cells[3].classList).not.toContain(inRangeClass);
522-
expect(cells[4].classList).not.toContain(endClass);
524+
expect(cells[2].classList).not.toContain(previewStartClass);
525+
expect(cells[3].classList).not.toContain(inPreviewClass);
526+
expect(cells[4].classList).not.toContain(previewEndClass);
523527

524528
// Move the pointer back in to a different cell.
525529
dispatchMouseEvent(cells[5], 'mouseenter');
526530
fixture.detectChanges();
527531

528-
expect(cells[2].classList).toContain(startClass);
529-
expect(cells[3].classList).toContain(inRangeClass);
530-
expect(cells[4].classList).toContain(inRangeClass);
531-
expect(cells[5].classList).toContain(endClass);
532+
expect(cells[2].classList).toContain(previewStartClass);
533+
expect(cells[3].classList).toContain(inPreviewClass);
534+
expect(cells[4].classList).toContain(inPreviewClass);
535+
expect(cells[5].classList).toContain(previewEndClass);
532536
});
533537

534538
it('should remove the preview if the user moves their focus away', () => {
@@ -538,26 +542,26 @@ describe('MatCalendarBody', () => {
538542
dispatchFakeEvent(cells[4], 'focus');
539543
fixture.detectChanges();
540544

541-
expect(cells[2].classList).toContain(startClass);
542-
expect(cells[3].classList).toContain(inRangeClass);
543-
expect(cells[4].classList).toContain(endClass);
545+
expect(cells[2].classList).toContain(previewStartClass);
546+
expect(cells[3].classList).toContain(inPreviewClass);
547+
expect(cells[4].classList).toContain(previewEndClass);
544548

545549
// Move the pointer away.
546550
dispatchFakeEvent(cells[4], 'blur');
547551
fixture.detectChanges();
548552

549-
expect(cells[2].classList).toContain(startClass);
550-
expect(cells[3].classList).not.toContain(inRangeClass);
551-
expect(cells[4].classList).not.toContain(endClass);
553+
expect(cells[2].classList).not.toContain(previewStartClass);
554+
expect(cells[3].classList).not.toContain(inPreviewClass);
555+
expect(cells[4].classList).not.toContain(previewEndClass);
552556

553557
// Move the pointer back in to a different cell.
554558
dispatchFakeEvent(cells[5], 'focus');
555559
fixture.detectChanges();
556560

557-
expect(cells[2].classList).toContain(startClass);
558-
expect(cells[3].classList).toContain(inRangeClass);
559-
expect(cells[4].classList).toContain(inRangeClass);
560-
expect(cells[5].classList).toContain(endClass);
561+
expect(cells[2].classList).toContain(previewStartClass);
562+
expect(cells[3].classList).toContain(inPreviewClass);
563+
expect(cells[4].classList).toContain(inPreviewClass);
564+
expect(cells[5].classList).toContain(previewEndClass);
561565
});
562566

563567
});

0 commit comments

Comments
 (0)