diff --git a/libs/core/calendar/calendar-legend/calendar-legend-focusing.service.spec.ts b/libs/core/calendar/calendar-legend/calendar-legend-focusing.service.spec.ts new file mode 100644 index 00000000000..f1cb5a41b28 --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend-focusing.service.spec.ts @@ -0,0 +1,85 @@ +import { TestBed } from '@angular/core/testing'; +import { CalendarLegendFocusingService } from './calendar-legend-focusing.service'; + +describe('CalendarLegendFocusingService', () => { + let service: CalendarLegendFocusingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CalendarLegendFocusingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should set focus on a cell and update the BehaviorSubject', () => { + const mockElement = document.createElement('div'); + const mockCalIndex = 1; + const mockSpecialNumber = 5; + + service.setFocusOnCell(mockElement, mockCalIndex, mockSpecialNumber); + + expect(service.focusedElement).toBe(mockElement); + expect(service.calIndex).toBe(mockCalIndex); + expect(service.specialNumber).toBe(mockSpecialNumber); + + service.cellSubject$.subscribe((value) => { + expect(value).toEqual({ + cell: mockElement, + calIndex: mockCalIndex, + cellNumber: mockSpecialNumber + }); + }); + }); + + it('should set focus on a cell without a special number', () => { + const mockElement = document.createElement('div'); + const mockCalIndex = 2; + + service.setFocusOnCell(mockElement, mockCalIndex); + + expect(service.focusedElement).toBe(mockElement); + expect(service.calIndex).toBe(mockCalIndex); + expect(service.specialNumber).toBeUndefined(); + + service.cellSubject$.subscribe((value) => { + expect(value).toEqual({ + cell: mockElement, + calIndex: mockCalIndex, + cellNumber: null + }); + }); + }); + + it('should get the currently focused special number', () => { + const mockSpecialNumber = 10; + const mockElement = document.createElement('div'); + + service.setFocusOnCell(mockElement, 0, mockSpecialNumber); + const focusedSpecialNumber = service.getFocusedElement(); + + expect(focusedSpecialNumber).toBe(mockSpecialNumber); + }); + + it('should clear the focused element and update the BehaviorSubject', () => { + const mockElement = document.createElement('div'); + const mockCalIndex = 3; + const mockSpecialNumber = 15; + + service.setFocusOnCell(mockElement, mockCalIndex, mockSpecialNumber); + + service.clearFocusedElement(); + + expect(service.focusedElement).toBeNull(); + expect(service.specialNumber).toBeNull(); + + service.cellSubject$.subscribe((value) => { + expect(value).toEqual({ + cell: null, + calIndex: null, + cellNumber: null + }); + }); + }); +}); diff --git a/libs/core/calendar/calendar-legend/calendar-legend-focusing.service.ts b/libs/core/calendar/calendar-legend/calendar-legend-focusing.service.ts new file mode 100644 index 00000000000..d608c1c2929 --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend-focusing.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class CalendarLegendFocusingService { + /** Subject to emit the focused element */ + cellSubject = new BehaviorSubject<{ cell: HTMLElement | null; calIndex: number | null; cellNumber: number | null }>( + { + cell: null, + calIndex: null, + cellNumber: null + } + ); + + /** Observable to emit the focused element */ + cellSubject$ = this.cellSubject.asObservable(); + + /** the current focused element */ + focusedElement: HTMLElement | null; + + /** Special Number */ + specialNumber: number | null; + + /** Calendar Index */ + calIndex: number; + + /** Setting the elements that are getting currently focused */ + setFocusOnCell(legendItem: HTMLElement, calIndex: number, specialNumber?: number): void { + this.focusedElement = legendItem; + this.calIndex = calIndex; + if (specialNumber) { + this.specialNumber = specialNumber; + this.cellSubject.next({ cell: legendItem, calIndex, cellNumber: specialNumber }); + } + } + + /** Getting the elements that are getting currently focused */ + getFocusedElement(): number | null { + return this.specialNumber; + } + + /** Setting the index of the calendar */ + setCalIndex(calIndex: number): void { + this.calIndex = calIndex; + } + + /** Getting the index of the calendar */ + getCalIndex(): number { + return this.calIndex; + } + + /** Clearing the focused element */ + clearFocusedElement(): void { + this.focusedElement = null; + this.specialNumber = null; + this.cellSubject.next({ cell: null, calIndex: null, cellNumber: null }); + } +} diff --git a/libs/core/calendar/calendar-legend/calendar-legend-item.component.scss b/libs/core/calendar/calendar-legend/calendar-legend-item.component.scss new file mode 100644 index 00000000000..95f839b0645 --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend-item.component.scss @@ -0,0 +1 @@ +@import 'fundamental-styles/dist/calendar'; diff --git a/libs/core/calendar/calendar-legend/calendar-legend-item.component.spec.ts b/libs/core/calendar/calendar-legend/calendar-legend-item.component.spec.ts new file mode 100644 index 00000000000..6bb5228eed5 --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend-item.component.spec.ts @@ -0,0 +1,90 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { LegendItemComponent } from './calendar-legend-item.component'; + +describe('LegendItemComponent', () => { + let component: LegendItemComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [LegendItemComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LegendItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit focusedElementEvent with the correct id on focus', () => { + const spy = jest.spyOn(component.focusedElementEvent, 'emit'); + component.onFocus(); + expect(spy).toHaveBeenCalledWith(component.id); + }); + + it('should apply the correct CSS classes when inputs change', () => { + component.type = 'appointment'; + component.circle = true; + component.color = 'placeholder-10'; + + // Trigger ngOnChanges to rebuild the CSS classes + component.ngOnChanges(); + + const cssClasses = component.buildComponentCssClass(); + expect(cssClasses).toContain('fd-calendar-legend__item'); + expect(cssClasses).toContain('fd-calendar-legend__item--appointment'); + expect(cssClasses).toContain('fd-calendar-legend__item--placeholder-10'); + }); + + it('should update CSS classes dynamically when input signals change', () => { + component.type = 'appointment'; + fixture.detectChanges(); + + let cssClasses = component.buildComponentCssClass(); + expect(cssClasses).toContain('fd-calendar-legend__item--appointment'); + + // Update inputSignal value + component.type = ''; + fixture.detectChanges(); + + cssClasses = component.buildComponentCssClass(); + expect(cssClasses).not.toContain('fd-calendar-legend__item--appointment'); + }); + + it('should handle color input dynamically via inputSignals', () => { + component.color = 'placeholder-9'; + fixture.detectChanges(); + + const cssClasses = component.buildComponentCssClass(); + expect(cssClasses).toContain('fd-calendar-legend__item--placeholder-9'); + + component.color = 'placeholder-10'; + fixture.detectChanges(); + + const updatedCssClasses = component.buildComponentCssClass(); + expect(updatedCssClasses).toContain('fd-calendar-legend__item--placeholder-10'); + expect(updatedCssClasses).not.toContain('fd-calendar-legend__item--placeholder-9'); + }); + + it('should add appointment class when circle input is true', () => { + component.circle = true; + fixture.detectChanges(); + + const appointmentClass = component.getAppointmentClass(); + expect(appointmentClass).toBe('fd-calendar-legend__item--appointment'); + }); + + it('should not add appointment class when circle is false and type is not appointment', () => { + component.circle = false; + component.type = ''; + fixture.detectChanges(); + + const appointmentClass = component.getAppointmentClass(); + expect(appointmentClass).toBe(''); + }); +}); diff --git a/libs/core/calendar/calendar-legend/calendar-legend-item.component.ts b/libs/core/calendar/calendar-legend/calendar-legend-item.component.ts new file mode 100644 index 00000000000..7763bc41252 --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend-item.component.ts @@ -0,0 +1,107 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + ViewEncapsulation, + input +} from '@angular/core'; +import { CssClassBuilder, Nullable, applyCssClass } from '@fundamental-ngx/cdk/utils'; + +let id = 0; + +@Component({ + selector: 'fd-calendar-legend-item', + standalone: true, + imports: [CommonModule], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + {{ text }} + + `, + host: { + '[attr.id]': 'id', + '(focus)': 'onFocus()', + tabindex: '0' + } +}) +export class LegendItemComponent implements OnChanges, OnInit, CssClassBuilder { + /** The text of the legend item */ + @Input() text: string | undefined; + + /** The color of the legend item marker */ + @Input() color: string; + + /** Sending the focused item to parent */ + @Output() focusedElementEvent = new EventEmitter(); + + /** The type of the legend item */ + @Input() type: Nullable = ''; + + /** If the marker is a circle or a square */ + @Input() circle = false; + + /** The id of the legend item */ + @Input() id = `fd-calendar-legend-item-${id++}`; + + /** The aria-label of the legend item */ + ariaLabel = input(); + + /** The aria-labelledby of the legend item */ + ariaLabelledBy = input(); + + /** The aria-describedby of the legend item */ + ariaDescribedBy = input(); + + /** @hidden */ + class: string; + + /** @hidden */ + constructor(public elementRef: ElementRef) {} + + /** @hidden */ + @applyCssClass + buildComponentCssClass(): string[] { + return [ + `fd-calendar-legend__item ${this.getTypeClass()} ${this.getAppointmentClass()} ${this.getColorClass()}` + ]; + } + + /** @hidden */ + ngOnChanges(): void { + this.buildComponentCssClass(); + } + + /** @hidden */ + ngOnInit(): void { + this.buildComponentCssClass(); + } + + /** @hidden */ + getTypeClass(): string { + return this.type ? `fd-calendar-legend__item--${this.type}` : ''; + } + + /** @hidden */ + getAppointmentClass(): string { + return this.circle || this.type === 'appointment' ? `fd-calendar-legend__item--appointment` : ''; + } + + /** @hidden */ + getColorClass(): string { + return this.color ? `fd-calendar-legend__item--${this.color}` : ''; + } + + /** @hidden */ + onFocus(): void { + this.focusedElementEvent.emit(this.id); + } +} diff --git a/libs/core/calendar/calendar-legend/calendar-legend.component.spec.ts b/libs/core/calendar/calendar-legend/calendar-legend.component.spec.ts new file mode 100644 index 00000000000..9c0780d0bde --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend.component.spec.ts @@ -0,0 +1,114 @@ +import { Component, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { + DATE_TIME_FORMATS, + DatetimeAdapter, + FD_DATETIME_FORMATS, + FdDate, + FdDatetimeAdapter +} from '@fundamental-ngx/core/datetime'; +import { SpecialDayRule } from '@fundamental-ngx/core/shared'; +import { CalendarLegendFocusingService } from './calendar-legend-focusing.service'; +import { LegendItemComponent } from './calendar-legend-item.component'; +import { CalendarLegendComponent } from './calendar-legend.component'; + +@Component({ + template: ` ` +}) +class CalendarLegendHostTestComponent { + @ViewChild(CalendarLegendComponent) calendarLegend: CalendarLegendComponent; + + specialDaysRules: SpecialDayRule[] = [ + { legendText: 'Holiday 1', specialDayNumber: 1, rule: (fdDate) => this.datetimeAdapter.getDate(fdDate) === 14 }, + { legendText: 'Holiday 2', specialDayNumber: 2, rule: (fdDate) => this.datetimeAdapter.getDate(fdDate) === 15 } + ]; + + constructor(private datetimeAdapter: DatetimeAdapter) {} +} + +describe('CalendarLegendComponent', () => { + let fixture: ComponentFixture; + let host: CalendarLegendHostTestComponent; + let focusingService: CalendarLegendFocusingService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [CalendarLegendComponent, LegendItemComponent], + declarations: [CalendarLegendHostTestComponent], + providers: [ + CalendarLegendFocusingService, + { + provide: DatetimeAdapter, + useClass: FdDatetimeAdapter + }, + { + provide: DATE_TIME_FORMATS, + useValue: FD_DATETIME_FORMATS + } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CalendarLegendHostTestComponent); + host = fixture.componentInstance; + focusingService = TestBed.inject(CalendarLegendFocusingService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(host).toBeTruthy(); + }); + + it('should render legend items correctly', () => { + const legendItemElements = fixture.debugElement.queryAll(By.directive(LegendItemComponent)); + expect(legendItemElements.length).toBe(2); + expect(legendItemElements[0].nativeElement.textContent).toContain('Holiday 1'); + expect(legendItemElements[1].nativeElement.textContent).toContain('Holiday 2'); + }); + + it('should pass specialDayNumber as color to legend items', () => { + const legendItemElements = fixture.debugElement.queryAll(By.directive(LegendItemComponent)); + expect(legendItemElements[0].componentInstance.color).toBe('placeholder-1'); + expect(legendItemElements[1].componentInstance.color).toBe('placeholder-2'); + }); + + it('should append the legend items to the DOM', () => { + const nativeElement = fixture.nativeElement; + expect(nativeElement.querySelectorAll('fd-calendar-legend-item').length).toBe(2); + }); + + it('should set focus on the cell when focusedElementEvent is triggered', () => { + const setFocusSpy = jest.spyOn(focusingService, 'setFocusOnCell'); + const event = 'focusEvent'; + const specialNumber = 1; + + host.calendarLegend.focusedElementEventHandle(event, specialNumber); + + expect(setFocusSpy).toHaveBeenCalledWith( + fixture.nativeElement.querySelector(`#${event}`), + host.calendarLegend.calIndex, + specialNumber + ); + }); + + it('should toggle column class based on "col" input', () => { + host.calendarLegend.col = true; + fixture.detectChanges(); + expect( + fixture.nativeElement + .querySelector('.fd-calendar-legend') + .classList.contains('fd-calendar-legend--auto-column') + ).toBeTruthy(); + + host.calendarLegend.col = false; + fixture.detectChanges(); + expect( + fixture.nativeElement + .querySelector('.fd-calendar-legend') + .classList.contains('fd-calendar-legend--auto-column') + ).toBeFalsy(); + }); +}); diff --git a/libs/core/calendar/calendar-legend/calendar-legend.component.ts b/libs/core/calendar/calendar-legend/calendar-legend.component.ts new file mode 100644 index 00000000000..1c3ad28b4ea --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend.component.ts @@ -0,0 +1,103 @@ +import { + AfterContentInit, + Component, + ContentChildren, + ElementRef, + Input, + OnChanges, + OnInit, + QueryList, + SimpleChanges, + ViewContainerRef, + input +} from '@angular/core'; +import { SpecialDayRule } from '@fundamental-ngx/core/shared'; +import { CalendarLegendFocusingService } from './calendar-legend-focusing.service'; +import { LegendItemComponent } from './calendar-legend-item.component'; + +@Component({ + selector: 'fd-calendar-legend', + standalone: true, + template: ` `, + host: { + class: 'fd-calendar-legend', + '[class.fd-calendar-legend--auto-column]': 'col', + '[attr.fd-data-calendar-index]': 'calIndex' + } +}) +export class CalendarLegendComponent implements OnInit, AfterContentInit, OnChanges { + /** Get all legend Items */ + @ContentChildren(LegendItemComponent, { descendants: true }) + legendItems: QueryList; + + /** + * Make it a column instead + */ + @Input() col = false; + + /** Special + * days rules to be displayed in the legend */ + specialDaysRules = input[]>([]); + + /** Calendar's index */ + calIndex: number; + + /** Element getting focused */ + focusedElement = input(''); + + /** @hidden */ + constructor( + private elementRef: ElementRef, + private viewContainer: ViewContainerRef, + private focusingService: CalendarLegendFocusingService + ) { + this.calIndex = this.focusingService.getCalIndex() - 1; + } + + /** @hidden */ + ngOnInit(): void { + this._updateCalendarLegend(); + } + + /** @hidden */ + ngOnChanges(changes: SimpleChanges): void { + if (changes.specialDaysRules) { + this._updateCalendarLegend(); + } + } + + /** @hidden */ + ngAfterContentInit(): void { + this.legendItems.forEach((item) => { + item.focusedElementEvent.subscribe((event: string) => { + this.focusedElementEventHandle(event); + }); + }); + } + + /** @hidden */ + _updateCalendarLegend(): void { + this.viewContainer.clear(); + this.specialDaysRules().forEach((day) => { + if (day.legendText) { + const componentRef = this.viewContainer.createComponent(LegendItemComponent); + componentRef.instance.text = day.legendText; + componentRef.instance.color = `placeholder-${day.specialDayNumber}`; + componentRef.instance.focusedElementEvent.subscribe((event: string) => { + this.focusedElementEventHandle(event, day.specialDayNumber); + }); + + this.elementRef.nativeElement.appendChild(componentRef.location.nativeElement); + } + }); + } + + /** @hidden */ + focusedElementEventHandle(event: string, specialNumber?: number): void { + this.focusingService.setFocusOnCell( + this.elementRef.nativeElement.querySelector(`#${event}`), + this.calIndex, + specialNumber + ); + } +} diff --git a/libs/core/calendar/calendar-legend/constants.ts b/libs/core/calendar/calendar-legend/constants.ts new file mode 100644 index 00000000000..6ea7e3e3c93 --- /dev/null +++ b/libs/core/calendar/calendar-legend/constants.ts @@ -0,0 +1,24 @@ +export type LegendItemColor = + | 'work' + | 'selected' + | 'non-work' + | 'placeholder-1' + | 'placeholder-2' + | 'placeholder-3' + | 'placeholder-4' + | 'placeholder-5' + | 'placeholder-6' + | 'placeholder-7' + | 'placeholder-8' + | 'placeholder-9' + | 'placeholder-10' + | 'placeholder-11' + | 'placeholder-12' + | 'placeholder-13' + | 'placeholder-14' + | 'placeholder-15' + | 'placeholder-16' + | 'placeholder-17' + | 'placeholder-18' + | 'placeholder-19' + | 'placeholder-20'; diff --git a/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.spec.ts b/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.spec.ts index 5bc0c9eee21..35c7b82946a 100644 --- a/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.spec.ts +++ b/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.spec.ts @@ -376,17 +376,4 @@ describe('CalendarDayViewComponent', () => { expect(component._calendarDayList.filter((_day) => _day.hoverRange).length).toBeGreaterThan(0); }); - - - it('should put additional property select on single day in multiple ranges', () => { - component.currentlyDisplayed.year = 2020; - component.currentlyDisplayed.month = 4; - const date = new FdDate(2020, 4, 15); - component.selectedDate = date; - component.allowMultipleSelection.set(true); - component.ngOnInit(); - component.selectDate(component._calendarDayList[15]); - expect(component.selectedDate).toEqual(component._calendarDayList[14].date); - expect(component._calendarDayList[15].selected).toBe(true); - }); }); diff --git a/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.ts b/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.ts index eb732179c8d..061779d71ef 100644 --- a/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.ts +++ b/libs/core/calendar/calendar-views/calendar-day-view/calendar-day-view.component.ts @@ -29,6 +29,7 @@ import { NgClass } from '@angular/common'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Nullable } from '@fundamental-ngx/cdk/utils'; import { FdTranslatePipe } from '@fundamental-ngx/i18n'; +import { CalendarLegendFocusingService } from '../../calendar-legend/calendar-legend-focusing.service'; import { CalendarService } from '../../calendar.service'; import { DisableDateFunction, EscapeFocusFunction, FocusableCalendarView } from '../../models/common'; import { CalendarType, CalendarTypeEnum, DaysOfWeek } from '../../types'; @@ -296,6 +297,7 @@ export class CalendarDayViewComponent implements OnInit, OnChanges, Focusable private eRef: ElementRef, private changeDetRef: ChangeDetectorRef, private calendarService: CalendarService, + private focusedService: CalendarLegendFocusingService, @Inject(DATE_TIME_FORMATS) private _dateTimeFormats: DateTimeFormats, public _dateTimeAdapter: DatetimeAdapter ) {} @@ -317,6 +319,12 @@ export class CalendarDayViewComponent implements OnInit, OnChanges, Focusable this._buildDayViewGrid(); this.changeDetRef.markForCheck(); }); + + this.focusedService.cellSubject$.subscribe(({ cell, calIndex, cellNumber }) => { + if (cell !== null && cellNumber !== null) { + this._focusOnLegendsDay(cell, calIndex, cellNumber); + } + }); } /** @hidden */ @@ -616,6 +624,40 @@ export class CalendarDayViewComponent implements OnInit, OnChanges, Focusable this.nextMonthSelect.emit(); } + /** @hidden */ + private _focusOnLegendsDay(cell: HTMLElement, calIndex: number | null, specialNumber: number): void { + const allElements = this.eRef.nativeElement.querySelectorAll('.fd-calendar__item'); + const elementToSpecialDayMap = new Map(); + const id = this.id(); + const legendClassName = 'fd-calendar__item--legend-'; + + if (calIndex !== null && id && Number.parseInt(id.split('')[id.length - 1], 10) === calIndex) { + allElements.forEach((element) => { + element.classList.forEach((className) => { + if (className.startsWith(legendClassName)) { + elementToSpecialDayMap.set(element, parseInt(className.split('-').pop()!, 10)); + } + }); + if (!element.classList.contains(`${legendClassName + specialNumber}`)) { + element.classList.forEach((className) => { + if (className.startsWith(legendClassName) && !className.endsWith(specialNumber.toString())) { + element.classList.remove(className); + } + }); + } + element.addEventListener('focusout', () => { + element.classList.add(`${legendClassName + elementToSpecialDayMap.get(element)}`); + }); + }); + + cell.addEventListener('focusout', () => { + elementToSpecialDayMap.forEach((specialElementNumber, element) => { + element.classList.add(`${legendClassName + specialElementNumber}`); + }); + }); + } + } + /** * @hidden * Method that creates array of CalendarDay models which will be shown on day grid, diff --git a/libs/core/calendar/calendar.component.ts b/libs/core/calendar/calendar.component.ts index 23238d872d5..f762ad11aa2 100644 --- a/libs/core/calendar/calendar.component.ts +++ b/libs/core/calendar/calendar.component.ts @@ -33,6 +33,7 @@ import { import { FD_LANGUAGE } from '@fundamental-ngx/i18n'; import { createMissingDateImplementationError } from './calendar-errors'; import { CalendarHeaderComponent } from './calendar-header/calendar-header.component'; +import { CalendarLegendFocusingService } from './calendar-legend/calendar-legend-focusing.service'; import { CalendarAggregatedYearViewComponent } from './calendar-views/calendar-aggregated-year-view/calendar-aggregated-year-view.component'; import { CalendarDayViewComponent } from './calendar-views/calendar-day-view/calendar-day-view.component'; import { CalendarMonthViewComponent } from './calendar-views/calendar-month-view/calendar-month-view.component'; @@ -312,6 +313,7 @@ export class CalendarComponent implements OnInit, OnChanges, ControlValueAcce constructor( private _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, + private _CalendarLegendFocusingService: CalendarLegendFocusingService, _contentDensityObserver: ContentDensityObserver, // Use @Optional to avoid angular injection error message and throw our own which is more precise one @Optional() private _dateTimeAdapter: DatetimeAdapter, @@ -329,6 +331,7 @@ export class CalendarComponent implements OnInit, OnChanges, ControlValueAcce this.selectedDate = this._dateTimeAdapter.today(); this._changeDetectorRef.markForCheck(); this._listenToLocaleChanges(); + this._CalendarLegendFocusingService.setCalIndex(calendarUniqueId); } /** That allows to define function that should happen, when focus should normally escape of component */ diff --git a/libs/core/calendar/index.ts b/libs/core/calendar/index.ts index 49cda2313b2..691fe31af8c 100644 --- a/libs/core/calendar/index.ts +++ b/libs/core/calendar/index.ts @@ -1,5 +1,6 @@ export * from './calendar-directives'; export * from './calendar-header/calendar-header.component'; +export * from './calendar-legend/calendar-legend.component'; export * from './calendar-views/calendar-aggregated-year-view/calendar-aggregated-year-view.component'; export * from './calendar-views/calendar-day-view/calendar-day-view.component'; export * from './calendar-views/calendar-month-view/calendar-month-view.component'; diff --git a/libs/core/shared/interfaces/special-day-rule.ts b/libs/core/shared/interfaces/special-day-rule.ts index cb2a1a0560e..2d96fc52436 100644 --- a/libs/core/shared/interfaces/special-day-rule.ts +++ b/libs/core/shared/interfaces/special-day-rule.ts @@ -8,4 +8,5 @@ export interface SpecialDayRule { specialDayNumber: number; rule: (date: D) => boolean; + legendText?: string; } diff --git a/libs/docs/core/calendar/calendar-docs.component.html b/libs/docs/core/calendar/calendar-docs.component.html index ad90feadd46..b734be8b3a0 100644 --- a/libs/docs/core/calendar/calendar-docs.component.html +++ b/libs/docs/core/calendar/calendar-docs.component.html @@ -71,6 +71,20 @@ + Calendar Legend + + Use fd-calendar-legend to add a legend to the calendar, either side-by-side or in a column layout. + Customize the legend using fd-calendar-legend-item to represent different events or special days. + Alternatively, link the legend to the calendar automatically using the specialDaysRules input for + seamless integration of special day rules. + + + + + + + + Calendar Years Grid Year Grid and Aggregated Year Grid can be customized by passing [yearGrid] and @@ -212,3 +226,10 @@ + + Calendar Legend Section +Calendar Legend + + + + diff --git a/libs/docs/core/calendar/calendar-docs.component.ts b/libs/docs/core/calendar/calendar-docs.component.ts index 2097756f74b..59c8525de49 100644 --- a/libs/docs/core/calendar/calendar-docs.component.ts +++ b/libs/docs/core/calendar/calendar-docs.component.ts @@ -15,6 +15,7 @@ import { CalendarDisabledNavigationButtonsExampleComponent } from './examples/ca import { CalendarFormExamplesComponent } from './examples/calendar-form-example/calendar-form-example.component'; import { CalendarGridExampleComponent } from './examples/calendar-grid-example/calendar-grid-example.component'; import { CalendarI18nExampleComponent } from './examples/calendar-i18n-example.component'; +import { CalendarLegendExampleComponent } from './examples/calendar-legend-example/calendar-legend-example.component'; import { CalendarMarkHoverComponent } from './examples/calendar-mark-hover/calendar-mark-hover.component'; import { CalendarMobileExampleComponent } from './examples/calendar-mobile-example/calendar-mobile-example.component'; import { CalendarMondayStartExampleComponent } from './examples/calendar-monday-start-example.component'; @@ -52,6 +53,8 @@ const calendarMobileHtml = 'calendar-mobile-example/calendar-mobile-example.comp const calendarFormSourceT = 'calendar-form-example/calendar-form-example.component.ts'; const calendarFormSourceH = 'calendar-form-example/calendar-form-example.component.html'; const calendarProgrammaticallySource = 'calendar-programmatically-change-example.component.ts'; +const calendarLegendSource = 'calendar-legend-example/calendar-legend-example.component.ts'; +const calendarLegendSourceHtml = 'calendar-legend-example/calendar-legend-example.component.html'; @Component({ selector: 'app-calendar', @@ -78,7 +81,8 @@ const calendarProgrammaticallySource = 'calendar-programmatically-change-example CalendarFormExamplesComponent, CalendarDisabledNavigationButtonsExampleComponent, CalendarMultiExampleComponent, - CalendarMultiRangeExampleComponent + CalendarMultiRangeExampleComponent, + CalendarLegendExampleComponent ] }) export class CalendarDocsComponent { @@ -361,4 +365,19 @@ specialDay: SpecialDayRule[] = [ code: getAssetFromModuleAssets(calendarProgrammaticallySource) } ]; + + calendarLegendSource: ExampleFile[] = [ + { + language: 'typescript', + component: 'CalendarLegendExampleComponent', + fileName: 'calendar-legend-example', + code: getAssetFromModuleAssets(calendarLegendSource) + }, + { + language: 'html', + component: 'CalendarLegendExampleComponent', + fileName: 'calendar-legend-example', + code: getAssetFromModuleAssets(calendarLegendSourceHtml) + } + ]; } diff --git a/libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.html b/libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.html new file mode 100644 index 00000000000..41dc2a2caaa --- /dev/null +++ b/libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.html @@ -0,0 +1,10 @@ +
+
+ + +
+
+ + +
+
diff --git a/libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.ts b/libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.ts new file mode 100644 index 00000000000..a03013cf8c9 --- /dev/null +++ b/libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; +import { CalendarComponent, CalendarLegendComponent } from '@fundamental-ngx/core/calendar'; +import { + DATE_TIME_FORMATS, + DatetimeAdapter, + FD_DATETIME_FORMATS, + FdDate, + FdDatetimeAdapter +} from '@fundamental-ngx/core/datetime'; +import { SpecialDayRule } from '@fundamental-ngx/core/shared'; + +@Component({ + selector: 'fd-calendar-legend-example', + standalone: true, + templateUrl: './calendar-legend-example.component.html', + providers: [ + { + provide: DatetimeAdapter, + useClass: FdDatetimeAdapter + }, + { + provide: DATE_TIME_FORMATS, + useValue: FD_DATETIME_FORMATS + } + ], + imports: [CalendarComponent, CalendarLegendComponent] +}) +export class CalendarLegendExampleComponent { + constructor(private datetimeAdapter: DatetimeAdapter) {} + + specialDays: SpecialDayRule[] = [ + { + specialDayNumber: 5, + rule: (fdDate) => this.datetimeAdapter.getDate(fdDate) in [2, 9, 16], + legendText: 'Placeholder-5' + }, + { + specialDayNumber: 6, + rule: (fdDate) => this.datetimeAdapter.getDayOfWeek(fdDate) === 2, + legendText: 'Placeholder-6' + }, + { + specialDayNumber: 10, + rule: (fdDate) => this.datetimeAdapter.getDate(fdDate) === 15, + legendText: 'Placeholder-10' + }, + { + specialDayNumber: 11, + rule: (fdDate) => this.datetimeAdapter.getDate(fdDate) === 30, + legendText: 'Placeholder-11' + } + ]; +} diff --git a/package.json b/package.json index 65f06b92255..628010ad0c5 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "fast-deep-equal": "3.1.3", "focus-trap": "7.1.0", "focus-visible": "5.2.1", - "fundamental-styles": "0.38.0", + "fundamental-styles": "0.39.0-rc.22", "fuse.js": "7.0.0", "highlight.js": "11.7.0", "intl": "1.2.5", diff --git a/yarn.lock b/yarn.lock index d140233ec1e..9be98469342 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13815,7 +13815,7 @@ __metadata: fast-glob: "npm:3.3.1" focus-trap: "npm:7.1.0" focus-visible: "npm:5.2.1" - fundamental-styles: "npm:0.38.0" + fundamental-styles: "npm:0.39.0-rc.22" fuse.js: "npm:7.0.0" highlight.js: "npm:11.7.0" husky: "npm:8.0.2" @@ -13862,13 +13862,13 @@ __metadata: languageName: unknown linkType: soft -"fundamental-styles@npm:0.38.0": - version: 0.38.0 - resolution: "fundamental-styles@npm:0.38.0" +"fundamental-styles@npm:0.39.0-rc.22": + version: 0.39.0-rc.22 + resolution: "fundamental-styles@npm:0.39.0-rc.22" peerDependencies: - "@sap-theming/theming-base-content": ^11.18.0 - "@sap-ui/common-css": 0.38.0 - checksum: 10/5df9e1bc5590ba9af9cdb9c4f6d32507267f4d5f9c985535aac23bddcff32bf23707a64ed367f0b16f80134775afcd432f03fe6d12ed654d64a32d7517f40e7e + "@sap-theming/theming-base-content": ^11.22.0 + "@sap-ui/common-css": 0.39.0-rc.22 + checksum: 10/286ea524ca0d35d3d1c076a8ab046be9f8c2a01ced7ad3ddc6c2a4002e5b27f9156fb2c3c18032cd45e2043820b772760a305d75a14a289442241fbc7f71f63e languageName: node linkType: hard