From 14c5dfb0761bfb888b66f1bd44258890c64a518d Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Tue, 18 Feb 2025 15:24:05 +0300 Subject: [PATCH] fix(kit): improve caret management for `InputNumber` on step action --- projects/cdk/utils/dom/value-binding.ts | 14 +++ .../kit/input-number/input-number.pw.spec.ts | 90 +++++++++++++++++++ .../input-number/input-number.component.ts | 14 ++- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/projects/cdk/utils/dom/value-binding.ts b/projects/cdk/utils/dom/value-binding.ts index a500b2ee001b..5c2c36c12941 100644 --- a/projects/cdk/utils/dom/value-binding.ts +++ b/projects/cdk/utils/dom/value-binding.ts @@ -19,7 +19,21 @@ export function tuiValueBinding( const el = tuiInjectElement(); effect(() => { + if (el.value === value()) { + return; + } + + const {selectionStart, selectionEnd} = el; + el.value = value(); + + if (el.matches(':focus')) { + /** + * After programmatic updates of input's value, caret is automatically placed at the end – + * revert to the previous position + */ + el.setSelectionRange(selectionStart, selectionEnd); + } }); return value; diff --git a/projects/demo-playwright/tests/kit/input-number/input-number.pw.spec.ts b/projects/demo-playwright/tests/kit/input-number/input-number.pw.spec.ts index 2cc26c26a685..b550996cb7b3 100644 --- a/projects/demo-playwright/tests/kit/input-number/input-number.pw.spec.ts +++ b/projects/demo-playwright/tests/kit/input-number/input-number.pw.spec.ts @@ -382,6 +382,96 @@ describe('InputNumber', () => { }); }); }); + + describe('caret position on step action', () => { + beforeEach(async ({page}) => { + await tuiGoto(page, `${DemoRoute.InputNumber}/API?step=1&postfix=kg`); + + await expect(inputNumber.textfield).toHaveValue(''); + await expect(inputNumber.textfield).not.toBeFocused(); + }); + + test('Empty unfocused textfield => Click + => Textfield is focused & Caret is placed before postfix', async () => { + await inputNumber.stepUp.click(); + + await expect(inputNumber.textfield).toHaveValue('1kg'); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionStart', + 1, + ); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionEnd', + 1, + ); + }); + + test('Focused textfield with postfix only => Press ArrowDown => Caret is placed before postfix', async () => { + await inputNumber.textfield.focus(); + await expect(inputNumber.textfield).toHaveValue('kg'); + await inputNumber.textfield.press('ArrowDown'); + + await expect(inputNumber.textfield).toHaveValue(`${CHAR_MINUS}1kg`); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionStart', + 2, + ); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionEnd', + 2, + ); + }); + + describe('Keeps caret position on step', () => { + beforeEach(async () => { + await inputNumber.textfield.fill('42'); + + await expect(inputNumber.textfield).toHaveValue('42kg'); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionStart', + 2, + ); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionEnd', + 2, + ); + await inputNumber.textfield.press('ArrowLeft'); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionStart', + 1, + ); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionEnd', + 1, + ); + }); + + test('via button', async () => { + await inputNumber.stepUp.click(); + await expect(inputNumber.textfield).toHaveValue('43kg'); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionStart', + 1, + ); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionEnd', + 1, + ); + }); + + test('via keyboard arrow', async () => { + await inputNumber.textfield.press('ArrowUp'); + await expect(inputNumber.textfield).toHaveValue('43kg'); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionStart', + 1, + ); + await expect(inputNumber.textfield).toHaveJSProperty( + 'selectionEnd', + 1, + ); + }); + }); + }); }); describe('[prefix] & [postfix] props', () => { diff --git a/projects/kit/components/input-number/input-number.component.ts b/projects/kit/components/input-number/input-number.component.ts index 2c62314e741c..54ccf49ac575 100644 --- a/projects/kit/components/input-number/input-number.component.ts +++ b/projects/kit/components/input-number/input-number.component.ts @@ -202,11 +202,17 @@ export class TuiInputNumber extends TuiControl { } protected onStep(step: number): void { - this.textfieldValue.set( - this.formatNumber( - tuiClamp((this.value() ?? 0) + step, this.min(), this.max()), - ), + const newValue = this.formatNumber( + tuiClamp((this.value() ?? 0) + step, this.min(), this.max()), ); + + if (this.value() === null) { + const caretIndex = newValue.length - this.postfix().length; + + setTimeout(() => this.element.setSelectionRange(caretIndex, caretIndex)); + } + + this.textfieldValue.set(newValue); } private formatNumber(value: number | null): string {