Skip to content

Commit 90e57ee

Browse files
committed
docs: add aria grid basic examples: pill list, calendar, and table
1 parent c1057c6 commit 90e57ee

11 files changed

Lines changed: 733 additions & 12 deletions

File tree

‎adev/src/app/routing/sub-navigation-data.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -718,9 +718,9 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
718718
contentPath: 'guide/aria/overview',
719719
},
720720
{
721-
label: 'Autocomplete',
722-
path: 'guide/aria/autocomplete',
723-
contentPath: 'guide/aria/autocomplete',
721+
label: 'Accordion',
722+
path: 'guide/aria/accordion',
723+
contentPath: 'guide/aria/accordion',
724724
},
725725
{
726726
label: 'Autocomplete',
@@ -732,6 +732,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
732732
path: 'guide/aria/combobox',
733733
contentPath: 'guide/aria/combobox',
734734
},
735+
{
736+
label: 'Grid',
737+
path: 'guide/aria/grid',
738+
contentPath: 'guide/aria/grid',
739+
},
735740
{
736741
label: 'Listbox',
737742
path: 'guide/aria/listbox',
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
:host {
2+
display: flex;
3+
justify-content: center;
4+
font-family: var(--inter-font);
5+
}
6+
7+
.calendar {
8+
display: flex;
9+
flex-direction: column;
10+
background-color: var(--septenary-contrast);
11+
border-radius: 0.5rem;
12+
padding: 0.5rem;
13+
}
14+
15+
button {
16+
border: unset;
17+
padding: unset;
18+
color: unset;
19+
background: unset;
20+
outline: none;
21+
}
22+
23+
button:hover,
24+
button:focus {
25+
background-color: var(--senary-contrast);
26+
}
27+
28+
button:focus {
29+
outline-offset: -1px;
30+
outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
31+
}
32+
33+
.visually-hidden {
34+
clip: rect(1px, 1px, 1px, 1px);
35+
height: 1px;
36+
width: 1px;
37+
overflow: hidden;
38+
position: absolute;
39+
white-space: nowrap;
40+
}
41+
42+
.calendar-header {
43+
display: flex;
44+
justify-content: space-between;
45+
align-items: center;
46+
}
47+
48+
.month-control {
49+
border-radius: 1rem;
50+
width: 45px;
51+
height: 45px;
52+
cursor: pointer;
53+
}
54+
55+
[ngGrid] {
56+
border-spacing: 0;
57+
}
58+
59+
[ngGridCell] {
60+
width: 50px;
61+
height: 50px;
62+
text-align: center;
63+
vertical-align: middle;
64+
}
65+
66+
[ngGridCell][aria-selected='true'] > button[ngGridCellWidget] {
67+
background-color: var(--electric-violet);
68+
color: var(--octonary-contrast);
69+
}
70+
71+
[ngGridCell][aria-disabled='true'] {
72+
color: var(--senary-contrast);
73+
}
74+
75+
thead {
76+
background-image: var(--pink-to-purple-horizontal-gradient);
77+
background-clip: text;
78+
color: transparent;
79+
}
80+
81+
button[ngGridCellWidget] {
82+
width: 45px;
83+
height: 45px;
84+
border-radius: 50%;
85+
cursor: pointer;
86+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<link href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined" rel="stylesheet" />
2+
3+
<div class="calendar">
4+
<div class="calendar-header">
5+
<button class="month-control" aria-label="previous month" (click)="prevMonth()">
6+
<span aria-hidden="true" class="material-symbols-outlined">chevron_left</span>
7+
</button>
8+
<h3>{{monthYearLabel()}}</h3>
9+
<button class="month-control" aria-label="next month" (click)="nextMonth()">
10+
<span aria-hidden="true" class="material-symbols-outlined">chevron_right</span>
11+
</button>
12+
</div>
13+
14+
<table
15+
ngGrid
16+
colWrap="continuous"
17+
rowWrap="nowrap"
18+
[enableSelection]="true"
19+
[softDisabled]="false"
20+
selectionMode="explicit"
21+
(keydown)="onKeyDown($event)"
22+
>
23+
<thead>
24+
<tr>
25+
@for (day of weekdays(); track day.long) {
26+
<th scope="col">
27+
<span class="visually-hidden">{{day.long}}</span>
28+
<span aria-hidden="true">{{day.narrow}}</span>
29+
</th>
30+
}
31+
</tr>
32+
</thead>
33+
34+
@for (week of calendar(); track week) {
35+
<tr ngGridRow>
36+
@if ($first) {
37+
@for (day of daysFromPrevMonth(); track day) {
38+
<td ngGridCell disabled>{{day}}</td>
39+
}
40+
}
41+
42+
@for (day of week; track day) {
43+
<td ngGridCell [(selected)]="day.selected">
44+
<button ngGridCellWidget [attr.aria-label]="day.ariaLabel" [attr.data-day]="day.day">{{day.displayName}}</button>
45+
</td>
46+
}
47+
48+
@if ($last && week.length < 7) {
49+
@for (day of [].constructor(7 - week.length); track $index) {
50+
<td ngGridCell disabled>{{$index + 1}}</td>
51+
}
52+
}
53+
</tr>
54+
}
55+
</table>
56+
</div>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {
2+
Component,
3+
computed,
4+
inject,
5+
signal,
6+
Signal,
7+
untracked,
8+
viewChildren,
9+
WritableSignal,
10+
} from '@angular/core';
11+
import {
12+
DateAdapter,
13+
MAT_DATE_FORMATS,
14+
MatDateFormats,
15+
provideNativeDateAdapter,
16+
} from '@angular/material/core';
17+
import {Grid, GridRow, GridCell, GridCellWidget} from '@angular/aria/grid';
18+
19+
const DAYS_PER_WEEK = 7;
20+
21+
interface CalendarCell<D = any> {
22+
displayName: string;
23+
ariaLabel: string;
24+
date: D;
25+
selected: WritableSignal<boolean>;
26+
day: number;
27+
}
28+
29+
@Component({
30+
selector: 'app-root',
31+
templateUrl: 'app.html',
32+
styleUrl: 'app.css',
33+
providers: [provideNativeDateAdapter()],
34+
imports: [Grid, GridRow, GridCell, GridCellWidget],
35+
})
36+
export class App<D> {
37+
private readonly _dayButtons = viewChildren(GridCellWidget);
38+
private readonly _dateAdapter = inject<DateAdapter<D>>(DateAdapter, {optional: true})!;
39+
private readonly _dateFormats = inject<MatDateFormats>(MAT_DATE_FORMATS, {optional: true})!;
40+
private readonly _firstWeekOffset = computed(() => {
41+
const firstDayOfMonth = this._dateAdapter.createDate(
42+
this._dateAdapter.getYear(this.viewMonth()),
43+
this._dateAdapter.getMonth(this.viewMonth()),
44+
1,
45+
);
46+
47+
return (
48+
(DAYS_PER_WEEK +
49+
this._dateAdapter.getDayOfWeek(firstDayOfMonth) -
50+
this._dateAdapter.getFirstDayOfWeek()) %
51+
DAYS_PER_WEEK
52+
);
53+
});
54+
55+
protected readonly monthYearLabel = computed(() =>
56+
this._dateAdapter
57+
.format(this.viewMonth(), this._dateFormats.display.monthYearLabel)
58+
.toLocaleUpperCase(),
59+
);
60+
61+
protected readonly daysFromPrevMonth: Signal<number[]> = computed(() => {
62+
const prevMonthNumDays = this._dateAdapter.getNumDaysInMonth(
63+
this._dateAdapter.addCalendarMonths(this.viewMonth(), -1),
64+
);
65+
66+
const days: number[] = [];
67+
for (let i = this._firstWeekOffset() - 1; i >= 0; i--) {
68+
days.push(prevMonthNumDays - i);
69+
}
70+
return days;
71+
});
72+
73+
readonly weekdays: Signal<{long: string; narrow: string}[]> = computed(() => {
74+
const firstDayOfWeek = this._dateAdapter.getFirstDayOfWeek();
75+
const narrowWeekdays = this._dateAdapter.getDayOfWeekNames('narrow');
76+
const longWeekdays = this._dateAdapter.getDayOfWeekNames('long');
77+
78+
const weekdays = longWeekdays.map((long, i) => {
79+
return {long, narrow: narrowWeekdays[i]};
80+
});
81+
return weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));
82+
});
83+
84+
/** The current selected date. */
85+
readonly selectedDate: WritableSignal<D> = signal(this._dateAdapter.today());
86+
87+
/** The current display month. */
88+
readonly viewMonth: WritableSignal<D> = signal(this.selectedDate());
89+
90+
/** Calendar day cells. */
91+
readonly calendar = computed(() => {
92+
const month = this.viewMonth();
93+
94+
const daysInMonth = this._dateAdapter.getNumDaysInMonth(month);
95+
const dateNames = this._dateAdapter.getDateNames();
96+
97+
const calendar: CalendarCell[][] = [[]];
98+
for (let i = 0, cell = this._firstWeekOffset(); i < daysInMonth; i++, cell++) {
99+
if (cell == DAYS_PER_WEEK) {
100+
calendar.push([]);
101+
cell = 0;
102+
}
103+
const date = this._dateAdapter.createDate(
104+
this._dateAdapter.getYear(month),
105+
this._dateAdapter.getMonth(month),
106+
i + 1,
107+
);
108+
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
109+
110+
calendar[calendar.length - 1].push({
111+
displayName: dateNames[i],
112+
ariaLabel,
113+
date,
114+
selected: signal(
115+
this._dateAdapter.compareDate(
116+
date,
117+
untracked(() => this.selectedDate()),
118+
) === 0,
119+
),
120+
day: i + 1,
121+
});
122+
}
123+
124+
return calendar;
125+
});
126+
127+
nextMonth(): void {
128+
this.viewMonth.set(this._dateAdapter.addCalendarMonths(this.viewMonth(), 1));
129+
}
130+
131+
prevMonth(): void {
132+
this.viewMonth.set(this._dateAdapter.addCalendarMonths(this.viewMonth(), -1));
133+
}
134+
135+
scrollDown(): void {
136+
this.nextMonth();
137+
setTimeout(() => this._dayButtons()[0]?.element.focus());
138+
}
139+
140+
scrollUp(): void {
141+
this.prevMonth();
142+
setTimeout(() => this._dayButtons()[this._dayButtons().length - 1]?.element.focus());
143+
}
144+
145+
onKeyDown(event: KeyboardEvent): void {
146+
const day = Number((event.target as Element).getAttribute('data-day'));
147+
if (!day) return;
148+
149+
const viewMonthNumDays = this._dateAdapter.getNumDaysInMonth(this.viewMonth());
150+
if (day > 7 && day <= viewMonthNumDays - 7) return;
151+
152+
const arrowLeft = event.key === 'ArrowLeft';
153+
const arrowRight = event.key === 'ArrowRight';
154+
const arrowUp = event.key === 'ArrowUp';
155+
const arrowDown = event.key === 'ArrowDown';
156+
157+
if ((day === 1 && arrowLeft) || (day <= 7 && arrowUp)) {
158+
this.scrollUp();
159+
}
160+
161+
if ((day === viewMonthNumDays && arrowRight) || (day > viewMonthNumDays - 7 && arrowDown)) {
162+
this.scrollDown();
163+
}
164+
}
165+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
:host {
2+
display: flex;
3+
justify-content: center;
4+
font-family: var(--inter-font);
5+
}
6+
7+
[ngGrid] {
8+
display: flex;
9+
flex-wrap: wrap;
10+
justify-content: center;
11+
gap: 0.5rem;
12+
max-width: 400px;
13+
background-image: var(--pink-to-purple-horizontal-gradient);
14+
background-clip: text;
15+
color: transparent;
16+
}
17+
18+
[ngGridRow] {
19+
display: flex;
20+
gap: 0.5rem;
21+
border: 1px solid var(--senary-contrast);
22+
border-radius: 0.25rem;
23+
padding: 0.5rem 0.75rem;
24+
}
25+
26+
[ngGridRow]:focus-within,
27+
[ngGridRow]:hover {
28+
outline-offset: -1px;
29+
outline: 1px solid color-mix(in srgb, var(--vivid-pink) 80%, transparent);
30+
}
31+
32+
[ngGridRow]:has(button[ngGridCellWidget]:focus),
33+
[ngGridRow]:has(button[ngGridCellWidget]:hover) {
34+
outline: none;
35+
}
36+
37+
[ngGridCell] {
38+
display: flex;
39+
outline: none;
40+
}
41+
42+
button[ngGridCellWidget] {
43+
border: unset;
44+
padding: unset;
45+
color: unset;
46+
background: unset;
47+
font-size: 1.25rem;
48+
width: 1.25rem;
49+
height: 1.25rem;
50+
border-radius: 50%;
51+
cursor: pointer;
52+
}
53+
54+
button[ngGridCellWidget]:focus,
55+
button[ngGridCellWidget]:hover {
56+
outline: 1px solid color-mix(in srgb, var(--vivid-pink) 80%, transparent);
57+
}

0 commit comments

Comments
 (0)