diff --git a/cdek-it-angular-ui-kit-0.2.4-test.tgz b/cdek-it-angular-ui-kit-0.2.4-test.tgz new file mode 100644 index 00000000..11c7bf7a Binary files /dev/null and b/cdek-it-angular-ui-kit-0.2.4-test.tgz differ diff --git a/docs/components-api/select.md b/docs/components-api/select.md index 3ffb7c0b..5711689b 100644 --- a/docs/components-api/select.md +++ b/docs/components-api/select.md @@ -10,7 +10,8 @@ Single-select: выбор одного значения из списка. У о | --------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------- | | `placeholder` | текст подсказки внутри поля | `string` | | `label` | текст названия поля | `string` | -| `labelPosition` | положение лейбла | `left \| top \| float` | +| `labelPosition` | положение лейбла | `top \| left` | +| `floatLabel` | плавающий лейбл внутри поля (перекрывает `labelPosition`) | `boolean` | | `options` | список элементов или список групп с элементами | `ExtraSelectGroup[] \| ExtraSelectOption[] \| any[]` | | `optionLabel` | наименование поля, содержащего отображаемое значение | `string` | | `optionValue` | наименование поля, значение которого используется как значение модели | `string` | diff --git a/src/lib/components/select/select.component.ts b/src/lib/components/select/select.component.ts index 54fcad63..5829dbfa 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, @@ -11,21 +12,38 @@ import { Output, TemplateRef } from '@angular/core'; -import { NgClass, NgTemplateOutlet } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms'; 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 = 'top' | 'left'; -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, NgTemplateOutlet, PrimeTemplate, FormsModule, FloatLabel, ExtraTooltipDirective], + host: { style: 'display: contents' }, providers: [ { provide: NG_VALUE_ACCESSOR, @@ -48,18 +67,47 @@ export class ExtraSelectOptionGroupDirective {} } ], template: ` - @if (floatLabel) { - - - - + @if (label || caption) { + +
+ @if (label && labelPosition === 'left' && !floatLabel) { + + } +
+ @if (label && labelPosition === 'top' && !floatLabel) { + + } + @if (floatLabel) { + + + @if (label) { + + } + + } @else { + + } + @if (caption) { +
{{ caption }}
+ } +
+
} @else { - + } + + + + @@ -126,25 +174,30 @@ 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; @Input() optionGroupLabel: string | undefined; @Input() optionGroupChildren = 'items'; - @Input() group = false; + @Input({ transform: booleanAttribute }) group = false; @Input() placeholder = ''; + @Input() label = ''; + @Input() labelPosition: ExtraSelectLabelPosition = 'top'; + /** Плавающий лейбл внутри поля (PrimeNG `p-floatlabel`); перекрывает `labelPosition`. */ + @Input({ transform: booleanAttribute }) floatLabel = false; + @Input() caption = ''; + @Input() info = ''; @Input() size: ExtraSelectSize = 'base'; - @Input() filter = false; - @Input() showClear = false; - @Input() editable = false; - @Input() readonly = false; - @Input() loading = false; - @Input() inputId: string | undefined; + @Input({ transform: booleanAttribute }) showFilter = false; + /** Отображение иконки очистки поля при наличии значения. */ + @Input({ transform: booleanAttribute }) clearable = false; + @Input({ transform: booleanAttribute }) showCheckbox = true; + @Input({ transform: booleanAttribute }) editable = false; + @Input({ transform: booleanAttribute }) readonly = false; + @Input({ transform: booleanAttribute }) loading = false; + @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 +210,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 +224,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; } @@ -179,19 +233,32 @@ export class ExtraSelectComponent implements ControlValueAccessor, OnInit { return { '--p-select-checkmark-content': `"${char}"` }; } - get selectClasses(): Record { - return { - 'p-select-xlg': this.size === 'xlarge', - 'p-invalid': this.invalid - }; + /** + * Классы на сам p-select. `p-select-readonly` навешиваем сами: PrimeNG ставит + * атрибут `readonly` только на внутренний input, который существует лишь в + * режиме `editable`, — в обычном селекте зацепиться в CSS не за что. + */ + get selectClass(): string { + return [ + this.size === 'xlarge' ? 'p-select-xlg' : '', + this.readonly ? 'p-select-readonly' : '', + this.invalid ? 'p-invalid' : '' + ] + .filter(Boolean) + .join(' '); } 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..d019a877 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,44 @@ updated: '2026-06-22' | Свойство | Тип | По умолчанию | Описание | |----------|-----|--------------|---------| -| `options` | `any[] \| null \| undefined` | `undefined` | Массив опций для выбора | +| `placeholder` | `string` | `''` | Подсказка при пустом поле — соответствует Figma-свойству `text-placeholder` / `has-placeholder` | +| `label` | `string` | `''` | Текст названия поля | +| `labelPosition` | `'top' \| 'left'` | `'top'` | Положение лейбла — `top` над полем, `left` — слева от поля | +| `floatLabel` | `boolean` | `false` | Плавающая метка внутри поля (Figma-свойство `has-floatlabel`); перекрывает `labelPosition` | +| `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` нативного инпута для связи с внешним `