From 8957ff73badbf7ad29b2fa175cb5a970f84252ec Mon Sep 17 00:00:00 2001 From: teodosiah Date: Tue, 3 Nov 2026 13:57:37 +0200 Subject: [PATCH 1/2] feat(calendar): add calendar templates sample Demonstrates the day, header, header title and subheader templates of igx-calendar using an events schedule. Co-Authored-By: Claude Opus 5.5 --- .../configs/CalendarConfigGenerator.ts | 6 + .../calendar-templates.component.html | 75 +++++++++++++ .../calendar-templates.component.scss | 104 ++++++++++++++++++ .../calendar-templates.component.ts | 67 +++++++++++ src/app/scheduling/scheduling-routes-data.ts | 1 + src/app/scheduling/scheduling.routes.ts | 6 + 6 files changed, 259 insertions(+) create mode 100644 src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html create mode 100644 src/app/scheduling/calendar/calendar-templates/calendar-templates.component.scss create mode 100644 src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts diff --git a/live-editing/configs/CalendarConfigGenerator.ts b/live-editing/configs/CalendarConfigGenerator.ts index 8bd3922d9..726795d00 100644 --- a/live-editing/configs/CalendarConfigGenerator.ts +++ b/live-editing/configs/CalendarConfigGenerator.ts @@ -73,6 +73,12 @@ export class CalendarConfigGenerator implements IConfigGenerator { shortenComponentPathBy: "/scheduling/calendar/" })); + configs.push(new Config({ + component: 'CalendarTemplatesComponent', + appConfig: BaseAppConfig, + shortenComponentPathBy: "/scheduling/calendar/" + })); + configs.push(new Config({ component: 'CalendarStylingSampleComponent', appConfig: BaseAppConfig, diff --git a/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html new file mode 100644 index 000000000..b98e4d88a --- /dev/null +++ b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html @@ -0,0 +1,75 @@ +
+
+ + + + @let count = getEvents(parts.date).length; + {{ count ? pluralize(count) : 'No events' }} + + + + + {{ parts.weekday.value }}, {{ parts.month.value }} {{ parts.day.value }} + + + + + @if (calendar.activeView === 'month') { + + {{ parts.month.value }} + + } + + {{ parts.year.value }} + + @if (calendar.activeView === 'month') { + @let monthCount = getMonthEventsCount(parts.date); + @if (monthCount) { + {{ pluralize(monthCount) }} + } + } + + + + + @let events = getEvents(date); + + {{ formattedDate }} + + @for (event of events; track $index) { + + } + + @if (events.length) { + + {{ pluralize(events.length) }} + } + + + +
+ +
+
{{ selectedDate | date: 'fullDate' }}
+ @for (event of getEvents(selectedDate); track $index) { +
+ + {{ event.title }} +
+ } @empty { +

No events scheduled.

+ } +
+
diff --git a/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.scss b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.scss new file mode 100644 index 000000000..0090d15a3 --- /dev/null +++ b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.scss @@ -0,0 +1,104 @@ +.sample-wrapper { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin: 8px; +} + +.calendar-wrapper { + max-width: 400px; + min-width: 200px; + flex: 1; +} + +.igx-calendar { + box-shadow: 0 1px 3px 0 rgba(0,0,0,.26), + 0 1px 1px 0 rgba(0,0,0,.12), + 0 2px 1px -1px rgba(0,0,0,.08); +} + +.month-badge { + align-self: center; + margin-inline-start: 8px; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + line-height: 1.5; + color: var(--ig-primary-500-contrast); + background: var(--ig-primary-500); +} + +.day { + display: flex; + flex-direction: column; + align-items: center; + line-height: 1; + + &__dots { + display: flex; + gap: 2px; + height: 4px; + margin-top: 2px; + } + + &__dot { + width: 4px; + height: 4px; + border-radius: 50%; + background: currentColor; + + &--meeting { + color: var(--ig-primary-500); + } + + &--deadline { + color: var(--ig-error-500); + } + + &--personal { + color: var(--ig-success-500); + } + } + + // Fall back to the cell foreground so the dots stay readable on the selected background. + &--selected &__dot { + color: inherit; + } +} + +.agenda { + min-width: 200px; + padding: 16px; + + &__title { + margin: 0 0 12px; + } + + &__item { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + + &__marker { + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; + } + + &__empty { + margin: 0; + color: var(--ig-gray-600); + } +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} diff --git a/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts new file mode 100644 index 000000000..9e75429e0 --- /dev/null +++ b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts @@ -0,0 +1,67 @@ +import { Component } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { + IGX_CALENDAR_DIRECTIVES, + IgxCalendarComponent +} from 'igniteui-angular/calendar'; + +interface CalendarEvent { + title: string; + type: 'meeting' | 'deadline' | 'personal'; +} + +const today = new Date(); + +// Events are placed relative to the current month so the sample always has data to show. +const eventsByDay: Record = { + 3: [{ title: 'Sprint planning', type: 'meeting' }], + 7: [{ title: 'Design review', type: 'meeting' }, { title: 'Dentist', type: 'personal' }], + 12: [{ title: 'Release candidate', type: 'deadline' }], + 15: [ + { title: 'Team sync', type: 'meeting' }, + { title: 'Budget report', type: 'deadline' }, + { title: 'Gym', type: 'personal' } + ], + 21: [{ title: 'Customer demo', type: 'meeting' }], + 26: [{ title: 'Code freeze', type: 'deadline' }, { title: 'Retrospective', type: 'meeting' }] +}; + +@Component({ + selector: 'app-calendar-templates', + styleUrls: ['./calendar-templates.component.scss'], + templateUrl: './calendar-templates.component.html', + imports: [ + IgxCalendarComponent, + IGX_CALENDAR_DIRECTIVES, + DatePipe + ] +}) +export class CalendarTemplatesComponent { + public selectedDate: Date = new Date(today.getFullYear(), today.getMonth(), 15); + + public getEvents(date: Date): CalendarEvent[] { + if (!this.isCurrentMonth(date)) { + return []; + } + return eventsByDay[date.getDate()] ?? []; + } + + public getMonthEventsCount(date: Date): number { + if (!this.isCurrentMonth(date)) { + return 0; + } + return Object.values(eventsByDay).reduce((count, events) => count + events.length, 0); + } + + public pluralize(count: number): string { + return `${count} ${count === 1 ? 'event' : 'events'}`; + } + + public onSelection(date: Date | Date[]) { + this.selectedDate = date as Date; + } + + private isCurrentMonth(date: Date): boolean { + return date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth(); + } +} diff --git a/src/app/scheduling/scheduling-routes-data.ts b/src/app/scheduling/scheduling-routes-data.ts index 882051a88..a5557f58d 100644 --- a/src/app/scheduling/scheduling-routes-data.ts +++ b/src/app/scheduling/scheduling-routes-data.ts @@ -9,6 +9,7 @@ export const schedulingRoutesData = { 'calendar-sample-8': { displayName: 'Calendar Range Mode', parentName: 'Calendar' }, 'calendar-sample-9': { displayName: 'Calendar Orientation', parentName: 'Calendar' }, 'calendar-rtl': { displayName: 'Calendar RTL', parentName: 'Calendar' }, + 'calendar-templates': { displayName: 'Calendar Templates', parentName: 'Calendar' }, 'calendar-days-view': { displayName: 'Calendar Days View', parentName: 'Calendar' }, 'calendar-months-view': { displayName: 'Calendar Months View', parentName: 'Calendar' }, 'calendar-years-view': { displayName: 'Calendar Years View', parentName: 'Calendar' }, diff --git a/src/app/scheduling/scheduling.routes.ts b/src/app/scheduling/scheduling.routes.ts index 593b2512b..661328259 100644 --- a/src/app/scheduling/scheduling.routes.ts +++ b/src/app/scheduling/scheduling.routes.ts @@ -11,6 +11,7 @@ import { CalendarSample7Component } from './calendar/calendar-sample-7/calendar- import { CalendarSample8Component } from './calendar/calendar-sample-8/calendar-sample-8.component'; import { CalendarSample9Component } from './calendar/calendar-sample-9/calendar-sample-9.component'; import { CalendarRtlComponent } from './calendar/calendar-rtl/calendar-rtl.component'; +import { CalendarTemplatesComponent } from './calendar/calendar-templates/calendar-templates.component'; import { CalendarStylingSampleComponent } from './calendar/calendar-styling-sample/calendar-styling-sample.component'; import { CalendarTailwindStylingSampleComponent } from './calendar/calendar-tailwind-styling-sample/calendar-tailwind-styling-sample.component'; import { CalendarYearsViewComponent } from './calendar/calendar-years-view/calendar-years-view.component'; @@ -102,6 +103,11 @@ export const SchedulingRoutes: Routes = [ data: schedulingRoutesData['calendar-rtl'], path: 'calendar-rtl' }, + { + component: CalendarTemplatesComponent, + data: schedulingRoutesData['calendar-templates'], + path: 'calendar-templates' + }, { component: CalendarStylingSampleComponent, data: schedulingRoutesData['calendar-styling-sample'], From 45d82d19b9f76bb6739685b56da36d412edecedf Mon Sep 17 00:00:00 2001 From: teodosiah Date: Thu, 5 Nov 2026 16:07:50 +0200 Subject: [PATCH 2/2] fix(calendar): handle subheader keyboard in templates sample Stop subheader button keydown from reaching the calendar's own keyboard navigation, support Enter and Space, and move focus to the calendar body after switching views so navigation continues there. Co-Authored-By: Claude Opus 5.5 --- .../calendar-templates.component.html | 8 +++---- .../calendar-templates.component.ts | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html index b98e4d88a..8b20f6664 100644 --- a/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html +++ b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.html @@ -20,8 +20,8 @@ tabindex="0" role="button" [attr.aria-label]="(parts.date | date: 'LLLL') + ', select month'" - (click)="calendar.activeView = 'year'" - (keydown.enter)="calendar.activeView = 'year'"> + (click)="switchView($event, calendar, 'year')" + (keydown)="onSubheaderKeydown($event, calendar, 'year')"> {{ parts.month.value }} } @@ -30,8 +30,8 @@ tabindex="0" role="button" [attr.aria-label]="(parts.date | date: 'yyyy') + ', select year'" - (click)="calendar.activeView = 'decade'" - (keydown.enter)="calendar.activeView = 'decade'"> + (click)="switchView($event, calendar, 'decade')" + (keydown)="onSubheaderKeydown($event, calendar, 'decade')"> {{ parts.year.value }} @if (calendar.activeView === 'month') { diff --git a/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts index 9e75429e0..beeb21c4c 100644 --- a/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts +++ b/src/app/scheduling/calendar/calendar-templates/calendar-templates.component.ts @@ -2,7 +2,8 @@ import { Component } from '@angular/core'; import { DatePipe } from '@angular/common'; import { IGX_CALENDAR_DIRECTIVES, - IgxCalendarComponent + IgxCalendarComponent, + IgxCalendarView } from 'igniteui-angular/calendar'; interface CalendarEvent { @@ -61,6 +62,24 @@ export class CalendarTemplatesComponent { this.selectedDate = date as Date; } + public onSubheaderKeydown(event: KeyboardEvent, calendar: IgxCalendarComponent, view: IgxCalendarView) { + // Keep the key from reaching the calendar's own keyboard navigation. + event.stopPropagation(); + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.switchView(event, calendar, view); + } + } + + public switchView(event: Event, calendar: IgxCalendarComponent, view: IgxCalendarView) { + // The subheader is re-rendered for the new view, which removes the focused button. + // Move focus to the calendar body (the grid around the subheader) so keyboard navigation continues in the new view. + const grid = (event.target as HTMLElement).closest('[role="grid"]'); + calendar.activeView = view; + grid?.focus(); + } + private isCurrentMonth(date: Date): boolean { return date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth(); }