/// /// Copyright 2015-2026 Micro Focus or one of its affiliates. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import { LiveAnnouncer } from '@angular/cdk/a11y'; import { AsyncPipe } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnDestroy, } from '@angular/core'; import { merge, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { FocusIndicatorOriginDirective } from '../../../directives/accessibility/focus-indicator/focus-indicator-origin/focus-indicator-origin.directive'; import { FocusIndicatorDirective } from '../../../directives/accessibility/focus-indicator/focus-indicator.directive'; import { FocusIfDirective } from '../../../directives/focus-if/focus-if.directive'; import { DateRangeOptions } from '../../date-range-picker/date-range-picker.directive'; import { DateRangePicker, DateRangeService } from '../../date-range-picker/date-range.service'; import { DateTimePickerService } from '../date-time-picker.service'; import { isDateAfter, isDateBefore } from '../date-time-picker.utils'; import { YearViewItem, YearViewService } from './year-view.service'; @Component({ selector: 'ux-date-time-picker-year-view', templateUrl: './year-view.component.html', providers: [YearViewService], changeDetection: ChangeDetectionStrategy.OnPush, imports: [FocusIndicatorDirective, FocusIndicatorOriginDirective, FocusIfDirective, AsyncPipe], }) export class YearViewComponent implements AfterViewInit, OnDestroy { readonly yearService = inject(YearViewService); private readonly _datePicker = inject(DateTimePickerService); private readonly _liveAnnouncer = inject(LiveAnnouncer); private readonly _changeDetector = inject(ChangeDetectorRef); private readonly _rangeService = inject(DateRangeService, { optional: true }); private readonly _rangeOptions = inject(DateRangeOptions, { optional: true }); /** Determine if we are in range selection mode */ get _isRangeMode(): boolean { return !!this._rangeOptions; } /** Determine if this picker is the start picker */ get _isRangeStart(): boolean { return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.Start; } /** Determine if this picker is the end picker */ get _isRangeEnd(): boolean { return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.End; } get _rangeStart(): Date | null { return this._isRangeMode && this._rangeService ? this._rangeService.start : null; } get _rangeEnd(): Date | null { return this._isRangeMode && this._rangeService ? this._rangeService.end : null; } get _minYear(): Date | null { return this._datePicker.min$.value ? new Date(this._datePicker.min$.value.getFullYear(), 0) : null; } get _maxYear(): Date | null { return this._datePicker.max$.value ? new Date(this._datePicker.max$.value.getFullYear(), 0) : null; } private readonly _onDestroy = new Subject(); constructor() { if (this._rangeService) { this._rangeService.onRangeChange .pipe(takeUntil(this._onDestroy)) .subscribe(() => this._changeDetector.detectChanges()); } } ngAfterViewInit(): void { // update on min/max changes merge(this._datePicker.min$, this._datePicker.max$) .pipe(takeUntil(this._onDestroy)) .subscribe(() => this._changeDetector.detectChanges()); } ngOnDestroy(): void { this._onDestroy.next(); this._onDestroy.complete(); } select(year: number): void { this._datePicker.setViewportYear(year); // show the month picker this._datePicker.goToChildMode(); } /** Get the disabled state of a month */ getDisabled(item: YearViewItem): boolean { const date = new Date(item.year, 0); // if we are not in range mode then it will always be enabled if (this._isRangeMode) { // if we are range start and dates are after the range end then they should also be disabled if ( this._isRangeStart && !this._rangeStart && this._rangeEnd && isDateAfter(date, new Date(this._rangeEnd.getFullYear(), 0)) ) { return true; } // if we are range end and dates are before the range start then they should also be disabled if ( this._isRangeEnd && !this._rangeEnd && this._rangeStart && isDateBefore(date, new Date(this._rangeStart.getFullYear(), 0)) ) { return true; } } if (this._minYear && isDateBefore(date, this._minYear)) { return true; } if (this._maxYear && isDateAfter(date, this._maxYear)) { return true; } return false; } focusYear(item: YearViewItem, yearOffset: number): void { this.yearService.setFocus(item.year + yearOffset); } trackRowByFn(index: number): number { return index; } trackYearByFn(_index: number, item: YearViewItem): number { return item.year; } getTabbable(item: YearViewItem): boolean { const focused = this.yearService.focused$.value; const grid = this.yearService.grid$.value; // if there is a focused year check if this is it if (focused) { // check if the focused year is visible const isFocusedYearVisible = !!grid.find(row => !!row.find(_item => _item.year === focused)); if (isFocusedYearVisible) { return focused === item.year; } } // if there is no focusable year then check if there is a selected year const isSelectedYearVisible = !!grid.find(row => !!row.find(year => year.isActiveYear)); if (isSelectedYearVisible) { return item.isActiveYear; } // otherwise find the first non-disabled month for (const row of this.yearService.grid$.value) { for (const column of row) { if (!this.getDisabled(column)) { return item === column; } } } // otherwise make the first month tabbable return false; } /** Announce the date when we focus on a date */ announceRangeMode(): void { if (this._isRangeMode) { this._liveAnnouncer.announce( this._isRangeStart ? this._rangeService.startPickerAriaLabel : this._rangeService.endPickerAriaLabel ); } } }