diff --git a/projects/demo-playwright/tests/kit/input-date-range/input-date-range.spec.ts b/projects/demo-playwright/tests/kit/input-date-range/input-date-range.spec.ts index b951be4b9dc8..3f2386b7fbc6 100644 --- a/projects/demo-playwright/tests/kit/input-date-range/input-date-range.spec.ts +++ b/projects/demo-playwright/tests/kit/input-date-range/input-date-range.spec.ts @@ -168,6 +168,39 @@ test.describe('InputDateRange', () => { '07-item-and-calendar-interactions.png', ); }); + + test('Prevent selection of range with disabled days', async ({page}) => { + const calendar = new TuiCalendarPO( + inputDateRange.calendarRange.locator('tui-calendar'), + ); + + const getCellSelectors = async (cell: Locator): Promise => + cell.getAttribute('class'); + + const daysSelectors = async (): Promise> => + Promise.all((await calendar.getDays()).map(getCellSelectors)); + + await tuiGoto(page, 'components/input-date-range/API?disabledItemHandler$=1'); + + await inputDateRange.textfield.click(); + + // check disabled items length before day selection + expect( + (await daysSelectors()).filter((selectors) => + selectors?.includes('t-cell_disabled'), + ), + ).toHaveLength(20); + + await calendar.clickOnCalendarDay(7); + + // check range which includes disabled days + // range should have only 2 enabled items + expect( + (await daysSelectors()).filter( + (selectors) => !selectors?.includes('t-cell_disabled'), + ), + ).toHaveLength(2); + }); }); test.describe('Examples', () => { diff --git a/projects/kit/components/calendar-range/calendar-range.component.ts b/projects/kit/components/calendar-range/calendar-range.component.ts index 80378d4886e5..51af4aec48f9 100644 --- a/projects/kit/components/calendar-range/calendar-range.component.ts +++ b/projects/kit/components/calendar-range/calendar-range.component.ts @@ -49,6 +49,7 @@ export class TuiCalendarRange implements OnInit, OnChanges { protected previousValue: TuiDayRange | null = null; protected hoveredItem: TuiDay | null = null; protected readonly capsMapper = TUI_DAY_CAPS_MAPPER; + protected availableRange: TuiDayRange | null = null; @Input() public defaultViewedMonth: TuiMonth = TuiMonth.currentLocal(); @@ -172,6 +173,8 @@ export class TuiCalendarRange implements OnInit, OnChanges { } else { this.updateValue(TuiDayRange.sort(this.value.from, day)); } + + this.availableRange = this.findAvailableRange(); } protected updateValue(value: TuiDayRange | null): void { @@ -203,7 +206,7 @@ export class TuiCalendarRange implements OnInit, OnChanges { ): TuiBooleanHandler { return (item) => { if (!value?.isSingleDay || !minLength) { - return disabledItemHandler(item); + return this.isDisabledItem(disabledItemHandler, value, item); } const negativeMinLength = Object.fromEntries( @@ -214,10 +217,63 @@ export class TuiCalendarRange implements OnInit, OnChanges { const inDisabledRange = disabledBefore.dayBefore(item) && disabledAfter.dayAfter(item); - return inDisabledRange || disabledItemHandler(item); + return ( + inDisabledRange || this.isDisabledItem(disabledItemHandler, value, item) + ); }; } + private isDisabledItem( + disabledItemHandler: TuiBooleanHandler, + value: TuiDayRange | null, + item: TuiDay, + ): boolean { + return ( + disabledItemHandler(item) || + (!!value?.isSingleDay && !this.availableRangeContainsItem(item)) + ); + } + + private availableRangeContainsItem(item: TuiDay): boolean { + if (this.availableRange === null) { + return true; + } + + const {from, to} = this.availableRange; + + return from.daySameOrBefore(item) && to.daySameOrAfter(item); + } + + private findAvailableRange(): TuiDayRange | null { + const {disabledItemHandler, value} = this; + + if (!value?.isSingleDay || disabledItemHandler === TUI_FALSE_HANDLER) { + return null; + } + + let from = value.from; + let to = value.from; + + let leftShift = true; + let rightShift = true; + + while (leftShift || rightShift) { + leftShift = !disabledItemHandler(from.append({day: -1})); + + if (leftShift) { + from = from.append({day: -1}); + } + + rightShift = !disabledItemHandler(to.append({day: 1})); + + if (rightShift) { + to = to.append({day: 1}); + } + } + + return new TuiDayRange(from, to); + } + private updateDefaultViewedMonth(): void { if (this.max && this.defaultViewedMonth.monthSameOrAfter(this.max)) { this.defaultViewedMonth = this.max.append({month: -1});