diff --git a/projects/addon-mobile/components/mobile-calendar-dropdown/mobile-calendar-dropdown.component.ts b/projects/addon-mobile/components/mobile-calendar-dropdown/mobile-calendar-dropdown.component.ts index 387dceab43df..75e9328b3855 100644 --- a/projects/addon-mobile/components/mobile-calendar-dropdown/mobile-calendar-dropdown.component.ts +++ b/projects/addon-mobile/components/mobile-calendar-dropdown/mobile-calendar-dropdown.component.ts @@ -66,7 +66,8 @@ export class TuiMobileCalendarDropdown { protected readonly range = this.is('tui-input-date-range'); protected readonly multi = this.data.multi || this.is('tui-input-date[multiple]'); protected readonly single = - this.data.single || this.is('tui-input-date:not([multiple])'); + this.data.single || // TODO(v5): use `rangeMode` from DI token `TUI_CALENDAR_SHEET_DEFAULT_OPTIONS` + this.is('tui-input-date:not([multiple])'); constructor() { this.keyboard.hide(); diff --git a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts index 492376605c4c..1704c84d794e 100644 --- a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts +++ b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts @@ -37,6 +37,7 @@ import {TuiMapperPipe} from '@taiga-ui/cdk/pipes/mapper'; import {TUI_IS_E2E, TUI_IS_IOS} from '@taiga-ui/cdk/tokens'; import type {TuiBooleanHandler, TuiMapper} from '@taiga-ui/cdk/types'; import {TuiButton} from '@taiga-ui/core/components/button'; +import {TUI_CALENDAR_SHEET_OPTIONS} from '@taiga-ui/core/components/calendar'; import {TuiLink} from '@taiga-ui/core/components/link'; import {TuiMonthPipe} from '@taiga-ui/core/pipes/month'; import {TuiOrderWeekDaysPipe} from '@taiga-ui/core/pipes/order-week-days'; @@ -153,8 +154,15 @@ export class TuiMobileCalendar implements AfterViewInit { ), ); + /** + * @deprecated use static DI options instead + * ``` + * tuiCalendarSheetOptionsProvider({rangeMode: boolean}) + * ``` + * TODO(v5): delete it + */ @Input() - public single = true; + public single = !inject(TUI_CALENDAR_SHEET_OPTIONS).rangeMode; @Input() public multi = false; @@ -180,7 +188,7 @@ export class TuiMobileCalendar implements AfterViewInit { public readonly valueChange = this.value$.pipe( skip(1), distinctUntilChanged((a, b) => a?.toString() === b?.toString()), - takeUntilDestroyed(), + map((x) => (!this.single && x instanceof TuiDay ? new TuiDayRange(x, x) : x)), ); constructor() { @@ -245,10 +253,8 @@ export class TuiMobileCalendar implements AfterViewInit { this.value = tuiToggleDay(this.value, day); } else if (this.value instanceof TuiDay) { this.value = TuiDayRange.sort(this.value, day); - } else if (this.value instanceof TuiDayRange && !this.value.isSingleDay) { - this.value = day; } else if (this.value instanceof TuiDayRange) { - this.value = TuiDayRange.sort(this.value.from, day); + this.value = day; } else if (!this.value) { this.value = day; } diff --git a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.template.html b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.template.html index 0b724559abcf..88f4c72b438c 100644 --- a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.template.html +++ b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.template.html @@ -84,6 +84,7 @@

{{ month | tuiMonth | async }}

class="t-calendar" [disabledItemHandler]="disabledItemHandler | tuiMapper: disabledItemHandlerMapper : min : max" [month]="month" + [single]="single" [value]="value" (dayClick)="onDayClick($event)" /> diff --git a/projects/cdk/date-time/day-range.ts b/projects/cdk/date-time/day-range.ts index 86a1443c2948..c91689484247 100644 --- a/projects/cdk/date-time/day-range.ts +++ b/projects/cdk/date-time/day-range.ts @@ -102,4 +102,19 @@ export class TuiDayRange extends TuiMonthRange { ): string { return this.getFormattedDayRange(dateFormat, dateSeparator); } + + public toArray(): readonly TuiDay[] { + const {from, to} = this; + const arr = []; + + for ( + const day = from.toUtcNativeDate(); + day <= to.toUtcNativeDate(); + day.setDate(day.getDate() + 1) + ) { + arr.push(TuiDay.fromLocalNativeDate(day)); + } + + return arr; + } } diff --git a/projects/cdk/date-time/test/day-range.spec.ts b/projects/cdk/date-time/test/day-range.spec.ts index 39734109f7c6..abd44b6afa50 100644 --- a/projects/cdk/date-time/test/day-range.spec.ts +++ b/projects/cdk/date-time/test/day-range.spec.ts @@ -183,6 +183,24 @@ describe('TuiDayRange', () => { }); }); + describe('toArray', () => { + it('returns array with all dates between `from` and `to` (including `from` and `to` day)', () => { + const range = new TuiDayRange( + new TuiDay(2024, 11, 29), + new TuiDay(2025, 0, 3), + ); + + expect(range.toArray()).toEqual([ + new TuiDay(2024, 11, 29), + new TuiDay(2024, 11, 30), + new TuiDay(2024, 11, 31), + new TuiDay(2025, 0, 1), + new TuiDay(2025, 0, 2), + new TuiDay(2025, 0, 3), + ]); + }); + }); + describe('dayLimit', () => { it('limits from one side if the other is null', () => { const y2000m0d1 = new TuiDay(2000, 0, 1); diff --git a/projects/core/components/calendar/calendar-sheet.component.ts b/projects/core/components/calendar/calendar-sheet.component.ts index bb4cdb129b4c..7f1c13605eb1 100644 --- a/projects/core/components/calendar/calendar-sheet.component.ts +++ b/projects/core/components/calendar/calendar-sheet.component.ts @@ -18,6 +18,8 @@ import {tuiNullableSame, tuiPure} from '@taiga-ui/cdk/utils/miscellaneous'; import {TuiCalendarSheetPipe, TuiOrderWeekDaysPipe} from '@taiga-ui/core/pipes'; import {TUI_DAY_TYPE_HANDLER, TUI_SHORT_WEEK_DAYS} from '@taiga-ui/core/tokens'; +import {TUI_CALENDAR_SHEET_OPTIONS} from './calendar-sheet.options'; + export type TuiMarkerHandler = TuiHandler; @Component({ @@ -36,10 +38,11 @@ export type TuiMarkerHandler = TuiHandler(); @Output() public readonly dayClick = new EventEmitter(); + /** + * @deprecated TODO(v5): delete it. It is used nowhere except unit tests + */ public itemIsInterval(day: TuiDay): boolean { const {value, hoveredItem} = this; @@ -96,17 +112,25 @@ export class TuiCalendarSheet { public getItemRange(item: TuiDay): 'active' | 'end' | 'middle' | 'start' | null { const {value, hoveredItem} = this; - if (value instanceof TuiDay) { + if (!value) { + return null; + } + + if (value instanceof TuiDay && !this.computedRangeMode) { return value.daySame(item) ? 'active' : null; } - if (!value || !(value instanceof TuiDayRange)) { - return value?.find((day) => day.daySame(item)) ? 'active' : null; + if (value instanceof TuiDayRange && value.isSingleDay) { + return value.from.daySame(item) ? 'active' : null; + } + + if (!(value instanceof TuiDay) && !(value instanceof TuiDayRange)) { + return value.find((day) => day.daySame(item)) ? 'active' : null; } const range = this.getRange(value, hoveredItem); - if (value.isSingleDay && range.isSingleDay && value.from.daySame(item)) { + if (range.isSingleDay && range.from.daySame(item)) { return 'active'; } @@ -121,8 +145,18 @@ export class TuiCalendarSheet { return range.from.dayBefore(item) && range.to.dayAfter(item) ? 'middle' : null; } - protected get isSingleDayRange(): boolean { - return this.value instanceof TuiDayRange && this.value.isSingleDay; + protected get computedRangeMode(): boolean { + return !this.single || this.options.rangeMode; + } + + protected get isRangePicking(): boolean { + return this.computedRangeMode + ? this.value instanceof TuiDay + : /** + * Only for backward compatibility! + * TODO(v5): replace with `this.options.rangeMode && this.value instanceof TuiDay` + */ + this.value instanceof TuiDayRange && this.value.isSingleDay; } protected readonly toMarkers = ( @@ -157,7 +191,14 @@ export class TuiCalendarSheet { } @tuiPure - private getRange(value: TuiDayRange, hoveredItem: TuiDay | null): TuiDayRange { + private getRange( + value: TuiDay | TuiDayRange, + hoveredItem: TuiDay | null, + ): TuiDayRange { + if (value instanceof TuiDay) { + return TuiDayRange.sort(value, hoveredItem ?? value); + } + return value.isSingleDay ? TuiDayRange.sort(value.from, hoveredItem ?? value.to) : value; @@ -173,20 +214,10 @@ export class TuiCalendarSheet { } private rangeHasDisabledDay(item: TuiDay): boolean { - if (this.value instanceof TuiDayRange) { - const range = this.getRange(this.value, item); - - for ( - const day = range.from.toUtcNativeDate(); - day <= range.to.toUtcNativeDate(); - day.setDate(day.getDate() + 1) - ) { - const tuiDay = TuiDay.fromLocalNativeDate(day); - - if (this.disabledItemHandler(tuiDay)) { - return true; - } - } + if (this.isRangePicking && this.value instanceof TuiDay) { + return this.getRange(this.value, item) + .toArray() + .some((x) => this.disabledItemHandler(x)); } return false; diff --git a/projects/core/components/calendar/calendar-sheet.options.ts b/projects/core/components/calendar/calendar-sheet.options.ts new file mode 100644 index 000000000000..179c9a3a76ff --- /dev/null +++ b/projects/core/components/calendar/calendar-sheet.options.ts @@ -0,0 +1,24 @@ +import type {Provider} from '@angular/core'; +import {tuiCreateToken, tuiProvideOptions} from '@taiga-ui/cdk/utils/miscellaneous'; + +export interface TuiCalendarSheetOptions { + readonly rangeMode: boolean; +} + +export const TUI_CALENDAR_SHEET_DEFAULT_OPTIONS: TuiCalendarSheetOptions = { + rangeMode: false, +}; + +export const TUI_CALENDAR_SHEET_OPTIONS = tuiCreateToken( + TUI_CALENDAR_SHEET_DEFAULT_OPTIONS, +); + +export function tuiCalendarSheetOptionsProvider( + options: Partial, +): Provider { + return tuiProvideOptions( + TUI_CALENDAR_SHEET_OPTIONS, + options, + TUI_CALENDAR_SHEET_DEFAULT_OPTIONS, + ); +} diff --git a/projects/core/components/calendar/index.ts b/projects/core/components/calendar/index.ts index b6f2d29e18e6..ed9fbde1dadd 100644 --- a/projects/core/components/calendar/index.ts +++ b/projects/core/components/calendar/index.ts @@ -1,4 +1,5 @@ export * from './calendar.component'; export * from './calendar-sheet.component'; +export * from './calendar-sheet.options'; export * from './calendar-spin.component'; export * from './calendar-year.component'; diff --git a/projects/demo-playwright/tests/kit/calendar-range/calendar-range.pw.spec.ts b/projects/demo-playwright/tests/kit/calendar-range/calendar-range.pw.spec.ts index 96d539ab38bc..c820a4553053 100644 --- a/projects/demo-playwright/tests/kit/calendar-range/calendar-range.pw.spec.ts +++ b/projects/demo-playwright/tests/kit/calendar-range/calendar-range.pw.spec.ts @@ -7,7 +7,10 @@ import { import type {Locator} from '@playwright/test'; import {expect, test} from '@playwright/test'; -test.describe('CalendarRange', () => { +const {describe, beforeEach} = test; + +describe('CalendarRange', () => { + const today = new Date(2020, 8, 25); let example!: Locator; let calendarRange!: TuiCalendarRangePO; let documentationPage!: TuiDocumentationPagePO; @@ -16,24 +19,22 @@ test.describe('CalendarRange', () => { viewport: {width: 650, height: 650}, }); - test.beforeEach(async ({page}) => { - await tuiGoto(page, DemoRoute.CalendarRange, { - date: new Date(2020, 8, 25), - }); - + beforeEach(({page}) => { documentationPage = new TuiDocumentationPagePO(page); - example = documentationPage.apiPageExample; - - calendarRange = new TuiCalendarRangePO(example.locator('tui-calendar-range')); }); - test.describe('Examples', () => { + describe('Examples', () => { + beforeEach(async ({page}) => { + await tuiGoto(page, DemoRoute.CalendarRange, { + date: today, + }); + }); + test('With another range switcher', async () => { example = documentationPage.getExample('#with-another-range-switcher'); calendarRange = new TuiCalendarRangePO(example.locator('tui-calendar-range')); - const getRangeSwitcher = (): Locator => - example.locator('p button[data-appearance="action"]'); + const resetButton = example.locator('p button[data-appearance="action"]'); await expect(example).toHaveScreenshot( '05-calendar-range-correct-display-defaults-items-and-values-1.png', @@ -46,36 +47,33 @@ test.describe('CalendarRange', () => { '05-calendar-range-correct-display-range-switcher-after-select-week.png', ); - await getRangeSwitcher().click(); + await resetButton.click(); await expect(example).toHaveScreenshot( '05-calendar-range-correct-display-items-and-values-after-click-on-month-range-switcher.png', ); - await getRangeSwitcher().click(); + await resetButton.click(); await expect(example).toHaveScreenshot( '05-calendar-range-correct-display-items-and-values-after-click-on-quarter-range-switcher.png', ); - await getRangeSwitcher().click(); + await resetButton.click(); await expect(example).toHaveScreenshot( '05-calendar-range-correct-display-defaults-items-and-values-2.png', ); }); - test.describe('With value', () => { + describe('With value', () => { test('Month switching via chevron', async ({page}) => { example = documentationPage.getExample('#with-value'); calendarRange = new TuiCalendarRangePO( example.locator('tui-calendar-range'), ); - const getPreviousMonthChevron = (): Locator => - example.locator('tui-spin-button > button').first(); - - await getPreviousMonthChevron().click(); + await calendarRange.previousMonth.click(); await page.mouse.click(100, 100); // clear focus @@ -86,7 +84,18 @@ test.describe('CalendarRange', () => { }); }); - test.describe('API', () => { + describe('API', () => { + beforeEach(async ({page}) => { + await tuiGoto(page, DemoRoute.CalendarRange, { + date: today, + }); + + documentationPage = new TuiDocumentationPagePO(page); + example = documentationPage.apiPageExample; + + calendarRange = new TuiCalendarRangePO(example.locator('tui-calendar-range')); + }); + test('Maximum month when items not empty', async ({page}) => { await tuiGoto(page, `${DemoRoute.CalendarRange}/API?items$=1&max$=4`); @@ -119,5 +128,89 @@ test.describe('CalendarRange', () => { await expect(example).toHaveScreenshot('08-disabled-dates-5-click-to.png'); }); + + describe('Selecting range consisting of the same start/end date', () => { + let alert!: Locator; + + beforeEach(({page}) => { + alert = page.locator('tui-alerts tui-notification'); + }); + + test('double click on the same day - selects single-day range', async ({ + page, + }) => { + await tuiGoto(page, `${DemoRoute.CalendarRange}/API`); + + const [calendarSheet] = await calendarRange + .getCalendars() + .then(async ([x]) => x.getCalendarSheets()); + + await calendarSheet.clickOnDay(15); + + await expect(alert).not.toBeAttached(); + + await calendarSheet.clickOnDay(15); + + await expect(alert).toContainText( + '{from: {year: 2020, month: 8, day: 15}, to: {year: 2020, month: 8, day: 15}}', + ); + await expect(example).toHaveScreenshot( + '07-double-click-on-the-same-day.png', + ); + }); + + test('allows to select new range start after double click on the same day', async ({ + page, + }) => { + await tuiGoto(page, `${DemoRoute.CalendarRange}/API`); + + const [calendarSheet] = await calendarRange + .getCalendars() + .then(async ([x]) => x.getCalendarSheets()); + + await calendarSheet.clickOnDay(15); + await calendarSheet.clickOnDay(15); + + await calendarSheet.clickOnDay(22); + + await expect(alert).not.toBeAttached(); + + await calendarSheet.clickOnDay(25); + + await expect(alert).toContainText( + '{from: {year: 2020, month: 8, day: 22}, to: {year: 2020, month: 8, day: 25}}', + ); + await expect(example).toHaveScreenshot( + '08-new-range-after-double-click-on-the-same-day.png', + ); + }); + + test('no highlighting hover effect after double click on the same day', async ({ + page, + }) => { + await tuiGoto(page, `${DemoRoute.CalendarRange}/API`); + + const [calendarSheet] = await calendarRange + .getCalendars() + .then(async ([x]) => x.getCalendarSheets()); + + await calendarSheet.clickOnDay(15); + await calendarSheet.getCalendarDay(20).then(async (x) => x!.hover()); + + await expect(example).toHaveScreenshot('09-1-has-hover-effect.png'); + + await calendarSheet.clickOnDay(15); + + await calendarSheet.getCalendarDay(22).then(async (x) => x!.hover()); + + await expect(example).toHaveScreenshot('09-2-no-hover-effect.png'); + + await calendarSheet.clickOnDay(22); + + await calendarSheet.getCalendarDay(25).then(async (x) => x!.hover()); + + await expect(example).toHaveScreenshot('09-3-has-hover-effect.png'); + }); + }); }); }); diff --git a/projects/demo-playwright/tests/legacy/input-date-range/input-date-range.pw.spec.ts b/projects/demo-playwright/tests/legacy/input-date-range/input-date-range.pw.spec.ts index 360ce4e37e04..5575e6671958 100644 --- a/projects/demo-playwright/tests/legacy/input-date-range/input-date-range.pw.spec.ts +++ b/projects/demo-playwright/tests/legacy/input-date-range/input-date-range.pw.spec.ts @@ -13,7 +13,9 @@ import { TuiMobileCalendarPO, } from '../../../utils'; -test.describe('InputDateRange', () => { +const {describe, beforeEach} = test; + +describe('InputDateRange', () => { let inputDateRange!: TuiInputDateRangePO; let documentationPage!: TuiDocumentationPagePO; @@ -21,16 +23,16 @@ test.describe('InputDateRange', () => { viewport: {width: 650, height: 650}, }); - test.beforeEach(({page}) => { + beforeEach(({page}) => { documentationPage = new TuiDocumentationPagePO(page); }); - test.describe('API', () => { + describe('API', () => { let example!: Locator; let calendar!: TuiCalendarPO; - test.beforeEach(() => { + beforeEach(() => { example = documentationPage.apiPageExample; inputDateRange = new TuiInputDateRangePO( @@ -120,7 +122,7 @@ test.describe('InputDateRange', () => { ); }); - test.describe('prevents changes if you enter an invalid date', () => { + describe('prevents changes if you enter an invalid date', () => { test('day > 31', async ({page}) => { await tuiGoto(page, `${DemoRoute.InputDateRange}/API`); @@ -183,6 +185,7 @@ test.describe('InputDateRange', () => { await inputDateRange.textfield.click(); await calendarSheet.clickOnDay(21); + await calendarSheet.clickOnDay(25); await expect(inputDateRange.textfield).toHaveValue( `21.09.2020${CHAR_NO_BREAK_SPACE}–${CHAR_NO_BREAK_SPACE}25.09.2020`, @@ -230,12 +233,12 @@ test.describe('InputDateRange', () => { ); }); - test.describe('Mobile emulation', () => { + describe('Mobile emulation', () => { test.use(TUI_PLAYWRIGHT_MOBILE); let mobileCalendar!: TuiMobileCalendarPO; - test.beforeEach(() => { + beforeEach(() => { mobileCalendar = new TuiMobileCalendarPO(inputDateRange.calendar); }); @@ -255,10 +258,108 @@ test.describe('InputDateRange', () => { ); }); }); + + describe('Selecting range consisting of the same start/end date', () => { + test('double click on the same day - selects single-day range', async ({ + page, + }) => { + await tuiGoto(page, `${DemoRoute.InputDateRange}/API`); + + await inputDateRange.textfield.click(); + + const calendarSheet = new TuiCalendarSheetPO( + inputDateRange.calendar.locator('tui-calendar-sheet'), + ); + + await calendarSheet.clickOnDay(15); + + await expect(inputDateRange.textfield).toHaveValue(''); + + await calendarSheet.clickOnDay(15); + + await expect(inputDateRange.textfield).toHaveValue( + '15.09.2020 – 15.09.2020', + ); + }); + + test('allows to select new range start after double click on the same day', async ({ + page, + }) => { + await tuiGoto(page, `${DemoRoute.InputDateRange}/API`); + + await inputDateRange.textfield.click(); + + const calendarSheet = new TuiCalendarSheetPO( + inputDateRange.calendar.locator('tui-calendar-sheet'), + ); + + await calendarSheet.clickOnDay(15); + await calendarSheet.clickOnDay(15); + + await expect(inputDateRange.calendar).not.toBeAttached(); + + await inputDateRange.textfield.click(); + + await expect(inputDateRange.calendar).toBeAttached(); + + await calendarSheet.clickOnDay(22); + + await expect(inputDateRange.textfield).toHaveValue( + '15.09.2020 – 15.09.2020', + ); + + await calendarSheet.clickOnDay(25); + + await expect(inputDateRange.textfield).toHaveValue( + '22.09.2020 – 25.09.2020', + ); + }); + + test('no highlighting hover effect after double click on the same day', async ({ + page, + }) => { + await tuiGoto(page, `${DemoRoute.InputDateRange}/API`); + + await inputDateRange.textfield.click(); + + const calendarSheet = new TuiCalendarSheetPO( + inputDateRange.calendar.locator('tui-calendar-sheet'), + ); + + await calendarSheet.clickOnDay(15); + await calendarSheet.getCalendarDay(20).then(async (x) => x!.hover()); + + await expect(inputDateRange.calendar).toHaveScreenshot( + '12-1-has-hover-effect.png', + ); + + await calendarSheet.clickOnDay(15); + + await expect(inputDateRange.textfield).toHaveValue( + '15.09.2020 – 15.09.2020', + ); + await expect(inputDateRange.calendar).not.toBeAttached(); + + await inputDateRange.textfield.click(); + await calendarSheet.getCalendarDay(22).then(async (x) => x!.hover()); + + await expect(inputDateRange.calendar).toHaveScreenshot( + '12-2-no-hover-effect.png', + ); + + await calendarSheet.clickOnDay(22); + + await calendarSheet.getCalendarDay(25).then(async (x) => x!.hover()); + + await expect(inputDateRange.calendar).toHaveScreenshot( + '12-3-has-hover-effect.png', + ); + }); + }); }); - test.describe('Examples', () => { - test.beforeEach(async ({page}) => { + describe('Examples', () => { + beforeEach(async ({page}) => { await tuiGoto(page, DemoRoute.InputDateRange); }); diff --git a/projects/demo-playwright/utils/page-objects/calendar-range.po.ts b/projects/demo-playwright/utils/page-objects/calendar-range.po.ts index e3be75cf15af..dc33a9883fc6 100644 --- a/projects/demo-playwright/utils/page-objects/calendar-range.po.ts +++ b/projects/demo-playwright/utils/page-objects/calendar-range.po.ts @@ -1,9 +1,29 @@ import type {Locator} from '@playwright/test'; import {expect} from '@playwright/test'; +import {TuiCalendarPO} from './calendar.po'; + export class TuiCalendarRangePO { + public previousMonth = this.host + .locator('tui-calendar-spin tui-spin-button > button') + .first(); + + public nextMonth = this.host + .locator('tui-calendar-spin tui-spin-button > button') + .last(); + constructor(private readonly host: Locator) {} + public async getCalendars(): Promise< + [TuiCalendarPO, TuiCalendarPO] | [TuiCalendarPO] + > { + const calendars = await this.host.locator('tui-calendar').all(); + + return calendars.map((x) => new TuiCalendarPO(x)) as + | [TuiCalendarPO, TuiCalendarPO] + | [TuiCalendarPO]; + } + public async getItems(): Promise { const dataList = this.host.locator('[automation-id="tui-calendar-range__menu"]'); diff --git a/projects/demo-playwright/utils/page-objects/calendar-sheet.po.ts b/projects/demo-playwright/utils/page-objects/calendar-sheet.po.ts index 7e222fe0b480..a6d552e463cf 100644 --- a/projects/demo-playwright/utils/page-objects/calendar-sheet.po.ts +++ b/projects/demo-playwright/utils/page-objects/calendar-sheet.po.ts @@ -11,15 +11,21 @@ export class TuiCalendarSheetPO { .all(); } - public async clickOnDay(day: number): Promise { + public async getCalendarDay(day: number): Promise { const cells = await this.getDays(); for (const cell of cells) { if ((await cell.textContent())?.trim() === day.toString()) { - await cell.click(); - - break; + return cell; } } + + return null; + } + + public async clickOnDay(day: number): Promise { + const element = await this.getCalendarDay(day); + + return element!.click(); } } diff --git a/projects/demo-playwright/utils/page-objects/calendar.po.ts b/projects/demo-playwright/utils/page-objects/calendar.po.ts index af10c5f2e800..99aa79154544 100644 --- a/projects/demo-playwright/utils/page-objects/calendar.po.ts +++ b/projects/demo-playwright/utils/page-objects/calendar.po.ts @@ -9,12 +9,17 @@ export class TuiCalendarPO { constructor(private readonly host: Locator) {} - public async getCalendarSheets(): Promise { + public async getCalendarSheets(): Promise< + [TuiCalendarSheetPO, ...TuiCalendarSheetPO[]] + > { const locators = await this.host .page() .locator('tui-calendar-sheet, tui-mobile-calendar-sheet') .all(); - return locators.map((x) => new TuiCalendarSheetPO(x)); + return locators.map((x) => new TuiCalendarSheetPO(x)) as [ + TuiCalendarSheetPO, + ...TuiCalendarSheetPO[], + ]; } } diff --git a/projects/demo/src/modules/components/mobile-calendar/examples/2/index.html b/projects/demo/src/modules/components/mobile-calendar/examples/2/index.html index cc76cca398e5..8633812836bd 100644 --- a/projects/demo/src/modules/components/mobile-calendar/examples/2/index.html +++ b/projects/demo/src/modules/components/mobile-calendar/examples/2/index.html @@ -2,6 +2,5 @@ diff --git a/projects/demo/src/modules/components/mobile-calendar/examples/2/index.ts b/projects/demo/src/modules/components/mobile-calendar/examples/2/index.ts index deb96ff91b21..9a1f4a11aaad 100644 --- a/projects/demo/src/modules/components/mobile-calendar/examples/2/index.ts +++ b/projects/demo/src/modules/components/mobile-calendar/examples/2/index.ts @@ -3,6 +3,7 @@ import {changeDetection} from '@demo/emulate/change-detection'; import {encapsulation} from '@demo/emulate/encapsulation'; import {TuiMobileCalendar} from '@taiga-ui/addon-mobile'; import {TuiDay} from '@taiga-ui/cdk'; +import {tuiCalendarSheetOptionsProvider} from '@taiga-ui/core'; @Component({ standalone: true, @@ -11,6 +12,7 @@ import {TuiDay} from '@taiga-ui/cdk'; styleUrls: ['./index.less'], encapsulation, changeDetection, + providers: [tuiCalendarSheetOptionsProvider({rangeMode: true})], }) export default class Example { protected min = new TuiDay(new Date().getFullYear(), new Date().getMonth(), 1); diff --git a/projects/demo/src/modules/components/mobile-calendar/examples/import/template.md b/projects/demo/src/modules/components/mobile-calendar/examples/import/template.md index 61769426df3f..dab883b09668 100644 --- a/projects/demo/src/modules/components/mobile-calendar/examples/import/template.md +++ b/projects/demo/src/modules/components/mobile-calendar/examples/import/template.md @@ -2,7 +2,6 @@ ``` diff --git a/projects/demo/src/modules/components/mobile-calendar/index.html b/projects/demo/src/modules/components/mobile-calendar/index.html index 39d736dd4d9b..57fc7bf40d83 100644 --- a/projects/demo/src/modules/components/mobile-calendar/index.html +++ b/projects/demo/src/modules/components/mobile-calendar/index.html @@ -121,14 +121,6 @@ > Min date - - Single date or a range - "Confirm" button clicked + + Single date or a range + +

+ Use + tuiCalendarSheetOptionsProvider({rangeMode: boolean"}) + instead +

+
diff --git a/projects/demo/src/modules/components/mobile-calendar/index.less b/projects/demo/src/modules/components/mobile-calendar/index.less index 1c577cfeef28..b69c1585b79b 100644 --- a/projects/demo/src/modules/components/mobile-calendar/index.less +++ b/projects/demo/src/modules/components/mobile-calendar/index.less @@ -1,3 +1,3 @@ .calendar { - block-size: 28.75rem; + block-size: min(28.75rem, 60vh); } diff --git a/projects/kit/components/calendar-range/calculate-disabled-item-handler.ts b/projects/kit/components/calendar-range/calculate-disabled-item-handler.ts index 970ad1f909a1..a96449f92141 100644 --- a/projects/kit/components/calendar-range/calculate-disabled-item-handler.ts +++ b/projects/kit/components/calendar-range/calculate-disabled-item-handler.ts @@ -1,20 +1,21 @@ -import type {TuiDay, TuiDayLike, TuiDayRange} from '@taiga-ui/cdk/date-time'; +import type {TuiDay, TuiDayLike} from '@taiga-ui/cdk/date-time'; +import {TuiDayRange} from '@taiga-ui/cdk/date-time'; import type {TuiBooleanHandler} from '@taiga-ui/cdk/types'; export const calculateDisabledItemHandler: ( disabledItemHandler: TuiBooleanHandler, - value: TuiDayRange | null, + value: TuiDay | TuiDayRange | null, minLength: TuiDayLike | null, ) => TuiBooleanHandler = (disabledItemHandler, value, minLength) => (item) => { - if (!value?.isSingleDay || !minLength) { + if (!value || value instanceof TuiDayRange || !minLength) { return disabledItemHandler(item); } const negativeMinLength = Object.fromEntries( Object.entries(minLength).map(([key, value]) => [key, -value]), ); - const disabledBefore = value.from.append(negativeMinLength).append({day: 1}); - const disabledAfter = value.from.append(minLength).append({day: -1}); + const disabledBefore = value.append(negativeMinLength).append({day: 1}); + const disabledAfter = value.append(minLength).append({day: -1}); const inDisabledRange = disabledBefore.dayBefore(item) && disabledAfter.dayAfter(item); diff --git a/projects/kit/components/calendar-range/calendar-range.component.ts b/projects/kit/components/calendar-range/calendar-range.component.ts index 37942240e629..a809874e3100 100644 --- a/projects/kit/components/calendar-range/calendar-range.component.ts +++ b/projects/kit/components/calendar-range/calendar-range.component.ts @@ -10,10 +10,11 @@ import { } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {TUI_FALSE_HANDLER} from '@taiga-ui/cdk/constants'; -import type {TuiDay, TuiDayLike} from '@taiga-ui/cdk/date-time'; +import type {TuiDayLike} from '@taiga-ui/cdk/date-time'; import { TUI_FIRST_DAY, TUI_LAST_DAY, + TuiDay, TuiDayRange, TuiMonth, } from '@taiga-ui/cdk/date-time'; @@ -22,7 +23,10 @@ import {TuiMapperPipe} from '@taiga-ui/cdk/pipes/mapper'; import type {TuiBooleanHandler, TuiMapper} from '@taiga-ui/cdk/types'; import {tuiIsString, tuiNullableSame, tuiPure} from '@taiga-ui/cdk/utils/miscellaneous'; import type {TuiMarkerHandler} from '@taiga-ui/core/components/calendar'; -import {TuiCalendar} from '@taiga-ui/core/components/calendar'; +import { + TuiCalendar, + tuiCalendarSheetOptionsProvider, +} from '@taiga-ui/core/components/calendar'; import {TuiDataList} from '@taiga-ui/core/components/data-list'; import {TuiIcon} from '@taiga-ui/core/components/icon'; import {TUI_COMMON_ICONS} from '@taiga-ui/core/tokens'; @@ -40,6 +44,7 @@ import type {TuiDayRangePeriod} from './day-range-period'; templateUrl: './calendar-range.template.html', styleUrls: ['./calendar-range.style.less'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [tuiCalendarSheetOptionsProvider({rangeMode: true})], host: { '(document:keydown.capture)': 'onEsc($event)', }, @@ -50,7 +55,8 @@ export class TuiCalendarRange implements OnInit, OnChanges { */ private selectedPeriod: TuiDayRangePeriod | null = null; - protected previousValue: TuiDayRange | null = null; + protected value: TuiDay | TuiDayRange | null = null; + protected previousValue: TuiDay | TuiDayRange | null = null; protected hoveredItem: TuiDay | null = null; protected month: TuiMonth = TuiMonth.currentLocal(); @@ -79,9 +85,6 @@ export class TuiCalendarRange implements OnInit, OnChanges { @Input() public maxLength: TuiDayLike | null = null; - @Input() - public value: TuiDayRange | null = null; - @Input() public item: TuiDayRangePeriod | null = null; @@ -100,6 +103,11 @@ export class TuiCalendarRange implements OnInit, OnChanges { }); } + @Input('value') + public set valueSetter(value: TuiDayRange | null) { + this.value = value; + } + @Input() public set defaultViewedMonth(month: TuiMonth) { if (!this.value) { @@ -144,7 +152,7 @@ export class TuiCalendarRange implements OnInit, OnChanges { } protected onEsc(event: KeyboardEvent): void { - if (event.key !== 'Escape' || !this.value?.isSingleDay) { + if (event.key !== 'Escape' || !(this.value instanceof TuiDay)) { return; } @@ -212,14 +220,14 @@ export class TuiCalendarRange implements OnInit, OnChanges { this.previousValue = this.value; this.selectedActivePeriod = null; - if (!this.value?.isSingleDay) { - this.value = new TuiDayRange(day, day); - this.itemChange.emit(this.findItemByDayRange(this.value)); - } else { - const sortedDayRange = TuiDayRange.sort(this.value.from, day); + if (this.value instanceof TuiDay) { + const range = TuiDayRange.sort(this.value, day); - this.updateValue(sortedDayRange); - this.itemChange.emit(this.findItemByDayRange(sortedDayRange)); + this.value = range; + this.updateValue(range); + this.itemChange.emit(this.findItemByDayRange(range)); + } else { + this.value = day; } } @@ -234,7 +242,9 @@ export class TuiCalendarRange implements OnInit, OnChanges { this.selectedActivePeriod ?? (this.items.find((item) => tuiNullableSame( - this.value, + this.value instanceof TuiDay + ? new TuiDayRange(this.value, this.value) + : this.value, item.range, (a, b) => a.from.daySame(b.from.dayLimit(this.min, this.max)) && @@ -248,14 +258,16 @@ export class TuiCalendarRange implements OnInit, OnChanges { @tuiPure private calculateDisabledItemHandler( disabledItemHandler: TuiBooleanHandler, - value: TuiDayRange | null, + value: TuiDay | TuiDayRange | null, minLength: TuiDayLike | null, ): TuiBooleanHandler { return calculateDisabledItemHandler(disabledItemHandler, value, minLength); } private initDefaultViewedMonth(): void { - if (this.value) { + if (this.value instanceof TuiDay) { + this.month = this.value; + } else if (this.value) { this.month = this.items.length ? this.value.to : this.value.from; } else if (this.max && this.month.monthSameOrAfter(this.max)) { this.month = this.items.length ? this.max : this.max.append({month: -1}); diff --git a/projects/kit/components/calendar-range/day-caps-mapper.ts b/projects/kit/components/calendar-range/day-caps-mapper.ts index b68eff5d9651..62dde2a22575 100644 --- a/projects/kit/components/calendar-range/day-caps-mapper.ts +++ b/projects/kit/components/calendar-range/day-caps-mapper.ts @@ -1,12 +1,12 @@ -import type {TuiDay, TuiDayLike, TuiDayRange} from '@taiga-ui/cdk/date-time'; -import {TUI_FIRST_DAY, TUI_LAST_DAY} from '@taiga-ui/cdk/date-time'; +import type {TuiDayLike, TuiDayRange} from '@taiga-ui/cdk/date-time'; +import {TUI_FIRST_DAY, TUI_LAST_DAY, TuiDay} from '@taiga-ui/cdk/date-time'; import type {TuiMapper} from '@taiga-ui/cdk/types'; export const TUI_DAY_CAPS_MAPPER: TuiMapper< - [TuiDay | null, TuiDayRange | null, TuiDayLike | null, boolean], + [TuiDay | null, TuiDay | TuiDayRange | null, TuiDayLike | null, boolean], TuiDay > = (current, value, maxLength, backwards) => { - if (!value?.isSingleDay || !maxLength) { + if (value instanceof TuiDay || !value?.isSingleDay || !maxLength) { return backwards ? current || TUI_FIRST_DAY : current || TUI_LAST_DAY; }