From 127309e8e10ef32b749acdfd85a220df9e126dc4 Mon Sep 17 00:00:00 2001 From: Hristov Date: Thu, 14 Nov 2024 17:38:11 -0500 Subject: [PATCH] feat(core): created a new legend section added a few inputs to legend item minor classes changes used BuildClass can insert a legend programatically with SpecialDays made programmable through SpecialDayRules adding a focusing service to filter legend days modified the calendar legend docs connected the legend to days, can be logged remove unfocused classes calendar cells focus/unfocus dynamically legends are only focusing on their own calendar and not others some written tests calendar legend test passing finished the tests for legend item finished last test and made couple changes to docs added path to index.ts --- .../calendar-legend-focusing.service.spec.ts | 85 +++++++++++++ .../calendar-legend-focusing.service.ts | 60 +++++++++ .../calendar-legend-item.component.scss | 1 + .../calendar-legend-item.component.spec.ts | 90 ++++++++++++++ .../calendar-legend-item.component.ts | 107 ++++++++++++++++ .../calendar-legend.component.spec.ts | 114 ++++++++++++++++++ .../calendar-legend.component.ts | 93 ++++++++++++++ .../calendar/calendar-legend/constants.ts | 24 ++++ .../calendar-day-view.component.spec.ts | 13 -- .../calendar-day-view.component.ts | 42 +++++++ libs/core/calendar/calendar.component.ts | 3 + libs/core/calendar/index.ts | 1 + .../shared/interfaces/special-day-rule.ts | 1 + .../calendar/calendar-docs.component.html | 21 ++++ .../core/calendar/calendar-docs.component.ts | 21 +++- .../calendar-legend-example.component.html | 10 ++ .../calendar-legend-example.component.scss | 0 .../calendar-legend-example.component.ts | 53 ++++++++ package.json | 2 +- yarn.lock | 14 +-- 20 files changed, 733 insertions(+), 22 deletions(-) create mode 100644 libs/core/calendar/calendar-legend/calendar-legend-focusing.service.spec.ts create mode 100644 libs/core/calendar/calendar-legend/calendar-legend-focusing.service.ts create mode 100644 libs/core/calendar/calendar-legend/calendar-legend-item.component.scss create mode 100644 libs/core/calendar/calendar-legend/calendar-legend-item.component.spec.ts create mode 100644 libs/core/calendar/calendar-legend/calendar-legend-item.component.ts create mode 100644 libs/core/calendar/calendar-legend/calendar-legend.component.spec.ts create mode 100644 libs/core/calendar/calendar-legend/calendar-legend.component.ts create mode 100644 libs/core/calendar/calendar-legend/constants.ts create mode 100644 libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.html create mode 100644 libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.scss create mode 100644 libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.ts 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..6585d0735d8 --- /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; + + /** 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..28ade23291c --- /dev/null +++ b/libs/core/calendar/calendar-legend/calendar-legend.component.ts @@ -0,0 +1,93 @@ +import { + AfterContentInit, + Component, + ContentChildren, + ElementRef, + Input, + OnInit, + QueryList, + 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.data-calendar-index]': 'calIndex' + } +}) +export class CalendarLegendComponent implements OnInit, AfterContentInit { + /** Get all legend Items */ + @ContentChildren(LegendItemComponent, { descendants: true }) + legendItems: QueryList; + + /** Special + * days rules to be displayed in the legend */ + @Input() specialDaysRules: SpecialDayRule[] = []; + + /** + * Make it a column instead + */ + @Input() col = false; + + /** 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._addCalendarLegend(); + } + + /** @hidden */ + ngAfterContentInit(): void { + this.legendItems.forEach((item) => { + item.focusedElementEvent.subscribe((event: string) => { + this.focusedElementEventHandle(event); + }); + }); + } + + /** @hidden */ + _addCalendarLegend(): void { + 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.scss b/libs/docs/core/calendar/examples/calendar-legend-example/calendar-legend-example.component.scss new file mode 100644 index 00000000000..e69de29bb2d 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