diff --git a/community/components/form/input/input.component.ts b/community/components/form/input/input.component.ts index ab6f8daf6..ff47b4785 100644 --- a/community/components/form/input/input.component.ts +++ b/community/components/form/input/input.component.ts @@ -9,6 +9,9 @@ import { export type InputSize = "small" | "default"; export type InputState = "valid" | "error" | "default"; +/** + * @deprecated Use TextField from TEDI-ready instead. This component will be removed from future versions. + */ @Component({ selector: "[tedi-input]", standalone: true, diff --git a/community/components/form/input/textfield.stories.ts b/community/components/form/input/textfield.stories.ts index 39090c654..ff0fb2963 100644 --- a/community/components/form/input/textfield.stories.ts +++ b/community/components/form/input/textfield.stories.ts @@ -33,6 +33,11 @@ export default { }, }, }, + parameters: { + status: { + type: ["deprecated", "existsInTediReady"], + }, + }, } as Meta; type InputStory = StoryObj; diff --git a/tedi/components/form/feedback-text/feedback-text.component.scss b/tedi/components/form/feedback-text/feedback-text.component.scss index 39d88635e..426eb912d 100644 --- a/tedi/components/form/feedback-text/feedback-text.component.scss +++ b/tedi/components/form/feedback-text/feedback-text.component.scss @@ -4,11 +4,11 @@ color: var(--general-text-tertiary); &--valid { - color: var(--general-status-success-text); + color: var(--form-general-feedback-success-text); } &--error { - color: var(--general-status-danger-text); + color: var(--form-general-feedback-error-text); } &--left { diff --git a/tedi/components/form/form-field/form-field-control.ts b/tedi/components/form/form-field/form-field-control.ts new file mode 100644 index 000000000..c08368787 --- /dev/null +++ b/tedi/components/form/form-field/form-field-control.ts @@ -0,0 +1,35 @@ +import { InjectionToken, Signal } from "@angular/core"; + +/** + * Interface implemented by controls that can be used inside `tedi-form-field`. + * Allows the form field container to interact with the underlying control. + */ +export interface FormFieldControl { + /** + * Current value of the control. + */ + value: Signal; + /** + * Whether the control is disabled. + */ + disabled: Signal; + /** + * Whether the control is currently in an invalid state. + * Usually derived from Angular form validation state. + */ + invalid: Signal; + + /** + * Optional method used by the form field clear button. + * If implemented, the form field can trigger clearing the value. + */ + clearField?(): void; +} + +/** + * Injection token used by `tedi-form-field` to obtain + * the associated control instance. + */ +export const TEDI_FORM_FIELD_CONTROL = new InjectionToken( + "TEDI_FORM_FIELD_CONTROL", +); diff --git a/tedi/components/form/form-field/form-field.component.html b/tedi/components/form/form-field/form-field.component.html new file mode 100644 index 000000000..a8f01d420 --- /dev/null +++ b/tedi/components/form/form-field/form-field.component.html @@ -0,0 +1,43 @@ + + +
+ + + @if (showClearButton()) { +
+ + + @if (icon()) { + + } +
+ } + + @if (resolvedIcon(); as icon) { +
+ +
+ } +
+ +@if (feedback) { + +} diff --git a/tedi/components/form/form-field/form-field.component.scss b/tedi/components/form/form-field/form-field.component.scss new file mode 100644 index 000000000..2b259c934 --- /dev/null +++ b/tedi/components/form/form-field/form-field.component.scss @@ -0,0 +1,101 @@ +.tedi-form-field { + display: flex; + flex-direction: column; + + &__input { + position: relative; + display: flex; + gap: var(--form-field-inner-spacing); + align-items: center; + width: 100%; + height: var(--form-field-height); + background: var(--form-input-background-default); + border: 1px solid var(--form-input-border-default); + border-radius: var(--form-field-radius); + } + + &--small { + .tedi-label { + font-size: var(--body-small-regular-size); + } + + .tedi-form-field__input { + height: var(--form-field-height-sm); + } + } + + &--large .tedi-form-field__input { + height: var(--form-field-height-lg); + } + + &--valid .tedi-form-field__input { + border-color: var(--form-general-feedback-success-border); + + &:focus-within { + box-shadow: 0 0 0 1px var(--form-general-feedback-success-border); + } + } + + &--invalid .tedi-form-field__input { + border-color: var(--form-general-feedback-error-border); + + &:focus-within { + box-shadow: 0 0 0 1px var(--form-general-feedback-error-border); + } + } + + &--disabled .tedi-form-field__input { + cursor: not-allowed; + background: var(--form-input-background-disabled); + border-color: var(--form-input-border-disabled); + box-shadow: none; + } + + &--with-icon .tedi-form-field__input { + padding-right: var(--form-field-padding-x-md-default); + } + + &--with-icon.tedi-form-field--large .tedi-form-field__input { + padding-right: var(--form-field-padding-x-lg); + } + + &:not( + .tedi-form-field--disabled, + .tedi-form-field--valid, + .tedi-form-field--invalid + ) + .tedi-form-field__input { + &:hover, + &:has(input:hover) { + border-color: var(--form-input-border-hover); + } + + &:active, + &:has(input:active) { + border-color: var(--form-input-border-active); + box-shadow: 0 0 0 1px var(--form-input-border-active); + } + + &:focus-within, + &:has(input:focus-visible) { + border-color: var(--form-input-border-focus); + box-shadow: 0 0 0 1px var(--form-input-border-focus); + } + } + + &__clear { + &:disabled { + cursor: not-allowed; + } + } + + &__feedback { + margin-top: var(--form-field-outer-spacing); + } + + &__buttons { + display: flex; + gap: var(--layout-grid-gutters-04); + align-items: center; + } +} diff --git a/tedi/components/form/form-field/form-field.component.spec.ts b/tedi/components/form/form-field/form-field.component.spec.ts new file mode 100644 index 000000000..50a072340 --- /dev/null +++ b/tedi/components/form/form-field/form-field.component.spec.ts @@ -0,0 +1,172 @@ +import { Component, signal, ViewChild } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { + FormFieldComponent, + FormFieldIcon, + InputSize, +} from "./form-field.component"; +import { + TEDI_FORM_FIELD_CONTROL, + FormFieldControl, +} from "./form-field-control"; +import { FeedbackTextComponent } from "../feedback-text/feedback-text.component"; + +@Component({ + selector: "mock-control", + standalone: true, + template: "", + providers: [ + { + provide: TEDI_FORM_FIELD_CONTROL, + useExisting: MockControlComponent, + }, + ], +}) +class MockControlComponent implements FormFieldControl { + value = signal(""); + disabled = signal(false); + invalid = signal(false); + clearField = jest.fn(); +} + +@Component({ + selector: "mock-feedback", + standalone: true, + template: "", +}) +export class MockFeedbackComponent extends FeedbackTextComponent {} + +@Component({ + standalone: true, + imports: [FormFieldComponent, MockControlComponent, MockFeedbackComponent], + template: ` + + + + + `, +}) +class TestHostComponent { + @ViewChild("formField", { static: true }) formField!: FormFieldComponent; + @ViewChild("mockControl", { static: true }) + mockControl!: MockControlComponent; + @ViewChild("feedback", { static: true }) feedback!: FeedbackTextComponent; + + size: InputSize = "default"; + icon?: string | FormFieldIcon; + clearable = false; + inputClass?: string; + feedbackType: "valid" | "error" | "default" = "default"; +} + +describe("FormFieldComponent", () => { + let fixture: ComponentFixture; + let host: TestHostComponent; + let formField: FormFieldComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestHostComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + host = fixture.componentInstance; + fixture.detectChanges(); + + formField = host.formField; + }); + + it("should create", () => { + expect(formField).toBeTruthy(); + }); + + it("should apply small size class", () => { + host.size = "small"; + fixture.detectChanges(); + expect(formField.hostClasses()["tedi-form-field--small"]).toBe(true); + }); + + it("should resolve string icon to config object", () => { + host.icon = "person"; + fixture.detectChanges(); + + const icon = fixture.nativeElement.querySelector("tedi-icon"); + expect(icon).toBeTruthy(); + }); + + it("should use full icon config object", () => { + host.icon = { name: "search", size: 24 }; + fixture.detectChanges(); + + const icon = fixture.nativeElement.querySelector("tedi-icon"); + expect(icon).toBeTruthy(); + }); + + it("should not render icon when icon is undefined", () => { + host.icon = undefined; + fixture.detectChanges(); + + const icon = fixture.nativeElement.querySelector("tedi-icon"); + expect(icon).toBeNull(); + }); + + it("should not show clear button when value is empty", () => { + host.clearable = true; + host.mockControl.value.set(""); + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector("button"); + expect(button).toBeNull(); + }); + + it("should not show clear button when clearable is false", () => { + host.clearable = false; + host.mockControl.value.set("Test"); + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector("button"); + expect(button).toBeNull(); + }); + + it("should call control.clearField when clear is triggered", () => { + formField.clear(); + + expect(host.mockControl.clearField).toHaveBeenCalled(); + }); + + it("should be invalid when control invalid", () => { + host.mockControl.invalid.set(true); + + fixture.detectChanges(); + + expect(formField.validationState()).toBe("invalid"); + }); + + it("should reflect disabled state from control", () => { + host.mockControl.disabled.set(true); + fixture.detectChanges(); + expect(formField.isDisabled()).toBe(true); + + host.mockControl.disabled.set(false); + fixture.detectChanges(); + expect(formField.isDisabled()).toBe(false); + }); + + it("should apply custom class", () => { + host.inputClass = "custom-class"; + fixture.detectChanges(); + + const classes = formField.inputClasses() as Record; + expect(classes["custom-class"]).toBe(true); + }); +}); diff --git a/tedi/components/form/form-field/form-field.component.ts b/tedi/components/form/form-field/form-field.component.ts new file mode 100644 index 000000000..ed62a332d --- /dev/null +++ b/tedi/components/form/form-field/form-field.component.ts @@ -0,0 +1,130 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + ContentChild, + input, + ViewEncapsulation, +} from "@angular/core"; +import { NgClass } from "@angular/common"; +import { + FormFieldControl, + TEDI_FORM_FIELD_CONTROL, +} from "./form-field-control"; +import { + IconColor, + IconComponent, + IconSize, + IconType, + IconVariant, +} from "../../../components/base/icon/icon.component"; +import { ClosingButtonComponent } from "../../../components/buttons/closing-button/closing-button.component"; +import { SeparatorComponent } from "../../../components/helpers/separator/separator.component"; +import { TediTranslationPipe } from "../../../services/translation/translation.pipe"; +import { FeedbackTextComponent } from "../feedback-text/feedback-text.component"; + +export type InputSize = "small" | "large" | "default"; +export type InputState = "valid" | "error" | "default"; +type ValidationState = "invalid" | "valid" | "neutral"; + +export interface FormFieldIcon { + name: string; + size?: IconSize; + color?: IconColor; + type?: IconType; + variant?: IconVariant; +} + +@Component({ + selector: "tedi-form-field", + standalone: true, + templateUrl: "./form-field.component.html", + styleUrl: "./form-field.component.scss", + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + NgClass, + IconComponent, + ClosingButtonComponent, + SeparatorComponent, + TediTranslationPipe, + ], + host: { + "[class]": "hostClasses()", + }, +}) +export class FormFieldComponent { + /** + * The size of the form field. + * @default "default" + */ + size = input("default"); + /** + * Icon name or configuration object. + */ + icon = input(); + /** + * Whether the form field includes a clear button. + * @default false + */ + clearable = input(false); + /** + * Custom CSS classes for the input. + */ + inputClass = input(null); + + @ContentChild(TEDI_FORM_FIELD_CONTROL) + control?: FormFieldControl; + + @ContentChild(FeedbackTextComponent) + feedback?: FeedbackTextComponent; + + readonly resolvedIcon = computed(() => { + const icon = this.icon(); + if (!icon) return undefined; + + return typeof icon === "string" ? { name: icon } : icon; + }); + + readonly validationState = computed(() => { + const feedbackType = this.feedback?.type(); + const fieldInvalid = this.control?.invalid(); + + if (fieldInvalid || feedbackType === "error") return "invalid"; + if (feedbackType === "valid") return "valid"; + + return "neutral"; + }); + + showClearButton = computed(() => { + const value = this.control?.value(); + return this.clearable() && !!value; + }); + + readonly isDisabled = computed(() => this.control?.disabled() ?? false); + + readonly hostClasses = computed(() => { + return { + "tedi-form-field": true, + "tedi-form-field--valid": this.validationState() === "valid", + "tedi-form-field--invalid": this.validationState() === "invalid", + "tedi-form-field--disabled": this.control?.disabled(), + "tedi-form-field--small": this.size() === "small", + "tedi-form-field--large": this.size() === "large", + "tedi-form-field--with-icon": this.showClearButton() || !!this.icon(), + }; + }); + + readonly inputClasses = computed(() => { + const customClass = this.inputClass(); + + return { + "tedi-form-field__input": true, + ...(customClass ? { [customClass]: true } : {}), + }; + }); + + clear() { + this.control?.clearField?.(); + } +} diff --git a/tedi/components/form/index.ts b/tedi/components/form/index.ts index 9f93af829..644875c23 100644 --- a/tedi/components/form/index.ts +++ b/tedi/components/form/index.ts @@ -4,3 +4,5 @@ export * from "./feedback-text/feedback-text.component"; export * from "./label/label.component"; export * from "./number-field/number-field.component"; export * from "./toggle/toggle.component"; +export * from "./form-field/form-field.component"; +export * from "./text-field/text-field.component"; diff --git a/tedi/components/form/number-field/number-field.component.scss b/tedi/components/form/number-field/number-field.component.scss index d34ccc303..8577fe5a4 100644 --- a/tedi/components/form/number-field/number-field.component.scss +++ b/tedi/components/form/number-field/number-field.component.scss @@ -138,13 +138,14 @@ grid-column: span 2; width: 100%; font-size: var(--heading-h6-size); + color: var(--form-input-text-filled); text-align: center; outline: none; background-color: var(--form-input-background-default); border: 0; border-radius: 0; - // /* Chrome, Safari, Edge, Opera */ + /* Chrome, Safari, Edge, Opera */ &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { appearance: none; diff --git a/tedi/components/form/number-field/number-field.stories.ts b/tedi/components/form/number-field/number-field.stories.ts index 03ae66fe7..b426552e2 100644 --- a/tedi/components/form/number-field/number-field.stories.ts +++ b/tedi/components/form/number-field/number-field.stories.ts @@ -152,7 +152,7 @@ export default { }, feedbackText: { description: - "[FeedbackText](/?path=/docs/community-angular-form-feedbacktext--docs) component inputs.", + "[FeedbackText](/?path=/docs/tedi-ready-components-form-feedbacktext--docs) component inputs.", control: { type: "object", }, diff --git a/tedi/components/form/text-field/text-field.component.scss b/tedi/components/form/text-field/text-field.component.scss new file mode 100644 index 000000000..bf38d409f --- /dev/null +++ b/tedi/components/form/text-field/text-field.component.scss @@ -0,0 +1,57 @@ +.tedi-text-field { + flex: 1; + height: 100%; + padding: var(--form-field-padding-y-md-default) + var(--form-field-padding-x-md-default); + font-size: var(--body-regular-size); + font-weight: var(--body-regular-weight); + line-height: var(--body-regular-line-height); + color: var(--form-input-text-filled); + outline: none; + background: transparent; + border: 0; + border-radius: var(--form-field-radius); + + &::placeholder { + color: var(--form-input-text-placeholder); + } + + &:disabled { + color: var(--form-input-text-disabled); + cursor: not-allowed; + background: transparent; + } + + /* Chrome, Safari, Edge, Opera */ + &--arrows-hidden::-webkit-outer-spin-button, + &--arrows-hidden::-webkit-inner-spin-button { + appearance: none; + } + + /* Firefox */ + &--arrows-hidden[type="number"] { + appearance: textfield; + } +} + +.tedi-form-field { + &--with-icon { + .tedi-text-field { + padding-right: 0; + } + } + + &--small { + .tedi-text-field { + padding-block: var(--form-field-padding-y-sm); + padding-inline: var(--form-field-padding-x-md-default); + } + } + + &--large { + .tedi-text-field { + padding-block: var(--form-field-padding-y-lg); + padding-inline: var(--form-field-padding-x-lg); + } + } +} diff --git a/tedi/components/form/text-field/text-field.component.spec.ts b/tedi/components/form/text-field/text-field.component.spec.ts new file mode 100644 index 000000000..b51bfe242 --- /dev/null +++ b/tedi/components/form/text-field/text-field.component.spec.ts @@ -0,0 +1,117 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { TextFieldComponent } from "./text-field.component"; +import { ReactiveFormsModule } from "@angular/forms"; +import { Component } from "@angular/core"; +import { By } from "@angular/platform-browser"; +import { TEDI_TRANSLATION_DEFAULT_TOKEN } from "../../../tokens/translation.token"; + +@Component({ + standalone: true, + imports: [TextFieldComponent, ReactiveFormsModule], + template: ``, +}) +class TestHostComponent {} + +describe("TextFieldComponent", () => { + let fixture: ComponentFixture; + let input: HTMLInputElement; + let textField: TextFieldComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestHostComponent], + providers: [{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "en" }], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + + const textFieldDebug = fixture.debugElement.query( + By.directive(TextFieldComponent), + ); + textField = textFieldDebug.componentInstance; + input = textFieldDebug.nativeElement; + }); + + it("should create", () => { + expect(textField).toBeTruthy(); + expect(input).toBeTruthy(); + }); + + it("writeValue() should set the passed-in value", () => { + textField.writeValue("test"); + fixture.detectChanges(); + + expect(input.value).toBe("test"); + }); + + it("registerOnChange() triggers when input changes", () => { + const onChangeSpy = jest.fn(); + textField.registerOnChange(onChangeSpy); + + input.value = "changed"; + input.dispatchEvent(new Event("input")); + fixture.detectChanges(); + + expect(onChangeSpy).toHaveBeenCalledWith("changed"); + expect(textField.value()).toBe("changed"); + }); + + it("registerOnTouched() triggers when blurred", () => { + const onTouchedSpy = jest.fn(); + textField.registerOnTouched(onTouchedSpy); + + input.dispatchEvent(new Event("blur")); + fixture.detectChanges(); + + expect(onTouchedSpy).toHaveBeenCalled(); + }); + + it("setDisabledState() should disable control", () => { + textField.setDisabledState(true); + fixture.detectChanges(); + + expect(textField.disabled()).toBe(true); + expect(input.disabled).toBe(true); + }); + + it("clearField() should clear value", () => { + textField.writeValue("test"); + + textField.clearField(); + + expect(textField.value()).toBe(""); + expect(input.value).toBe(""); + }); + + it("clearField() should not clear when disabled", () => { + textField.writeValue("test"); + textField.setDisabledState(true); + + textField.clearField(); + + expect(textField.value()).toBe("test"); + }); + + it("should call onChange when input changes", () => { + const onChangeSpy = jest.fn(); + textField.registerOnChange(onChangeSpy); + + input.value = "test"; + input.dispatchEvent(new Event("input")); + fixture.detectChanges(); + + expect(onChangeSpy).toHaveBeenCalledWith("test"); + expect(textField.value()).toBe("test"); + }); + + it("should call onTouched when input is blurred", () => { + const onTouchedSpy = jest.fn(); + textField.registerOnTouched(onTouchedSpy); + + input.dispatchEvent(new Event("blur")); + fixture.detectChanges(); + + expect(onTouchedSpy).toHaveBeenCalled(); + }); +}); diff --git a/tedi/components/form/text-field/text-field.component.ts b/tedi/components/form/text-field/text-field.component.ts new file mode 100644 index 000000000..01ed145b5 --- /dev/null +++ b/tedi/components/form/text-field/text-field.component.ts @@ -0,0 +1,131 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, + model, + ViewEncapsulation, + forwardRef, + signal, + output, + ElementRef, + Self, + Optional, +} from "@angular/core"; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + NgControl, +} from "@angular/forms"; +import { + FormFieldControl, + TEDI_FORM_FIELD_CONTROL, +} from "../form-field/form-field-control"; + +@Component({ + selector: "input[tedi-text-field]", + standalone: true, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TextFieldComponent), + multi: true, + }, + { + provide: TEDI_FORM_FIELD_CONTROL, + useExisting: forwardRef(() => TextFieldComponent), + }, + ], + template: "", + styleUrl: "./text-field.component.scss", + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: "tedi-text-field", + "[class.tedi-text-field--arrows-hidden]": "arrowsHidden()", + "[attr.aria-invalid]": "invalid() || null", + "(input)": "handleInputChange($event)", + "(blur)": "handleBlur()", + }, +}) +export class TextFieldComponent + implements ControlValueAccessor, FormFieldControl +{ + /** + * Value of the input field. Supports two-way binding, use with form controls. + */ + value = model(""); + /** + * Whether to hide arrows for number inputs. + * @default true + */ + arrowsHidden = input(true); + /** + * Callback triggered when the clear button is clicked. + */ + readonly clear = output(); + + constructor( + private el: ElementRef, + @Optional() @Self() private ngControl: NgControl | null, + ) { + if (this.ngControl) { + this.ngControl.valueAccessor = this; + } + } + + readonly disabled = computed( + () => this.el.nativeElement.disabled || this.formDisabled(), + ); + readonly invalid = computed(() => { + const control = this.ngControl?.control; + return !!(control?.invalid && (control?.touched || control?.dirty)); + }); + + private formDisabled = signal(false); + private onChange: (value: string) => void = () => {}; + private onTouched: () => void = () => {}; + + private setValue(value: string) { + this.el.nativeElement.value = value; + this.value.set(value); + } + + writeValue(value: string | null): void { + this.setValue(value ?? ""); + } + + registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.formDisabled.set(isDisabled); + this.el.nativeElement.disabled = isDisabled; + } + + handleInputChange(event: Event) { + const input = event.target as HTMLInputElement; + const value = input.value; + + this.value.set(value); + this.onChange(value); + } + + handleBlur() { + this.onTouched(); + } + + clearField() { + if (this.disabled()) return; + + this.setValue(""); + this.onChange(""); + this.clear.emit(); + this.onTouched(); + } +} diff --git a/tedi/components/form/text-field/text-field.stories.ts b/tedi/components/form/text-field/text-field.stories.ts new file mode 100644 index 000000000..53a86594e --- /dev/null +++ b/tedi/components/form/text-field/text-field.stories.ts @@ -0,0 +1,244 @@ +import { + argsToTemplate, + Meta, + moduleMetadata, + StoryObj, +} from "@storybook/angular"; +import { + TextFieldComponent, + FormFieldComponent, + ColComponent, + RowComponent, + FeedbackTextComponent, + TextComponent, + LabelComponent, +} from "@tedi-design-system/angular/tedi"; + +const PSEUDO_STATE = ["Default", "Hover", "Active", "Disabled", "Focus"]; + +/** + * Figma ↗
+ * Zeroheight ↗ + * Can be used with Reactive forms and with Template-driven forms + */ + +export default { + title: "TEDI-Ready/Components/Form/TextField", + component: TextFieldComponent, + decorators: [ + moduleMetadata({ + imports: [ + RowComponent, + ColComponent, + LabelComponent, + TextComponent, + FormFieldComponent, + FeedbackTextComponent, + ], + }), + ], + argTypes: { + size: { + description: "Input field size.", + control: { + type: "radio", + }, + options: ["default", "small", "large"], + table: { + category: "Form Field inputs", + type: { summary: "InputSize", detail: "default \nsmall \nlarge" }, + defaultValue: { summary: "default" }, + }, + }, + icon: { + description: "Icon name or configuration for the input field.", + control: { + type: "object", + }, + table: { + category: "Form Field inputs", + type: { summary: "string | TextFieldIcon" }, + }, + }, + clearable: { + description: "Whether the input includes a clear button.", + control: { + type: "boolean", + }, + table: { + category: "Form Field inputs", + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + }, + inputClass: { + control: "text", + description: "Custom CSS classes for the input.", + table: { + category: "Form Field inputs", + type: { summary: "string" }, + }, + }, + arrowsHidden: { + description: "Whether to hide arrows for number inputs.", + control: { + type: "boolean", + }, + table: { + category: "Text Field inputs", + type: { summary: "boolean" }, + defaultValue: { summary: "true" }, + }, + }, + clear: { + description: "Callback triggered when the clear button is clicked.", + control: false, + action: "clear", + table: { + category: "Text Field outputs", + type: { summary: "void" }, + }, + }, + }, +} as Meta; + +export const Default: StoryObj = { + args: { + size: "default", + clearable: false, + arrowsHidden: true, + }, + render: ({ arrowsHidden, ...formFieldArgs }) => ({ + props: { + arrowsHidden, + ...formFieldArgs, + }, + template: ` + + + + + `, + }), +}; + +export const Size: StoryObj = { + render: () => ({ + template: ` + + +

Default

+ + + + + + + + +
+ +

Small

+ + + + + + + + +
+
+ `, + }), +}; + +export const States: StoryObj = { + parameters: { + pseudo: { + hover: "#Hover", + active: "#Active", + focusVisible: "#Focus", + }, + }, + render: () => ({ + props: { PSEUDO_STATE }, + template: ` + + + +

{{ state }}

+
+ + + + + + +
+ + +

Error

+
+ + + + + + + +
+ + +

Success

+
+ + + + + + + +
+
+ `, + }), +}; + +export const WithHint: StoryObj = { + render: () => ({ + template: ` + + + + + + `, + }), +}; + +export const Password: StoryObj = { + render: () => ({ + template: ` + + + + + `, + }), +}; + +export const Placeholder: StoryObj = { + render: () => ({ + template: ` + + + + + `, + }), +};