Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions docs/components-api/textarea.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,41 @@

# ExtraTextarea

> ✅ **Реализован**: `ExtraTextareaComponent` (`@cdek-it/angular-ui-kit`) соответствует спецификации.

| Свойство | Описание | Типизация |
| ---------------- | ----------------------------------------------------------------------- | -------------------------- |
| `placeholder` | текст подсказки внутри поля | `string` |
| `label` | текст названия поля | `string` |
| `labelPosition` | положение лейбла | `left \| top \| float` |
| `labelPosition` | положение лейбла | `top \| left` |
| `floatLabel` | лейбл-плейсхолдер, всплывающий над полем при фокусе/значении | `boolean` |
| `clearable` | отображение иконки для очистки поля | `boolean` |
| `resize` | разрешить изменение размера поля | `boolean` |
| `resizable` | разрешить изменение размера поля перетаскиванием (нативный resize-уголок) | `boolean` |
| `caption` | текст пояснения под полем | `string` |
| `info` | текст с доп. информацией (показывается в тултипе иконки ti-info-circle) | `string` |
| `size` | размер поля | `small \| base \| large \| xlarge` |
| `fluid` | растягивает поле на всю ширину контейнера | `boolean` |
| `autoResize` | автоподстройка высоты поля под содержимое при вводе | `boolean` |
| `rows` | стартовая высота поля в строках | `number` |
| `cols` | ширина поля в символах | `number \| undefined` |
| `autofocus` | автофокус при монтировании компонента | `boolean` |

`labelPosition`/`floatLabel` — раздельные пропы (не тройной enum `left \| top \| float`, как было в
черновике спеки): `labelPosition` отвечает только за top/left, `floatLabel` — отдельный независимый
переключатель поверх любого из них (см. `docs/naming-rules.md`, раздел «Позиции»). При
`floatLabel=true` значение `labelPosition` для позиционирования самого лейбла не используется —
лейбл всегда всплывает поверх поля.

`resizable` — исправленное по `docs/naming-rules.md` имя (булево «возможность действия» → суффикс
`*able`, в черновике спеки было `resize`).

`size`/`fluid`/`autoResize`/`rows`/`cols`/`autofocus` — полезные пропы сверх первоначального
черновика спеки, уже реализованы и добавлены сюда по итогам доработки (см. чеклист в корневом
`CLAUDE.md`, п.3).

## События

| Событие | Описание | Типизация |
| -------- | ------------------------------------------------------ | -------------------------------------------- |
| `resize` | срабатывает при изменении высоты поля пользователем | `(event: ExtraTextareaResizeEvent) => void` |
| `clear` | срабатывает при нажатии на иконку очистки (`clearable`) | `() => void` |
140 changes: 102 additions & 38 deletions src/lib/components/textarea/textarea.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
EventEmitter,
Expand All @@ -10,18 +11,25 @@ import {
Output
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import { NgClass } from '@angular/common';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import { Textarea } from 'primeng/textarea';
import { IconField } from 'primeng/iconfield';
import { InputIcon } from 'primeng/inputicon';
import { FloatLabel } from 'primeng/floatlabel';
import { ExtraTooltipDirective } from '@cdek-it/angular-ui-kit/components/tooltip';

export type ExtraTextareaSize = 'small' | 'base' | 'large' | 'xlarge';
export type ExtraTextareaLabelPosition = 'top' | 'left';
export type ExtraTextareaResizeEvent = Event | Record<string, unknown>;

let nextInputId = 0;

@Component({
selector: 'extra-textarea',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [Textarea, IconField, InputIcon, NgClass],
imports: [Textarea, IconField, InputIcon, FloatLabel, NgClass, NgTemplateOutlet, ExtraTooltipDirective],
host: { style: 'display: contents' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
Expand All @@ -30,10 +38,82 @@ export type ExtraTextareaSize = 'small' | 'base' | 'large' | 'xlarge';
}
],
template: `
@if (showClear) {
<p-iconfield [ngClass]="{ '!w-full': fluid }">
<!-- Без label/caption поле не оборачиваем — сохраняем прежнюю DOM-структуру
(важно для p-inputgroup, где textarea обязана быть прямым flex-элементом). -->
@if (label || caption) {
<div class="extra-textarea" [class.extra-textarea--left]="labelPosition === 'left' && !floatLabel">
@if (label && labelPosition === 'left' && !floatLabel) {
<ng-container [ngTemplateOutlet]="labelTpl" />
}
<div class="extra-textarea-body">
@if (floatLabel) {
<p-floatlabel variant="in">
<ng-container [ngTemplateOutlet]="fieldTpl" />
@if (label) {
<ng-container [ngTemplateOutlet]="labelTpl" />
}
</p-floatlabel>
} @else {
@if (label && labelPosition === 'top') {
<ng-container [ngTemplateOutlet]="labelTpl" />
}
<ng-container [ngTemplateOutlet]="fieldTpl" />
}
@if (caption) {
<div class="extra-textarea-caption">{{ caption }}</div>
}
</div>
</div>
} @else {
<ng-container [ngTemplateOutlet]="fieldTpl" />
}

<ng-template #labelTpl>
<label class="extra-textarea-label" [for]="inputId">
{{ label }}
@if (info) {
<i class="extra-textarea-label-icon ti ti-info-circle" [extra-tooltip]="info"></i>
}
</label>
</ng-template>

<ng-template #fieldTpl>
@if (clearable) {
<p-iconfield [ngClass]="{ '!w-full': fluid }">
<textarea
pTextarea
[id]="inputId"
[ngClass]="sizeClass"
[pSize]="primeSize"
[disabled]="disabled"
[readOnly]="readonly"
[invalid]="invalid"
[fluid]="fluid"
[autoResize]="autoResize"
[rows]="rows"
[cols]="cols"
[style.resize]="resizable ? 'vertical' : 'none'"
[placeholder]="placeholder"
[autofocus]="autofocus"
[value]="modelValue"
(input)="onInput($event)"
(blur)="onTouched()"
(onResize)="onResize.emit($event)"
></textarea>
<p-inputicon
class="ti ti-x"
tabindex="0"
[style.visibility]="modelValue ? 'visible' : 'hidden'"
[style.pointerEvents]="modelValue ? 'auto' : 'none'"
(click)="clearValue()"
(keydown.enter)="clearValue()"
(keydown.space)="clearValue()"
></p-inputicon>
</p-iconfield>
} @else {
<textarea
pTextarea
[id]="inputId"
[ngClass]="sizeClass"
[pSize]="primeSize"
[disabled]="disabled"
Expand All @@ -43,41 +123,15 @@ export type ExtraTextareaSize = 'small' | 'base' | 'large' | 'xlarge';
[autoResize]="autoResize"
[rows]="rows"
[cols]="cols"
[style.resize]="resizable ? 'vertical' : 'none'"
[placeholder]="placeholder"
[value]="modelValue"
(input)="onInput($event)"
(blur)="onTouched()"
(onResize)="onResize.emit($event)"
></textarea>
<p-inputicon
class="ti ti-x"
tabindex="0"
[style.visibility]="modelValue ? 'visible' : 'hidden'"
[style.pointerEvents]="modelValue ? 'auto' : 'none'"
(click)="clearValue()"
(keydown.enter)="clearValue()"
(keydown.space)="clearValue()"
></p-inputicon>
</p-iconfield>
} @else {
<textarea
pTextarea
[ngClass]="sizeClass"
[pSize]="primeSize"
[disabled]="disabled"
[readOnly]="readonly"
[invalid]="invalid"
[fluid]="fluid"
[autoResize]="autoResize"
[rows]="rows"
[cols]="cols"
[placeholder]="placeholder"
[value]="modelValue"
(input)="onInput($event)"
(blur)="onTouched()"
(onResize)="onResize.emit($event)"
></textarea>
}
}
</ng-template>
`
})
export class ExtraTextareaComponent implements ControlValueAccessor, OnInit {
Expand All @@ -89,21 +143,31 @@ export class ExtraTextareaComponent implements ControlValueAccessor, OnInit {
}

@Input() placeholder = '';
@Input() label = '';
@Input() labelPosition: ExtraTextareaLabelPosition = 'top';
@Input({ transform: booleanAttribute }) floatLabel = false;
@Input() caption = '';
@Input() info = '';
@Input({ transform: booleanAttribute }) clearable = false;
@Input({ transform: booleanAttribute }) resizable = true;
@Input() size: ExtraTextareaSize = 'base';
@Input() readonly = false;
@Input() showClear = false;
@Input() fluid = false;
@Input() autoResize = false;
@Input({ transform: booleanAttribute }) readonly = false;
@Input({ transform: booleanAttribute }) fluid = false;
@Input({ transform: booleanAttribute }) autoResize = false;
@Input() rows = 3;
@Input() cols?: number;
@Input({ transform: booleanAttribute }) autofocus = false;

/** Уникальный id поля для связи label ↔ textarea. */
readonly inputId = `extra-textarea-${nextInputId++}`;

disabled = false;

get invalid(): boolean {
return this._ngControl?.invalid ?? false;
}

@Output() onResize = new EventEmitter<{ height: string } | {}>();
@Output() onResize = new EventEmitter<ExtraTextareaResizeEvent>();
@Output() onClear = new EventEmitter<void>();

modelValue = '';
Expand Down
Loading
Loading