From 8671e4568a3d690e33a7e7e522dc479cbf28403d Mon Sep 17 00:00:00 2001 From: andrasincze Date: Wed, 14 Oct 2026 12:10:46 +0200 Subject: [PATCH] fix(datepicker): correct month navigation and focus updates within date bounds --- .../datepicker/calendar/calendar.component.ts | 18 ++++++++++++++---- .../widgets/datepicker/datepicker.directive.ts | 4 ++-- .../day-selector/day-selector.component.ts | 16 ++++++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/projects/widgets/datepicker/calendar/calendar.component.ts b/projects/widgets/datepicker/calendar/calendar.component.ts index e73f95e8..048c7067 100644 --- a/projects/widgets/datepicker/calendar/calendar.component.ts +++ b/projects/widgets/datepicker/calendar/calendar.component.ts @@ -7,7 +7,7 @@ import { IdsDatepickerView, IdsDatepickerViewType } from '../tokens/datepicker-v import { IdsYearSelectorComponent } from '../year-selector/year-selector.component'; import { CdkTrapFocus } from '@angular/cdk/a11y'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, linkedSignal, OnInit, output, viewChild, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, linkedSignal, OnInit, output, untracked, viewChild, ViewEncapsulation } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { IdsButtonComponent } from '@i-cell/ids-angular/button'; import { clampDate, ComponentBaseWithDefaults, IdsSize, IdsSizeType, isValidDate, startOfMonth, startOfYear } from '@i-cell/ids-angular/core'; @@ -91,11 +91,21 @@ export class IdsCalendarComponent extends ComponentBaseWithDefaults impl this._view() === IdsDatepickerView.DAY ? this._intl.switchToYearSelectorLabel : this._intl.switchToDaySelectorLabel )); - protected _focusedDate: Date = new Date(); + protected _focusedDate = linkedSignal<{ min: Date | null; max: Date | null }, Date>({ + source: () => ({ + min: this.min(), + max: this.max(), + }), + computation: ({ min, max }, previous): Date => { + const date = previous ? previous.value : untracked(() => this.value()); + const safeValue = isValidDate(date) ? date : new Date(); + return clampDate(safeValue, min, max); + }, + equal: (previous, current) => previous.getTime() === current.getTime(), + }); public ngOnInit(): void { this._intl.changes.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => this._cdRef.markForCheck()); - this._setFocusedDate(this.value()); } protected _selectYear(selectedYear: Date): void { @@ -144,7 +154,7 @@ export class IdsCalendarComponent extends ComponentBaseWithDefaults impl private _setFocusedDate(date: Date | null): void { const safeValue = isValidDate(date) ? date : new Date(); - this._focusedDate = clampDate(safeValue, this.min(), this.max()); + this._focusedDate.set(clampDate(safeValue, this.min(), this.max())); this._cdRef.markForCheck(); } } diff --git a/projects/widgets/datepicker/datepicker.directive.ts b/projects/widgets/datepicker/datepicker.directive.ts index 69d648db..9b179766 100644 --- a/projects/widgets/datepicker/datepicker.directive.ts +++ b/projects/widgets/datepicker/datepicker.directive.ts @@ -66,10 +66,10 @@ export class IdsDatepickerDirective extends DirectiveBaseWithDefaults(() => this._days().slice(0, daysPerWeek).map((date: Date) => this._intl.getWeekdayHeaderLabel(date) ?? '')); + private _isRangeEmpty = computed(() => { + const min = this.min(); + const max = this.max(); + return isValidDate(min) && isValidDate(max) && compareDates(min, max) > 0; + }); + public override hasPreviousPage(): boolean { - const dayOnPrevPage = addDays(this._gridStartDate(), -1); - const min = this.min() ?? dayOnPrevPage; + const min = this.min(); - return compareDates(dayOnPrevPage, min) > -1; + return !this._isRangeEmpty() && (!isValidDate(min) || compareDates(startOfMonth(min), this._currentMonth()) < 0); } public override gotoPreviousPage(): void { @@ -50,10 +55,9 @@ export class IdsDaySelectorComponent extends IdsCalendarPage { } public override hasNextPage(): boolean { - const dayOnNextPage = addDays(this._gridEndDate(), 1); - const max = this.max() ?? dayOnNextPage; + const max = this.max(); - return compareDates(dayOnNextPage, max) < 1; + return !this._isRangeEmpty() && (!isValidDate(max) || compareDates(startOfMonth(max), this._currentMonth()) > 0); } public override gotoNextPage(): void {