diff --git a/docs/components-api/toggleswitch.md b/docs/components-api/toggleswitch.md
index e95cd40c..ac0e347a 100644
--- a/docs/components-api/toggleswitch.md
+++ b/docs/components-api/toggleswitch.md
@@ -2,6 +2,8 @@
# ExtraToggleSwitch
+> ✅ **Реализован**: `ExtraToggleSwitchComponent` (`@cdek-it/angular-ui-kit`) соответствует спецификации.
+
| Свойство | Описание | Типизация |
| ---------------- | --------------------------- | ------------------ |
| `label` | текст названия | `string` |
@@ -13,6 +15,8 @@
| Событие | Описание | Типизация |
| ----------- | ----------------------------- | -------------------------------------------------- |
| `change` | срабатывает при переключении | `(event: ExtraToggleSwitchChangeEvent) => void` |
+| `focus` | срабатывает при получении фокуса | `(event: Event) => void` |
+| `blur` | срабатывает при потере фокуса | `(event: Event) => void` |
# ExtraToggleSwitchChangeEvent
diff --git a/src/lib/components/toggleswitch/toggleswitch.component.ts b/src/lib/components/toggleswitch/toggleswitch.component.ts
index 55e0cc60..d5d7e537 100644
--- a/src/lib/components/toggleswitch/toggleswitch.component.ts
+++ b/src/lib/components/toggleswitch/toggleswitch.component.ts
@@ -1,57 +1,122 @@
-import { Component, EventEmitter, Optional, Output, Self } from '@angular/core';
-import { ControlValueAccessor, FormsModule, NgControl } from '@angular/forms';
-import { ToggleSwitch } from 'primeng/toggleswitch';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ forwardRef,
+ inject,
+ Injector,
+ Input,
+ OnDestroy,
+ OnInit,
+ Output
+} from '@angular/core';
+import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
+import { NgTemplateOutlet } from '@angular/common';
+import { ToggleSwitch, ToggleSwitchChangeEvent } from 'primeng/toggleswitch';
+import { Subscription } from 'rxjs';
+
+export type ExtraToggleSwitchLabelPosition = 'right' | 'left';
+
+export interface ExtraToggleSwitchChangeEvent {
+ checked: boolean;
+ originalEvent: Event;
+}
+
+let nextInputId = 0;
@Component({
selector: 'extra-toggleswitch',
standalone: true,
- imports: [ToggleSwitch, FormsModule],
+ imports: [ToggleSwitch, FormsModule, NgTemplateOutlet],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: { style: 'display: contents' },
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => ExtraToggleSwitchComponent),
+ multi: true
+ }
+ ],
template: `
-
+ @if (label || caption) {
+
+ } @else {
+
+ }
+
+
+
+
`
})
-export class ExtraToggleSwitchComponent implements ControlValueAccessor {
- @Output() onChange = new EventEmitter();
+export class ExtraToggleSwitchComponent implements ControlValueAccessor, OnInit, OnDestroy {
+ private readonly _injector = inject(Injector);
+ private readonly _cdr = inject(ChangeDetectorRef);
+ private _ngControl: NgControl | null = null;
+ private _statusSub?: Subscription;
+
+ @Input() label = '';
+ @Input() labelPosition: ExtraToggleSwitchLabelPosition = 'right';
+ @Input() caption = '';
+
+ @Output() onChange = new EventEmitter();
@Output() onFocus = new EventEmitter();
@Output() onBlur = new EventEmitter();
+ /** Уникальный id поля для связи label ↔ input. */
+ readonly inputId = `extra-toggleswitch-${nextInputId++}`;
+
+ disabled = false;
modelValue = false;
- private _disabled = false;
+ get invalid(): boolean {
+ return this._ngControl?.invalid ?? false;
+ }
private _onChange: (value: boolean) => void = () => {};
private _onTouched: () => void = () => {};
- constructor(@Optional() @Self() private ngControl: NgControl) {
- if (ngControl) {
- ngControl.valueAccessor = this;
- }
- }
-
- get isDisabled(): boolean {
- return this._disabled;
+ ngOnInit(): void {
+ this._ngControl = this._injector.get(NgControl, null, { self: true, optional: true });
+ this._statusSub = this._ngControl?.statusChanges?.subscribe(() => this._cdr.markForCheck());
}
- get isInvalid(): boolean {
- return !!this.ngControl?.invalid;
+ ngOnDestroy(): void {
+ this._statusSub?.unsubscribe();
}
- handleChange(value: boolean): void {
- this.modelValue = value;
- this._onChange(value);
+ onChangeHandler(event: ToggleSwitchChangeEvent): void {
+ const checked = !!event.checked;
+ this.modelValue = checked;
+ this._onChange(checked);
this._onTouched();
+ this.onChange.emit({ checked, originalEvent: event.originalEvent as Event });
}
writeValue(value: boolean): void {
- this.modelValue = value ?? false;
+ this.modelValue = !!value;
+ this._cdr.markForCheck();
}
registerOnChange(fn: (value: boolean) => void): void {
@@ -63,6 +128,7 @@ export class ExtraToggleSwitchComponent implements ControlValueAccessor {
}
setDisabledState(isDisabled: boolean): void {
- this._disabled = isDisabled;
+ this.disabled = isDisabled;
+ this._cdr.markForCheck();
}
}
diff --git a/src/lib/components/toggleswitch/toggleswitch.figma.md b/src/lib/components/toggleswitch/toggleswitch.figma.md
index 9d1c1749..c372ab44 100644
--- a/src/lib/components/toggleswitch/toggleswitch.figma.md
+++ b/src/lib/components/toggleswitch/toggleswitch.figma.md
@@ -15,20 +15,24 @@ updated: '2026-06-22'
## Overview
-`ExtraToggleSwitch` — переключатель-свитч (`role=switch`) для мгновенного включения/выключения одной бинарной настройки: эффект применяется сразу, без отдельного сохранения. Оборачивает PrimeNG `p-toggleswitch` и реализует `ControlValueAccessor`, поэтому интегрируется с `[(ngModel)]` и реактивными формами через `formControl` / `formControlName`.
+`ExtraToggleSwitch` — переключатель-свитч (`role=switch`) для мгновенного включения/выключения одной бинарной настройки: эффект применяется сразу, без отдельного сохранения. Оборачивает PrimeNG `p-toggleswitch`, сам рисует `label`/`caption` рядом с собой (как `ExtraCheckbox`) и реализует `ControlValueAccessor`, поэтому интегрируется с `[(ngModel)]` и реактивными формами через `formControl` / `formControlName`.
Компонент соответствует Figma-компоненту `` (nodeId `19:13673`). Figma-свойства `state` и `checked` маппируются на состояние компонента и модель значения (см. раздел Props mapping).
## Props mapping
-У компонента нет собственных `@Input()` для конфигурации внешнего вида: состояние задаётся моделью и формой через `ControlValueAccessor`. Доступны три события вывода (`@Output()`):
-
| Свойство | Тип | По умолчанию | Описание |
|----------|-----|--------------|---------|
-| `onChange` | `EventEmitter` | — | Событие при изменении значения переключателя |
+| `label` | `string` | `''` | Текст названия рядом с переключателем |
+| `labelPosition` | `'right' \| 'left'` | `'right'` | Положение лейбла относительно свитча |
+| `caption` | `string` | `''` | Пояснение под лейблом |
+| `onChange` | `EventEmitter` | — | Событие при изменении значения переключателя |
| `onFocus` | `EventEmitter` | — | Событие при получении фокуса |
| `onBlur` | `EventEmitter` | — | Событие при потере фокуса |
+Если ни `label`, ни `caption` не заданы — рендерится голый свитч без обёртки (тот же приём, что у
+`ExtraCheckbox`/полей ввода, важно для `p-inputgroup` и плотных layout'ов).
+
Состояние «включён» (Figma-свойство `checked`) задаётся не отдельным инпутом, а моделью через `ControlValueAccessor`: используйте `[(ngModel)]`, `formControl` или `formControlName`. Значение `true` / `false` соответствует `checked=true` / `checked=false` в Figma.
Отключённое состояние (Figma `state=disabled`) задаётся не отдельным инпутом, а через форму: атрибут `[disabled]` на `formControl` или `disabled`-состояние реактивной формы передаётся в компонент методом `setDisabledState` из `ControlValueAccessor`.
@@ -53,6 +57,19 @@ Figma: state=default, checked=true
```
+### С лейблом и пояснением
+
+```html
+
+
+
+
+```
+
В реактивных формах эквивалентно:
```html
@@ -81,7 +98,59 @@ Figma: state=danger, checked=false
## Slots
-Не используются. Подпись к переключателю размещается рядом во внешнем шаблоне.
+Не используются. `label`/`caption` — не слоты, а обычные `@Input()` (см. Props mapping); компонент
+сам рисует обёртку по тому же принципу, что `ExtraCheckbox`/`ExtraInputText` (см.
+`docs/components-api/common-info.md` и чеклист в корневом `CLAUDE.md`, раздел «Label / caption /
+info»).
+
+⚠️ Для классов `.extra-toggleswitch`/`.toggleswitch-label`/`.toggleswitch-caption` в
+`tokens/components/toggleswitch.ts` пока нет стилей (там только focus-ring для `.p-toggleswitch`) —
+это зона дизайнера, агент её не трогает. Сниппет-заготовка 1:1 с уже рабочим `checkbox.ts`
+(`.extra-checkbox`/`.checkbox-label`/`.checkbox-caption`), для дизайнера:
+
+```ts
+.extra-toggleswitch {
+ display: flex;
+ align-items: center;
+ gap: ${dt('dimension.space.200')};
+}
+
+.extra-toggleswitch--left {
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+}
+
+.toggleswitch-label {
+ display: flex;
+ align-items: center;
+ color: ${dt('color.fg.default')};
+ font-family: ${dt('fonts.fontFamily.base')};
+ font-size: ${dt('fonts.fontSize.300')};
+ font-weight: ${dt('fonts.fontWeight.regular')};
+ line-height: ${dt('fonts.lineHeight.300')};
+ cursor: pointer;
+}
+
+.toggleswitch-label--disabled {
+ color: ${dt('color.fg.muted')};
+ cursor: default;
+}
+
+.toggleswitch-caption {
+ color: ${dt('color.fg.subtle')};
+ font-family: ${dt('fonts.fontFamily.heading')};
+ font-size: ${dt('fonts.fontSize.200')};
+ font-weight: ${dt('fonts.fontWeight.regular')};
+ line-height: ${dt('fonts.lineHeight.250')};
+}
+
+.toggleswitch-caption--disabled {
+ color: ${dt('color.fg.muted')};
+}
+```
+
+До добавления этого CSS лейбл/caption рендерятся функционально верно, но без типографики токенов
+(голый браузерный шрифт).
## Related
@@ -95,7 +164,7 @@ Figma: state=danger, checked=false
- Используйте `[(ngModel)]` или `formControl` для управления состоянием вкл/выкл
- Применяйте свитч для мгновенного эффекта (тёмная тема, уведомления), без отдельной кнопки Save
- Для отключения и невалидного состояния управляйте состоянием формы (`disabled` / валидаторы `formControl`)
-- Размещайте подпись рядом с переключателем во внешнем шаблоне для доступности
+- Задавайте подпись через `label`/`caption` — так `