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 da1585501bf2..32bf90329b25 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 @@ -160,6 +160,35 @@ 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 getCellState = async (cell: Locator): Promise => + cell.getAttribute('data-state'); + + const getDaysState = async (): Promise> => + Promise.all((await calendar.getDays()).map(getCellState)); + + await tuiGoto(page, 'components/input-date-range/API?disabledItemHandler$=1'); + + await inputDateRange.textfield.click(); + + // check disabled items length before day selection + expect( + (await getDaysState()).filter(state => state === 'disabled'), + ).toHaveLength(20); + + await calendar.clickOnCalendarDay(7); + + // check range which includes disabled days + // range should have only 2 enabled items + expect( + (await getDaysState()).filter(state => state !== '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 44ed3699af81..d34bece62ddd 100644 --- a/projects/kit/components/calendar-range/calendar-range.component.ts +++ b/projects/kit/components/calendar-range/calendar-range.component.ts @@ -78,6 +78,7 @@ export class TuiCalendarRangeComponent implements TuiWithOptionalMinMax @Output() readonly valueChange = new EventEmitter(); + availableRange: TuiDayRange | null = null; previousValue: TuiDayRange | null = null; selectedActivePeriod: TuiDayRangePeriod | null = null; @@ -171,6 +172,7 @@ export class TuiCalendarRangeComponent implements TuiWithOptionalMinMax if (value === null || !value.isSingleDay) { this.value = new TuiDayRange(day, day); + this.availableRange = this.findAvailableRange(); return; } @@ -221,7 +223,7 @@ export class TuiCalendarRangeComponent implements TuiWithOptionalMinMax ): TuiBooleanHandler { return item => { if (!value?.isSingleDay || !minLength) { - return disabledItemHandler(item); + return this.isDisabledItem(disabledItemHandler, value, item); } const negativeMinLength = tuiObjectFromEntries( @@ -232,7 +234,60 @@ export class TuiCalendarRangeComponent implements TuiWithOptionalMinMax 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 === ALWAYS_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); + } }