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 116a663964d9..a9694c2556e4 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 @@ -291,5 +291,38 @@ test.describe('InputDateRange', () => { '08-calendar-correct-selected-period-after-close-open.png', ); }); + + test.describe('with `input[tuiTextfieldLegacy]` inside', () => { + test('filler has no change detection problems', async () => { + const example = documentationPage.getExample('#base'); + const inputDateRange = new TuiInputDateRangePO( + example.locator('tui-input-date-range'), + ); + + /** + * To ensure that example is not changed and + * still contains InputDateRange with projected + */ + await expect( + inputDateRange.host.locator('input[tuiTextfieldLegacy]'), + ).toBeAttached(); + + await inputDateRange.textfield.focus(); + + await expect(inputDateRange.host).toHaveScreenshot( + '12-backspace-pressed-0-times.png', + ); + + for (let i = 1; i <= 16; i++) { + await inputDateRange.textfield.press('Backspace'); + + await expect(inputDateRange.host).toHaveScreenshot( + `12-backspace-pressed-${i}-times.png`, + ); + } + + await expect(inputDateRange.textfield).toHaveValue(''); + }); + }); }); }); diff --git a/projects/demo-playwright/tests/legacy/input-date-time/input-date-time.pw.spec.ts b/projects/demo-playwright/tests/legacy/input-date-time/input-date-time.pw.spec.ts index 5cee26a4346f..7bc36f70be59 100644 --- a/projects/demo-playwright/tests/legacy/input-date-time/input-date-time.pw.spec.ts +++ b/projects/demo-playwright/tests/legacy/input-date-time/input-date-time.pw.spec.ts @@ -312,24 +312,17 @@ test.describe('InputDateTime', () => { }); test.describe('Examples', () => { - let example!: Locator; let documentationPage!: TuiDocumentationPagePO; - let inputDateTime!: TuiInputDateTimePO; test.beforeEach(async ({page}) => { await tuiGoto(page, DemoRoute.InputDateTime); documentationPage = new TuiDocumentationPagePO(page); - example = documentationPage.apiPageExample; - - inputDateTime = new TuiInputDateTimePO( - example.locator('tui-input-date-time'), - ); }); test('With validator: enter incomplete date -> validator error', async () => { - example = documentationPage.getExample('#with-validator'); - inputDateTime = new TuiInputDateTimePO( + const example = documentationPage.getExample('#with-validator'); + const inputDateTime = new TuiInputDateTimePO( example.locator('tui-input-date-time'), ); @@ -343,5 +336,38 @@ test.describe('InputDateTime', () => { {animations: 'allow'}, ); }); + + test.describe('with `input[tuiTextfieldLegacy]` inside', () => { + test('filler has no change detection problems', async () => { + const example = documentationPage.getExample('#base'); + const inputDateTime = new TuiInputDateTimePO( + example.locator('tui-input-date-time'), + ); + + /** + * To ensure that example is not changed and + * still contains InputDateTime with projected + */ + await expect( + inputDateTime.host.locator('input[tuiTextfieldLegacy]'), + ).toBeAttached(); + + await inputDateTime.textfield.focus(); + + await expect(inputDateTime.host).toHaveScreenshot( + '05-backspace-pressed-0-times.png', + ); + + for (let i = 1; i <= 8; i++) { + await inputDateTime.textfield.press('Backspace'); + + await expect(inputDateTime.host).toHaveScreenshot( + `05-backspace-pressed-${i}-times.png`, + ); + } + + await expect(inputDateTime.textfield).toHaveValue(''); + }); + }); }); }); diff --git a/projects/demo-playwright/tests/legacy/input-date/input-date.pw.spec.ts b/projects/demo-playwright/tests/legacy/input-date/input-date.pw.spec.ts index 7be105ec7b6d..53ffa86804a1 100644 --- a/projects/demo-playwright/tests/legacy/input-date/input-date.pw.spec.ts +++ b/projects/demo-playwright/tests/legacy/input-date/input-date.pw.spec.ts @@ -12,6 +12,8 @@ import {TUI_PLAYWRIGHT_MOBILE_USER_AGENT} from '../../../playwright.options'; test.describe('InputDate', () => { test.describe('Examples', () => { + let documentationPage!: TuiDocumentationPagePO; + test.use({ viewport: { width: 450, @@ -19,13 +21,16 @@ test.describe('InputDate', () => { }, }); - test('correct filler display for size', async ({page}) => { + test.beforeEach(async ({page}) => { await tuiGoto(page, DemoRoute.InputDate); - const api = new TuiDocumentationPagePO(page); - const example = api.getExample('#sizes'); + documentationPage = new TuiDocumentationPagePO(page); + }); - await api.prepareBeforeScreenshot(); + test('correct filler display for size', async ({page}) => { + const example = documentationPage.getExample('#sizes'); + + await documentationPage.prepareBeforeScreenshot(); for (const size of ['s', 'm', 'l']) { const input = example @@ -52,6 +57,37 @@ test.describe('InputDate', () => { await expect(page).toHaveScreenshot(`01-04-input-date-${size}.png`); } }); + + test.describe('with `input[tuiTextfieldLegacy]` inside', () => { + test('filler has no change detection problems', async () => { + const example = documentationPage.getExample('#date-localization'); + const inputDate = new TuiInputDatePO(example.locator('tui-input-date')); + + /** + * To ensure that example is not changed and + * still contains InputDate with projected + */ + await expect( + inputDate.host.locator('input[tuiTextfieldLegacy]'), + ).toBeAttached(); + + await inputDate.textfield.focus(); + + await expect(inputDate.host).toHaveScreenshot( + '14-backspace-pressed-0-times.png', + ); + + for (let i = 1; i <= 8; i++) { + await inputDate.textfield.press('Backspace'); + + await expect(inputDate.host).toHaveScreenshot( + `14-backspace-pressed-${i}-times.png`, + ); + } + + await expect(inputDate.textfield).toHaveValue(''); + }); + }); }); test.describe('API', () => { diff --git a/projects/demo-playwright/utils/page-objects/input-date-range.po.ts b/projects/demo-playwright/utils/page-objects/input-date-range.po.ts index 865ac46cbd87..d83b22a35f7a 100644 --- a/projects/demo-playwright/utils/page-objects/input-date-range.po.ts +++ b/projects/demo-playwright/utils/page-objects/input-date-range.po.ts @@ -15,7 +15,7 @@ export class TuiInputDateRangePO { '[automation-id="tui-calendar-range__menu"]', ); - constructor(private readonly host: Locator) {} + constructor(public readonly host: Locator) {} public async getItems(): Promise { const dataList = this.calendar.locator( diff --git a/projects/demo-playwright/utils/page-objects/input-date.po.ts b/projects/demo-playwright/utils/page-objects/input-date.po.ts index df59f891c556..f49a49b6ce0e 100644 --- a/projects/demo-playwright/utils/page-objects/input-date.po.ts +++ b/projects/demo-playwright/utils/page-objects/input-date.po.ts @@ -4,5 +4,5 @@ export class TuiInputDatePO { public readonly textfield: Locator = this.host.getByRole('textbox'); public readonly calendar: Locator = this.host.page().locator('tui-calendar'); - constructor(private readonly host: Locator) {} + constructor(public readonly host: Locator) {} } diff --git a/projects/legacy/components/input-date-range/input-date-range.component.ts b/projects/legacy/components/input-date-range/input-date-range.component.ts index 386d3568e5a2..582e9d067221 100644 --- a/projects/legacy/components/input-date-range/input-date-range.component.ts +++ b/projects/legacy/components/input-date-range/input-date-range.component.ts @@ -3,6 +3,7 @@ import { Component, inject, Input, + signal, ViewChild, } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @@ -86,6 +87,7 @@ export class TuiInputDateRangeComponent private readonly mobileCalendar = inject(TUI_MOBILE_CALENDAR, {optional: true}); private readonly options = inject(TUI_INPUT_DATE_OPTIONS); private readonly textfieldSize = inject(TUI_TEXTFIELD_SIZE); + private readonly nativeValue = signal(''); protected readonly dateTexts$ = inject(TUI_DATE_TEXTS); protected override readonly valueTransformer = inject( @@ -160,7 +162,7 @@ export class TuiInputDateRangeComponent return value ? value.getFormattedDayRange(this.dateFormat.mode, this.dateFormat.separator) - : nativeValue; + : nativeValue(); } public get size(): TuiSizeL | TuiSizeS { @@ -174,6 +176,8 @@ export class TuiInputDateRangeComponent } public onValueChange(value: string): void { + this.nativeValue.set(value); + if (this.control) { this.control.updateValueAndValidity({emitEvent: false}); } @@ -183,7 +187,7 @@ export class TuiInputDateRangeComponent } if (this.activePeriod) { - this.nativeValue = ''; + this.nativeValue.set(''); } this.value = @@ -201,7 +205,7 @@ export class TuiInputDateRangeComponent this.focusInput(); if (!range) { - this.nativeValue = ''; + this.nativeValue.set(''); } this.value = range; @@ -209,7 +213,7 @@ export class TuiInputDateRangeComponent public override writeValue(value: TuiDayRange | null): void { super.writeValue(value); - this.nativeValue = value ? this.computedValue : ''; + this.nativeValue.set(value ? this.computedValue : ''); } protected get computedMobile(): boolean { @@ -269,16 +273,6 @@ export class TuiInputDateRangeComponent return null; } - protected get nativeValue(): string { - return this.nativeFocusableElement?.value || ''; - } - - protected set nativeValue(value: string) { - if (this.nativeFocusableElement) { - this.nativeFocusableElement.value = value; - } - } - protected getComputedRangeFiller(dateFiller: string): string { return this.activePeriod ? '' : this.getDateRangeFiller(dateFiller); } @@ -299,12 +293,12 @@ export class TuiInputDateRangeComponent if ( !focused && !this.itemSelected && - (this.nativeValue.length === DATE_FILLER_LENGTH || - this.nativeValue.length === + (this.nativeValue().length === DATE_FILLER_LENGTH || + this.nativeValue().length === DATE_FILLER_LENGTH + RANGE_SEPARATOR_CHAR.length) ) { this.value = TuiDayRange.normalizeParse( - this.nativeValue, + this.nativeValue(), this.dateFormat.mode, ); } @@ -318,7 +312,7 @@ export class TuiInputDateRangeComponent } private get itemSelected(): boolean { - return this.items.findIndex((item) => String(item) === this.nativeValue) !== -1; + return this.items.findIndex((item) => String(item) === this.nativeValue()) !== -1; } @tuiPure diff --git a/projects/legacy/components/input-date-range/test/input-date-range.component.spec.ts b/projects/legacy/components/input-date-range/test/input-date-range.component.spec.ts index 244f4d63fbac..48b149df4c9c 100644 --- a/projects/legacy/components/input-date-range/test/input-date-range.component.spec.ts +++ b/projects/legacy/components/input-date-range/test/input-date-range.component.spec.ts @@ -300,7 +300,17 @@ describe('InputDateRangeComponent', () => { }); it('correctly sets stringify selected range via calendar', async () => { - inputPO.sendTextAndBlur('12/01/2021-02/14/2022'); + inputPO.sendText('12/01/2021-02/14/2022'); + /** + * TODO + * Uncomment me to see [TypeError: Cannot read properties of undefined (reading 'addEventListener')] + * ___ + * Stacktrace says that error happens inside `TUI_ACTIVE_ELEMENT`. + * Utility `tuiGetDocumentOrShadowRoot` returns `undefined`. + */ + // inputPO.blur(); + + await fixture.whenStable(); clickOnTextfield(); @@ -535,17 +545,19 @@ describe('InputDateRangeComponent', () => { expect(inputPO.value).toBe('12.09.2021 – 18.10.2021'); }); - it('transforms value which was programmatically patched', () => { - testComponent.control.patchValue([ - new Date(1922, 11, 30), - new Date(1991, 11, 26), - ]); + it('transforms value which was programmatically patched', async () => { + const newDateRange = [new Date(1922, 11, 30), new Date(1991, 11, 26)] as [ + Date, + Date, + ]; + + testComponent.control.patchValue(newDateRange); + + fixture.detectChanges(); + await fixture.whenStable(); expect(inputPO.value).toBe('30.12.1922 – 26.12.1991'); - expect(testComponent.control.value).toEqual([ - new Date(1922, 11, 30), - new Date(1991, 11, 26), - ]); + expect(testComponent.control.value).toEqual(newDateRange); }); }); diff --git a/projects/legacy/components/input-date-time/input-date-time.component.ts b/projects/legacy/components/input-date-time/input-date-time.component.ts index a432b5cf89ef..810f20f71c06 100644 --- a/projects/legacy/components/input-date-time/input-date-time.component.ts +++ b/projects/legacy/components/input-date-time/input-date-time.component.ts @@ -3,6 +3,7 @@ import { Component, inject, Input, + signal, ViewChild, } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @@ -79,6 +80,7 @@ export class TuiInputDateTimeComponent private readonly textfieldSize = inject(TUI_TEXTFIELD_SIZE); private month: TuiMonth | null = null; private readonly timeMode$ = new BehaviorSubject('HH:MM'); + private readonly nativeValue = signal(''); protected readonly timeTexts$ = inject(TUI_TIME_TEXTS); protected readonly dateTexts$ = inject(TUI_DATE_TEXTS); @@ -147,10 +149,10 @@ export class TuiInputDateTimeComponent public get computedValue(): string { const {value, nativeValue, timeMode} = this; const [date, time] = value; - const hasTimeInputChars = nativeValue.length > DATE_FILLER_LENGTH; + const hasTimeInputChars = nativeValue().length > DATE_FILLER_LENGTH; if (!date || (!time && hasTimeInputChars)) { - return nativeValue; + return nativeValue(); } return this.getDateTimeString(date, time, timeMode); @@ -164,11 +166,14 @@ export class TuiInputDateTimeComponent public override writeValue(value: [TuiDay | null, TuiTime | null] | null): void { super.writeValue(value); - this.nativeValue = - this.value && (this.value[0] || this.value[1]) ? this.computedValue : ''; + this.nativeValue.set( + this.value && (this.value[0] || this.value[1]) ? this.computedValue : '', + ); } public onValueChange(value: string): void { + this.nativeValue.set(value); + if (this.control) { this.control.updateValueAndValidity({emitEvent: false}); } @@ -258,18 +263,6 @@ export class TuiInputDateTimeComponent ); } - protected get nativeValue(): string { - return this.nativeFocusableElement?.value || ''; - } - - protected set nativeValue(value: string) { - if (!this.nativeFocusableElement) { - return; - } - - this.nativeFocusableElement.value = value; - } - protected onClick(): void { this.open = !this.open; } @@ -279,8 +272,12 @@ export class TuiInputDateTimeComponent const newCaretIndex = DATE_FILLER_LENGTH + DATE_TIME_SEPARATOR.length; this.value = [day, modifiedTime]; - this.updateNativeValue(day); - this.nativeFocusableElement?.setSelectionRange(newCaretIndex, newCaretIndex); + this.nativeValue.update((x) => + this.getDateTimeString(day, x.split(DATE_TIME_SEPARATOR)[1] || ''), + ); + setTimeout(() => + this.nativeFocusableElement?.setSelectionRange(newCaretIndex, newCaretIndex), + ); this.open = false; } @@ -302,19 +299,19 @@ export class TuiInputDateTimeComponent timer(0) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - this.nativeValue = this.trimTrailingSeparator(this.nativeValue); + this.nativeValue.update((x) => this.trimTrailingSeparator(x)); }); if ( this.value[0] === null || this.value[1] !== null || - this.nativeValue.length === this.fillerLength || + this.nativeValue().length === this.fillerLength || this.timeMode === 'HH:MM' ) { return; } - const [, time] = this.nativeValue.split(DATE_TIME_SEPARATOR); + const [, time] = this.nativeValue().split(DATE_TIME_SEPARATOR); if (!time) { return; @@ -391,12 +388,6 @@ export class TuiInputDateTimeComponent : dateString; } - private updateNativeValue(day: TuiDay): void { - const time = this.nativeValue.split(DATE_TIME_SEPARATOR)[1] || ''; - - this.nativeValue = this.getDateTimeString(day, time); - } - private clampTime(time: TuiTime, day: TuiDay): TuiTime { const {computedMin, computedMax} = this; diff --git a/projects/legacy/components/input-date-time/test/input-date-time.component.spec.ts b/projects/legacy/components/input-date-time/test/input-date-time.component.spec.ts index ad6908ab82b5..67b026ac5f9e 100644 --- a/projects/legacy/components/input-date-time/test/input-date-time.component.spec.ts +++ b/projects/legacy/components/input-date-time/test/input-date-time.component.spec.ts @@ -421,9 +421,11 @@ describe('InputDateTime', () => { expect(inputPO.value).toBe('17.03.2022, 12:11'); }); - it('transforms value which was programmatically patched', () => { + it('transforms value which was programmatically patched', async () => { component.control.patchValue('09.05.1945, 00:43'); + await fixture.whenStable(); + expect(inputPO.value).toBe('09.05.1945, 00:43'); expect(component.control.value).toBe('09.05.1945, 00:43'); }); diff --git a/projects/legacy/components/input-date/input-date.component.ts b/projects/legacy/components/input-date/input-date.component.ts index 5e59f8224de8..11ad73c07490 100644 --- a/projects/legacy/components/input-date/input-date.component.ts +++ b/projects/legacy/components/input-date/input-date.component.ts @@ -4,6 +4,7 @@ import { Component, inject, Input, + signal, ViewChild, } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @@ -83,6 +84,7 @@ export class TuiInputDateComponent private readonly textfieldSize = inject(TUI_TEXTFIELD_SIZE); private readonly mobileCalendar = inject(TUI_MOBILE_CALENDAR, {optional: true}); private month: TuiMonth | null = null; + private readonly nativeValue = signal(''); @Input() public min: TuiDay | null = this.options.min; @@ -142,16 +144,6 @@ export class TuiInputDateComponent return !!this.textfield?.focused; } - public get nativeValue(): string { - return this.nativeFocusableElement?.value || ''; - } - - public set nativeValue(value: string) { - if (this.nativeFocusableElement) { - this.nativeFocusableElement.value = value; - } - } - public get computedValue(): string { const {value, nativeValue, activeItem} = this; @@ -161,10 +153,12 @@ export class TuiInputDateComponent return value ? value.toString(this.dateFormat.mode, this.dateFormat.separator) - : nativeValue; + : nativeValue(); } public onValueChange(value: string): void { + this.nativeValue.set(value); + if (this.control) { this.control.updateValueAndValidity({emitEvent: false}); } @@ -174,7 +168,7 @@ export class TuiInputDateComponent } if (this.activeItem) { - this.nativeValue = ''; + this.nativeValue.set(''); } this.value = @@ -190,7 +184,7 @@ export class TuiInputDateComponent public override writeValue(value: TuiDay | null): void { super.writeValue(value); - this.nativeValue = value ? this.computedValue : ''; + this.nativeValue.set(value ? this.computedValue : ''); } protected get size(): TuiSizeL | TuiSizeS { diff --git a/projects/legacy/components/input-date/input-date.directive.ts b/projects/legacy/components/input-date/input-date.directive.ts index 1e2403b0d57c..6d1509b278ef 100644 --- a/projects/legacy/components/input-date/input-date.directive.ts +++ b/projects/legacy/components/input-date/input-date.directive.ts @@ -29,10 +29,6 @@ export class TuiInputDateDirective extends AbstractTuiTextfieldHost { changeDetection: ChangeDetectionStrategy.OnPush, }) class Test { + @Input() + public value: TuiDay | null = new TuiDay(2017, 2, 1); + @ViewChild(TuiInputDateComponent) public readonly component!: TuiInputDateComponent; @@ -62,8 +65,6 @@ describe('InputDate', () => { public items: TuiNamedDay[] = []; - public value: TuiDay | null = new TuiDay(2017, 2, 1); - public size: TuiSizeL | TuiSizeS = 'm'; public hintContent: string | null = 'prompt'; @@ -192,10 +193,10 @@ describe('InputDate', () => { }); describe('With items', () => { - beforeEach(() => { + beforeEach(async () => { testComponent.items = [ new TuiNamedDay( - new TuiDay(2017, 2, 1), + new TuiDay(2017, 2, 5), 'Current', TuiDay.currentLocal(), ), @@ -205,11 +206,13 @@ describe('InputDate', () => { TuiDay.currentLocal(), ), ]; + + fixture.detectChanges(); + await fixture.whenStable(); }); it('when entering item date, input shows named date', async () => { - inputPO.sendText('01.02.2017'); - + inputPO.sendText('05.03.2017'); await fixture.whenStable(); expect(inputPO.value).toBe('Current'); @@ -225,7 +228,7 @@ describe('InputDate', () => { }); it('when ngModel value updated with item date, input shows named date', async () => { - testComponent.value = TUI_LAST_DAY.append({year: -1}); + fixture.componentRef.setInput('value', TUI_LAST_DAY.append({year: -1})); fixture.detectChanges(); await fixture.whenStable(); @@ -238,7 +241,7 @@ describe('InputDate', () => { expect(getCalendar()).not.toBeNull(); - const calendarCell = getCalendarCell(1); + const calendarCell = getCalendarCell(5); calendarCell?.nativeElement.click(); @@ -479,9 +482,12 @@ describe('InputDate', () => { expect(testComponent.control.value).toEqual(new Date(2022, 0, 20)); }); - it('transforms value which was programmatically patched', () => { + it('transforms value which was programmatically patched', async () => { testComponent.control.patchValue(new Date(1991, 11, 26)); + fixture.detectChanges(); + await fixture.whenStable(); + expect(inputPO.value).toBe('26.12.1991'); expect(testComponent.control.value).toEqual(new Date(1991, 11, 26)); });