diff --git a/docs/components-api/inputnumber.md b/docs/components-api/inputnumber.md
index aa7f4062..4fd7e739 100644
--- a/docs/components-api/inputnumber.md
+++ b/docs/components-api/inputnumber.md
@@ -2,35 +2,49 @@
# ExtraInputNumber
-| Свойство | Описание | Типизация |
-| ---------------- | ---------------------------------------------------------------------------------------------- | -------------------------- |
-| `placeholder` | текст подсказки внутри поля | `string` |
-| `label` | текст названия поля | `string` |
-| `labelPosition` | положение лейбла | `left \| top \| float` |
-| `buttons` | отображение кнопок для увеличения/уменьшения значения (недоступно при `labelPosition: float`) | `boolean` |
-| `min` | минимальное допустимое значение | `number` |
-| `max` | максимальное допустимое значение | `number` |
-| `step` | шаг изменения значения (кнопками `buttons`) | `number` |
-| `prefix` | текст перед значением внутри поля | `string` |
-| `suffix` | текст после значения внутри поля | `string` |
-| `clearable` | отображение иконки для очистки поля | `boolean` |
-| `caption` | текст пояснения под полем | `string` |
-| `info` | текст с доп. информацией (показывается в тултипе иконки ti-info-circle) | `string` |
-| `size` | размер поля | `small \| base \| large \| xlarge` |
+> ✅ **Реализован**: `ExtraInputNumberComponent` (`@cdek-it/angular-ui-kit`) соответствует спецификации.
+
+| Свойство | Описание | Типизация |
+| --------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------- |
+| `placeholder` | текст подсказки внутри поля | `string` |
+| `label` | текст названия поля | `string` |
+| `labelPosition` | положение лейбла | `top \| left` |
+| `floatLabel` | плавающий лейбл внутри поля (недоступен вместе с `showButtons`) | `boolean` |
+| `showButtons` | отображение кнопок для увеличения/уменьшения значения (недоступно при `floatLabel: true`) | `boolean` |
+| `min` | минимальное допустимое значение | `number` |
+| `max` | максимальное допустимое значение | `number` |
+| `step` | шаг изменения значения (кнопками `showButtons`) | `number` |
+| `prefix` | текст перед значением внутри поля | `string` |
+| `suffix` | текст после значения внутри поля | `string` |
+| `clearable` | отображение иконки для очистки поля | `boolean` |
+| `caption` | текст пояснения под полем | `string` |
+| `info` | текст с доп. информацией (показывается в тултипе иконки ti-info-circle) | `string` |
+| `size` | размер поля | `small \| base \| large \| xlarge` |
+| `readonly` | только для чтения | `boolean` |
+| `fluid` | растягивает поле на всю ширину контейнера | `boolean` |
+| `mode` | режим форматирования числа | `decimal \| currency` |
+| `currency` | код валюты (ISO 4217), используется при `mode: currency` | `string` |
+| `locale` | локаль форматирования числа | `string` |
+| `useGrouping` | группировка разрядов (разделитель тысяч) | `boolean` |
+| `minFractionDigits` | минимальное число знаков после запятой | `number` |
+| `maxFractionDigits` | максимальное число знаков после запятой | `number` |
+| `buttonLayout` | расположение кнопок `showButtons` | `stacked \| horizontal \| vertical` |
+| `incrementButtonIcon` | CSS-класс иконки кнопки увеличения | `string` |
+| `decrementButtonIcon` | CSS-класс иконки кнопки уменьшения | `string` |
# События
-| Событие | Описание | Типизация |
-| --------- | ---------------------------------- | ------------------------------------------ |
-| `input` | срабатывает при вводе значения | `(event: ExtraInputNumberInputEvent) => void` |
-| `clear` | срабатывает при очистке значения | `() => void` |
+| Событие | Описание | Типизация |
+| ------- | -------------------------------- | --------------------------------------------- |
+| `input` | срабатывает при вводе значения | `(event: ExtraInputNumberInputEvent) => void` |
+| `clear` | срабатывает при очистке значения | `() => void` |
# ExtraInputNumberInputEvent
Событие ввода значения, передаётся в `input`.
-| Свойство | Описание | Типизация |
-| ---------------- | ---------------------- | ---------------- |
-| `value` | введённое значение | `number \| null` |
-| `formattedValue` | отформатированное значение | `string` |
-| `originalEvent` | исходное событие | `Event` |
+| Свойство | Описание | Типизация |
+| ---------------- | -------------------------- | ---------------- |
+| `value` | введённое значение | `number \| null` |
+| `formattedValue` | отформатированное значение | `string` |
+| `originalEvent` | исходное событие | `Event` |
diff --git a/src/lib/components/inputnumber/inputnumber.component.ts b/src/lib/components/inputnumber/inputnumber.component.ts
index cbfdaccc..28b93989 100644
--- a/src/lib/components/inputnumber/inputnumber.component.ts
+++ b/src/lib/components/inputnumber/inputnumber.component.ts
@@ -1,16 +1,41 @@
-import { Component, EventEmitter, forwardRef, inject, Injector, Input, OnInit, Output } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ forwardRef,
+ inject,
+ Injector,
+ Input,
+ OnInit,
+ Output
+} from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
-import { NgClass } from '@angular/common';
+import { NgClass, NgTemplateOutlet } from '@angular/common';
import { InputNumber } from 'primeng/inputnumber';
+import { FloatLabel } from 'primeng/floatlabel';
import { SharedModule } from 'primeng/api';
+import { ExtraTooltipDirective } from '@cdek-it/angular-ui-kit/components/tooltip';
export type ExtraInputNumberSize = 'small' | 'base' | 'large' | 'xlarge';
export type ExtraInputNumberButtonLayout = 'stacked' | 'horizontal' | 'vertical';
+export type ExtraInputNumberLabelPosition = 'top' | 'left';
+export type ExtraInputNumberMode = 'decimal' | 'currency';
+
+export interface ExtraInputNumberInputEvent {
+ value: number | null;
+ formattedValue: string;
+ originalEvent: Event;
+}
+
+let nextInputId = 0;
@Component({
selector: 'extra-input-number',
standalone: true,
- imports: [InputNumber, SharedModule, FormsModule, NgClass],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [InputNumber, SharedModule, FormsModule, NgClass, NgTemplateOutlet, FloatLabel, ExtraTooltipDirective],
+ host: { style: 'display: contents' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
@@ -19,85 +44,136 @@ export type ExtraInputNumberButtonLayout = 'stacked' | 'horizontal' | 'vertical'
}
],
template: `
-
- @if (!incrementButtonIcon) {
-
-
-
- }
- @if (!decrementButtonIcon) {
-
-
-
- }
-
+
+ @if (label || caption) {
+
+ } @else {
+
+ }
+
+
+
+
+
+
+
+ @if (!incrementButtonIcon) {
+
+
+
+ }
+ @if (!decrementButtonIcon) {
+
+
+
+ }
+
+
`
})
export class ExtraInputNumberComponent implements ControlValueAccessor, OnInit {
private readonly _injector = inject(Injector);
+ private readonly _cdr = inject(ChangeDetectorRef);
private _ngControl: NgControl | null = null;
ngOnInit(): void {
this._ngControl = this._injector.get(NgControl, null, { self: true, optional: true });
}
+ @Input() placeholder = '';
+ @Input() label = '';
+ @Input() labelPosition: ExtraInputNumberLabelPosition = 'top';
+ @Input() floatLabel = false;
+ @Input() clearable = false;
+ @Input() caption = '';
+ @Input() info = '';
@Input() size: ExtraInputNumberSize = 'base';
@Input() showButtons = false;
@Input() buttonLayout: ExtraInputNumberButtonLayout = 'stacked';
- @Input() mode = 'decimal';
+ @Input() mode: ExtraInputNumberMode = 'decimal';
@Input() currency: string | undefined;
@Input() locale: string | undefined;
- @Input() placeholder = '';
@Input() readonly = false;
@Input() fluid = false;
@Input() min: number | undefined;
@@ -111,19 +187,15 @@ export class ExtraInputNumberComponent implements ControlValueAccessor, OnInit {
@Input() incrementButtonIcon: string | undefined;
@Input() decrementButtonIcon: string | undefined;
+ /** Уникальный id поля для связи label ↔ input. */
+ readonly inputId = `extra-input-number-${nextInputId++}`;
+
disabled = false;
get invalid(): boolean {
return this._ngControl?.invalid ?? false;
}
- @Output() onInput = new EventEmitter<{ value: number | null }>();
-
- modelValue: number | null = null;
-
- private _onChange: (value: number | null) => void = () => {};
- onTouched: () => void = () => {};
-
get inputSizeClass(): string {
if (this.size === 'small') return 'p-inputtext-sm';
if (this.size === 'large' || this.size === 'xlarge') return 'p-inputtext-lg';
@@ -134,14 +206,29 @@ export class ExtraInputNumberComponent implements ControlValueAccessor, OnInit {
return { 'p-inputnumber-xlg': this.size === 'xlarge' };
}
- onModelChange(value: number | null): void {
- this.modelValue = value;
- this._onChange(value);
- this.onInput.emit({ value });
+ @Output() onInput = new EventEmitter();
+ @Output() onClear = new EventEmitter();
+
+ modelValue: number | null = null;
+
+ private _onChange: (value: number | null) => void = () => {};
+ onTouched: () => void = () => {};
+
+ handleInput(event: ExtraInputNumberInputEvent): void {
+ this.modelValue = event.value;
+ this._onChange(event.value);
+ this.onInput.emit(event);
+ }
+
+ handleClear(): void {
+ this.modelValue = null;
+ this._onChange(null);
+ this.onClear.emit();
}
writeValue(value: number | null): void {
this.modelValue = value ?? null;
+ this._cdr.markForCheck();
}
registerOnChange(fn: (value: number | null) => void): void {
@@ -154,5 +241,6 @@ export class ExtraInputNumberComponent implements ControlValueAccessor, OnInit {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
+ this._cdr.markForCheck();
}
}
diff --git a/src/lib/components/inputnumber/inputnumber.figma.md b/src/lib/components/inputnumber/inputnumber.figma.md
index f87fd14f..c04227a7 100644
--- a/src/lib/components/inputnumber/inputnumber.figma.md
+++ b/src/lib/components/inputnumber/inputnumber.figma.md
@@ -10,60 +10,92 @@ figma:
componentKey: '00bb1604181c52a43b1c9357ef8f88ccf882dd59'
name: ''
status: stable
-updated: '2026-06-22'
+updated: '2026-09-14'
---
## Overview
`ExtraInputNumberComponent` — числовое поле для точного ввода числа с клавиатуры и пошагово через stepper-кнопки (+/−). Поддерживает форматирование (валюта, проценты, разделители разрядов), ограничения диапазона (`min`/`max`) и шаг (`step`). Оборачивает PrimeNG `p-inputNumber` и реализует `ControlValueAccessor`, поэтому работает с `[(ngModel)]` и `[formControl]` «из коробки».
-Компонент соответствует Figma-компоненту `` (nodeId `320:1754`, библиотека «UI Kit (DS) v2.0»). Label, подсказка и сообщение об ошибке навешиваются снаружи — через `` или ``. В отличие от `` с `type=number`, поддерживает stepper-кнопки и форматирование.
+Компонент соответствует Figma-компоненту `` (nodeId `320:1754`, библиотека «UI Kit (DS) v2.1»). Как и ``, умеет самостоятельно рисовать `label`/`caption`/`info`-тултип рядом с полем — оборачивать в ``/`` не обязательно.
## Props mapping
-| Свойство | Тип | По умолчанию | Описание |
-|----------|-----|--------------|---------|
-| `size` | `'small' \| 'base' \| 'large' \| 'xlarge'` | `'base'` | Размер поля |
-| `showButtons` | `boolean` | `false` | Показывает stepper-кнопки (+/−) — соответствует Figma-свойству `buttons` |
-| `buttonLayout` | `'stacked' \| 'horizontal' \| 'vertical'` | `'stacked'` | Расположение stepper-кнопок (действует при `showButtons=true`) |
-| `mode` | `string` (`'decimal' \| 'currency'`) | `'decimal'` | Режим форматирования числа; `currency` включает отображение валюты |
-| `currency` | `string \| undefined` | `undefined` | Код валюты (ISO 4217, например `'RUB'`); используется при `mode='currency'` |
-| `locale` | `string \| undefined` | `undefined` | Локаль форматирования (например `'ru-RU'`) |
-| `placeholder` | `string` | `''` | Подсказка при пустом поле — соответствует Figma-свойствам `text-placeholder` / `has-placeholder` |
-| `readonly` | `boolean` | `false` | Только для чтения — соответствует Figma-состоянию `state=readonly` |
-| `fluid` | `boolean` | `false` | Растягивает поле на всю ширину контейнера |
-| `min` | `number \| undefined` | `undefined` | Минимальное допустимое значение |
-| `max` | `number \| undefined` | `undefined` | Максимальное допустимое значение |
-| `step` | `number` | `1` | Шаг изменения при нажатии stepper-кнопок |
-| `prefix` | `string \| undefined` | `undefined` | Текст перед числом — соответствует Figma-свойствам `show-text-prefix` / `text-prefix` |
-| `suffix` | `string \| undefined` | `undefined` | Текст после числа — соответствует Figma-свойствам `show-text-suffix` / `text-suffix` |
-| `minFractionDigits` | `number \| undefined` | `undefined` | Минимальное число знаков после запятой |
-| `maxFractionDigits` | `number \| undefined` | `undefined` | Максимальное число знаков после запятой |
-| `useGrouping` | `boolean` | `true` | Группировка разрядов (разделитель тысяч) |
-| `incrementButtonIcon` | `string \| undefined` | `undefined` | CSS-класс иконки кнопки «+»; по умолчанию используется встроенная SVG-иконка плюса |
-| `decrementButtonIcon` | `string \| undefined` | `undefined` | CSS-класс иконки кнопки «−»; по умолчанию используется встроенная SVG-иконка минуса |
-| `(onInput)` | `EventEmitter<{ value: number \| null }>` | — | Событие при изменении значения |
-| `[(ngModel)]` / `[formControl]` | `number \| null` | `null` | Значение поля через ControlValueAccessor — соответствует Figma-свойству `text-inputnumber` |
+| Свойство | Тип | По умолчанию | Описание |
+| ------------------------------- | ------------------------------------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| `placeholder` | `string` | `''` | Подсказка при пустом поле — соответствует Figma-свойству `text-placeholder` / `has-placeholder` |
+| `label` | `string` | `''` | Текст названия поля |
+| `labelPosition` | `'top' \| 'left'` | `'top'` | Положение лейбла относительно поля |
+| `floatLabel` | `boolean` | `false` | Плавающий лейбл внутри поля (PrimeNG `p-floatlabel`); недоступен вместе со stepper-кнопками — при `floatLabel=true` `showButtons` игнорируется |
+| `showButtons` | `boolean` | `false` | Показывает stepper-кнопки (+/−) — соответствует Figma-свойству `buttons`; недоступно при `floatLabel=true` |
+| `buttonLayout` | `'stacked' \| 'horizontal' \| 'vertical'` | `'stacked'` | Расположение stepper-кнопок (действует при `showButtons=true`) |
+| `mode` | `string` (`'decimal' \| 'currency'`) | `'decimal'` | Режим форматирования числа; `currency` включает отображение валюты |
+| `currency` | `string \| undefined` | `undefined` | Код валюты (ISO 4217, например `'RUB'`); используется при `mode='currency'` |
+| `locale` | `string \| undefined` | `undefined` | Локаль форматирования (например `'ru-RU'`) |
+| `readonly` | `boolean` | `false` | Только для чтения — соответствует Figma-состоянию `state=readonly` |
+| `fluid` | `boolean` | `false` | Растягивает поле на всю ширину контейнера |
+| `min` | `number \| undefined` | `undefined` | Минимальное допустимое значение |
+| `max` | `number \| undefined` | `undefined` | Максимальное допустимое значение |
+| `step` | `number` | `1` | Шаг изменения при нажатии stepper-кнопок |
+| `prefix` | `string \| undefined` | `undefined` | Текст перед числом — соответствует Figma-свойствам `show-text-prefix` / `text-prefix` |
+| `suffix` | `string \| undefined` | `undefined` | Текст после числа — соответствует Figma-свойствам `show-text-suffix` / `text-suffix` |
+| `clearable` | `boolean` | `false` | Показывает иконку очистки поля — соответствует Figma-свойству `clearable` |
+| `caption` | `string` | `''` | Текст пояснения под полем |
+| `info` | `string` | `''` | Текст доп. информации в тултипе иконки `ti-info-circle` рядом с лейблом |
+| `size` | `'small' \| 'base' \| 'large' \| 'xlarge'` | `'base'` | Размер поля |
+| `minFractionDigits` | `number \| undefined` | `undefined` | Минимальное число знаков после запятой |
+| `maxFractionDigits` | `number \| undefined` | `undefined` | Максимальное число знаков после запятой |
+| `useGrouping` | `boolean` | `true` | Группировка разрядов (разделитель тысяч) |
+| `incrementButtonIcon` | `string \| undefined` | `undefined` | CSS-класс иконки кнопки «+»; по умолчанию используется встроенная SVG-иконка плюса |
+| `decrementButtonIcon` | `string \| undefined` | `undefined` | CSS-класс иконки кнопки «−»; по умолчанию используется встроенная SVG-иконка минуса |
+| `(onInput)` | `EventEmitter` | — | Событие при вводе значения |
+| `(onClear)` | `EventEmitter` | — | Событие при очистке значения (иконка `clearable`) |
+| `[(ngModel)]` / `[formControl]` | `number \| null` | `null` | Значение поля через ControlValueAccessor — соответствует Figma-свойству `text-inputnumber` |
> `invalid` — вычисляемое свойство: берётся автоматически из связанного `NgControl` (соответствует Figma-состоянию `state=danger`). Устанавливать вручную нельзя.
> `disabled` — управляется через `FormControl.disable()` или `ControlValueAccessor.setDisabledState`; компонент не объявляет `@Input() disabled`. Соответствует Figma-состоянию `state=disabled`.
+## ExtraInputNumberInputEvent
+
+| Свойство | Тип | Описание |
+| ---------------- | ---------------- | -------------------------- |
+| `value` | `number \| null` | Введённое значение |
+| `formattedValue` | `string` | Отформатированное значение |
+| `originalEvent` | `Event` | Исходное событие |
+
## Variants
### Default / Base (базовое поле)
Figma: ``, state=default, buttons=false, has-placeholder=true — nodeId `320:1780`
+```html
+
+```
+
+### С лейблом (label / labelPosition)
+
+Figma: ``, has-label=true — nodeId `320:1780`
+
```html
```
-### Со stepper-кнопками (buttons)
+### С плавающим лейблом (floatLabel)
+
+```html
+
+```
+
+### Со stepper-кнопками (showButtons)
Figma: ``, state=default, buttons=true, has-placeholder=true — nodeId `379:1793`
@@ -83,13 +115,7 @@ Figma: ``, state=default, buttons=true, has-placeholder=true — no
Figma: ``, state=default, show-text-suffix=true — nodeId `320:1780`
```html
-
+
```
### С префиксом и суффиксом (prefix / suffix)
@@ -97,12 +123,13 @@ Figma: ``, state=default, show-text-suffix=true — nodeId `320:178
Figma: ``, state=default, show-text-prefix=true, show-text-suffix=true — nodeId `320:1780`
```html
-
+
+```
+
+### С очисткой (clearable)
+
+```html
+
```
### Disabled (отключённое поле)
@@ -110,10 +137,7 @@ Figma: ``, state=default, show-text-prefix=true, show-text-suffix=t
Figma: ``, state=disabled, buttons=false — nodeId `320:1755`
```html
-
+
```
```ts
@@ -125,11 +149,7 @@ disabledControl = new FormControl({ value: 0, disabled: true });
Figma: ``, state=readonly, buttons=false — nodeId `14980:33596`
```html
-
+
```
### С реактивной формой (formControl + валидация)
@@ -137,17 +157,11 @@ Figma: ``, state=readonly, buttons=false — nodeId `14980:33596`
Figma: ``, state=danger — nodeId `320:1761` (невалидное состояние определяется через NgControl автоматически)
```ts
-amountControl = new FormControl(null, [
- Validators.required,
- Validators.min(1),
-]);
+amountControl = new FormControl(null, [Validators.required, Validators.min(1)]);
```
```html
-
+
```
### Fluid / Large (на всю ширину, большой размер)
@@ -178,15 +192,18 @@ Figma: ``, state=default
## Do / Don't
**Do:**
+
- Используйте `[(ngModel)]` или `[formControl]` — компонент реализует `ControlValueAccessor` и именно через них передаётся значение.
- Управляйте состоянием `disabled` через `FormControl.disable()` / `FormControl.enable()` — это сохраняет dirty/touched-флаги.
- Задавайте `[min]` и `[max]` для ограничения диапазона и `[step]` для шага stepper-кнопок.
- Для денежных значений используйте `mode="currency"` с `currency` и `locale` вместо ручного `suffix`.
- Используйте `[fluid]="true"` в формах на всю ширину и мобильных макетах.
-- Оберните поле в `` или `` для добавления label, подсказки и сообщения об ошибке.
+- Используйте встроенные `label`/`caption`/`info` вместо внешнего ``, если не нужны дополнительные слоты.
**Don't:**
+
- Не подменяйте `value` через прямой DOM — теряется `dirty`-state и Angular-реактивность.
- Не задавайте `[disabled]="true"` как Input-атрибут напрямую — компонент не объявляет `@Input() disabled`; передавайте через `FormControl` или `ControlValueAccessor`.
- Не используйте поле без `[(ngModel)]` / `[formControl]` — без привязки значение не синхронизируется с моделью.
- Не применяйте `mode="currency"` без `currency` — валюта не отобразится корректно.
+- Не включайте `[showButtons]="true"` вместе с `[floatLabel]="true"` — Figma не поддерживает такое сочетание, кнопки будут скрыты.
diff --git a/src/stories/components/inputnumber/examples/inputnumber-clearable.component.ts b/src/stories/components/inputnumber/examples/inputnumber-clearable.component.ts
new file mode 100644
index 00000000..599437fe
--- /dev/null
+++ b/src/stories/components/inputnumber/examples/inputnumber-clearable.component.ts
@@ -0,0 +1,74 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { ExtraInputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component';
+
+const template = `
+
+
+
+
+`;
+
+@Component({
+ selector: 'app-inputnumber-clearable',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [ExtraInputNumberComponent, ReactiveFormsModule],
+ template
+})
+export class InputNumberClearableComponent {
+ filled = new FormControl(42);
+ empty = new FormControl(null);
+}
+
+export const Clearable: StoryObj = {
+ render: () => ({
+ template: ``
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: {
+ story:
+ 'Поле с иконкой очистки (clearable) — соответствует PrimeNG `showClear`. Иконка появляется только при наличии значения.'
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { ExtraInputNumberComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-inputnumber-clearable',
+ standalone: true,
+ imports: [ExtraInputNumberComponent, ReactiveFormsModule],
+ template: \`
+
+ \`,
+})
+export class InputNumberClearableComponent {
+ control = new FormControl(42);
+}
+ `
+ }
+ }
+ }
+};
diff --git a/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts b/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts
index ecdd9637..b19b9212 100644
--- a/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts
+++ b/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts
@@ -1,34 +1,24 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { StoryObj } from '@storybook/angular';
-import { InputNumber } from 'primeng/inputnumber';
-import { FloatLabel } from 'primeng/floatlabel';
-import { SharedModule } from 'primeng/api';
+import { ExtraInputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component';
const template = `
-
-
-
-
-
-
-
-
-
-
-
+
`;
-const styles = '';
@Component({
selector: 'app-inputnumber-float-label',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [InputNumber, FloatLabel, ReactiveFormsModule, SharedModule],
- template,
- styles,
+ imports: [ExtraInputNumberComponent, ReactiveFormsModule],
+ template
})
export class InputNumberFloatLabelComponent {
control = new FormControl(null);
@@ -37,42 +27,38 @@ export class InputNumberFloatLabelComponent {
export const FloatLabelStory: StoryObj = {
name: 'FloatLabel',
render: () => ({
- template: ``,
+ template: ``
}),
parameters: {
controls: { disable: true },
docs: {
description: {
story:
- 'Интеграция с `p-floatlabel` — плавающая метка внутри поля. Требует нативный `p-inputNumber` как прямой дочерний элемент `p-floatlabel`.',
+ 'Плавающая метка внутри поля через `[floatLabel]="true"` — обёртка сама рисует `p-floatlabel` и лейбл, ничего дополнительно оборачивать не нужно. Несовместимо с `showButtons` (кнопки будут скрыты).'
},
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { ExtraInputNumberComponent } from '@cdek-it/angular-ui-kit';
@Component({
standalone: true,
- imports: [InputNumber, FloatLabel, ReactiveFormsModule, SharedModule],
+ imports: [ExtraInputNumberComponent, ReactiveFormsModule],
template: \`
-
-
-
-
-
-
-
-
-
-
-
+
\`,
})
export class InputNumberFloatLabelExample {
control = new FormControl(null);
}
- `,
- },
- },
- },
+ `
+ }
+ }
+ }
};
diff --git a/src/stories/components/inputnumber/examples/inputnumber-labels.component.ts b/src/stories/components/inputnumber/examples/inputnumber-labels.component.ts
new file mode 100644
index 00000000..783b9944
--- /dev/null
+++ b/src/stories/components/inputnumber/examples/inputnumber-labels.component.ts
@@ -0,0 +1,98 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { ExtraInputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component';
+
+const template = `
+
+
+
+
+
+
+`;
+
+@Component({
+ selector: 'app-inputnumber-labels',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [ExtraInputNumberComponent, ReactiveFormsModule],
+ template
+})
+export class InputNumberLabelsComponent {
+ control = new FormControl(null);
+}
+
+export const Labels: StoryObj = {
+ render: () => ({
+ template: ``
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: {
+ story:
+ 'Положения лейбла (labelPosition): top — сверху (по умолчанию), left — слева от поля; floatLabel — плавающий лейбл внутри поля (несовместим с showButtons). Info показывается иконкой ti-info-circle с тултипом, caption — пояснение под полем.'
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { ExtraInputNumberComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-inputnumber-labels',
+ standalone: true,
+ imports: [ExtraInputNumberComponent, ReactiveFormsModule],
+ template: \`
+
+
+
+
+
+
+
+
+ \`,
+})
+export class InputNumberLabelsComponent {
+ control = new FormControl(null);
+}
+ `
+ }
+ }
+ }
+};
diff --git a/src/stories/components/inputnumber/inputnumber.stories.ts b/src/stories/components/inputnumber/inputnumber.stories.ts
index 9c171e80..0df75614 100644
--- a/src/stories/components/inputnumber/inputnumber.stories.ts
+++ b/src/stories/components/inputnumber/inputnumber.stories.ts
@@ -2,6 +2,8 @@ import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { ExtraInputNumberComponent } from '../../../lib/components/inputnumber/inputnumber.component';
import { InputNumberFloatLabelComponent, FloatLabelStory } from './examples/inputnumber-float-label.component';
+import { InputNumberLabelsComponent, Labels } from './examples/inputnumber-labels.component';
+import { InputNumberClearableComponent, Clearable } from './examples/inputnumber-clearable.component';
import { Currency } from './examples/inputnumber-currency.component';
import { Buttons } from './examples/inputnumber-buttons.component';
import { Disabled } from './examples/inputnumber-disabled.component';
@@ -18,215 +20,290 @@ const meta: Meta = {
ExtraInputNumberComponent,
ReactiveFormsModule,
InputNumberFloatLabelComponent,
- ],
- }),
+ InputNumberLabelsComponent,
+ InputNumberClearableComponent
+ ]
+ })
],
parameters: {
designTokens: { prefix: '--p-inputnumber' },
docs: {
description: {
- component: `Числовое поле ввода с поддержкой форматирования, валюты и кнопок увеличения/уменьшения.
+ component: `Числовое поле ввода с поддержкой форматирования, валюты, кнопок увеличения/уменьшения и встроенных label/caption/info.
+
+Реализован по спецификации \`docs/components-api/inputnumber.md\`.
\`\`\`typescript
import { ExtraInputNumberComponent } from '@cdek-it/angular-ui-kit';
-\`\`\``,
- },
- },
+\`\`\``
+ }
+ }
},
argTypes: {
- // ── Props ────────────────────────────────────────────────
- size: {
+ // ── Свойства ─────────────────────────────────────────────
+ placeholder: {
+ control: 'text',
+ description: 'Подсказка при пустом поле',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "''" },
+ type: { summary: 'string' }
+ }
+ },
+ label: {
+ control: 'text',
+ description: 'Текст названия поля',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "''" },
+ type: { summary: 'string' }
+ }
+ },
+ labelPosition: {
control: 'select',
- options: ['small', 'base', 'large', 'xlarge'],
- description: 'Размер компонента',
+ options: ['top', 'left'],
+ description: 'Положение лейбла',
table: {
- category: 'Props',
- defaultValue: { summary: "'base'" },
- type: { summary: "'small' | 'base' | 'large' | 'xlarge'" },
- },
+ category: 'Свойства',
+ defaultValue: { summary: "'top'" },
+ type: { summary: "'top' | 'left'" }
+ }
},
- placeholder: {
+ floatLabel: {
+ control: 'boolean',
+ description: 'Плавающий лейбл внутри поля; несовместим со `showButtons`',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' }
+ }
+ },
+ clearable: {
+ control: 'boolean',
+ description: 'Отображение иконки для очистки поля',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' }
+ }
+ },
+ caption: {
control: 'text',
- description: 'Подсказка при пустом поле',
+ description: 'Текст пояснения под полем',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: "''" },
- type: { summary: 'string' },
- },
+ type: { summary: 'string' }
+ }
+ },
+ info: {
+ control: 'text',
+ description: 'Текст с доп. информацией (показывается в тултипе иконки ti-info-circle)',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "''" },
+ type: { summary: 'string' }
+ }
+ },
+ size: {
+ control: 'select',
+ options: ['small', 'base', 'large', 'xlarge'],
+ description: 'Размер поля',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "'base'" },
+ type: { summary: "'small' | 'base' | 'large' | 'xlarge'" }
+ }
},
showButtons: {
control: 'boolean',
- description: 'Отображает кнопки увеличения/уменьшения',
+ description: 'Отображает кнопки увеличения/уменьшения; недоступно при `floatLabel: true`',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'false' },
- type: { summary: 'boolean' },
- },
+ type: { summary: 'boolean' }
+ }
},
buttonLayout: {
control: 'select',
options: ['stacked', 'horizontal', 'vertical'],
description: 'Расположение кнопок',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: "'stacked'" },
- type: { summary: "'stacked' | 'horizontal' | 'vertical'" },
- },
+ type: { summary: "'stacked' | 'horizontal' | 'vertical'" }
+ }
},
mode: {
control: 'select',
options: ['decimal', 'currency'],
description: 'Режим форматирования',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: "'decimal'" },
- type: { summary: "'decimal' | 'currency'" },
- },
+ type: { summary: "'decimal' | 'currency'" }
+ }
},
currency: {
control: 'text',
description: 'ISO 4217 код валюты (при `mode="currency"`)',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'string' },
- },
+ type: { summary: 'string' }
+ }
},
locale: {
control: 'text',
description: 'Локаль для форматирования',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'string' },
- },
- },
- disabled: {
- control: 'boolean',
- description: 'Отключает взаимодействие — управляется через FormControl',
- table: {
- category: 'Props',
- defaultValue: { summary: 'false' },
- type: { summary: 'boolean' },
- },
- },
- invalid: {
- control: 'boolean',
- description: 'Невалидное состояние — управляется через FormControl',
- table: {
- category: 'Props',
- defaultValue: { summary: 'false' },
- type: { summary: 'boolean' },
- },
+ type: { summary: 'string' }
+ }
},
readonly: {
control: 'boolean',
description: 'Только для чтения',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'false' },
- type: { summary: 'boolean' },
- },
+ type: { summary: 'boolean' }
+ }
},
fluid: {
control: 'boolean',
description: 'Растягивает поле на всю ширину контейнера',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'false' },
- type: { summary: 'boolean' },
- },
+ type: { summary: 'boolean' }
+ }
},
min: {
control: 'number',
description: 'Минимальное значение',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'number' },
- },
+ type: { summary: 'number' }
+ }
},
max: {
control: 'number',
description: 'Максимальное значение',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'number' },
- },
+ type: { summary: 'number' }
+ }
},
step: {
control: 'number',
description: 'Шаг изменения значения',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: '1' },
- type: { summary: 'number' },
- },
+ type: { summary: 'number' }
+ }
},
useGrouping: {
control: 'boolean',
description: 'Использовать разделитель групп разрядов',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'true' },
- type: { summary: 'boolean' },
- },
+ type: { summary: 'boolean' }
+ }
},
prefix: {
control: 'text',
description: 'Текст перед значением',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'string' },
- },
+ type: { summary: 'string' }
+ }
},
suffix: {
control: 'text',
description: 'Текст после значения',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'string' },
- },
+ type: { summary: 'string' }
+ }
},
minFractionDigits: {
control: 'number',
description: 'Минимальное количество знаков после запятой',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'number' },
- },
+ type: { summary: 'number' }
+ }
},
maxFractionDigits: {
control: 'number',
description: 'Максимальное количество знаков после запятой',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'undefined' },
- type: { summary: 'number' },
- },
+ type: { summary: 'number' }
+ }
+ },
+ // ── Состояния (управляются через FormControl) ─────────────
+ disabled: {
+ control: 'boolean',
+ description: 'Отключает взаимодействие — управляется через FormControl',
+ table: {
+ category: 'Состояния',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' }
+ }
+ },
+ invalid: {
+ control: 'boolean',
+ description: 'Невалидное состояние — вычисляется из NgControl (Validators)',
+ table: {
+ category: 'Состояния',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' }
+ }
},
// Hidden computed props
modelValue: { table: { disable: true } },
inputSizeClass: { table: { disable: true } },
sizeClass: { table: { disable: true } },
+ inputId: { table: { disable: true } },
- // ── Events ───────────────────────────────────────────────
+ // ── События ────────────────────────────────────────────────
onInput: {
control: false,
- description: 'Событие при изменении значения',
+ description: 'Срабатывает при вводе значения',
table: {
- category: 'Events',
- type: { summary: 'EventEmitter<{ value: number | null }>' },
- },
+ category: 'События',
+ type: { summary: 'EventEmitter' }
+ }
},
+ onClear: {
+ control: false,
+ description: 'Срабатывает при очистке значения (иконка `clearable`)',
+ table: {
+ category: 'События',
+ type: { summary: 'EventEmitter' }
+ }
+ }
},
args: {
- size: 'base',
placeholder: 'Введите число...',
+ label: '',
+ labelPosition: 'top',
+ floatLabel: false,
+ clearable: false,
+ caption: '',
+ info: '',
+ size: 'base',
showButtons: false,
buttonLayout: 'stacked',
mode: 'decimal',
@@ -235,8 +312,8 @@ import { ExtraInputNumberComponent } from '@cdek-it/angular-ui-kit';
readonly: false,
fluid: false,
step: 1,
- useGrouping: true,
- },
+ useGrouping: true
+ }
};
export default meta;
@@ -248,8 +325,14 @@ export const Default: Story = {
render: (args) => {
const parts: string[] = [];
- if (args.size && args.size !== 'base') parts.push(`size="${args.size}"`);
if (args.placeholder) parts.push(`placeholder="${args.placeholder}"`);
+ if (args.label) parts.push(`label="${args.label}"`);
+ if (args.labelPosition && args.labelPosition !== 'top') parts.push(`labelPosition="${args.labelPosition}"`);
+ if (args.floatLabel) parts.push(`[floatLabel]="true"`);
+ if (args.clearable) parts.push(`[clearable]="true"`);
+ if (args.caption) parts.push(`caption="${args.caption}"`);
+ if (args.info) parts.push(`info="${args.info}"`);
+ if (args.size && args.size !== 'base') parts.push(`size="${args.size}"`);
if (args.showButtons) parts.push(`[showButtons]="true"`);
if (args.buttonLayout && args.buttonLayout !== 'stacked') parts.push(`buttonLayout="${args.buttonLayout}"`);
if (args.mode && args.mode !== 'decimal') parts.push(`mode="${args.mode}"`);
@@ -278,11 +361,11 @@ export const Default: Story = {
parameters: {
docs: {
description: {
- story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.',
- },
- },
- },
+ story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.'
+ }
+ }
+ }
};
// ── Re-exports from example components ────────────────────────────────────
-export { Currency, Buttons, Disabled, FloatLabelStory as FloatLabel };
+export { Currency, Buttons, Disabled, FloatLabelStory as FloatLabel, Labels, Clearable };