-
-
-
{{ option.name }}
-
{{ option.description }}
+
+
+
+
+
+
+
{{ option.name }}
+
{{ option.description }}
+
-
-
-
+
+
+
`;
const styles = '';
@@ -44,7 +46,7 @@ const styles = '';
})
export class SelectCustomComponent {
@Input() size: ExtraSelectSize = 'base';
- @Input() showClear = false;
+ @Input() clearable = false;
@Input() readonly = false;
control = new FormControl(null);
options = OPTIONS;
@@ -64,12 +66,12 @@ export const Custom = {
render: (args: any) => ({
props: {
size: args['size'],
- showClear: args['showClear'],
+ clearable: args['clearable'],
readonly: args['readonly'],
disabled: args['disabled'],
invalid: args['invalid']
},
- template: `
`
+ template: `
`
}),
parameters: {
docs: {
diff --git a/src/stories/components/select/examples/select-disabled.component.ts b/src/stories/components/select/examples/select-disabled.component.ts
index 68f24cd0..4b964a7b 100644
--- a/src/stories/components/select/examples/select-disabled.component.ts
+++ b/src/stories/components/select/examples/select-disabled.component.ts
@@ -15,12 +15,14 @@ export const Disabled: StoryObj = {
return {
props: { control, options: OPTIONS },
template: `
-
+
+
+
`
};
},
diff --git a/src/stories/components/select/examples/select-editable.component.ts b/src/stories/components/select/examples/select-editable.component.ts
index 99c6a2e1..eb197530 100644
--- a/src/stories/components/select/examples/select-editable.component.ts
+++ b/src/stories/components/select/examples/select-editable.component.ts
@@ -9,16 +9,18 @@ const OPTIONS = [
];
const template = `
-
+
+
+
`;
const styles = '';
@@ -31,7 +33,7 @@ const styles = '';
})
export class SelectEditableComponent {
@Input() size: ExtraSelectSize = 'base';
- @Input() showClear = false;
+ @Input() clearable = false;
@Input() readonly = false;
control = new FormControl(null);
options = OPTIONS;
@@ -51,12 +53,12 @@ export const Editable = {
render: (args: any) => ({
props: {
size: args['size'],
- showClear: args['showClear'],
+ clearable: args['clearable'],
readonly: args['readonly'],
disabled: args['disabled'],
invalid: args['invalid']
},
- template: `
`
+ template: `
`
}),
parameters: {
docs: {
diff --git a/src/stories/components/select/examples/select-filter.component.ts b/src/stories/components/select/examples/select-filter.component.ts
index 4d1b37e7..16229085 100644
--- a/src/stories/components/select/examples/select-filter.component.ts
+++ b/src/stories/components/select/examples/select-filter.component.ts
@@ -11,16 +11,18 @@ const OPTIONS = [
];
const template = `
-
+
+
+
`;
const styles = '';
@@ -72,8 +74,8 @@ import { ExtraSelectComponent } from '@cdek-it/angular-ui-kit';
[options]="options"
optionLabel="name"
placeholder="Выберите город..."
- [filter]="true"
- [showClear]="true"
+ [showFilter]="true"
+ [clearable]="true"
>
\`,
})
diff --git a/src/stories/components/select/examples/select-float-label.component.ts b/src/stories/components/select/examples/select-float-label.component.ts
deleted file mode 100644
index 911cbde5..00000000
--- a/src/stories/components/select/examples/select-float-label.component.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { Component, Input } from '@angular/core';
-import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
-import { ExtraSelectComponent } from '../../../../lib/components/select/select.component';
-
-const OPTIONS = [
- { name: 'Новосибирск', code: 'NSK' },
- { name: 'Москва', code: 'MSK' },
- { name: 'Санкт-Петербург', code: 'SPB' },
- { name: 'Екатеринбург', code: 'EKB' }
-];
-
-const template = `
-
-
-
-`;
-const styles = '';
-
-@Component({
- selector: 'app-select-float-label',
- standalone: true,
- imports: [ExtraSelectComponent, ReactiveFormsModule],
- template,
- styles
-})
-export class SelectFloatLabelComponent {
- @Input() showClear = false;
- @Input() readonly = false;
- @Input() label = 'Город';
- control = new FormControl(null);
- options = OPTIONS;
-
- @Input() set disabled(val: boolean) {
- val ? this.control.disable() : this.control.enable();
- }
-
- @Input() set invalid(val: boolean) {
- this.control.setValidators(val ? [Validators.required] : []);
- this.control.updateValueAndValidity();
- if (val) this.control.markAsTouched();
- }
-}
-
-export const FloatLabelStory = {
- name: 'FloatLabel',
- render: (args: any) => ({
- props: {
- showClear: args['showClear'],
- label: 'Город',
- readonly: args['readonly'],
- disabled: args['disabled'],
- invalid: args['invalid']
- },
- template: `
`
- }),
- argTypes: {
- size: { table: { disable: true } }
- },
- parameters: {
- docs: {
- description: {
- story: 'Плавающая метка внутри поля. Используйте пропс `floatLabel` для встроенной интеграции с `p-floatlabel`.'
- },
- source: {
- language: 'ts',
- code: `
-import { Component } from '@angular/core';
-import { FormControl, ReactiveFormsModule } from '@angular/forms';
-import { ExtraSelectComponent } from '@cdek-it/angular-ui-kit';
-
-@Component({
- standalone: true,
- imports: [ExtraSelectComponent, ReactiveFormsModule],
- template: \`
-
-
-
- \`,
-})
-export class SelectFloatLabelExample {
- control = new FormControl(null);
- options = [
- { name: 'Новосибирск', code: 'NSK' },
- { name: 'Москва', code: 'MSK' },
- { name: 'Санкт-Петербург', code: 'SPB' },
- ];
-}
- `
- }
- }
- }
-};
diff --git a/src/stories/components/select/examples/select-grouped.component.ts b/src/stories/components/select/examples/select-grouped.component.ts
index 52ed441b..d1e80f0c 100644
--- a/src/stories/components/select/examples/select-grouped.component.ts
+++ b/src/stories/components/select/examples/select-grouped.component.ts
@@ -26,25 +26,27 @@ const GROUPED_OPTIONS = [
];
const template = `
-
-
-
-
- {{ group.label }}
-
-
-
+
+
+
+
+
+ {{ group.label }}
+
+
+
+
`;
const styles = '';
@@ -57,7 +59,7 @@ const styles = '';
})
export class SelectGroupedComponent {
@Input() size: ExtraSelectSize = 'base';
- @Input() showClear = false;
+ @Input() clearable = false;
@Input() readonly = false;
control = new FormControl(null);
options = GROUPED_OPTIONS;
@@ -77,12 +79,12 @@ export const Grouped = {
render: (args: any) => ({
props: {
size: args['size'],
- showClear: args['showClear'],
+ clearable: args['clearable'],
readonly: args['readonly'],
disabled: args['disabled'],
invalid: args['invalid']
},
- template: `
`
+ template: `
`
}),
parameters: {
docs: {
diff --git a/src/stories/components/select/examples/select-label-position.component.ts b/src/stories/components/select/examples/select-label-position.component.ts
new file mode 100644
index 00000000..a15c329e
--- /dev/null
+++ b/src/stories/components/select/examples/select-label-position.component.ts
@@ -0,0 +1,82 @@
+import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { ExtraSelectComponent } from '../../../../lib/components/select/select.component';
+
+const OPTIONS = [
+ { name: 'Новосибирск', code: 'NSK' },
+ { name: 'Москва', code: 'MSK' },
+ { name: 'Санкт-Петербург', code: 'SPB' }
+];
+
+const template = `
+
+`;
+
+@Component({
+ selector: 'app-select-label-position',
+ standalone: true,
+ imports: [ExtraSelectComponent, ReactiveFormsModule],
+ template
+})
+export class SelectLabelPositionComponent {
+ readonly options = OPTIONS;
+ control1 = new FormControl(null);
+ control2 = new FormControl(null);
+ control3 = new FormControl(null);
+}
+
+export const LabelPosition: StoryObj = {
+ name: 'Label Position',
+ render: () => ({
+ template: `
`
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: {
+ story:
+ '`labelPosition` — `top` (по умолчанию) над полем или `left` слева от поля. Плавающая метка внутри поля включается отдельным булевым свойством `floatLabel`, которое перекрывает `labelPosition`.'
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { ExtraSelectComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ standalone: true,
+ imports: [ExtraSelectComponent, ReactiveFormsModule],
+ template: \`
+
+
+
+
+
+ \`,
+})
+export class ExampleComponent {
+ options = [
+ { name: 'Новосибирск', code: 'NSK' },
+ { name: 'Москва', code: 'MSK' },
+ ];
+ control1 = new FormControl(null);
+ control2 = new FormControl(null);
+ control3 = new FormControl(null);
+}
+ `
+ }
+ }
+ }
+};
diff --git a/src/stories/components/select/examples/select-selected-item.component.ts b/src/stories/components/select/examples/select-selected-item.component.ts
index 9845a9a9..0bae187c 100644
--- a/src/stories/components/select/examples/select-selected-item.component.ts
+++ b/src/stories/components/select/examples/select-selected-item.component.ts
@@ -15,36 +15,38 @@ const OPTIONS = [
];
const template = `
-
-
-
-
- {{ option.name }}
- {{ option.code }}
-
-
+
+
+
+
+
+ {{ option.name }}
+ {{ option.code }}
+
+
-
-
-
- {{ option.name }}
- ({{ option.code }})
-
-
-
+
+
+
+ {{ option.name }}
+ ({{ option.code }})
+
+
+
+
`;
-const styles = '';
@Component({
selector: 'app-select-selected-item',
standalone: true,
imports: [ExtraSelectComponent, ExtraSelectOptionDirective, ExtraSelectSelectedItemDirective, ReactiveFormsModule],
- template,
- styles
+ template
})
export class SelectSelectedItemComponent {
- control = new FormControl(null);
- options = OPTIONS;
+ readonly options = OPTIONS;
+ // значение задано сразу: без него поле показывает плейсхолдер,
+ // и шаблон выбранного значения — то, ради чего история, — не виден
+ control = new FormControl(OPTIONS[0]);
}
export const SelectedItem: StoryObj = {
@@ -54,8 +56,14 @@ export const SelectedItem: StoryObj = {
controls: { disable: true },
docs: {
description: {
- story:
- 'Кастомное отображение выбранного значения (в закрытом состоянии) через `extraSelectSelectedItem`, а пунктов списка — через `extraSelectOption`. Оба получают `let-option`.'
+ story: `Два независимых шаблона на одном поле:
+
+- \`extraSelectOption\` — как выглядит пункт в раскрытом списке;
+- \`extraSelectSelectedItem\` — как выглядит выбранное значение в закрытом поле.
+
+Оба получают опцию через \`let-option\` — это элемент массива \`options\` целиком, а не строка из \`optionLabel\`. Поле открывается с уже выбранной «Москвой», чтобы было видно оформление выбранного значения: жирное название, иконка цветом акцента и код в скобках. Раскройте список — пункты оформлены иначе: код прижат вправо, иконка нейтральная.
+
+Если задать только \`extraSelectOption\`, закрытое поле покажет обычный текст из \`optionLabel\`: шаблоны не наследуют друг друга.`
},
source: {
language: 'ts',
@@ -69,7 +77,9 @@ import {
} from '@cdek-it/angular-ui-kit';
@Component({
+ selector: 'app-cities',
standalone: true,
+ // обе директивы обязательны: без импорта ng-template просто не будет найден
imports: [
ExtraSelectComponent,
ExtraSelectOptionDirective,
@@ -78,6 +88,7 @@ import {
],
template: \`
+
@@ -86,6 +97,7 @@ import {
+
@@ -96,9 +108,17 @@ import {
\`,
})
-export class SelectSelectedItemExample {
- control = new FormControl(null);
- options = [ /* ... */ ];
+export class CitiesComponent {
+ options = [
+ { name: 'Москва', code: 'MOW' },
+ { name: 'Санкт-Петербург', code: 'LED' },
+ { name: 'Новосибирск', code: 'OVB' },
+ { name: 'Екатеринбург', code: 'SVX' },
+ ];
+
+ // без optionValue в модели лежит сам объект опции —
+ // поэтому и предвыбор задаётся объектом из options
+ control = new FormControl(this.options[0]);
}
`
}
diff --git a/src/stories/components/select/examples/select-sizes.component.ts b/src/stories/components/select/examples/select-sizes.component.ts
new file mode 100644
index 00000000..6f017f8d
--- /dev/null
+++ b/src/stories/components/select/examples/select-sizes.component.ts
@@ -0,0 +1,85 @@
+import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { ExtraSelectComponent, ExtraSelectSize } from '../../../../lib/components/select/select.component';
+
+const OPTIONS = [
+ { name: 'Новосибирск', code: 'NSK' },
+ { name: 'Москва', code: 'MSK' },
+ { name: 'Санкт-Петербург', code: 'SPB' }
+];
+
+const SIZES: ExtraSelectSize[] = ['small', 'base', 'large', 'xlarge'];
+
+const template = `
+
+ @for (size of sizes; track size) {
+
+ }
+
+`;
+
+@Component({
+ selector: 'app-select-sizes',
+ standalone: true,
+ imports: [ExtraSelectComponent, ReactiveFormsModule],
+ template
+})
+export class SelectSizesComponent {
+ readonly sizes = SIZES;
+ readonly options = OPTIONS;
+ // по контролу на размер: с общим значение менялось бы сразу во всех полях
+ readonly controls: Record
= {
+ small: new FormControl(null),
+ base: new FormControl(null),
+ large: new FormControl(null),
+ xlarge: new FormControl(null)
+ };
+}
+
+export const Sizes: StoryObj = {
+ render: () => ({
+ template: ``
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: {
+ story: `Размеры поля: \`small\`, \`base\`, \`large\`, \`xlarge\`. \`large\` маппится на PrimeNG \`size="large"\`, \`xlarge\` — на собственный класс \`p-select-xlg\`.
+
+Визуально различается только \`xlarge\` (58px против 46px): в токенах \`select.root.sm\` и \`select.root.lg\` заданы теми же значениями, что и \`select.root\` — одинаковые \`paddingX\`, \`paddingY\` и \`fontSize\`. Поэтому \`small\`, \`base\` и \`large\` совпадают пиксель в пиксель.`
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { ExtraSelectComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ standalone: true,
+ imports: [ExtraSelectComponent],
+ template: \`
+
+
+
+
+ \`,
+})
+export class SelectSizesExample {
+ options = [
+ { name: 'Новосибирск', code: 'NSK' },
+ { name: 'Москва', code: 'MSK' },
+ ];
+}
+ `
+ }
+ }
+ }
+};
diff --git a/src/stories/components/select/select.stories.ts b/src/stories/components/select/select.stories.ts
index 964b5488..9247cd23 100644
--- a/src/stories/components/select/select.stories.ts
+++ b/src/stories/components/select/select.stories.ts
@@ -1,5 +1,5 @@
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
-import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
+import { FormControl, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
import { ExtraSelectComponent } from '../../../lib/components/select/select.component';
import { Filter as FilterStory, SelectFilterComponent } from './examples/select-filter.component';
import { Grouped as GroupedStory, SelectGroupedComponent } from './examples/select-grouped.component';
@@ -10,7 +10,11 @@ import {
} from './examples/select-selected-item.component';
import { Editable as EditableStory, SelectEditableComponent } from './examples/select-editable.component';
import { Disabled as DisabledStory } from './examples/select-disabled.component';
-import { FloatLabelStory, SelectFloatLabelComponent } from './examples/select-float-label.component';
+import { Sizes as SizesStory, SelectSizesComponent } from './examples/select-sizes.component';
+import {
+ LabelPosition as LabelPositionStory,
+ SelectLabelPositionComponent
+} from './examples/select-label-position.component';
const BASIC_OPTIONS = [
{ name: 'Новосибирск', code: 'NSK' },
@@ -20,9 +24,38 @@ const BASIC_OPTIONS = [
{ name: 'Казань', code: 'KZN' }
];
+/**
+ * Контрол истории живёт между рендерами: Storybook вызывает render() на каждое
+ * изменение args, и пересоздание FormControl сбрасывало бы выбранное значение —
+ * а вместе с ним всё, что видно только у заполненного поля (иконка очистки,
+ * отметка выбранного пункта).
+ */
+const control = new FormControl(null);
+
+/**
+ * `invalid` в сторибуке показывает визуальное состояние, а не результат конкретного
+ * правила: с Validators.required поле становится валидным, как только выбрано
+ * значение, и переключатель переставал что-либо менять.
+ */
+const alwaysInvalid = (): ValidationErrors => ({ invalid: true });
+
type SelectArgs = Pick<
ExtraSelectComponent,
- 'size' | 'placeholder' | 'showClear' | 'filter' | 'readonly' | 'checkmark'
+ | 'size'
+ | 'placeholder'
+ | 'label'
+ | 'labelPosition'
+ | 'floatLabel'
+ | 'caption'
+ | 'info'
+ | 'clearable'
+ | 'showFilter'
+ | 'readonly'
+ | 'showCheckbox'
+ | 'onChange'
+ | 'onShow'
+ | 'onHide'
+ | 'onClear'
> & {
disabled: boolean;
invalid: boolean;
@@ -42,14 +75,22 @@ const meta: Meta = {
SelectCustomComponent,
SelectSelectedItemComponent,
SelectEditableComponent,
- SelectFloatLabelComponent
+ SelectSizesComponent,
+ SelectLabelPositionComponent
]
})
],
parameters: {
+ designTokens: { prefix: '--p-select' },
docs: {
description: {
- component: `Выпадающий список для выбора одного значения из набора опций. Поддерживает фильтрацию, группировку, кастомные шаблоны и редактируемый ввод.
+ component: `Single-select — выпадающий список для выбора одного значения из набора опций.
+
+Реализован по спецификации [select.md](https://github.com/cdek-it/angular-ui-kit/blob/main/docs/components-api/select.md).
+
+\`\`\`typescript
+import { ExtraSelectComponent } from '@cdek-it/angular-ui-kit';
+\`\`\`
Шаблоны (передаются между тегами компонента):
- \`extraSelectOption\` — пункт списка (контекст \`let-option\`)
@@ -57,94 +98,179 @@ const meta: Meta = {
- \`extraSelectOptionGroup\` — заголовок группы (контекст \`let-group\`)
\`\`\`typescript
-import { ExtraSelectComponent, ExtraSelectOptionDirective, ExtraSelectSelectedItemDirective, ExtraSelectOptionGroupDirective } from '@cdek-it/angular-ui-kit';
+import { ExtraSelectOptionDirective, ExtraSelectSelectedItemDirective, ExtraSelectOptionGroupDirective } from '@cdek-it/angular-ui-kit';
\`\`\``
}
- },
- designTokens: { prefix: '--p-select' }
+ }
},
argTypes: {
- size: {
- control: 'select',
- options: ['small', 'base', 'large', 'xlarge'],
- description: 'Размер поля',
+ // ── Свойства (docs/components-api/select.md) ──────────────
+ placeholder: {
+ control: 'text',
+ description: 'Текст подсказки внутри поля',
table: {
- category: 'Props',
- defaultValue: { summary: "'base'" },
- type: { summary: "'small' | 'base' | 'large' | 'xlarge'" }
+ category: 'Свойства',
+ defaultValue: { summary: "''" },
+ type: { summary: 'string' }
}
},
- placeholder: {
+ label: {
control: 'text',
- description: 'Текст подсказки при пустом поле',
+ description: 'Текст названия поля',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: "''" },
type: { summary: 'string' }
}
},
- showClear: {
+ labelPosition: {
+ control: 'select',
+ options: ['top', 'left'],
+ description: 'Положение лейбла',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "'top'" },
+ type: { summary: "'top' | 'left'" }
+ }
+ },
+ floatLabel: {
control: 'boolean',
- description: 'Отображает иконку очистки выбранного значения',
+ description: 'Плавающий лейбл внутри поля — перекрывает labelPosition',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'false' },
type: { summary: 'boolean' }
}
},
- filter: {
+ caption: {
+ control: 'text',
+ description: 'Текст пояснения под полем',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "''" },
+ type: { summary: 'string' }
+ }
+ },
+ info: {
+ control: 'text',
+ description: 'Текст с доп. информацией (показывается в тултипе иконки ti-info-circle)',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "''" },
+ type: { summary: 'string' }
+ }
+ },
+ size: {
+ control: 'select',
+ options: ['small', 'base', 'large', 'xlarge'],
+ description: 'Размер поля',
+ table: {
+ category: 'Свойства',
+ defaultValue: { summary: "'base'" },
+ type: { summary: "'small' | 'base' | 'large' | 'xlarge'" }
+ }
+ },
+ clearable: {
control: 'boolean',
- description: 'Включает строку поиска в выпадающем списке',
+ description: 'Отображение иконки для очистки поля',
table: {
- category: 'Props',
+ category: 'Свойства',
defaultValue: { summary: 'false' },
type: { summary: 'boolean' }
}
},
- readonly: {
+ showCheckbox: {
control: 'boolean',
- description: 'Режим только для чтения',
+ description: 'Отображать отметку выбранного option',
table: {
- category: 'Props',
- defaultValue: { summary: 'false' },
+ category: 'Свойства',
+ defaultValue: { summary: 'true' },
type: { summary: 'boolean' }
}
},
- checkmark: {
+ showFilter: {
control: 'boolean',
- description: 'Отображает иконку выбранного элемента в списке',
+ description: 'Включает строку поиска в выпадающем списке',
table: {
- category: 'Props',
- defaultValue: { summary: 'true' },
+ category: 'Свойства',
+ defaultValue: { summary: 'false' },
type: { summary: 'boolean' }
}
},
+ // ── Состояния (управляются через FormControl) ───────────────────
disabled: {
control: 'boolean',
description: 'Отключает взаимодействие — управляется через FormControl',
table: {
- category: 'Props',
+ category: 'Состояния',
defaultValue: { summary: 'false' },
type: { summary: 'boolean' }
}
},
invalid: {
control: 'boolean',
- description: 'Невалидное состояние — управляется через FormControl',
+ description: 'Невалидное состояние — вычисляется из NgControl',
table: {
- category: 'Props',
+ category: 'Состояния',
defaultValue: { summary: 'false' },
type: { summary: 'boolean' }
}
+ },
+ readonly: {
+ control: 'boolean',
+ description: 'Режим только для чтения',
+ table: {
+ category: 'Состояния',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' }
+ }
+ },
+ // ── События ──────────────────────────────────────────────────
+ onChange: {
+ control: false,
+ description: 'Срабатывает при выборе значения',
+ table: {
+ category: 'События',
+ type: { summary: 'EventEmitter' }
+ }
+ },
+ onShow: {
+ control: false,
+ description: 'Срабатывает при открытии списка',
+ table: {
+ category: 'События',
+ type: { summary: 'EventEmitter' }
+ }
+ },
+ onHide: {
+ control: false,
+ description: 'Срабатывает при закрытии списка',
+ table: {
+ category: 'События',
+ type: { summary: 'EventEmitter' }
+ }
+ },
+ onClear: {
+ control: false,
+ description: 'Срабатывает при очистке значения',
+ table: {
+ category: 'События',
+ type: { summary: 'EventEmitter' }
+ }
}
},
args: {
size: 'base',
placeholder: 'Выберите город...',
- showClear: true,
- filter: false,
+ label: '',
+ labelPosition: 'top',
+ floatLabel: false,
+ caption: '',
+ info: '',
+ clearable: true,
+ showFilter: false,
readonly: false,
- checkmark: true,
+ showCheckbox: true,
disabled: false,
invalid: false
}
@@ -157,63 +283,58 @@ type Story = StoryObj;
export const Default: Story = {
name: 'Default',
- render: (args) => {
- const control = new FormControl(
- { value: null, disabled: !!args['disabled'] },
- args['invalid'] ? [Validators.required] : []
- );
- if (args['invalid']) control.markAsTouched();
+ render: ({ disabled, invalid, ...args }) => {
+ // disabled и invalid — не свойства компонента: первое приходит через
+ // ControlValueAccessor.setDisabledState, второе вычисляется из NgControl.
+ // Поэтому задаём их состоянием самого контрола, а не входами.
+ disabled ? control.disable() : control.enable();
+ control.setValidators(invalid ? [alwaysInvalid] : []);
+ control.updateValueAndValidity();
+ invalid ? control.markAsTouched() : control.markAsUntouched();
+ // В props кладём только реальные входы: Storybook присваивает пропсы прямо
+ // на инстанс ExtraSelectComponent, а invalid у него — геттер без сеттера.
return {
props: { ...args, control, options: BASIC_OPTIONS },
template: `
-
+
+
+
`
};
},
parameters: {
docs: {
description: {
- story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.'
+ story:
+ 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов; disabled и invalid — не входы компонента и задаются состоянием FormControl.'
}
}
}
};
-// ── Filter ────────────────────────────────────────────────────────────────────
+// ── Комбинаторные истории ──────────────────────────────────────────────────
export const Filter: Story = FilterStory;
-
-// ── Grouped ───────────────────────────────────────────────────────────────────
-
export const Grouped: Story = GroupedStory;
-
-// ── Custom ────────────────────────────────────────────────────────────────────
-
export const Custom: Story = CustomStory;
-
-// ── Selected Item ─────────────────────────────────────────────────────────────
-
export const SelectedItem: Story = SelectedItemStory;
-
-// ── Editable ──────────────────────────────────────────────────────────────────
-
export const Editable: Story = EditableStory;
-
-// ── Disabled ──────────────────────────────────────────────────────────────────
-
+export const Sizes: Story = SizesStory;
export const Disabled: Story = DisabledStory;
-
-// ── FloatLabel ────────────────────────────────────────────────────────────────
-
-export const FloatLabel: Story = FloatLabelStory;
+export const LabelPosition: Story = LabelPositionStory;