diff --git a/src/lib/components/toggleswitch/toggleswitch.component.ts b/src/lib/components/toggleswitch/toggleswitch.component.ts new file mode 100644 index 0000000..7135892 --- /dev/null +++ b/src/lib/components/toggleswitch/toggleswitch.component.ts @@ -0,0 +1,67 @@ +import { Component, EventEmitter, Optional, Output, Self } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NgControl } from '@angular/forms'; +import { ToggleSwitch } from 'primeng/toggleswitch'; + +@Component({ + selector: 'toggleswitch', + standalone: true, + imports: [ToggleSwitch, FormsModule], + template: ` + + `, +}) +export class ToggleSwitchComponent implements ControlValueAccessor { + @Output() onChange = new EventEmitter(); + @Output() onFocus = new EventEmitter(); + @Output() onBlur = new EventEmitter(); + + modelValue = false; + private _disabled = 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; + } + + get isInvalid(): boolean { + return !!this.ngControl?.invalid; + } + + handleChange(value: boolean): void { + this.modelValue = value; + this._onChange(value); + this._onTouched(); + } + + writeValue(value: boolean): void { + this.modelValue = value ?? false; + } + + registerOnChange(fn: (value: boolean) => void): void { + this._onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this._onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this._disabled = isDisabled; + } +} diff --git a/src/prime-preset/tokens/components/toggleswitch.ts b/src/prime-preset/tokens/components/toggleswitch.ts new file mode 100644 index 0000000..8793c20 --- /dev/null +++ b/src/prime-preset/tokens/components/toggleswitch.ts @@ -0,0 +1,11 @@ +export const toggleswitchCss = ({ dt }: { dt: (token: string) => string }): string => ` + /* Focus ring для валидных состояний */ + .p-toggleswitch:not(.p-disabled):not(.p-invalid):has(.p-toggleswitch-input:focus-visible) .p-toggleswitch-slider { + box-shadow: 0 0 0 ${dt('toggleswitch.root.focusRing.width')} ${dt('focusRing.extend.success')}; + } + + /* Focus ring для состояния ошибки */ + .p-toggleswitch.p-invalid:not(.p-disabled):has(.p-toggleswitch-input:focus-visible) .p-toggleswitch-slider { + box-shadow: 0 0 0 ${dt('focusRing.width')} ${dt('focusRing.extend.invalid')}; + } +`; diff --git a/src/stories/components/toggleswitch/examples/toggleswitch-checked.component.ts b/src/stories/components/toggleswitch/examples/toggleswitch-checked.component.ts new file mode 100644 index 0000000..b0c6a88 --- /dev/null +++ b/src/stories/components/toggleswitch/examples/toggleswitch-checked.component.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { StoryObj } from '@storybook/angular'; +import { ToggleSwitchComponent } from '../../../../lib/components/toggleswitch/toggleswitch.component'; + +@Component({ + selector: 'app-toggleswitch-checked', + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: ` + + `, +}) +export class ToggleSwitchCheckedComponent { + control = new FormControl(true); +} + +export const Checked: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Переключатель во включённом состоянии.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ToggleSwitchComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-toggleswitch-checked', + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: \` + + \`, +}) +export class ToggleSwitchCheckedComponent { + control = new FormControl(true); +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/toggleswitch/examples/toggleswitch-disabled.component.ts b/src/stories/components/toggleswitch/examples/toggleswitch-disabled.component.ts new file mode 100644 index 0000000..7d041f4 --- /dev/null +++ b/src/stories/components/toggleswitch/examples/toggleswitch-disabled.component.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { StoryObj } from '@storybook/angular'; +import { ToggleSwitchComponent } from '../../../../lib/components/toggleswitch/toggleswitch.component'; + +@Component({ + selector: 'app-toggleswitch-disabled', + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: ` + + `, +}) +export class ToggleSwitchDisabledComponent { + control = new FormControl({ value: false, disabled: true }); +} + +export const Disabled: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Заблокированное состояние переключателя через FormControl.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ToggleSwitchComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-toggleswitch-disabled', + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: \` + + \`, +}) +export class ToggleSwitchDisabledComponent { + control = new FormControl({ value: false, disabled: true }); +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/toggleswitch/examples/toggleswitch-invalid.component.ts b/src/stories/components/toggleswitch/examples/toggleswitch-invalid.component.ts new file mode 100644 index 0000000..aced21f --- /dev/null +++ b/src/stories/components/toggleswitch/examples/toggleswitch-invalid.component.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { StoryObj } from '@storybook/angular'; +import { ToggleSwitchComponent } from '../../../../lib/components/toggleswitch/toggleswitch.component'; + +@Component({ + selector: 'app-toggleswitch-invalid', + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: ` + + `, +}) +export class ToggleSwitchInvalidComponent { + // Validators.requiredTrue требует значение true, поэтому false делает контрол невалидным + control = new FormControl(false, [Validators.requiredTrue]); +} + +export const Invalid: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Невалидное состояние переключателя через FormControl и Validators.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ToggleSwitchComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-toggleswitch-invalid', + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: \` + + \`, +}) +export class ToggleSwitchInvalidComponent { + control = new FormControl(false, [Validators.requiredTrue]); +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/toggleswitch/toggleswitch.stories.ts b/src/stories/components/toggleswitch/toggleswitch.stories.ts new file mode 100644 index 0000000..640568d --- /dev/null +++ b/src/stories/components/toggleswitch/toggleswitch.stories.ts @@ -0,0 +1,181 @@ +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ToggleSwitchComponent } from '../../../lib/components/toggleswitch/toggleswitch.component'; +import { ToggleSwitchCheckedComponent } from './examples/toggleswitch-checked.component'; +import { ToggleSwitchInvalidComponent } from './examples/toggleswitch-invalid.component'; +import { ToggleSwitchDisabledComponent } from './examples/toggleswitch-disabled.component'; + +const meta: Meta = { + title: 'Components/Form/ToggleSwitch', + component: ToggleSwitchComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [ + ToggleSwitchComponent, + ReactiveFormsModule, + ToggleSwitchCheckedComponent, + ToggleSwitchInvalidComponent, + ToggleSwitchDisabledComponent, + ], + }), + ], + parameters: { + designTokens: { prefix: '--p-toggleswitch' }, + docs: { + description: { + component: `Компонент для переключения между двумя состояниями. Состояния \`disabled\` и \`invalid\` управляются через \`FormControl\`, не через пропсы.`, + }, + }, + }, + argTypes: { + // ── Events ─────────────────────────────────────────────── + onChange: { + control: false, + description: 'Событие изменения состояния', + table: { + category: 'Events', + type: { summary: 'EventEmitter' }, + }, + }, + onFocus: { + control: false, + description: 'Событие фокуса', + table: { + category: 'Events', + type: { summary: 'EventEmitter' }, + }, + }, + onBlur: { + control: false, + description: 'Событие потери фокуса', + table: { + category: 'Events', + type: { summary: 'EventEmitter' }, + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// ── Default ────────────────────────────────────────────────────────────────── +export const Default: Story = { + name: 'Default', + render: () => ({ + props: { control: new FormControl(false) }, + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Базовый пример. Управление значением и состоянием через `FormControl`.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ToggleSwitchComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: \`\`, +}) +export class Example { + control = new FormControl(false); +} + `, + }, + }, + }, +}; + +// ── Checked ─────────────────────────────────────────────────────────────────── +export const Checked: Story = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Переключатель во включённом состоянии.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ToggleSwitchComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: \`\`, +}) +export class ToggleSwitchCheckedComponent { + control = new FormControl(true); +} + `, + }, + }, + }, +}; + +// ── Invalid ─────────────────────────────────────────────────────────────────── +export const Invalid: Story = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Невалидное состояние через `FormControl` и `Validators`.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ToggleSwitchComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: \`\`, +}) +export class ToggleSwitchInvalidComponent { + control = new FormControl(false, [Validators.requiredTrue]); +} + `, + }, + }, + }, +}; + +// ── Disabled ────────────────────────────────────────────────────────────────── +export const Disabled: Story = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Заблокированное состояние через `FormControl`.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ToggleSwitchComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + standalone: true, + imports: [ToggleSwitchComponent, ReactiveFormsModule], + template: \`\`, +}) +export class ToggleSwitchDisabledComponent { + control = new FormControl({ value: false, disabled: true }); +} + `, + }, + }, + }, +};