diff --git a/projects/demo-cypress/src/tests/input-number/dynamic-postfix.cy.ts b/projects/demo-cypress/src/tests/input-number/dynamic-postfix.cy.ts
index 09e1237d9b1d..8a497fc203db 100644
--- a/projects/demo-cypress/src/tests/input-number/dynamic-postfix.cy.ts
+++ b/projects/demo-cypress/src/tests/input-number/dynamic-postfix.cy.ts
@@ -3,24 +3,21 @@ import '@angular/common/locales/global/ru';
import {I18nPluralPipe} from '@angular/common';
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
-import {TuiRoot} from '@taiga-ui/core';
-import {TuiInputNumberModule, TuiTextfieldControllerModule} from '@taiga-ui/legacy';
+import {TuiRoot, TuiTextfield} from '@taiga-ui/core';
+import {TuiInputNumber} from '@taiga-ui/kit';
@Component({
standalone: true,
- imports: [
- FormsModule,
- I18nPluralPipe,
- TuiInputNumberModule,
- TuiRoot,
- TuiTextfieldControllerModule,
- ],
+ imports: [FormsModule, I18nPluralPipe, TuiInputNumber, TuiRoot, TuiTextfield],
template: `
-
+
+
+
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -28,10 +25,10 @@ import {TuiInputNumberModule, TuiTextfieldControllerModule} from '@taiga-ui/lega
export class TestInputNumberWithPostfix {
protected value: number | null = null;
protected pluralMap = {
- one: 'секунда',
- few: 'секунды',
- many: 'секунд',
- other: 'секунды',
+ one: ' секунда',
+ few: ' секунды',
+ many: ' секунд',
+ other: ' секунды',
};
}
@@ -39,7 +36,7 @@ describe('InputNumber with dynamic postfix', () => {
describe('Plural forms of seconds (locale ru-RU)', () => {
beforeEach(() => {
cy.mount(TestInputNumberWithPostfix);
- cy.get('tui-input-number input').as('textfield');
+ cy.get('[tuiInputNumber]').as('textfield');
});
const withPostfix = (
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
new file mode 100644
index 000000000000..0e51794475a2
--- /dev/null
+++ b/projects/demo-playwright/tests/kit/input-number/input-number.pw.spec.ts
@@ -0,0 +1,636 @@
+import {DemoRoute} from '@demo/routes';
+import {
+ CHAR_EM_DASH,
+ CHAR_EN_DASH,
+ CHAR_HYPHEN,
+ CHAR_MINUS,
+ CMD,
+ TuiDocumentationApiPagePO,
+ tuiGoto,
+} from '@demo-playwright/utils';
+import type {Locator} from '@playwright/test';
+import {expect, test} from '@playwright/test';
+
+const {describe, beforeEach} = test;
+
+describe('InputNumber', () => {
+ let example: Locator;
+ let textfield: Locator;
+
+ describe('API', () => {
+ beforeEach(({page}) => {
+ example = new TuiDocumentationApiPagePO(page).apiPageExample;
+ textfield = example.locator('[tuiInputNumber]');
+ });
+
+ describe('[min] prop', () => {
+ describe('[min] property is positive number', () => {
+ test('rejects minus sign', async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?min=5`);
+ await textfield.fill(
+ `${CHAR_MINUS}${CHAR_HYPHEN}${CHAR_EN_DASH}${CHAR_EM_DASH}9`,
+ );
+
+ await expect(textfield).toHaveValue('9');
+ await expect(textfield).toHaveJSProperty('selectionStart', 1);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 1);
+ });
+
+ test('validates positive value (less than [min]) only on blur', async ({
+ page,
+ }) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?min=5`);
+ await textfield.fill('2');
+
+ await expect(textfield).toHaveValue('2');
+ await expect(textfield).toHaveJSProperty('selectionStart', 1);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 1);
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue('5');
+ });
+
+ test('allows to enter multi-length positive value (which is less than [min])', async ({
+ page,
+ }) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?min=100`);
+
+ await textfield.fill('3'); // less than min
+
+ await expect(textfield).toHaveValue('3');
+
+ await textfield.pressSequentially('3'); // still less than min
+
+ await expect(textfield).toHaveValue('33');
+
+ await textfield.fill('333'); // more than min
+
+ await expect(textfield).toHaveValue('333');
+ });
+ });
+
+ describe('[min] property is negative number', () => {
+ beforeEach(async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?min=-5`);
+
+ await textfield.clear();
+ });
+
+ test('immediately validates negative value', async () => {
+ await textfield.fill('-10'); // less than [min]
+
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+ await expect(textfield).toHaveValue(`${CHAR_MINUS}5`);
+ });
+
+ test('do not touch any positive value', async ({page}) => {
+ await textfield.fill('1');
+
+ await expect(textfield).toHaveJSProperty('selectionStart', 1);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 1);
+ await expect(textfield).toHaveValue('1');
+
+ await textfield.pressSequentially('0');
+
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+ await expect(textfield).toHaveValue('10');
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue('10');
+
+ await page.waitForTimeout(100); // to be sure that value is not changed even in case of some async validation
+
+ await expect(textfield).toHaveValue('10');
+ });
+ });
+ });
+
+ describe('[max] prop', () => {
+ describe('[max] property is negative number', () => {
+ beforeEach(async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?max=-5`);
+ });
+
+ test('validates negative value only on blur', async ({page}) => {
+ await textfield.fill('-1'); // more than [max]
+ await page.waitForTimeout(100); // to be sure that value is not changed even in case of some async validation
+
+ await expect(textfield).toHaveValue(`${CHAR_MINUS}1`);
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue(`${CHAR_MINUS}5`);
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+ });
+ });
+
+ describe('[max] property is positive number', () => {
+ beforeEach(async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?max=12`);
+ });
+
+ test('immediately validates positive value', async () => {
+ await textfield.fill('19'); // more than max
+
+ await expect(textfield).toHaveValue('12');
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+ });
+
+ test('do not touch any negative value', async ({page}) => {
+ await textfield.fill('-1');
+
+ await expect(textfield).toHaveValue(`${CHAR_MINUS}1`);
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+
+ await page.keyboard.down('9');
+
+ await expect(textfield).toHaveJSProperty('selectionStart', 3);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 3);
+ await expect(textfield).toHaveValue(`${CHAR_MINUS}19`);
+
+ await page.waitForTimeout(100); // to ensure that value is not changed even in case of some async validation
+
+ await expect(textfield).toHaveValue(`${CHAR_MINUS}19`);
+ await expect(textfield).toHaveJSProperty('selectionStart', 3);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 3);
+ });
+ });
+ });
+
+ describe('[prefix] & [postfix] props', () => {
+ (
+ [
+ {prefix: '$', postfix: ''},
+ {prefix: '', postfix: 'kg'},
+ {prefix: '$', postfix: 'kg'},
+ ] as const
+ ).forEach(({prefix, postfix}) => {
+ describe(`[prefix]="${prefix}" & [postfix]="${postfix}"`, () => {
+ beforeEach(async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?prefix=${prefix}&postfix=${postfix}`,
+ );
+ });
+
+ test('does not show suffixes for unfocused empty textfield', async () => {
+ await expect(textfield).toHaveValue('');
+ });
+
+ test('shows suffixes for empty textfield on focus', async () => {
+ await textfield.focus();
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+ });
+
+ test('does not shows prefix for READONLY empty textfield on focus', async ({
+ page,
+ }) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?prefix=${prefix}&postfix=${postfix}&readOnly=true`,
+ );
+ await textfield.focus();
+
+ await expect(textfield).toHaveValue('');
+
+ await textfield.click();
+
+ await expect(textfield).toHaveValue('');
+ });
+
+ describe('forbids to erase prefix', () => {
+ test('using Backspace many times', async () => {
+ await textfield.focus();
+ await textfield.press('Backspace');
+ await textfield.press('Backspace');
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+
+ await textfield.pressSequentially('42');
+
+ await expect(textfield).toHaveValue(`${prefix}42${postfix}`);
+
+ await textfield.press('Backspace');
+ await textfield.press('Backspace');
+ await textfield.press('Backspace');
+ await textfield.press('Backspace');
+ await textfield.press('Backspace');
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+ });
+
+ test('select all + Backspace', async ({page}) => {
+ await textfield.focus();
+ await page.keyboard.press(`${CMD}+A`);
+ await page.keyboard.press('Backspace');
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+
+ await textfield.pressSequentially('42');
+
+ await expect(textfield).toHaveValue(`${prefix}42${postfix}`);
+
+ await page.keyboard.press(`${CMD}+A`);
+ await page.keyboard.press('Backspace');
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+ });
+
+ test('select all + Delete', async ({page}) => {
+ await textfield.focus();
+ await page.keyboard.press(`${CMD}+A`);
+ await page.keyboard.press('Delete');
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+
+ await textfield.pressSequentially('42');
+
+ await expect(textfield).toHaveValue(`${prefix}42${postfix}`);
+
+ await page.keyboard.press(`${CMD}+A`);
+ await page.keyboard.press('Delete');
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+ });
+ });
+
+ test('textfield does not contain any digit (only suffixes) => clear textfield value on blur', async ({
+ browserName,
+ }) => {
+ // TODO
+ test.skip(
+ browserName !== 'chromium',
+ 'Investigate why it fails in Safari',
+ );
+
+ await textfield.focus();
+
+ await expect(textfield).toHaveValue(prefix + postfix);
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ prefix.length,
+ );
+ await expect(textfield).toHaveJSProperty(
+ 'selectionEnd',
+ prefix.length,
+ );
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue('');
+ });
+ });
+ });
+ });
+
+ describe('[precision] prop', () => {
+ test('[precision]=0', async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?precision=0`);
+ await textfield.focus();
+ await textfield.pressSequentially(',.');
+
+ await expect(textfield).toHaveValue('');
+
+ await textfield.pressSequentially('0,.');
+
+ await expect(textfield).toHaveValue('0');
+ });
+
+ test('[precision]=2', async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?precision=2`);
+ await textfield.focus();
+ await textfield.pressSequentially(',.');
+
+ await expect(textfield).toHaveValue('0.');
+
+ await textfield.pressSequentially('12345');
+
+ await expect(textfield).toHaveValue('0.12');
+ });
+ });
+
+ describe('[thousandSeparator] prop', () => {
+ test('_', async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumber}/API?thousandSeparator=_`);
+ await textfield.focus();
+ await textfield.pressSequentially('1234567890');
+
+ await expect(textfield).toHaveValue('1_234_567_890');
+ });
+
+ test('.', async ({page}) => {
+ await tuiGoto(
+ page,
+ /**
+ * TODO: drop `&decimalSeparator=,` after fixing this issue
+ * https://github.com/taiga-family/maskito/issues/1907
+ */
+ `${DemoRoute.InputNumber}/API?precision=0&thousandSeparator=.&decimalSeparator=,`,
+ );
+ await textfield.focus();
+ await textfield.pressSequentially('1234567890');
+
+ await expect(textfield).toHaveValue('1.234.567.890');
+ });
+ });
+
+ describe('[decimalSeparator] prop', () => {
+ test('.', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?precision=4&decimalSeparator=.`,
+ );
+ await textfield.focus();
+ await textfield.pressSequentially('.1234567890');
+
+ await expect(textfield).toHaveValue('0.1234');
+
+ await textfield.clear();
+
+ await textfield.pressSequentially(',42');
+
+ await expect(textfield).toHaveValue('0.42');
+ });
+ });
+
+ describe('[decimalMode] prop', () => {
+ test('decimalMode=not-zero | 42 => Blur => 42', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?precision=2&decimalMode=not-zero`,
+ );
+
+ await textfield.fill('42');
+
+ await expect(textfield).toHaveValue('42');
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+ await expect(textfield).toHaveValue('42');
+ });
+
+ test('decimalMode=not-zero | 42.1 => Blur => 42.1', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?precision=2&decimalMode=not-zero`,
+ );
+
+ await textfield.fill('42.1');
+
+ await expect(textfield).toHaveValue('42.1');
+ await expect(textfield).toHaveJSProperty('selectionStart', 4);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 4);
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue('42.1');
+ });
+
+ test('decimalMode=not-zero | 42.00 => Blur => 42', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?precision=2&decimalMode=not-zero`,
+ );
+
+ await textfield.fill('42.00');
+
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ '42.00'.length,
+ );
+ await expect(textfield).toHaveJSProperty('selectionEnd', '42.00'.length);
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue('42');
+ });
+
+ test('decimalMode=pad | 42.1 => Blur => 42.10', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?precision=2&decimalMode=pad`,
+ );
+
+ await textfield.fill('42.1');
+
+ await expect(textfield).toHaveValue('42.1');
+ await expect(textfield).toHaveJSProperty('selectionStart', '42.1'.length);
+ await expect(textfield).toHaveJSProperty('selectionEnd', '42.1'.length);
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue('42.10');
+ });
+
+ test('decimalMode=always | Enter 42 => 42.00', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?precision=2&decimalMode=always`,
+ );
+ await textfield.fill('42');
+
+ await expect(textfield).toHaveValue('42.00');
+ await expect(textfield).toHaveJSProperty('selectionStart', 2);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 2);
+ });
+ });
+
+ describe('Caret navigation', () => {
+ describe('if user tries to erase padded decimal zeroes (decimalMode="always"), mask triggers caret navigation', () => {
+ beforeEach(async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?decimalMode=always&precision=2`,
+ );
+
+ await textfield.clear();
+ await textfield.fill('105.00');
+ });
+
+ test('105.00| => Backspace => 105.0|0', async ({page}) => {
+ await page.keyboard.press('Backspace');
+
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ '105.0'.length,
+ );
+ await expect(textfield).toHaveJSProperty(
+ 'selectionEnd',
+ '105.0'.length,
+ );
+ await expect(textfield).toHaveValue('105.00');
+ });
+
+ test('105.0|0 => Backspace => 105.|00', async ({page}) => {
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('Backspace');
+
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ '105.'.length,
+ );
+ await expect(textfield).toHaveJSProperty(
+ 'selectionEnd',
+ '105.'.length,
+ );
+ await expect(textfield).toHaveValue('105.00');
+ });
+
+ test('105.|00 => Backspace => 105|.00', async ({page}) => {
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('Backspace');
+
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ '105'.length,
+ );
+ await expect(textfield).toHaveJSProperty(
+ 'selectionEnd',
+ '105'.length,
+ );
+ await expect(textfield).toHaveValue('105.00');
+ });
+
+ test('105.|00 => Delete => 105.0|0', async ({page}) => {
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('Delete');
+
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ '105.0'.length,
+ );
+ await expect(textfield).toHaveJSProperty(
+ 'selectionEnd',
+ '105.0'.length,
+ );
+ await expect(textfield).toHaveValue('105.00');
+ });
+ });
+
+ describe('if user tries to erase thousand separator, mask triggers caret navigation', () => {
+ beforeEach(async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?decimalMode=not-zero`,
+ );
+
+ await textfield.fill('1000');
+ });
+
+ test('1| 000 => Delete => 1 |000', async ({page}) => {
+ const length = (await textfield.inputValue()).length;
+
+ for (let i = 0; i < length; i++) {
+ await page.keyboard.press('ArrowLeft');
+ }
+
+ await expect(textfield).toHaveJSProperty('selectionStart', 0);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 0);
+
+ await page.keyboard.press('ArrowRight');
+ await page.keyboard.press('Delete');
+
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ '1 '.length,
+ );
+ await expect(textfield).toHaveJSProperty('selectionEnd', '1 '.length);
+ await expect(textfield).toHaveValue('1 000');
+ });
+
+ test('1 |000 => Backspace => 1| 000', async ({page}) => {
+ const length = (await textfield.inputValue()).length;
+
+ for (let i = 0; i < length; i++) {
+ await page.keyboard.press('ArrowLeft');
+ }
+
+ await expect(textfield).toHaveJSProperty('selectionStart', 0);
+ await expect(textfield).toHaveJSProperty('selectionEnd', 0);
+
+ await page.keyboard.press('ArrowRight');
+ await page.keyboard.press('ArrowRight');
+ await page.keyboard.press('Backspace');
+
+ await expect(textfield).toHaveJSProperty(
+ 'selectionStart',
+ '1'.length,
+ );
+ await expect(textfield).toHaveJSProperty('selectionEnd', '1'.length);
+ await expect(textfield).toHaveValue('1 000');
+ });
+ });
+ });
+
+ describe('[tuiTextfieldSize] prop', () => {
+ test.use({viewport: {width: 300, height: 500}});
+
+ ['s', 'm', 'l'].forEach((size) => {
+ test(`Empty textfield | ${size}`, async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?tuiTextfieldSize=${size}`,
+ );
+
+ await expect(textfield).toHaveValue('');
+ await expect(example).toHaveScreenshot(
+ `input-number-unfocused-empty-size-${size}.png`,
+ );
+
+ await textfield.focus();
+
+ await expect(example).toHaveScreenshot(
+ `input-number-focused-empty-size-${size}.png`,
+ );
+ });
+
+ test(`Textfield has value | ${size}`, async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?precision=2&tuiTextfieldSize=${size}`,
+ );
+
+ await textfield.fill('12.34');
+
+ await expect(textfield).toHaveValue('12.34');
+ await expect(example).toHaveScreenshot(
+ `input-number-focused-with-value-size-${size}.png`,
+ );
+
+ await textfield.blur();
+
+ await expect(example).toHaveScreenshot(
+ `input-number-unfocused-with-value-size-${size}.png`,
+ );
+ });
+ });
+ });
+
+ test('does not mutate already valid too large number on blur', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumber}/API?thousandSeparator=_&precision=2`,
+ );
+ await textfield.focus();
+ await textfield.clear();
+ await textfield.pressSequentially('123456789012345.6789');
+
+ await expect(textfield).toHaveValue('123_456_789_012_345.67');
+
+ await textfield.blur();
+
+ await expect(textfield).toHaveValue('123_456_789_012_345.67');
+ });
+ });
+});
diff --git a/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts b/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts
index 9193f7cce4ec..973306dd70f8 100644
--- a/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts
+++ b/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts
@@ -22,7 +22,7 @@ test.describe('InputNumber', () => {
});
test('Infinite precision', async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}/API?precision=Infinity`);
+ await tuiGoto(page, `${DemoRoute.InputNumberLegacy}/API?precision=Infinity`);
await input.focus();
await input.fill('1,2345');
@@ -32,7 +32,7 @@ test.describe('InputNumber', () => {
test('does not mutate already valid too large number on blur', async ({page}) => {
await tuiGoto(
page,
- `${DemoRoute.InputNumber}/API?thousandSeparator=_&precision=2`,
+ `${DemoRoute.InputNumberLegacy}/API?thousandSeparator=_&precision=2`,
);
await input.focus();
await input.clear();
@@ -48,7 +48,7 @@ test.describe('InputNumber', () => {
test('prefix + value + postfix', async ({page}) => {
await tuiGoto(
page,
- '/components/input-number/API?tuiTextfieldPrefix=$&tuiTextfieldPostfix=GBP',
+ `${DemoRoute.InputNumberLegacy}/API?tuiTextfieldPrefix=$&tuiTextfieldPostfix=GBP`,
);
await expect(example).toHaveScreenshot('02-input-number.png');
@@ -64,7 +64,7 @@ test.describe('InputNumber', () => {
await tuiGoto(
page,
- `/components/input-number/API?style.text-align=${align}&tuiTextfieldPrefix=${readableFormatText}&tuiTextfieldPostfix=${readableFormatText}`,
+ `${DemoRoute.InputNumberLegacy}/API?style.text-align=${align}&tuiTextfieldPrefix=${readableFormatText}&tuiTextfieldPostfix=${readableFormatText}`,
);
await expect(example).toHaveScreenshot(`04-input-number-${i}.png`);
@@ -76,421 +76,414 @@ test.describe('InputNumber', () => {
test(`sandboxWidth=${sandboxWidth}`, async ({page}) => {
await tuiGoto(
page,
- `/components/input-number/API?tuiTextfieldPostfix=$&tuiTextfieldPrefix=VeryLongText&sandboxWidth=${sandboxWidth}`,
+ `${DemoRoute.InputNumberLegacy}/API?tuiTextfieldPostfix=$&tuiTextfieldPrefix=VeryLongText&sandboxWidth=${sandboxWidth}`,
);
await expect(example).toHaveScreenshot(`05-input-number-${i}.png`);
});
});
});
- });
-
- test.describe('Examples', () => {
- test('cursor position', async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}`);
- const example = new TuiDocumentationPagePO(page).getExample('#currency');
-
- await expect(example).toHaveScreenshot('06-input-number.png');
-
- const inputs = await example
- .getByTestId('tui-primitive-textfield__native-input')
- .all();
+ test.describe('Caret navigation', () => {
+ test.describe('if user tries to erase padded decimal zeroes (decimalMode="always"), mask triggers caret navigation', () => {
+ test.beforeEach(async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?decimalMode=always&precision=2`,
+ );
- for (const [i, input] of inputs.entries()) {
- await input.clear();
- await input.focus();
+ await input.clear();
+ await input.fill('105,00');
+ });
- await expect(example).toHaveScreenshot(`06-input-number-${i}.png`);
- }
- });
- });
+ test('105,00| => Backspace => 105,0|0', async ({page}) => {
+ await page.keyboard.press('Backspace');
- test.describe('Caret navigation', () => {
- test.describe('if user tries to erase padded decimal zeroes (decimalMode="always"), mask triggers caret navigation', () => {
- test.beforeEach(async ({page}) => {
- await tuiGoto(
- page,
- '/components/input-number/API?decimalMode=always&precision=2',
- );
+ await expect(input).toHaveJSProperty(
+ 'selectionStart',
+ '105,0'.length,
+ );
+ await expect(input).toHaveJSProperty('selectionEnd', '105,0'.length);
+ await expect(example).toHaveScreenshot('07-input-number.png');
+ });
- example = new TuiDocumentationApiPagePO(page).apiPageExample;
- input = example.getByTestId('tui-primitive-textfield__native-input');
+ test('105,0|0 => Backspace => 105,|00', async ({page}) => {
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('Backspace');
- await input.clear();
- await input.fill('105,00');
- });
+ await expect(input).toHaveJSProperty('selectionStart', '105,'.length);
+ await expect(input).toHaveJSProperty('selectionEnd', '105,'.length);
+ await expect(example).toHaveScreenshot('08-input-number.png');
+ });
- test('105,00| => Backspace => 105,0|0', async ({page}) => {
- await page.keyboard.press('Backspace');
+ test('105,|00 => Backspace => 105|,00', async ({page}) => {
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('Backspace');
- await expect(input).toHaveJSProperty('selectionStart', '105,0'.length);
- await expect(input).toHaveJSProperty('selectionEnd', '105,0'.length);
- await expect(example).toHaveScreenshot('07-input-number.png');
- });
+ await expect(input).toHaveJSProperty('selectionStart', '105'.length);
+ await expect(input).toHaveJSProperty('selectionEnd', '105'.length);
+ await expect(example).toHaveScreenshot('09-input-number.png');
+ });
- test('105,0|0 => Backspace => 105,|00', async ({page}) => {
- await page.keyboard.press('ArrowLeft');
- await page.keyboard.press('Backspace');
+ test('105,|00 => Delete => 105,0|0', async ({page}) => {
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.press('Delete');
- await expect(input).toHaveJSProperty('selectionStart', '105,'.length);
- await expect(input).toHaveJSProperty('selectionEnd', '105,'.length);
- await expect(example).toHaveScreenshot('08-input-number.png');
+ await expect(input).toHaveJSProperty(
+ 'selectionStart',
+ '105,0'.length,
+ );
+ await expect(input).toHaveJSProperty('selectionEnd', '105,0'.length);
+ await expect(example).toHaveScreenshot('10-input-number.png');
+ });
});
- test('105,|00 => Backspace => 105|,00', async ({page}) => {
- await page.keyboard.press('ArrowLeft');
- await page.keyboard.press('ArrowLeft');
- await page.keyboard.press('Backspace');
+ test.describe('if user tries to erase thousand separator, mask triggers caret navigation', () => {
+ test.beforeEach(async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?decimalMode=not-zero`,
+ );
- await expect(input).toHaveJSProperty('selectionStart', '105'.length);
- await expect(input).toHaveJSProperty('selectionEnd', '105'.length);
- await expect(example).toHaveScreenshot('09-input-number.png');
- });
+ await input.fill('1000');
+ });
- test('105,|00 => Delete => 105,0|0', async ({page}) => {
- await page.keyboard.press('ArrowLeft');
- await page.keyboard.press('ArrowLeft');
- await page.keyboard.press('Delete');
+ test('1| 000 => Delete => 1 |000', async ({page}) => {
+ const length = (await input.inputValue()).length;
- await expect(input).toHaveJSProperty('selectionStart', '105,0'.length);
- await expect(input).toHaveJSProperty('selectionEnd', '105,0'.length);
- await expect(example).toHaveScreenshot('10-input-number.png');
- });
- });
+ for (let i = 0; i < length; i++) {
+ await page.keyboard.press('ArrowLeft');
+ }
- test.describe('if user tries to erase thousand separator, mask triggers caret navigation', () => {
- test.beforeEach(async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}/API?decimalMode=not-zero`);
+ await expect(input).toHaveJSProperty('selectionStart', 0);
+ await expect(input).toHaveJSProperty('selectionEnd', 0);
- example = new TuiDocumentationApiPagePO(page).apiPageExample;
- input = example.getByTestId('tui-primitive-textfield__native-input');
+ await page.keyboard.press('ArrowRight');
+ await page.keyboard.press('Delete');
- await input.fill('1000');
- });
+ await expect(input).toHaveJSProperty('selectionStart', '1 '.length);
+ await expect(input).toHaveJSProperty('selectionEnd', '1 '.length);
+ await expect(example).toHaveScreenshot('11-input-number.png');
+ });
- test('1| 000 => Delete => 1 |000', async ({page}) => {
- const length = (await input.inputValue()).length;
+ test('1 |000 => Backspace => 1| 000', async ({page}) => {
+ const length = (await input.inputValue()).length;
- for (let i = 0; i < length; i++) {
- await page.keyboard.press('ArrowLeft');
- }
+ for (let i = 0; i < length; i++) {
+ await page.keyboard.press('ArrowLeft');
+ }
- await expect(input).toHaveJSProperty('selectionStart', 0);
- await expect(input).toHaveJSProperty('selectionEnd', 0);
+ await expect(input).toHaveJSProperty('selectionStart', 0);
+ await expect(input).toHaveJSProperty('selectionEnd', 0);
- await page.keyboard.press('ArrowRight');
- await page.keyboard.press('Delete');
+ await page.keyboard.press('ArrowRight');
+ await page.keyboard.press('ArrowRight');
+ await page.keyboard.press('Backspace');
- await expect(input).toHaveJSProperty('selectionStart', '1 '.length);
- await expect(input).toHaveJSProperty('selectionEnd', '1 '.length);
- await expect(example).toHaveScreenshot('11-input-number.png');
+ await expect(input).toHaveJSProperty('selectionStart', '1'.length);
+ await expect(input).toHaveJSProperty('selectionEnd', '1'.length);
+ await expect(example).toHaveScreenshot('12-input-number.png');
+ });
});
+ });
- test('1 |000 => Backspace => 1| 000', async ({page}) => {
- const length = (await input.inputValue()).length;
+ test.describe('[min] prop', () => {
+ test.describe('[min] property is positive number', () => {
+ test.beforeEach(async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumberLegacy}/API?min=5`);
- for (let i = 0; i < length; i++) {
- await page.keyboard.press('ArrowLeft');
- }
+ await input.clear();
+ });
- await expect(input).toHaveJSProperty('selectionStart', 0);
- await expect(input).toHaveJSProperty('selectionEnd', 0);
+ test('rejects minus sign', async () => {
+ await input.fill(
+ `${CHAR_MINUS}${CHAR_HYPHEN}${CHAR_EN_DASH}${CHAR_EM_DASH}9`,
+ );
- await page.keyboard.press('ArrowRight');
- await page.keyboard.press('ArrowRight');
- await page.keyboard.press('Backspace');
+ await expect(input).toHaveJSProperty('selectionStart', 1);
+ await expect(input).toHaveJSProperty('selectionEnd', 1);
+ await expect(example).toHaveScreenshot('13-input-number.png');
+ });
- await expect(input).toHaveJSProperty('selectionStart', '1'.length);
- await expect(input).toHaveJSProperty('selectionEnd', '1'.length);
- await expect(example).toHaveScreenshot('12-input-number.png');
- });
- });
- });
+ test('validates positive value only on blur', async () => {
+ await input.fill('2');
- test.describe('[min] prop', () => {
- test.describe('[min] property is positive number', () => {
- test.beforeEach(async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}/API?min=5`);
+ await expect(input).toHaveJSProperty('selectionStart', 1);
+ await expect(input).toHaveJSProperty('selectionEnd', 1);
+ await expect(example).toHaveScreenshot('14-input-number.png');
- example = new TuiDocumentationApiPagePO(page).apiPageExample;
- input = example.getByTestId('tui-primitive-textfield__native-input');
+ await input.blur();
- await input.clear();
+ await expect(example).toHaveScreenshot('15-input-number.png');
+ });
});
- test('rejects minus sign', async () => {
- await input.fill(
- `${CHAR_MINUS}${CHAR_HYPHEN}${CHAR_EN_DASH}${CHAR_EM_DASH}9`,
- );
+ test.describe('[min] property is negative number', () => {
+ test.beforeEach(async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumberLegacy}/API?min=-5`);
- await expect(input).toHaveJSProperty('selectionStart', 1);
- await expect(input).toHaveJSProperty('selectionEnd', 1);
- await expect(example).toHaveScreenshot('13-input-number.png');
- });
+ await input.clear();
+ });
- test('validates positive value only on blur', async () => {
- await input.fill('2');
+ test('immediately validates negative value', async () => {
+ await input.fill('-10'); // less than [min]
- await expect(input).toHaveJSProperty('selectionStart', 1);
- await expect(input).toHaveJSProperty('selectionEnd', 1);
- await expect(example).toHaveScreenshot('14-input-number.png');
+ await expect(input).toHaveJSProperty('selectionStart', 2);
+ await expect(input).toHaveJSProperty('selectionEnd', 2);
+ await expect(input).toHaveValue(`${CHAR_MINUS}5`);
+ await expect(input).toHaveJSProperty('selectionStart', 2);
+ await expect(input).toHaveJSProperty('selectionEnd', 2);
+ await expect(example).toHaveScreenshot('16-input-number.png');
+ });
- await input.blur();
+ test("don't touch any positive value", async ({page}) => {
+ await input.fill('1');
- await expect(example).toHaveScreenshot('15-input-number.png');
- });
- });
+ await expect(input).toHaveJSProperty('selectionStart', 1);
+ await expect(input).toHaveJSProperty('selectionEnd', 1);
+ await expect(example).toHaveScreenshot('17-input-number.png');
- test.describe('[min] property is negative number', () => {
- test.beforeEach(async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}/API?min=-5`);
+ await input.fill('0');
- example = new TuiDocumentationApiPagePO(page).apiPageExample;
- input = example.getByTestId('tui-primitive-textfield__native-input');
+ await expect(input).toHaveJSProperty('selectionStart', 1);
+ await expect(input).toHaveJSProperty('selectionEnd', 1);
+ await expect(example).toHaveScreenshot('18-input-number.png');
- await input.clear();
- });
+ await input.blur();
- test('immediately validates negative value', async () => {
- await input.fill('-10'); // less than [min]
+ await expect(input).toHaveJSProperty('selectionStart', 1);
+ await expect(input).toHaveJSProperty('selectionEnd', 1);
- await expect(input).toHaveJSProperty('selectionStart', 2);
- await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(input).toHaveValue(`${CHAR_MINUS}5`);
- await expect(input).toHaveJSProperty('selectionStart', 2);
- await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(example).toHaveScreenshot('16-input-number.png');
- });
+ await page.waitForTimeout(100); // to be sure that value is not changed even in case of some async validation
- test("don't touch any positive value", async ({page}) => {
- await input.fill('1');
+ await expect(example).toHaveScreenshot('19-input-number.png');
+ });
+ });
- await expect(input).toHaveJSProperty('selectionStart', 1);
- await expect(input).toHaveJSProperty('selectionEnd', 1);
- await expect(example).toHaveScreenshot('17-input-number.png');
+ test('positive [min] property and positive value (which is less than [min])', async ({
+ page,
+ }) => {
+ await tuiGoto(page, DemoRoute.InputNumberLegacy);
- await input.fill('0');
+ const example = new TuiDocumentationPagePO(page).getExample('#min-max');
+ const textfield = example.getByRole('textbox');
- await expect(input).toHaveJSProperty('selectionStart', 1);
- await expect(input).toHaveJSProperty('selectionEnd', 1);
- await expect(example).toHaveScreenshot('18-input-number.png');
+ await expect(textfield).toHaveValue('');
- await input.blur();
+ textfield.fill('33'); // less than min
- await expect(input).toHaveJSProperty('selectionStart', 1);
- await expect(input).toHaveJSProperty('selectionEnd', 1);
+ await expect(example).toHaveScreenshot(
+ '20-input-number-positive-min-positive-wrong-value.png',
+ );
- await page.waitForTimeout(100); // to be sure that value is not changed even in case of some async validation
+ textfield.fill('333'); // more than min
- await expect(example).toHaveScreenshot('19-input-number.png');
+ await expect(example).toHaveScreenshot(
+ '20-input-number-positive-min-positive-valid-value.png',
+ );
});
});
- test('positive [min] property and positive value (which is less than [min])', async ({
- page,
- }) => {
- await tuiGoto(page, DemoRoute.InputNumber);
+ test.describe('[max] prop', () => {
+ test.describe('[max] property is negative number', () => {
+ test.beforeEach(async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumberLegacy}/API?max=-5`);
- const example = new TuiDocumentationPagePO(page).getExample('#min-max');
- const textfield = example.getByRole('textbox');
+ example = new TuiDocumentationApiPagePO(page).apiPageExample;
+ input = example.getByTestId('tui-primitive-textfield__native-input');
- await expect(textfield).toHaveValue('');
+ await input.clear();
+ });
- textfield.fill('33'); // less than min
+ test('validates negative value only on blur', async ({page}) => {
+ await input.fill('-1'); // more than [max]
+ await page.waitForTimeout(100); // to be sure that value is not changed even in case of some async validation
- await expect(example).toHaveScreenshot(
- '20-input-number-positive-min-positive-wrong-value.png',
- );
+ await expect(input).toHaveValue(`${CHAR_MINUS}1`);
+ await expect(input).toHaveJSProperty('selectionStart', 2);
+ await expect(input).toHaveJSProperty('selectionEnd', 2);
+ await expect(example).toHaveScreenshot(
+ '21-input-number-before-blur.png',
+ );
- textfield.fill('333'); // more than min
+ await input.blur();
- await expect(example).toHaveScreenshot(
- '20-input-number-positive-min-positive-valid-value.png',
- );
- });
- });
+ await expect(input).toHaveValue(`${CHAR_MINUS}5`);
+ await expect(input).toHaveJSProperty('selectionStart', 2);
+ await expect(input).toHaveJSProperty('selectionEnd', 2);
+ await expect(example).toHaveScreenshot(
+ '21-input-number-after-blur.png',
+ );
+ });
+ });
- test.describe('[max] prop', () => {
- test.describe('[max] property is negative number', () => {
- test.beforeEach(async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}/API?max=-5`);
+ test.describe('[max] property is positive number', () => {
+ test.beforeEach(async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumberLegacy}/API?max=12`);
- example = new TuiDocumentationApiPagePO(page).apiPageExample;
- input = example.getByTestId('tui-primitive-textfield__native-input');
+ await input.clear();
+ });
- await input.clear();
- });
+ test('immediately validates positive value', async () => {
+ await input.fill('19');
- test('validates negative value only on blur', async ({page}) => {
- await input.fill('-1'); // more than [max]
- await page.waitForTimeout(100); // to be sure that value is not changed even in case of some async validation
+ await expect(input).toHaveValue('12');
+ await expect(input).toHaveJSProperty('selectionStart', 2);
+ await expect(input).toHaveJSProperty('selectionEnd', 2);
+ await expect(example).toHaveScreenshot('22-input-number.png');
+ });
- await expect(input).toHaveValue(`${CHAR_MINUS}1`);
- await expect(input).toHaveJSProperty('selectionStart', 2);
- await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(example).toHaveScreenshot('21-input-number-before-blur.png');
+ test("don't touch any negative value", async ({page}) => {
+ await input.fill('-1');
- await input.blur();
+ await expect(input).toHaveValue(`${CHAR_MINUS}1`);
+ await expect(input).toHaveJSProperty('selectionStart', 2);
+ await expect(input).toHaveJSProperty('selectionEnd', 2);
+ await expect(example).toHaveScreenshot('23-input-number.png');
- await expect(input).toHaveValue(`${CHAR_MINUS}5`);
- await expect(input).toHaveJSProperty('selectionStart', 2);
- await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(example).toHaveScreenshot('21-input-number-after-blur.png');
- });
- });
+ await page.keyboard.down('9');
- test.describe('[max] property is positive number', () => {
- test.beforeEach(async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}/API?max=12`);
+ await expect(input).toHaveJSProperty('selectionStart', 3);
+ await expect(input).toHaveJSProperty('selectionEnd', 3);
+ await expect(input).toHaveValue(`${CHAR_MINUS}19`);
+ await expect(input).toHaveJSProperty('selectionStart', 3);
+ await expect(input).toHaveJSProperty('selectionEnd', 3);
+ await expect(example).toHaveScreenshot('24-input-number.png');
- example = new TuiDocumentationApiPagePO(page).apiPageExample;
- input = example.getByTestId('tui-primitive-textfield__native-input');
+ await page.waitForTimeout(100);
- await input.clear();
+ await expect(input).toHaveValue(`${CHAR_MINUS}19`);
+ await expect(input).toHaveJSProperty('selectionStart', 3);
+ await expect(input).toHaveJSProperty('selectionEnd', 3);
+ await expect(example).toHaveScreenshot('25-input-number.png');
+ });
});
+ });
+
+ test.describe('value formatting on blur', () => {
+ test('Value 42 (decimalMode=not-zero) => 42', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?precision=2&decimalMode=not-zero`,
+ );
- test('immediately validates positive value', async () => {
- await input.fill('19');
+ await input.fill('42');
- await expect(input).toHaveValue('12');
+ await expect(input).toHaveValue('42');
await expect(input).toHaveJSProperty('selectionStart', 2);
await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(example).toHaveScreenshot('22-input-number.png');
+ await expect(example).toHaveScreenshot('26-input-number.png');
});
- test("don't touch any negative value", async ({page}) => {
- await input.fill('-1');
-
- await expect(input).toHaveValue(`${CHAR_MINUS}1`);
- await expect(input).toHaveJSProperty('selectionStart', 2);
- await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(example).toHaveScreenshot('23-input-number.png');
+ test('Value 42,1 (decimalMode=not-zero) => 42,1', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?precision=2&decimalMode=not-zero`,
+ );
- await page.keyboard.down('9');
+ await input.fill('42.1');
- await expect(input).toHaveJSProperty('selectionStart', 3);
- await expect(input).toHaveJSProperty('selectionEnd', 3);
- await expect(input).toHaveValue(`${CHAR_MINUS}19`);
- await expect(input).toHaveJSProperty('selectionStart', 3);
- await expect(input).toHaveJSProperty('selectionEnd', 3);
- await expect(example).toHaveScreenshot('24-input-number.png');
+ await expect(input).toHaveValue('42.1');
+ await expect(input).toHaveJSProperty('selectionStart', 4);
+ await expect(input).toHaveJSProperty('selectionEnd', 4);
- await page.waitForTimeout(100);
+ await input.blur();
- await expect(input).toHaveValue(`${CHAR_MINUS}19`);
- await expect(input).toHaveJSProperty('selectionStart', 3);
- await expect(input).toHaveJSProperty('selectionEnd', 3);
- await expect(example).toHaveScreenshot('25-input-number.png');
+ await expect(input).toHaveValue('42.1');
+ await expect(example).toHaveScreenshot('27-input-number.png');
});
- });
- });
-
- test.describe('value formatting on blur', () => {
- test.beforeEach(({page}) => {
- example = new TuiDocumentationApiPagePO(page).apiPageExample;
- input = example.getByTestId('tui-primitive-textfield__native-input');
- });
- test('Value 42 (decimalMode=not-zero) => 42', async ({page}) => {
- await tuiGoto(
- page,
- 'components/input-number/API?precision=2&decimalMode=not-zero',
- );
+ test('Value 42,1 (decimalMode=pad) => 42,10', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?precision=2&decimalMode=pad`,
+ );
- await input.fill('42');
+ await input.fill('42.1');
- await expect(input).toHaveValue('42');
- await expect(input).toHaveJSProperty('selectionStart', 2);
- await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(example).toHaveScreenshot('26-input-number.png');
- });
+ await expect(input).toHaveValue('42.1');
+ await expect(input).toHaveJSProperty('selectionStart', 4);
+ await expect(input).toHaveJSProperty('selectionEnd', 4);
- test('Value 42,1 (decimalMode=not-zero) => 42,1', async ({page}) => {
- await tuiGoto(
- page,
- '/components/input-number/API?precision=2&decimalMode=not-zero',
- );
+ await input.blur();
- await input.fill('42.1');
+ await expect(input).toHaveValue('42.10');
+ await expect(example).toHaveScreenshot('28-input-number.png');
+ });
- await expect(input).toHaveValue('42.1');
- await expect(input).toHaveJSProperty('selectionStart', 4);
- await expect(input).toHaveJSProperty('selectionEnd', 4);
+ test('Value 42,00 (decimalMode=not-zero) => 42', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?precision=2&decimalMode=not-zero`,
+ );
- await input.blur();
+ await input.fill('42.00');
- await expect(input).toHaveValue('42.1');
- await expect(example).toHaveScreenshot('27-input-number.png');
- });
+ await expect(input).toHaveJSProperty('selectionStart', 5);
+ await expect(input).toHaveJSProperty('selectionEnd', 5);
+ await expect(example).toHaveScreenshot('29-input-number.png');
+ });
- test('Value 42,1 (decimalMode=pad) => 42,10', async ({page}) => {
- await tuiGoto(
- page,
- '/components/input-number/API?precision=2&decimalMode=pad',
- );
+ test('Value 42,1 (precision=0) => 42', async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumberLegacy}/API?precision=0`);
- await input.fill('42.1');
+ await input.fill('42.1');
- await expect(input).toHaveValue('42.1');
- await expect(input).toHaveJSProperty('selectionStart', 4);
- await expect(input).toHaveJSProperty('selectionEnd', 4);
+ await expect(input).toHaveValue('42');
+ await expect(example).toHaveScreenshot('30-input-number.png');
+ });
- await input.blur();
+ test('Value 42 (decimalMode=always) => 42.00', async ({page}) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?precision=2&decimalMode=always`,
+ );
+ await input.fill('42');
- await expect(input).toHaveValue('42.10');
- await expect(example).toHaveScreenshot('28-input-number.png');
- });
+ await expect(input).toHaveValue('42.00');
+ await expect(input).toHaveJSProperty('selectionStart', 2);
+ await expect(input).toHaveJSProperty('selectionEnd', 2);
+ await expect(example).toHaveScreenshot('31-input-number.png');
+ });
- test('Value 42,00 (decimalMode=not-zero) => 42', async ({page}) => {
- await tuiGoto(
+ test("text field does not contain any digit (only prefix + postfix) => clear text field's value on blur", async ({
page,
- '/components/input-number/API?precision=2&decimalMode=not-zero',
- );
-
- await input.fill('42.00');
+ }) => {
+ await tuiGoto(
+ page,
+ `${DemoRoute.InputNumberLegacy}/API?tuiTextfieldPrefix=$&tuiTextfieldPostfix=kg`,
+ );
+ await input.clear();
+ await input.focus();
- await expect(input).toHaveJSProperty('selectionStart', 5);
- await expect(input).toHaveJSProperty('selectionEnd', 5);
- await expect(example).toHaveScreenshot('29-input-number.png');
+ await expect(input).toHaveValue('$ kg');
+ await expect(input).toHaveJSProperty('selectionStart', 1);
+ await expect(input).toHaveJSProperty('selectionEnd', 1);
+ await expect(example).toHaveScreenshot('32-input-number.png');
+ });
});
+ });
- test('Value 42,1 (precision=0) => 42', async ({page}) => {
- await tuiGoto(page, `${DemoRoute.InputNumber}/API?precision=0`);
-
- await input.fill('42.1');
+ test.describe('Examples', () => {
+ test('cursor position', async ({page}) => {
+ await tuiGoto(page, `${DemoRoute.InputNumberLegacy}`);
- await expect(input).toHaveValue('42');
- await expect(example).toHaveScreenshot('30-input-number.png');
- });
+ const example = new TuiDocumentationPagePO(page).getExample('#currency');
- test('Value 42 (decimalMode=always) => 42.00', async ({page}) => {
- await tuiGoto(
- page,
- '/components/input-number/API?precision=2&decimalMode=always',
- );
- await input.fill('42');
+ await expect(example).toHaveScreenshot('06-input-number.png');
- await expect(input).toHaveValue('42.00');
- await expect(input).toHaveJSProperty('selectionStart', 2);
- await expect(input).toHaveJSProperty('selectionEnd', 2);
- await expect(example).toHaveScreenshot('31-input-number.png');
- });
+ const inputs = await example
+ .getByTestId('tui-primitive-textfield__native-input')
+ .all();
- test("text field does not contain any digit (only prefix + postfix) => clear text field's value on blur", async ({
- page,
- }) => {
- await tuiGoto(
- page,
- '/components/input-number/API?tuiTextfieldPrefix=$&tuiTextfieldPostfix=kg',
- );
- await input.clear();
- await input.focus();
+ for (const [i, input] of inputs.entries()) {
+ await input.clear();
+ await input.focus();
- await expect(input).toHaveValue('$ kg');
- await expect(input).toHaveJSProperty('selectionStart', 1);
- await expect(input).toHaveJSProperty('selectionEnd', 1);
- await expect(example).toHaveScreenshot('32-input-number.png');
+ await expect(example).toHaveScreenshot(`06-input-number-${i}.png`);
+ }
});
});
});
diff --git a/projects/demo/src/components/control/index.ts b/projects/demo/src/components/control/index.ts
index 03acc88675cc..c61e69548630 100644
--- a/projects/demo/src/components/control/index.ts
+++ b/projects/demo/src/components/control/index.ts
@@ -1,8 +1,28 @@
import {NgIf} from '@angular/common';
-import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Directive,
+ inject,
+ Input,
+} from '@angular/core';
+import {NgControl} from '@angular/forms';
import {TuiDocAPIItem} from '@taiga-ui/addon-doc';
import {TuiTitle} from '@taiga-ui/core';
+@Directive({
+ standalone: true,
+ selector: 'input[tuiDisabled][formControl]',
+})
+export class TuiDocReactiveFormDisable {
+ private readonly control = inject(NgControl);
+
+ @Input()
+ public set tuiDisabled(x: boolean) {
+ this.control.control?.[x ? 'disable' : 'enable']();
+ }
+}
+
@Component({
standalone: true,
selector: 'tbody[tuiDocControl]',
@@ -10,7 +30,7 @@ import {TuiTitle} from '@taiga-ui/core';
templateUrl: './index.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class TuiDocControl {
+export class TuiDocControlComponent {
@Input()
public hiddenOptions: ReadonlyArray<'disabled' | 'invalid' | 'readOnly'> = [];
@@ -18,3 +38,5 @@ export class TuiDocControl {
public disabled = false;
public invalid: boolean | null = null;
}
+
+export const TuiDocControl = [TuiDocControlComponent, TuiDocReactiveFormDisable];
diff --git a/projects/demo/src/components/number-format/index.html b/projects/demo/src/components/number-format/index.html
new file mode 100644
index 000000000000..dc600e460a8b
--- /dev/null
+++ b/projects/demo/src/components/number-format/index.html
@@ -0,0 +1,141 @@
+
+
+
+
+ TuiNumberFormat
+
+
+ Usage example:
+
+ [tuiNumberFormat]="{thousandSeparator, decimalSeparator, ..., rounding}"
+
+
+
+
+
+
+ Symbol for separating thousands
+
+
+
+ Symbol for separating fraction
+
+
+
+ A number of digits after
+ [decimalSeparator]
+ (
+ Infinity
+ for an untouched decimal part)
+
+
+
+
+
+ always
+
+
+ number of digits after
+ [decimalSeparator]
+ is
+ always
+ equal to the precision.
+
+
+
+ pad
+
+ pads trailing zeroes up to precision, if the number is fractional
+
+
+ not-zero
+
+ drops trailing zeroes
+
+
+
+
+
+
+ round
+
+
+ rounds to the
+ nearest
+ number with the specified
+ [precision]
+
+
+
+ floor
+
+
+ rounds down (the
+ largest
+ number with the specified
+ [precision]
+ less than or equal to a given number)
+
+
+
+ ceil
+
+
+ rounds up (the
+ smallest
+ number with the specified
+ [precision]
+ greater than or equal to a given number)
+
+
+
+ truncate
+
+
+ returns the number with the specified
+ [precision]
+ by just removing extra fractional digits
+
+
+
diff --git a/projects/demo/src/components/number-format/index.ts b/projects/demo/src/components/number-format/index.ts
new file mode 100644
index 000000000000..45abd332bb60
--- /dev/null
+++ b/projects/demo/src/components/number-format/index.ts
@@ -0,0 +1,48 @@
+import {NgIf} from '@angular/common';
+import type {WritableSignal} from '@angular/core';
+import {ChangeDetectionStrategy, Component, Input, signal} from '@angular/core';
+import {RouterLink} from '@angular/router';
+import {DemoRoute} from '@demo/routes';
+import {TuiDocAPIItem} from '@taiga-ui/addon-doc';
+import type {TuiLooseUnion, TuiRounding} from '@taiga-ui/cdk';
+import type {TuiDecimalMode, TuiNumberFormatSettings} from '@taiga-ui/core';
+import {TUI_DEFAULT_NUMBER_FORMAT, TuiLink, TuiTitle} from '@taiga-ui/core';
+import {tuiInputNumberOptionsProvider} from '@taiga-ui/legacy';
+
+@Component({
+ standalone: true,
+ selector: 'tbody[tuiDocNumberFormat]',
+ imports: [NgIf, RouterLink, TuiDocAPIItem, TuiLink, TuiTitle],
+ templateUrl: './index.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ tuiInputNumberOptionsProvider({
+ min: 0,
+ }),
+ ],
+})
+export class TuiDocNumberFormat
+ implements
+ Record<
+ keyof TuiNumberFormatSettings,
+ WritableSignal
+ >
+{
+ protected readonly routes = DemoRoute;
+ protected readonly decimalVariants: TuiDecimalMode[] = ['always', 'pad', 'not-zero'];
+ protected readonly roundingVariants: TuiRounding[] = [
+ 'truncate',
+ 'round',
+ 'ceil',
+ 'floor',
+ ];
+
+ @Input()
+ public hiddenOptions: Array> = [];
+
+ public thousandSeparator = signal(TUI_DEFAULT_NUMBER_FORMAT.thousandSeparator);
+ public decimalSeparator = signal(TUI_DEFAULT_NUMBER_FORMAT.decimalSeparator);
+ public precision = signal(TUI_DEFAULT_NUMBER_FORMAT.precision);
+ public decimalMode = signal(TUI_DEFAULT_NUMBER_FORMAT.decimalMode);
+ public rounding = signal(TUI_DEFAULT_NUMBER_FORMAT.rounding);
+}
diff --git a/projects/demo/src/modules/app/app.routes.ts b/projects/demo/src/modules/app/app.routes.ts
index e07ad4f1c191..ec4e46d903e8 100644
--- a/projects/demo/src/modules/app/app.routes.ts
+++ b/projects/demo/src/modules/app/app.routes.ts
@@ -424,6 +424,11 @@ export const ROUTES: Routes = [
loadComponent: async () => import('../components/input-number'),
title: 'InputNumber',
}),
+ route({
+ path: DemoRoute.InputNumberLegacy,
+ loadComponent: async () => import('../components/input-number-legacy'),
+ title: 'InputNumber [deprecated]',
+ }),
route({
path: DemoRoute.InputPhone,
loadComponent: async () => import('../components/input-phone'),
diff --git a/projects/demo/src/modules/app/demo-routes.ts b/projects/demo/src/modules/app/demo-routes.ts
index e056dd65fac4..18ccc9df6d59 100644
--- a/projects/demo/src/modules/app/demo-routes.ts
+++ b/projects/demo/src/modules/app/demo-routes.ts
@@ -81,6 +81,7 @@ export const DemoRoute = {
InputMonth: '/components/input-month',
InputMonthRange: '/components/input-month-range',
InputNumber: '/components/input-number',
+ InputNumberLegacy: '/legacy/input-number',
InputPhone: '/components/input-phone',
InputRange: '/components/input-range',
InputDateRange: '/components/input-date-range',
diff --git a/projects/demo/src/modules/app/pages.ts b/projects/demo/src/modules/app/pages.ts
index d978607349e5..efac68b72cc6 100644
--- a/projects/demo/src/modules/app/pages.ts
+++ b/projects/demo/src/modules/app/pages.ts
@@ -549,13 +549,22 @@ export const pages: DocRoutePages = [
keywords: 'поле, инпут, форма, ввод, input, month, месяц, год, дата',
route: DemoRoute.InputMonthRange,
},
+ // TODO: it will be revealed later
+ // {
+ // section: 'Components',
+ // title: 'InputNumber',
+ // keywords:
+ // 'поле, инпут, number, число, форма, ввод, input, money, деньги, ' +
+ // 'cash, копейки, рубли, доллары, евро, control, контрол',
+ // route: DemoRoute.InputNumber,
+ // },
{
section: 'Components',
- title: 'InputNumber',
+ title: 'InputNumber [deprecated]',
keywords:
'поле, инпут, number, число, форма, ввод, input, money, деньги, ' +
'cash, копейки, рубли, доллары, евро, control, контрол',
- route: DemoRoute.InputNumber,
+ route: DemoRoute.InputNumberLegacy,
},
{
section: 'Components',
diff --git a/projects/demo/src/modules/components/abstract/number-format-documentation/index.ts b/projects/demo/src/modules/components/abstract/number-format-documentation/index.ts
index c43690f24723..12fd61ed539e 100644
--- a/projects/demo/src/modules/components/abstract/number-format-documentation/index.ts
+++ b/projects/demo/src/modules/components/abstract/number-format-documentation/index.ts
@@ -11,6 +11,9 @@ import {TuiLink} from '@taiga-ui/core';
import {ABSTRACT_PROPS_ACCESSOR} from '../abstract-props-accessor';
import type {AbstractExampleTuiNumberFormat} from '../number-format';
+/**
+ * @deprecated use {@link TuiDocNumberFormat}
+ */
@Component({
standalone: true,
selector: 'number-format-documentation',
diff --git a/projects/demo/src/modules/components/input-number/examples/1/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/1/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/1/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/1/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/1/index.less b/projects/demo/src/modules/components/input-number-legacy/examples/1/index.less
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/1/index.less
rename to projects/demo/src/modules/components/input-number-legacy/examples/1/index.less
diff --git a/projects/demo/src/modules/components/input-number/examples/1/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/1/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/1/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/1/index.ts
diff --git a/projects/demo/src/modules/components/input-number/examples/2/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/2/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/2/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/2/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/2/index.less b/projects/demo/src/modules/components/input-number-legacy/examples/2/index.less
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/2/index.less
rename to projects/demo/src/modules/components/input-number-legacy/examples/2/index.less
diff --git a/projects/demo/src/modules/components/input-number/examples/2/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/2/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/2/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/2/index.ts
diff --git a/projects/demo/src/modules/components/input-number/examples/3/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/3/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/3/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/3/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/3/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/3/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/3/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/3/index.ts
diff --git a/projects/demo/src/modules/components/input-number/examples/4/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/4/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/4/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/4/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/4/index.less b/projects/demo/src/modules/components/input-number-legacy/examples/4/index.less
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/4/index.less
rename to projects/demo/src/modules/components/input-number-legacy/examples/4/index.less
diff --git a/projects/demo/src/modules/components/input-number/examples/4/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/4/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/4/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/4/index.ts
diff --git a/projects/demo/src/modules/components/input-number/examples/5/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/5/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/5/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/5/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/5/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/5/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/5/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/5/index.ts
diff --git a/projects/demo/src/modules/components/input-number/examples/6/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/6/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/6/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/6/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/6/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/6/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/6/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/6/index.ts
diff --git a/projects/demo/src/modules/components/input-number/examples/7/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/7/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/7/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/7/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/7/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/7/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/7/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/7/index.ts
diff --git a/projects/demo/src/modules/components/input-number/examples/8/index.html b/projects/demo/src/modules/components/input-number-legacy/examples/8/index.html
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/8/index.html
rename to projects/demo/src/modules/components/input-number-legacy/examples/8/index.html
diff --git a/projects/demo/src/modules/components/input-number/examples/8/index.ts b/projects/demo/src/modules/components/input-number-legacy/examples/8/index.ts
similarity index 100%
rename from projects/demo/src/modules/components/input-number/examples/8/index.ts
rename to projects/demo/src/modules/components/input-number-legacy/examples/8/index.ts
diff --git a/projects/demo/src/modules/components/input-number-legacy/examples/import/import.md b/projects/demo/src/modules/components/input-number-legacy/examples/import/import.md
new file mode 100644
index 000000000000..5261a0d39849
--- /dev/null
+++ b/projects/demo/src/modules/components/input-number-legacy/examples/import/import.md
@@ -0,0 +1,15 @@
+```ts
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {TuiInputNumberModule} from '@taiga-ui/legacy';
+
+@Component({
+ standalone: true,
+ imports: [FormsModule, ReactiveFormsModule, TuiInputNumberModule],
+ // ...
+})
+export class Example {
+ testForm = new FormGroup({
+ testValue: new FormControl(5000),
+ });
+}
+```
diff --git a/projects/demo/src/modules/components/input-number-legacy/examples/import/template.md b/projects/demo/src/modules/components/input-number-legacy/examples/import/template.md
new file mode 100644
index 000000000000..6427b847151e
--- /dev/null
+++ b/projects/demo/src/modules/components/input-number-legacy/examples/import/template.md
@@ -0,0 +1,5 @@
+```html
+
+```
diff --git a/projects/demo/src/modules/components/input-number-legacy/index.html b/projects/demo/src/modules/components/input-number-legacy/index.html
new file mode 100644
index 000000000000..2c0c25652852
--- /dev/null
+++ b/projects/demo/src/modules/components/input-number-legacy/index.html
@@ -0,0 +1,250 @@
+
+
+ A component to input numbers. Control value is also of number type.
+
+
+ There are also other components to input numbers:
+
+
+
+
+ Number formatting can be customized with
+
+ TUI_NUMBER_FORMAT
+
+ token.
+
+
+
+
+
+ To input money use properties
+ [postfix]
+ or
+ [prefix]
+ . To get currency symbol use pipe
+
+ tuiCurrency
+
+ .
+
+
+
+
+
+
+ Customize input via
+
+ TextfieldControllers
+
+ .
+
+
+
+ If you need to set some attributes or listen to events on native
+ input
+ , you can put it inside with
+ Textfield
+ directive as shown below
+
+
+
+
+
+ Use property
+ [precision]
+ to configure a number of digits after comma.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Type a sum
+
+
+
+
+
+ Disabled state (use
+ formControl.disable()
+ )
+
+
+ Min value
+
+
+ Max value
+
+
+ Step to increase/decrease value with keyboard and buttons on the side
+
+
+
+
+
+ Custom align content by text-align
+
+
+
+
+
+
diff --git a/projects/demo/src/modules/components/input-number-legacy/index.ts b/projects/demo/src/modules/components/input-number-legacy/index.ts
new file mode 100644
index 000000000000..b131110fa629
--- /dev/null
+++ b/projects/demo/src/modules/components/input-number-legacy/index.ts
@@ -0,0 +1,46 @@
+import {Component} from '@angular/core';
+import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
+import {changeDetection} from '@demo/emulate/change-detection';
+import {DemoRoute} from '@demo/routes';
+import {TuiDemo} from '@demo/utils';
+import {tuiProvide} from '@taiga-ui/cdk';
+import {TuiHint, TuiNumberFormat} from '@taiga-ui/core';
+import {TuiInputNumberModule, TuiTextfieldControllerModule} from '@taiga-ui/legacy';
+
+import {ABSTRACT_PROPS_ACCESSOR} from '../abstract/abstract-props-accessor';
+import {InheritedDocumentation} from '../abstract/inherited-documentation';
+import {AbstractExampleTuiNumberFormat} from '../abstract/number-format';
+
+@Component({
+ standalone: true,
+ imports: [
+ InheritedDocumentation,
+ ReactiveFormsModule,
+ TuiDemo,
+ TuiHint,
+ TuiInputNumberModule,
+ TuiNumberFormat,
+ TuiTextfieldControllerModule,
+ ],
+ templateUrl: './index.html',
+ changeDetection,
+ providers: [tuiProvide(ABSTRACT_PROPS_ACCESSOR, PageComponent)],
+})
+export default class PageComponent extends AbstractExampleTuiNumberFormat {
+ protected readonly routes = DemoRoute;
+ protected docPages = DemoRoute;
+
+ protected readonly minVariants: readonly number[] = [-Infinity, -500, 5, 25];
+
+ protected min = this.minVariants[0]!;
+
+ protected readonly maxVariants: readonly number[] = [Infinity, 10, 500];
+
+ protected max = this.maxVariants[0]!;
+
+ protected step = 0;
+
+ public override cleaner = false;
+ public override precision = 2;
+ public readonly control = new FormControl(6432, Validators.required);
+}
diff --git a/projects/demo/src/modules/components/input-number/examples/import/import.md b/projects/demo/src/modules/components/input-number/examples/import/import.md
index 5261a0d39849..787a7dbd04df 100644
--- a/projects/demo/src/modules/components/input-number/examples/import/import.md
+++ b/projects/demo/src/modules/components/input-number/examples/import/import.md
@@ -1,15 +1,25 @@
```ts
-import {FormsModule, ReactiveFormsModule} from '@angular/forms';
-import {TuiInputNumberModule} from '@taiga-ui/legacy';
+import {ReactiveFormsModule} from '@angular/forms';
+import {TuiNumberFormat} from '@taiga-ui/core';
+import {TuiInputNumber, tuiInputNumberOptionsProvider} from '@taiga-ui/kit';
@Component({
standalone: true,
- imports: [FormsModule, ReactiveFormsModule, TuiInputNumberModule],
- // ...
+ imports: [ReactiveFormsModule, TuiInputNumber, TuiNumberFormat],
+ providers: [
+ /**
+ * (Optional)
+ * Customize default behavior for all InputNumber-s
+ * inside specific Dependency Injection scope
+ */
+ tuiInputNumberOptionsProvider({
+ min: 0,
+ max: 100,
+ postfix: '%',
+ }),
+ ],
})
export class Example {
- testForm = new FormGroup({
- testValue: new FormControl(5000),
- });
+ protected readonly control = new FormControl(42);
}
```
diff --git a/projects/demo/src/modules/components/input-number/examples/import/template.md b/projects/demo/src/modules/components/input-number/examples/import/template.md
index 6427b847151e..b6e540fa7df1 100644
--- a/projects/demo/src/modules/components/input-number/examples/import/template.md
+++ b/projects/demo/src/modules/components/input-number/examples/import/template.md
@@ -1,5 +1,9 @@
```html
-
+
+
+
```
diff --git a/projects/demo/src/modules/components/input-number/index.html b/projects/demo/src/modules/components/input-number/index.html
index 1f2a2103e5b5..bfa7f99c660b 100644
--- a/projects/demo/src/modules/components/input-number/index.html
+++ b/projects/demo/src/modules/components/input-number/index.html
@@ -1,249 +1,100 @@
-
- A component to input numbers. Control value is also of number type.
-
-
- There are also other components to input numbers:
-
-
+ Coming soon, stay tuned...
-
- Number formatting can be customized with
-
- TUI_NUMBER_FORMAT
-
- token.
-
-
-
-
-
- To input money use properties
- [postfix]
- or
- [prefix]
- . To get currency symbol use pipe
-
- tuiCurrency
-
- .
-
-
-
-
-
-
- Customize input via
-
+
+
+
- TextfieldControllers
-
- .
-
+ Enter a number
-
- If you need to set some attributes or listen to events on native
- input
- , you can put it inside with
- Textfield
- directive as shown below
-
-
-
-
-
- Use property
- [precision]
- to configure a number of digits after comma.
+
+
-
+
+
+
+
+ The lowest value in the range of permitted values
+
-
+
+ The greatest value in the range of permitted values
+
-
+
+ Uneditable text
+ before
+ number
+
-
+
+ Uneditable text
+ after
+ number
+
+
-
+
-
-
+
-
-
-
-
- Type a sum
-
-
-
-
-
- Disabled state (use
- formControl.disable()
- )
-
-
- Min value
-
-
- Max value
-
-
- Step to increase/decrease value with keyboard and buttons on the side
-
-
-
-
-
- Custom align content by text-align
-
-
+
+
diff --git a/projects/demo/src/modules/components/input-number/index.ts b/projects/demo/src/modules/components/input-number/index.ts
index b131110fa629..45bdb4d1a9aa 100644
--- a/projects/demo/src/modules/components/input-number/index.ts
+++ b/projects/demo/src/modules/components/input-number/index.ts
@@ -1,46 +1,36 @@
import {Component} from '@angular/core';
import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
+import {TuiDocControl} from '@demo/components/control';
+import {TuiDocNumberFormat} from '@demo/components/number-format';
+import {TuiDocTextfield} from '@demo/components/textfield';
import {changeDetection} from '@demo/emulate/change-detection';
-import {DemoRoute} from '@demo/routes';
import {TuiDemo} from '@demo/utils';
-import {tuiProvide} from '@taiga-ui/cdk';
-import {TuiHint, TuiNumberFormat} from '@taiga-ui/core';
-import {TuiInputNumberModule, TuiTextfieldControllerModule} from '@taiga-ui/legacy';
-
-import {ABSTRACT_PROPS_ACCESSOR} from '../abstract/abstract-props-accessor';
-import {InheritedDocumentation} from '../abstract/inherited-documentation';
-import {AbstractExampleTuiNumberFormat} from '../abstract/number-format';
+import {TuiHint, TuiNumberFormat, TuiTextfield} from '@taiga-ui/core';
+import {TuiInputNumber} from '@taiga-ui/kit';
@Component({
standalone: true,
imports: [
- InheritedDocumentation,
ReactiveFormsModule,
TuiDemo,
+ TuiDocControl,
+ TuiDocNumberFormat,
+ TuiDocTextfield,
TuiHint,
- TuiInputNumberModule,
+ TuiInputNumber,
TuiNumberFormat,
- TuiTextfieldControllerModule,
+ TuiTextfield,
],
templateUrl: './index.html',
changeDetection,
- providers: [tuiProvide(ABSTRACT_PROPS_ACCESSOR, PageComponent)],
})
-export default class PageComponent extends AbstractExampleTuiNumberFormat {
- protected readonly routes = DemoRoute;
- protected docPages = DemoRoute;
-
+export default class PageComponent {
+ protected readonly control = new FormControl(null, Validators.required);
+ protected readonly maxVariants: readonly number[] = [Infinity, 10, 500];
protected readonly minVariants: readonly number[] = [-Infinity, -500, 5, 25];
protected min = this.minVariants[0]!;
-
- protected readonly maxVariants: readonly number[] = [Infinity, 10, 500];
-
protected max = this.maxVariants[0]!;
-
- protected step = 0;
-
- public override cleaner = false;
- public override precision = 2;
- public readonly control = new FormControl(6432, Validators.required);
+ protected prefix = '';
+ protected postfix = '';
}
diff --git a/projects/demo/src/modules/components/input/index.html b/projects/demo/src/modules/components/input/index.html
index 5fe7efabc156..a3240d3bd0ae 100644
--- a/projects/demo/src/modules/components/input/index.html
+++ b/projects/demo/src/modules/components/input/index.html
@@ -43,7 +43,7 @@
InputNumber
diff --git a/projects/demo/src/modules/components/utils/tokens/examples/5/index.html b/projects/demo/src/modules/components/utils/tokens/examples/5/index.html
index 904ea55f6dbb..4aa86769076f 100644
--- a/projects/demo/src/modules/components/utils/tokens/examples/5/index.html
+++ b/projects/demo/src/modules/components/utils/tokens/examples/5/index.html
@@ -51,7 +51,7 @@ Components that are customizable:
TuiInputNumberComponent
diff --git a/projects/demo/src/modules/pipes/currency/index.html b/projects/demo/src/modules/pipes/currency/index.html
index d7c8a1c83d3f..6a66915ff79f 100644
--- a/projects/demo/src/modules/pipes/currency/index.html
+++ b/projects/demo/src/modules/pipes/currency/index.html
@@ -8,7 +8,7 @@
Pipe for transforming number into money. It is usually used with
InputNumber
diff --git a/projects/kit/components/index.ts b/projects/kit/components/index.ts
index d9599844ac38..3e64f6678962 100644
--- a/projects/kit/components/index.ts
+++ b/projects/kit/components/index.ts
@@ -21,6 +21,7 @@ export * from '@taiga-ui/kit/components/elastic-container';
export * from '@taiga-ui/kit/components/files';
export * from '@taiga-ui/kit/components/filter';
export * from '@taiga-ui/kit/components/input-inline';
+export * from '@taiga-ui/kit/components/input-number';
export * from '@taiga-ui/kit/components/input-password';
export * from '@taiga-ui/kit/components/input-phone-international';
export * from '@taiga-ui/kit/components/items-with-more';
diff --git a/projects/kit/components/input-number/index.ts b/projects/kit/components/input-number/index.ts
new file mode 100644
index 000000000000..e4e250600fe9
--- /dev/null
+++ b/projects/kit/components/input-number/index.ts
@@ -0,0 +1,2 @@
+export * from './input-number.component';
+export * from './input-number.options';
diff --git a/projects/kit/components/input-number/input-number.component.ts b/projects/kit/components/input-number/input-number.component.ts
new file mode 100644
index 000000000000..8791c84cca2c
--- /dev/null
+++ b/projects/kit/components/input-number/input-number.component.ts
@@ -0,0 +1,255 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ inject,
+ Input,
+ signal,
+} from '@angular/core';
+import {toSignal} from '@angular/core/rxjs-interop';
+import {MaskitoDirective} from '@maskito/angular';
+import type {MaskitoOptions} from '@maskito/core';
+import {maskitoInitialCalibrationPlugin} from '@maskito/core';
+import {
+ maskitoCaretGuard,
+ maskitoNumberOptionsGenerator,
+ maskitoParseNumber,
+} from '@maskito/kit';
+import {TuiControl} from '@taiga-ui/cdk/classes';
+import {CHAR_HYPHEN, CHAR_MINUS} from '@taiga-ui/cdk/constants';
+import {TUI_IS_IOS, tuiFallbackValueProvider} from '@taiga-ui/cdk/tokens';
+import {tuiInjectElement} from '@taiga-ui/cdk/utils/dom';
+import {tuiIsSafeToRound} from '@taiga-ui/cdk/utils/math';
+import {TuiTextfieldDirective} from '@taiga-ui/core/components/textfield';
+import {TUI_DEFAULT_NUMBER_FORMAT, TUI_NUMBER_FORMAT} from '@taiga-ui/core/tokens';
+import {tuiFormatNumber} from '@taiga-ui/core/utils/format';
+import {tuiMaskito} from '@taiga-ui/kit/utils';
+
+import {TUI_INPUT_NUMBER_OPTIONS} from './input-number.options';
+
+const DEFAULT_MAX_LENGTH = 18;
+
+@Component({
+ standalone: true,
+ selector: 'input[tuiInputNumber]',
+ template: '',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [tuiFallbackValueProvider(null)],
+ hostDirectives: [
+ // TODO: replace with `TuiWithTextfield` after merging of https://github.com/taiga-family/taiga-ui/pull/9976
+ {
+ directive: TuiTextfieldDirective,
+ inputs: ['invalid', 'focused', 'readOnly', 'state'],
+ },
+ MaskitoDirective,
+ ],
+ host: {
+ '[value]': 'textfieldValue()',
+ '[disabled]': 'disabled()',
+ '[attr.inputMode]': 'inputMode()',
+ '[attr.maxLength]': 'maxLength()',
+ '(input)': 'onInput()',
+ '(blur)': 'onBlur()',
+ '(focus)': 'onFocus()',
+ },
+})
+export class TuiInputNumber extends TuiControl {
+ private readonly element = tuiInjectElement();
+ private readonly isIOS = inject(TUI_IS_IOS);
+ private readonly options = inject(TUI_INPUT_NUMBER_OPTIONS);
+ private readonly numberFormat = toSignal(inject(TUI_NUMBER_FORMAT), {
+ initialValue: TUI_DEFAULT_NUMBER_FORMAT,
+ });
+
+ private readonly min = signal(this.options.min);
+ private readonly max = signal(this.options.max);
+ private readonly prefix = signal(this.options.prefix);
+ private readonly postfix = signal(this.options.postfix);
+
+ private readonly precision = computed(() =>
+ Number.isNaN(this.numberFormat().precision) ? 2 : this.numberFormat().precision,
+ );
+
+ private readonly isIntermediateState = computed(() => {
+ const value = maskitoParseNumber(
+ this.textfieldValue(),
+ this.numberFormat().decimalSeparator,
+ );
+
+ return value < 0 ? value > this.max() : value < this.min();
+ });
+
+ protected textfieldValue = signal(this.element.value || '');
+ protected override readonly transformer = this.options.valueTransformer;
+
+ protected readonly inputMode = computed(() => {
+ if (this.isIOS && this.min() < 0) {
+ // iPhone does not have minus sign if inputMode is equal to 'numeric' / 'decimal'
+ return 'text';
+ }
+
+ return this.precision() ? 'decimal' : 'numeric';
+ });
+
+ protected readonly maxLength = computed(() => {
+ const {decimalSeparator, thousandSeparator} = this.numberFormat();
+ const decimalPart =
+ !!this.precision() && this.textfieldValue().includes(decimalSeparator);
+ const precision = decimalPart ? Math.min(this.precision() + 1, 20) : 0;
+ const takeThousand = thousandSeparator.repeat(5).length;
+
+ return DEFAULT_MAX_LENGTH + precision + takeThousand;
+ });
+
+ protected readonly mask = tuiMaskito(
+ computed((numberFormat = this.numberFormat()) =>
+ this.computeMask({
+ ...numberFormat,
+ precision: this.precision(),
+ min: this.min(),
+ max: this.max(),
+ prefix: this.prefix(),
+ postfix: this.postfix(),
+ decimalZeroPadding: numberFormat.decimalMode === 'always',
+ }),
+ ),
+ );
+
+ @Input('min')
+ public set minSetter(x: number | null) {
+ this.updateMinMaxLimits(x, this.max());
+ }
+
+ @Input('max')
+ public set maxSetter(x: number | null) {
+ this.updateMinMaxLimits(this.min(), x);
+ }
+
+ // TODO(v5): replace with signal input
+ @Input('prefix')
+ public set prefixSetter(x: string) {
+ this.prefix.set(x);
+ }
+
+ // TODO(v5): replace with signal input
+ @Input('postfix')
+ public set postfixSetter(x: string) {
+ this.postfix.set(x);
+ }
+
+ public override writeValue(value: number | null): void {
+ super.writeValue(value);
+ this.textfieldValue.set(this.formatNumber(value));
+ }
+
+ protected onInput(): void {
+ const value = this.element.value;
+ const parsedValue = maskitoParseNumber(
+ value,
+ this.numberFormat().decimalSeparator,
+ );
+
+ this.textfieldValue.set(value);
+
+ if (Number.isNaN(parsedValue)) {
+ this.onChange(null);
+
+ return;
+ }
+
+ if (
+ this.isIntermediateState() ||
+ parsedValue < this.min() ||
+ parsedValue > this.max()
+ ) {
+ return;
+ }
+
+ this.onChange(parsedValue);
+ }
+
+ protected onBlur(): void {
+ this.onTouched();
+
+ if (!this.isIntermediateState()) {
+ this.textfieldValue.set(this.formatNumber(this.value()));
+ }
+ }
+
+ protected onFocus(): void {
+ const value = maskitoParseNumber(
+ this.textfieldValue(),
+ this.numberFormat().decimalSeparator,
+ );
+
+ if (Number.isNaN(value) && !this.readOnly()) {
+ this.textfieldValue.set(this.prefix() + this.postfix());
+ }
+ }
+
+ private formatNumber(value: number | null): string {
+ if (value === null) {
+ return '';
+ }
+
+ return (
+ this.prefix() +
+ tuiFormatNumber(value, {
+ ...this.numberFormat(),
+ /**
+ * Number can satisfy interval [Number.MIN_SAFE_INTEGER; Number.MAX_SAFE_INTEGER]
+ * but its rounding can violate it.
+ * Before BigInt support there is no perfect solution – only trade off.
+ * No rounding is better than lose precision and incorrect mutation of already valid value.
+ */
+ precision: tuiIsSafeToRound(value, this.precision())
+ ? this.precision()
+ : Infinity,
+ }).replace(CHAR_HYPHEN, CHAR_MINUS) +
+ this.postfix()
+ );
+ }
+
+ private updateMinMaxLimits(
+ nullableMin: number | null,
+ nullableMax: number | null,
+ ): void {
+ const min =
+ this.transformer?.fromControlValue(nullableMin) ??
+ nullableMin ??
+ this.options.min;
+ const max =
+ this.transformer?.fromControlValue(nullableMax) ??
+ nullableMax ??
+ this.options.max;
+
+ this.min.set(Math.min(min, max));
+ this.max.set(Math.max(min, max));
+ }
+
+ private computeMask(
+ params: NonNullable[0]>,
+ ): MaskitoOptions {
+ const {prefix = '', postfix = ''} = params;
+ const {plugins, ...options} = maskitoNumberOptionsGenerator(params);
+ const initialCalibrationPlugin = maskitoInitialCalibrationPlugin(
+ maskitoNumberOptionsGenerator({
+ ...params,
+ min: Number.MIN_SAFE_INTEGER,
+ max: Number.MAX_SAFE_INTEGER,
+ }),
+ );
+
+ return {
+ ...options,
+ plugins: [
+ ...plugins,
+ initialCalibrationPlugin,
+ maskitoCaretGuard((value) => [
+ prefix.length,
+ value.length - postfix.length,
+ ]),
+ ],
+ };
+ }
+}
diff --git a/projects/kit/components/input-number/input-number.options.ts b/projects/kit/components/input-number/input-number.options.ts
new file mode 100644
index 000000000000..4df74d2fb6fc
--- /dev/null
+++ b/projects/kit/components/input-number/input-number.options.ts
@@ -0,0 +1,31 @@
+import type {Provider} from '@angular/core';
+import type {TuiValueTransformer} from '@taiga-ui/cdk/classes';
+import {tuiCreateToken, tuiProvideOptions} from '@taiga-ui/cdk/utils/miscellaneous';
+
+export interface TuiInputNumberOptions {
+ readonly max: number;
+ readonly min: number;
+ readonly prefix: string;
+ readonly postfix: string;
+ readonly valueTransformer: TuiValueTransformer | null;
+}
+
+export const TUI_INPUT_NUMBER_DEFAULT_OPTIONS: TuiInputNumberOptions = {
+ min: Number.MIN_SAFE_INTEGER,
+ max: Number.MAX_SAFE_INTEGER,
+ prefix: '',
+ postfix: '',
+ valueTransformer: null,
+};
+
+export const TUI_INPUT_NUMBER_OPTIONS = tuiCreateToken(TUI_INPUT_NUMBER_DEFAULT_OPTIONS);
+
+export function tuiInputNumberOptionsProvider(
+ options: Partial,
+): Provider {
+ return tuiProvideOptions(
+ TUI_INPUT_NUMBER_OPTIONS,
+ options,
+ TUI_INPUT_NUMBER_DEFAULT_OPTIONS,
+ );
+}
diff --git a/projects/kit/components/input-number/ng-package.json b/projects/kit/components/input-number/ng-package.json
new file mode 100644
index 000000000000..bebf62dcb5e5
--- /dev/null
+++ b/projects/kit/components/input-number/ng-package.json
@@ -0,0 +1,5 @@
+{
+ "lib": {
+ "entryFile": "index.ts"
+ }
+}
diff --git a/projects/kit/components/input-password/input-password.component.ts b/projects/kit/components/input-password/input-password.component.ts
index 39c81a9b7876..c39c15cf71f6 100644
--- a/projects/kit/components/input-password/input-password.component.ts
+++ b/projects/kit/components/input-password/input-password.component.ts
@@ -20,9 +20,6 @@ import {TUI_PASSWORD_TEXTS} from '@taiga-ui/kit/tokens';
import {TUI_INPUT_PASSWORD_OPTIONS} from './input-password.options';
-/**
- * @deprecated use {@link TuiPassword} with {@link TuiTextfield}
- */
@Component({
standalone: true,
selector: 'input[tuiInputPassword]',