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) { +
+ @if (label && labelPosition === 'left' && !floatLabel) { + + } +
+ @if (label && labelPosition === 'top' && !floatLabel) { + + } + @if (floatLabel) { + + + @if (label) { + + } + + } @else { + + } + @if (caption) { +
{{ 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 };