diff --git a/src/lib/components/rating/rating.component.ts b/src/lib/components/rating/rating.component.ts new file mode 100644 index 00000000..cbb6a242 --- /dev/null +++ b/src/lib/components/rating/rating.component.ts @@ -0,0 +1,68 @@ +import { Component, EventEmitter, Input, Output, forwardRef } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Rating } from 'primeng/rating'; + +export type RatingValue = number | null; + +@Component({ + selector: 'rating', + standalone: true, + imports: [Rating, FormsModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RatingComponent), + multi: true, + }, + ], + template: ` + + `, +}) +export class RatingComponent implements ControlValueAccessor { + @Input() stars = 5; + @Input() readonly = false; + @Input() disabled = false; + @Input() autofocus = false; + + @Output() onRate = new EventEmitter(); + @Output() onFocus = new EventEmitter(); + @Output() onBlur = new EventEmitter(); + + modelValue: RatingValue = null; + + private onChange: (value: RatingValue) => void = () => {}; + private onTouched: () => void = () => {}; + + handleChange(value: RatingValue): void { + this.modelValue = value; + this.onChange(value); + this.onTouched(); + } + + writeValue(value: RatingValue): void { + this.modelValue = value; + } + + registerOnChange(fn: (value: RatingValue) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } +} diff --git a/src/stories/components/rating/examples/rating-disabled.component.ts b/src/stories/components/rating/examples/rating-disabled.component.ts new file mode 100644 index 00000000..e2bf475b --- /dev/null +++ b/src/stories/components/rating/examples/rating-disabled.component.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { StoryObj } from '@storybook/angular'; +import { RatingComponent } from '../../../../lib/components/rating/rating.component'; + +const template = ` +
+ +
+`; + +@Component({ + selector: 'app-rating-disabled', + standalone: true, + imports: [RatingComponent, FormsModule], + template, +}) +export class RatingDisabledComponent { + value = 2; +} + +export const Disabled: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Заблокированное состояние — компонент недоступен для взаимодействия.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { RatingComponent } from '@cdek-it/angular-ui-kit'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-rating-disabled', + standalone: true, + imports: [RatingComponent, FormsModule], + template: \` + + \`, +}) +export class RatingDisabledComponent { + value = 2; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/rating/examples/rating-readonly.component.ts b/src/stories/components/rating/examples/rating-readonly.component.ts new file mode 100644 index 00000000..6b82adaf --- /dev/null +++ b/src/stories/components/rating/examples/rating-readonly.component.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { StoryObj } from '@storybook/angular'; +import { RatingComponent } from '../../../../lib/components/rating/rating.component'; + +const template = ` +
+ +
+`; + +@Component({ + selector: 'app-rating-readonly', + standalone: true, + imports: [RatingComponent, FormsModule], + template, +}) +export class RatingReadonlyComponent { + value = 4; +} + +export const ReadOnly: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Режим только для чтения — значение отображается, но не может быть изменено.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { RatingComponent } from '@cdek-it/angular-ui-kit'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-rating-readonly', + standalone: true, + imports: [RatingComponent, FormsModule], + template: \` + + \`, +}) +export class RatingReadonlyComponent { + value = 4; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/rating/rating.stories.ts b/src/stories/components/rating/rating.stories.ts new file mode 100644 index 00000000..2d544ebe --- /dev/null +++ b/src/stories/components/rating/rating.stories.ts @@ -0,0 +1,120 @@ +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { FormsModule } from '@angular/forms'; +import { RatingComponent } from '../../../lib/components/rating/rating.component'; +import { RatingReadonlyComponent, ReadOnly } from './examples/rating-readonly.component'; +import { RatingDisabledComponent, Disabled } from './examples/rating-disabled.component'; + +type RatingArgs = RatingComponent; + +const meta: Meta = { + title: 'Components/Form/Rating', + component: RatingComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [ + RatingComponent, + FormsModule, + RatingReadonlyComponent, + RatingDisabledComponent, + ], + }), + ], + parameters: { + designTokens: { prefix: '--p-rating' }, + docs: { + description: { + component: `Компонент для отображения и выбора оценки в виде звёзд.`, + }, + }, + }, + argTypes: { + // ── Props ──────────────────────────────────────────────── + stars: { + control: 'number', + description: 'Количество отображаемых звёзд', + table: { + category: 'Props', + defaultValue: { summary: '5' }, + type: { summary: 'number' }, + }, + }, + readonly: { + control: 'boolean', + description: 'Режим только для чтения — значение нельзя изменить', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + disabled: { + control: 'boolean', + description: 'Отключает возможность взаимодействия', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + autofocus: { table: { disable: true } }, + + // ── Events ─────────────────────────────────────────────── + onRate: { + 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' }, + }, + }, + }, + args: { + stars: 5, + readonly: false, + disabled: false, + }, +}; + +export default meta; +type Story = StoryObj; + +// ── Default ────────────────────────────────────────────────────────────────── +export const Default: Story = { + name: 'Default', + render: (args) => { + const parts: string[] = [`[(ngModel)]="value"`]; + if (args.stars !== 5) parts.push(`[stars]="${args.stars}"`); + if (args.readonly) parts.push(`[readonly]="true"`); + if (args.disabled) parts.push(`[disabled]="true"`); + + const template = ``; + return { props: { ...args, value: 3 }, template }; + }, + parameters: { + docs: { + description: { + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.', + }, + }, + }, +}; + +// ── Re-exports from example components ──────────────────────────────────── +export { ReadOnly, Disabled };