Skip to content

Put buttons in calendar cells #24171

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
Jan 25, 2022
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
81 changes: 46 additions & 35 deletions src/material/datepicker/calendar-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,51 @@
[style.paddingBottom]="_cellPadding">
{{_firstRowOffset >= labelMinRequiredCells ? label : ''}}
</td>
<td *ngFor="let item of row; let colIndex = index"
role="gridcell"
class="mat-calendar-body-cell"
[ngClass]="item.cssClasses"
[tabindex]="_isActiveCell(rowIndex, colIndex) ? 0 : -1"
[attr.data-mat-row]="rowIndex"
[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-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)"
[class.mat-calendar-body-preview-start]="_isPreviewStart(item.compareValue)"
[class.mat-calendar-body-preview-end]="_isPreviewEnd(item.compareValue)"
[class.mat-calendar-body-in-preview]="_isInPreview(item.compareValue)"
[attr.aria-label]="item.ariaLabel"
[attr.aria-disabled]="!item.enabled || null"
[attr.aria-selected]="_isSelected(item.compareValue)"
[attr.aria-current]="todayValue === item.compareValue ? 'date' : null"
(click)="_cellClicked(item, $event)"
[style.width]="_cellWidth"
[style.paddingTop]="_cellPadding"
[style.paddingBottom]="_cellPadding">
<div class="mat-calendar-body-cell-content mat-focus-indicator"
[class.mat-calendar-body-selected]="_isSelected(item.compareValue)"
[class.mat-calendar-body-comparison-identical]="_isComparisonIdentical(item.compareValue)"
[class.mat-calendar-body-today]="todayValue === item.compareValue">
{{item.displayValue}}
</div>
<div class="mat-calendar-body-cell-preview" aria-hidden="true"></div>
<!--
Each gridcell in the calendar contains a button, which signals to assistive technology that the
cell is interactable, as well as the selection state via `aria-pressed`. See #23476 for
background.
-->
<td
*ngFor="let item of row; let colIndex = index"
role="gridcell"
class="mat-calendar-body-cell-container"
[style.width]="_cellWidth"
[style.paddingTop]="_cellPadding"
[style.paddingBottom]="_cellPadding"
[attr.data-mat-row]="rowIndex"
[attr.data-mat-col]="colIndex"
Comment on lines +41 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these two attrs for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are used only in our js.

_getCellFromElement uses them to map a dom element back to the MatCalendarCell it corresponds to.

https://github.com/angular/components/blob/master/src/material/datepicker/calendar-body.ts#L357

>
<button
type="button"
class="mat-calendar-body-cell"
[ngClass]="item.cssClasses"
[tabindex]="_isActiveCell(rowIndex, colIndex) ? 0 : -1"
[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-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)"
[class.mat-calendar-body-preview-start]="_isPreviewStart(item.compareValue)"
[class.mat-calendar-body-preview-end]="_isPreviewEnd(item.compareValue)"
[class.mat-calendar-body-in-preview]="_isInPreview(item.compareValue)"
[attr.aria-label]="item.ariaLabel"
[attr.aria-disabled]="!item.enabled || null"
[attr.aria-pressed]="_isSelected(item.compareValue)"
[attr.aria-current]="todayValue === item.compareValue ? 'date' : null"
(click)="_cellClicked(item, $event)">
<div class="mat-calendar-body-cell-content mat-focus-indicator"
[class.mat-calendar-body-selected]="_isSelected(item.compareValue)"
[class.mat-calendar-body-comparison-identical]="_isComparisonIdentical(item.compareValue)"
[class.mat-calendar-body-today]="todayValue === item.compareValue">
{{item.displayValue}}
</div>
<div class="mat-calendar-body-cell-preview" aria-hidden="true"></div>
</button>
</td>
</tr>
16 changes: 14 additions & 2 deletions src/material/datepicker/calendar-body.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use 'sass:math';
@use '../core/style/button-common';
@use '../../cdk/a11y';

$calendar-body-label-padding-start: 5% !default;
Expand Down Expand Up @@ -31,13 +32,24 @@ $calendar-range-end-body-cell-size:
padding-right: $calendar-body-label-side-padding;
}

.mat-calendar-body-cell {
.mat-calendar-body-cell-container {
position: relative;
height: 0;
line-height: 0;
}

.mat-calendar-body-cell {
@include button-common.reset();
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
Comment on lines +43 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this really need to be position: absolute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that was the only way I could find to get that layout to be correct after spending an hour wrangling the CSS. It seemed liked the easiest way to me. We're already using absolute position to layout the other content inside the cell like the.mat-calendar-body-cell-content and the preview, so it seemed like a reasonable approach to me. I'm open to suggestions :)

Here's what is looks like without the position: absolute

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crisbeto the container being display: table-cell does seem to make this annoying (otherwise flexbox would solve it). I'm rusty here- is this really the best way to make this element 100% width/height?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is our only option. The problem is that we set padding-top, padding-bottom and width on the table cell which distorts the content.

background: none;
text-align: center;
outline: none;
cursor: pointer;
font-family: inherit;
margin: 0;
}

// We use ::before to apply a background to the body cell, because we need to apply a border
Expand Down
16 changes: 7 additions & 9 deletions src/material/datepicker/calendar-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,13 @@ describe('MatCalendarBody', () => {
expect(selectedCell.innerHTML.trim()).toBe('4');
});

it('should set aria-selected correctly', () => {
const selectedCells = cellEls.filter(c => c.getAttribute('aria-selected') === 'true');
const deselectedCells = cellEls.filter(c => c.getAttribute('aria-selected') === 'false');

expect(selectedCells.length)
.withContext('Expected one cell to be marked as selected.')
.toBe(1);
expect(deselectedCells.length)
.withContext('Expected remaining cells to be marked as deselected.')
it('should set aria-pressed correctly', () => {
const pressedCells = cellEls.filter(c => c.getAttribute('aria-pressed') === 'true');
const depressedCells = cellEls.filter(c => c.getAttribute('aria-pressed') === 'false');

expect(pressedCells.length).withContext('Expected one cell to be marked as pressed.').toBe(1);
expect(depressedCells.length)
.withContext('Expected remaining cells to be marked as not pressed.')
.toBe(cellEls.length - 1);
});

Expand Down
2 changes: 1 addition & 1 deletion src/material/datepicker/calendar-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
// Only reset the preview end 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.
if (event.target && isTableCell(event.target as HTMLElement)) {
if (event.target && this._getCellFromElement(event.target as HTMLElement)) {
this._ngZone.run(() => this.previewChange.emit({value: null, event}));
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/material/datepicker/testing/calendar-cell-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class MatCalendarCellHarness extends ComponentHarness {
/** Whether the cell is selected. */
async isSelected(): Promise<boolean> {
const host = await this.host();
return (await host.getAttribute('aria-selected')) === 'true';
return (await host.getAttribute('aria-pressed')) === 'true';
}

/** Whether the cell is disabled. */
Expand Down