From 447fec65fefb2cad078ea78ee497d441a3d7ff76 Mon Sep 17 00:00:00 2001 From: "ak.dmitriev" Date: Mon, 14 Sep 2026 11:46:25 +0700 Subject: [PATCH 1/2] API.select --- src/lib/components/select/select.component.ts | 117 +++++++--- src/lib/components/select/select.figma.md | 95 +++++--- .../examples/select-custom.component.ts | 8 +- .../examples/select-editable.component.ts | 8 +- .../examples/select-filter.component.ts | 8 +- .../examples/select-float-label.component.ts | 109 ---------- .../examples/select-grouped.component.ts | 8 +- .../select-label-position.component.ts | 81 +++++++ .../components/select/select.stories.ts | 205 ++++++++++++------ 9 files changed, 392 insertions(+), 247 deletions(-) delete mode 100644 src/stories/components/select/examples/select-float-label.component.ts create mode 100644 src/stories/components/select/examples/select-label-position.component.ts diff --git a/src/lib/components/select/select.component.ts b/src/lib/components/select/select.component.ts index 54fcad63..6e256e27 100644 --- a/src/lib/components/select/select.component.ts +++ b/src/lib/components/select/select.component.ts @@ -1,4 +1,5 @@ import { + booleanAttribute, Component, ContentChild, Directive, @@ -16,16 +17,33 @@ import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from import { Select } from 'primeng/select'; import { FloatLabel } from 'primeng/floatlabel'; import { PrimeTemplate } from 'primeng/api'; -import { AnimationEvent as NativeAnimationEvent } from '@angular/animations'; import type { SelectChangeEvent, SelectFilterEvent } from 'primeng/types/select'; +import { ExtraTooltipDirective } from '@cdek-it/angular-ui-kit/components/tooltip'; export type ExtraSelectSize = 'small' | 'base' | 'large' | 'xlarge'; -export type ExtraSelectChangeEvent = SelectChangeEvent; -export type ExtraSelectFilterEvent = SelectFilterEvent; +export type ExtraSelectLabelPosition = 'left' | 'top' | 'float'; -export interface ExtraAnimationEvent extends NativeAnimationEvent {} +export interface ExtraSelectOption { + name: string; + code: string | number; +} + +export interface ExtraSelectGroup { + options: ExtraSelectOption[] | any[]; + name: string; +} + +export interface ExtraSelectChangeEvent { + value: any; + originalEvent: Event; +} + +export interface ExtraSelectFilterEvent { + filter: string; + originalEvent: Event; +} -// export class ExtraAnimationEvent +let nextInputId = 0; @Directive({ selector: '[extraSelectOption]', standalone: true }) export class ExtraSelectOptionDirective {} @@ -39,7 +57,8 @@ export class ExtraSelectOptionGroupDirective {} @Component({ selector: 'extra-select', standalone: true, - imports: [Select, NgClass, NgTemplateOutlet, PrimeTemplate, FormsModule, FloatLabel], + imports: [Select, NgClass, NgTemplateOutlet, PrimeTemplate, FormsModule, FloatLabel, ExtraTooltipDirective], + host: { style: 'display: contents' }, providers: [ { provide: NG_VALUE_ACCESSOR, @@ -48,15 +67,43 @@ export class ExtraSelectOptionGroupDirective {} } ], template: ` - @if (floatLabel) { - - - - + @if (label || caption) { +
+ @if (label && labelPosition === 'left') { + + } +
+ @if (label && labelPosition === 'top') { + + } + @if (labelPosition === 'float') { + + + @if (label) { + + } + + } @else { + + } + @if (caption) { +
{{ caption }}
+ } +
+
} @else { - + } + + + + @@ -126,7 +173,7 @@ export class ExtraSelectComponent implements ControlValueAccessor, OnInit { this._ngControl = this._injector.get(NgControl, null, { self: true, optional: true }); } - @Input() options: any[] | null | undefined; + @Input() options: ExtraSelectGroup[] | ExtraSelectOption[] | any[] | null | undefined; @Input() optionLabel: string | undefined; @Input() optionValue: string | undefined; @Input() optionDisabled: string | undefined; @@ -134,17 +181,19 @@ export class ExtraSelectComponent implements ControlValueAccessor, OnInit { @Input() optionGroupChildren = 'items'; @Input() group = false; @Input() placeholder = ''; + @Input() label = ''; + @Input() labelPosition: ExtraSelectLabelPosition = 'top'; + @Input() caption = ''; + @Input() info = ''; @Input() size: ExtraSelectSize = 'base'; - @Input() filter = false; - @Input() showClear = false; + @Input() showFilter = false; + @Input({ transform: booleanAttribute }) clearable = false; + @Input() showCheckbox = true; @Input() editable = false; @Input() readonly = false; @Input() loading = false; - @Input() inputId: string | undefined; + @Input() inputId: string | undefined = `extra-select-${nextInputId++}`; @Input() appendTo: any = 'body'; - @Input() floatLabel = false; - @Input() label = ''; - @Input() checkmark = true; @Input() checkmarkIcon = 'ea5e'; @Input() emptyMessage = 'Нет данных'; @Input() emptyFilterMessage = 'Результаты не найдены'; @@ -157,10 +206,11 @@ export class ExtraSelectComponent implements ControlValueAccessor, OnInit { disabled = false; modelValue: any = null; - @Output() onClear = new EventEmitter(); + @Output() onChange = new EventEmitter(); + @Output() onClear = new EventEmitter(); @Output() onFilter = new EventEmitter(); - @Output() onShow = new EventEmitter(); - @Output() onHide = new EventEmitter(); + @Output() onShow = new EventEmitter(); + @Output() onHide = new EventEmitter(); @Output() onFocus = new EventEmitter(); @Output() onBlur = new EventEmitter(); @@ -170,7 +220,7 @@ export class ExtraSelectComponent implements ControlValueAccessor, OnInit { get primeSize(): 'small' | 'large' | undefined { if (this.size === 'small') return 'small'; - if (this.size === 'large') return 'large'; + if (this.size === 'large' || this.size === 'xlarge') return 'large'; return undefined; } @@ -189,9 +239,14 @@ export class ExtraSelectComponent implements ControlValueAccessor, OnInit { private _onChange: (value: any) => void = () => {}; private _onTouched: () => void = () => {}; - onSelectChange(event: ExtraSelectChangeEvent): void { + onSelectChange(event: SelectChangeEvent): void { this.modelValue = event.value; this._onChange(event.value); + this.onChange.emit({ value: event.value, originalEvent: event.originalEvent as Event }); + } + + onFilterHandler(event: SelectFilterEvent): void { + this.onFilter.emit({ filter: event.filter, originalEvent: event.originalEvent }); } handleBlur(event: Event): void { diff --git a/src/lib/components/select/select.figma.md b/src/lib/components/select/select.figma.md index a84b0ad7..7a6eaae7 100644 --- a/src/lib/components/select/select.figma.md +++ b/src/lib/components/select/select.figma.md @@ -10,12 +10,12 @@ figma: componentKey: '361-1561' name: '` (component set, nodeId `361:1561`, fileKey `Khh7arsuXss3ncqy1Dz3OZ`, библиотека «UI Kit (DS) v2.0»). Варианты задаются Figma-свойствами `state` (`default | hover | focus | danger | disabled | readonly`), `has-placeholder` и `has-floatlabel`. Выпадающая панель, пункты и группы вынесены в отдельные Figma-узлы ``, `` и `` и в коде описываются через шаблоны-директивы (см. ниже). @@ -23,36 +23,43 @@ updated: '2026-06-22' | Свойство | Тип | По умолчанию | Описание | |----------|-----|--------------|---------| -| `options` | `any[] \| null \| undefined` | `undefined` | Массив опций для выбора | +| `placeholder` | `string` | `''` | Подсказка при пустом поле — соответствует Figma-свойству `text-placeholder` / `has-placeholder` | +| `label` | `string` | `''` | Текст названия поля | +| `labelPosition` | `'left' \| 'top' \| 'float'` | `'top'` | Положение лейбла — `top` над полем, `float` — плавающая метка (`has-floatlabel`), `left` — слева от поля | +| `options` | `ExtraSelectGroup[] \| ExtraSelectOption[] \| any[] \| null \| undefined` | `undefined` | Список опций для выбора | | `optionLabel` | `string \| undefined` | `undefined` | Имя поля опции, отображаемое как подпись | | `optionValue` | `string \| undefined` | `undefined` | Имя поля опции, используемое как значение модели | | `optionDisabled` | `string \| undefined` | `undefined` | Имя булева поля опции, отключающего её выбор | -| `optionGroupLabel` | `string \| undefined` | `undefined` | Имя поля заголовка группы (при `group=true`) | -| `optionGroupChildren` | `string` | `'items'` | Имя поля группы со списком дочерних опций | -| `group` | `boolean` | `false` | Включает группировку опций | -| `placeholder` | `string` | `''` | Подсказка при пустом поле — соответствует Figma-свойству `text-placeholder` / `has-placeholder` | -| `size` | `'small' \| 'base' \| 'large' \| 'xlarge'` | `'base'` | Размер поля; `large` маппируется на PrimeNG `size="large"`, `xlarge` — на CSS-класс `p-select-xlg` | -| `filter` | `boolean` | `false` | Показывает строку поиска в выпадающем списке | -| `showClear` | `boolean` | `false` | Показывает иконку очистки (×) при наличии значения — соответствует Figma-свойству `show-clear` | -| `editable` | `boolean` | `false` | Разрешает ручной ввод значения в поле | +| `group` | `boolean` | `false` | Включает группировку опций (данные — `ExtraSelectGroup[]`) | +| `clearable` | `boolean` | `false` | Показывает иконку очистки (×) при наличии значения — соответствует Figma-свойству `show-clear` | +| `showCheckbox` | `boolean` | `true` | Показывает отметку (галочку) у выбранного пункта в списке | +| `showFilter` | `boolean` | `false` | Показывает строку поиска в выпадающем списке | +| `editable` | `boolean` | `false` | Разрешает ручной ввод произвольного значения в поле | | `readonly` | `boolean` | `false` | Только для чтения — соответствует Figma-состоянию `state=readonly` | | `loading` | `boolean` | `false` | Состояние загрузки опций — соответствует Figma-свойству `show-loader` | -| `inputId` | `string \| undefined` | `undefined` | `id` нативного инпута для связи с внешним `