diff --git a/apps/isa-app/stories/ui/buttons/ui-button.stories.ts b/apps/isa-app/stories/ui/buttons/ui-button.stories.ts index f237c4f2c..679f0e7ab 100644 --- a/apps/isa-app/stories/ui/buttons/ui-button.stories.ts +++ b/apps/isa-app/stories/ui/buttons/ui-button.stories.ts @@ -1,7 +1,5 @@ import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; import { ButtonColor, ButtonSize, ButtonComponent } from '@isa/ui/buttons'; -// import { within } from '@storybook/testing-library'; -// import { expect } from '@storybook/jest'; type UiButtonComponentInputs = { color: ButtonColor; @@ -15,23 +13,33 @@ const meta: Meta = { title: 'ui/buttons/Button', argTypes: { color: { - control: 'select', + control: { type: 'select' }, options: ['primary', 'secondary', 'brand', 'tertiary'] as ButtonColor[], + description: 'Determines the button color', }, size: { - control: 'select', + control: { type: 'select' }, options: ['small', 'medium', 'large'] as ButtonSize[], + description: 'Determines the button size', }, pending: { control: 'boolean', + description: 'Show a pending/loading state when true', }, disabled: { control: 'boolean', + description: 'Disables the button when true', }, }, + args: { + color: 'primary', + size: 'medium', + pending: false, + disabled: false, + }, render: (args) => ({ props: args, - template: `Button`, + template: ``, }), }; export default meta; diff --git a/apps/isa-app/stories/ui/buttons/ui-icon-button.stories.ts b/apps/isa-app/stories/ui/buttons/ui-icon-button.stories.ts index 50155e99e..30ff6f003 100644 --- a/apps/isa-app/stories/ui/buttons/ui-icon-button.stories.ts +++ b/apps/isa-app/stories/ui/buttons/ui-icon-button.stories.ts @@ -1,7 +1,5 @@ import { argsToTemplate, type Meta, type StoryObj, moduleMetadata } from '@storybook/angular'; import { IconButtonColor, IconButtonSize, IconButtonComponent } from '@isa/ui/buttons'; -// import { within } from '@storybook/testing-library'; -// import { expect } from '@storybook/jest'; import { IsaIcons } from '@isa/icons'; import { NgIconComponent, provideIcons } from '@ng-icons/core'; @@ -23,30 +21,42 @@ const meta: Meta = { ], title: 'ui/buttons/IconButton', argTypes: { - icon: { control: 'select', options: Object.keys(IsaIcons) }, + icon: { + control: { type: 'select' }, + options: Object.keys(IsaIcons), + description: 'Icon name for the button', + }, color: { - control: 'select', + control: { type: 'select' }, options: ['brand', 'primary', 'secondary', 'tertiary'] as IconButtonColor[], + description: 'Color style of the button', }, size: { - control: 'select', + control: { type: 'select' }, options: ['small', 'medium'] as IconButtonSize[], + description: 'Size of the icon button', }, pending: { control: 'boolean', + description: 'Show a pending state when true', }, disabled: { control: 'boolean', + description: 'Disable the button when true', }, }, args: { icon: 'isaActionCheck', + color: 'primary', + size: 'medium', + pending: false, + disabled: false, }, render: (args) => ({ props: args, - template: ` + template: ``, }), }; export default meta; diff --git a/apps/isa-app/stories/ui/buttons/ui-info-button.stories.ts b/apps/isa-app/stories/ui/buttons/ui-info-button.stories.ts index 58eda0276..67e8d5cf1 100644 --- a/apps/isa-app/stories/ui/buttons/ui-info-button.stories.ts +++ b/apps/isa-app/stories/ui/buttons/ui-info-button.stories.ts @@ -19,21 +19,37 @@ const meta: Meta = { }), ], title: 'ui/buttons/InfoButton', - args: {}, + args: { + icon: 'isaNavigationLogout', + label: 'STA', + disabled: false, + pending: false, + }, argTypes: { - icon: { control: 'select', options: Object.keys(IsaIcons) }, - label: { control: 'text' }, - disabled: { control: 'boolean' }, + icon: { + control: { type: 'select' }, + options: Object.keys(IsaIcons), + description: 'Icon to display on the button', + }, + label: { + control: 'text', + description: 'Label text inside the button', + }, + disabled: { + control: 'boolean', + description: 'Disables the button when true', + }, pending: { control: 'boolean', + description: 'Shows a pending state when true', }, }, render: (args) => ({ props: args, - template: ` + template: ``, }), }; diff --git a/apps/isa-app/stories/ui/buttons/ui-text-button.stories.ts b/apps/isa-app/stories/ui/buttons/ui-text-button.stories.ts index d64f43fbf..0a6c3c973 100644 --- a/apps/isa-app/stories/ui/buttons/ui-text-button.stories.ts +++ b/apps/isa-app/stories/ui/buttons/ui-text-button.stories.ts @@ -1,35 +1,48 @@ import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; import { TextButtonComponent, TextButtonColor, TextButtonSize } from '@isa/ui/buttons'; -// import { within } from '@storybook/testing-library'; -// import { expect } from '@storybook/jest'; -type UiTextButtonComponentInputs = { +export type UiTextButtonComponentInputs = { color: TextButtonColor; size: TextButtonSize; disabled: boolean; + pending: boolean; }; const meta: Meta = { - component: TextButtonComponent, title: 'ui/buttons/TextButton', + component: TextButtonComponent, argTypes: { color: { - control: 'select', + control: { type: 'select' }, options: ['strong', 'subtle'] as TextButtonColor[], + description: 'Color variation for the text button', }, size: { - control: 'select', + control: { type: 'select' }, options: ['small', 'medium', 'large'] as TextButtonSize[], + description: 'Size of the text button', }, disabled: { control: 'boolean', + description: 'Disables the button when true', }, + pending: { + control: 'boolean', + description: 'Displays a loading state when true', + }, + }, + args: { + color: 'subtle', + size: 'medium', + disabled: false, + pending: false, }, render: (args) => ({ props: args, - template: `Button`, + template: ``, }), }; + export default meta; type Story = StoryObj; @@ -38,14 +51,6 @@ export const Default: Story = { args: {}, }; -// export const Heading: Story = { -// args: {}, -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); -// expect(canvas.getByText(/ui-button works!/gi)).toBeTruthy(); -// }, -// }; - export const StrongSmall: Story = { args: { color: 'strong', diff --git a/docs/guidelines/code-style.md b/docs/guidelines/code-style.md index 288554497..f2751121b 100644 --- a/docs/guidelines/code-style.md +++ b/docs/guidelines/code-style.md @@ -175,6 +175,10 @@ function getUser(id) { } ``` +- **Templates** + + - Use new control flow syntax - instead if \*ngIf use the @if syntax + ## Project-Specific Preferences - **Frameworks**: Follow best practices for Nx, Hono, and Zod. diff --git a/libs/oms/data-access/jest.config.ts b/libs/oms/data-access/jest.config.ts index 30d3c2c7a..024724b93 100644 --- a/libs/oms/data-access/jest.config.ts +++ b/libs/oms/data-access/jest.config.ts @@ -18,14 +18,4 @@ export default { 'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/html-comment', ], - reporters: [ - 'default', - [ - 'jest-junit', - { - outputDirectory: 'testresults', - outputName: `TEST-oms-data-access.xml`, - }, - ], - ], }; diff --git a/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss index 40c5e5ae9..1280dc0e3 100644 --- a/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss +++ b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss @@ -51,6 +51,8 @@ align-items: center; gap: 0.5rem; align-self: stretch; + + @apply text-isa-secondary-900 isa-text-body-2-regular; } [lib-return-process-questions] { diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html index 8c6b1be43..10c728c70 100644 --- a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html @@ -6,12 +6,20 @@
- @@ -25,8 +33,8 @@
-
{{ p.contributors }}
-
{{ p.name }}
+
{{ p.contributors }}
+
{{ p.name }}
diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts index 6e1687379..f5a8bfcdc 100644 --- a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts @@ -9,7 +9,7 @@ import { } from '@angular/forms'; import { Product, ReturnProcessProductQuestion, ReturnProcessStore } from '@isa/oms/data-access'; import { TextButtonComponent } from '@isa/ui/buttons'; -import { TextFieldComponent } from '@isa/ui/input-controls'; +import { InputControlDirective, TextFieldComponent } from '@isa/ui/input-controls'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { filter, pipe, switchMap, tap } from 'rxjs'; import { CatalougeSearchService } from '@isa/catalogue/data-access'; @@ -29,7 +29,13 @@ const eanValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | templateUrl: './return-process-product-question.component.html', styleUrls: ['./return-process-product-question.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule, TextFieldComponent, TextButtonComponent, ProductImageDirective], + imports: [ + ReactiveFormsModule, + TextFieldComponent, + InputControlDirective, + TextButtonComponent, + ProductImageDirective, + ], }) export class ReturnProcessProductQuestionComponent { #returnProcessStore = inject(ReturnProcessStore); diff --git a/libs/ui/buttons/src/lib/_button.scss b/libs/ui/buttons/src/lib/_button.scss index 6f3cb9a32..92ee2ff2b 100644 --- a/libs/ui/buttons/src/lib/_button.scss +++ b/libs/ui/buttons/src/lib/_button.scss @@ -7,10 +7,24 @@ font-style: normal; font-weight: 700; border-radius: 6.25rem; + + position: relative; +} + +.ui-button__pending { + @apply invisible; } .ui-button__spinner { - @apply animate-spin; + @apply visible animate-spin; +} + +.ui-button__spinner-container { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; } .ui-button__small { diff --git a/libs/ui/buttons/src/lib/_text-button.scss b/libs/ui/buttons/src/lib/_text-button.scss index c9c91f1fa..79ab75910 100644 --- a/libs/ui/buttons/src/lib/_text-button.scss +++ b/libs/ui/buttons/src/lib/_text-button.scss @@ -10,6 +10,24 @@ font-weight: 700; cursor: pointer; + + position: relative; +} + +.ui-text-button__pending { + @apply invisible; +} + +.ui-text-button__spinner { + @apply visible animate-spin; +} + +.ui-text-button__spinner-container { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; } .ui-text-button__normal { diff --git a/libs/ui/buttons/src/lib/button.component.html b/libs/ui/buttons/src/lib/button.component.html index 6555ea0c6..90a5ce2f5 100644 --- a/libs/ui/buttons/src/lib/button.component.html +++ b/libs/ui/buttons/src/lib/button.component.html @@ -1,5 +1,7 @@ -@if (!pending()) { - -} @else { - + + +@if (pending()) { +
+ +
} diff --git a/libs/ui/buttons/src/lib/button.component.spec.ts b/libs/ui/buttons/src/lib/button.component.spec.ts index 0f5382cad..ac5892c27 100644 --- a/libs/ui/buttons/src/lib/button.component.spec.ts +++ b/libs/ui/buttons/src/lib/button.component.spec.ts @@ -1,9 +1,14 @@ import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { ButtonComponent } from './button.component'; +import { MockComponent } from 'ng-mocks'; +import { NgIconComponent } from '@ng-icons/core'; describe('ButtonComponent', () => { let spectator: Spectator; - const createComponent = createComponentFactory(ButtonComponent); + const createComponent = createComponentFactory({ + component: ButtonComponent, + declarations: [MockComponent(NgIconComponent)], + }); beforeEach(() => { spectator = createComponent(); @@ -42,4 +47,17 @@ describe('ButtonComponent', () => { // Ensure previous color class is no longer present expect(host.classList).not.toContain('ui-button__primary'); }); + + it('should update pending class when pending input changes', () => { + // Update the pending signal from its default value (false) to true + spectator.setInput('pending', true); + spectator.detectChanges(); + + const host = spectator.element; + expect(host.classList).toContain('ui-button__pending'); + // Ensure the pending class is removed when set back to false + spectator.setInput('pending', false); + spectator.detectChanges(); + expect(host.classList).not.toContain('ui-button__pending'); + }); }); diff --git a/libs/ui/buttons/src/lib/button.component.ts b/libs/ui/buttons/src/lib/button.component.ts index 779026a8e..2badda136 100644 --- a/libs/ui/buttons/src/lib/button.component.ts +++ b/libs/ui/buttons/src/lib/button.component.ts @@ -10,21 +10,24 @@ import { ButtonColor, ButtonSize } from './types'; import { isaLoading } from '@isa/icons'; /** - * A UI button component that allows customization of size, color, and state. + * A UI button component that supports customization of size, color, and state. * - * The component uses reactive signals to manage its properties, allowing dynamic - * updates to its CSS classes without manual DOM manipulation. + * The component uses reactive signals to dynamically update its CSS classes + * and properties without manual DOM manipulation. * - * @property size - The size of the button (e.g., 'small', 'medium', 'large'). The default is 'medium'. + * @property size - The size of the button (e.g., 'small', 'medium', 'large'). Default is 'medium'. * @property sizeClass - A computed CSS class based on the current size, formatted as 'ui-button__{size}'. - * @property color - The color theme of the button (e.g., 'primary', 'secondary'). The default is 'primary'. + * @property color - The color theme of the button (e.g., 'primary', 'secondary'). Default is 'primary'. * @property colorClass - A computed CSS class based on the current color, formatted as 'ui-button__{color}'. * @property pending - A boolean flag indicating whether the button is in a loading or pending state. + * @property pendingClass - A computed CSS class that adds a 'pending' style when the button is loading. * @property tabIndex - The tab index for the button, used to control keyboard navigation order. + * @property disabled - A boolean flag indicating whether the button is disabled. + * @property disabledClass - A computed CSS class that adds a 'disabled' style when the button is disabled. * * @remarks - * This component relies on reactive input and computed signals to update its state and classes, - * ensuring that transformations are automatically reflected in the UI. + * This component is designed to be lightweight and flexible, leveraging Angular's + * reactive input and computed signals for seamless updates. */ @Component({ selector: 'ui-button, [uiButton]', @@ -33,23 +36,38 @@ import { isaLoading } from '@isa/icons'; encapsulation: ViewEncapsulation.None, standalone: true, imports: [NgIconComponent], - // TODO: Gegen loader icon ersetzen providers: [provideIcons({ isaLoading })], host: { - '[class]': '["ui-button", sizeClass(), colorClass()]', + '[class]': '["ui-button", sizeClass(), colorClass(), pendingClass(), disabledClass()]', '[tabindex]': 'tabIndex()', + '[disabled]': 'disabled()', }, }) export class ButtonComponent { + /** The size of the button. */ size = input('medium'); + /** A computed CSS class based on the current size. */ sizeClass = computed(() => `ui-button__${this.size()}`); + /** The color theme of the button. */ color = input('primary'); + /** A computed CSS class based on the current color. */ colorClass = computed(() => `ui-button__${this.color()}`); + /** Indicates whether the button is in a loading or pending state. */ pending = input(false); + /** A computed CSS class for the pending state. */ + pendingClass = computed(() => (this.pending() ? 'ui-button__pending' : '')); + + /** The tab index for the button. */ tabIndex = input(0); + + /** Indicates whether the button is disabled. */ + disabled = input(false); + + /** A computed CSS class for the disabled state. */ + disabledClass = computed(() => (this.disabled() ? 'disabled' : '')); } diff --git a/libs/ui/buttons/src/lib/icon-button.component.spec.ts b/libs/ui/buttons/src/lib/icon-button.component.spec.ts index a5803fbfa..aa3a7ceb6 100644 --- a/libs/ui/buttons/src/lib/icon-button.component.spec.ts +++ b/libs/ui/buttons/src/lib/icon-button.component.spec.ts @@ -1,5 +1,7 @@ import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { IconButtonComponent } from './icon-button.component'; +import { MockComponent } from 'ng-mocks'; +import { NgIconComponent } from '@ng-icons/core'; describe('IconButtonComponent', () => { let spectator: Spectator; @@ -7,7 +9,7 @@ describe('IconButtonComponent', () => { const createComponent = createComponentFactory({ component: IconButtonComponent, - imports: [], // additional imports if needed + declarations: [MockComponent(NgIconComponent)], }); beforeEach(() => { diff --git a/libs/ui/buttons/src/lib/icon-button.component.ts b/libs/ui/buttons/src/lib/icon-button.component.ts index 66299fc78..39dfe8b83 100644 --- a/libs/ui/buttons/src/lib/icon-button.component.ts +++ b/libs/ui/buttons/src/lib/icon-button.component.ts @@ -18,8 +18,9 @@ import { isaLoading } from '@isa/icons'; imports: [NgIconComponent], providers: [provideIcons({ isaLoading })], host: { - '[class]': "['ui-icon-button', sizeClass(), colorClass()]", + '[class]': "['ui-icon-button', sizeClass(), colorClass(), disabledClass()]", '[tabindex]': 'tabIndex()', + '[disabled]': 'disabled()', }, }) export class IconButtonComponent { @@ -34,4 +35,10 @@ export class IconButtonComponent { pending = input(false); tabIndex = input(0); + + /** Indicates whether the button is disabled. */ + disabled = input(false); + + /** A computed CSS class for the disabled state. */ + disabledClass = computed(() => (this.disabled() ? 'disabled' : '')); } diff --git a/libs/ui/buttons/src/lib/info-button.component.spec.ts b/libs/ui/buttons/src/lib/info-button.component.spec.ts index f7ac83355..bc4f139fc 100644 --- a/libs/ui/buttons/src/lib/info-button.component.spec.ts +++ b/libs/ui/buttons/src/lib/info-button.component.spec.ts @@ -1,11 +1,13 @@ import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { InfoButtonComponent } from './info-button.component'; +import { NgIconComponent } from '@ng-icons/core'; +import { MockComponent } from 'ng-mocks'; describe('InfoButtonComponent', () => { let spectator: Spectator; const createComponent = createComponentFactory({ component: InfoButtonComponent, - shallow: true, + declarations: [MockComponent(NgIconComponent)], }); beforeEach(() => { diff --git a/libs/ui/buttons/src/lib/info-button.component.ts b/libs/ui/buttons/src/lib/info-button.component.ts index 6196cfe31..7110c756a 100644 --- a/libs/ui/buttons/src/lib/info-button.component.ts +++ b/libs/ui/buttons/src/lib/info-button.component.ts @@ -16,11 +16,21 @@ import { NgIconComponent, provideIcons } from '@ng-icons/core'; imports: [NgIconComponent], providers: [provideIcons({ isaLoading })], host: { - '[class]': '["ui-info-button", pendingClass()]', + '[class]': '["ui-info-button", pendingClass(), disabledClass()]', + '[tabindex]': 'tabIndex()', + '[disabled]': 'disabled()', }, }) export class InfoButtonComponent { pending = input(false); pendingClass = computed(() => (this.pending() ? 'ui-info-button--pending' : '')); + + tabIndex = input(0); + + /** Indicates whether the button is disabled. */ + disabled = input(false); + + /** A computed CSS class for the disabled state. */ + disabledClass = computed(() => (this.disabled() ? 'disabled' : '')); } diff --git a/libs/ui/buttons/src/lib/text-button.component.html b/libs/ui/buttons/src/lib/text-button.component.html index 6dbc74306..5b993aff1 100644 --- a/libs/ui/buttons/src/lib/text-button.component.html +++ b/libs/ui/buttons/src/lib/text-button.component.html @@ -1 +1,7 @@ + +@if (pending()) { +
+ +
+} diff --git a/libs/ui/buttons/src/lib/text-button.component.spec.ts b/libs/ui/buttons/src/lib/text-button.component.spec.ts index f34df20fd..d122bba1c 100644 --- a/libs/ui/buttons/src/lib/text-button.component.spec.ts +++ b/libs/ui/buttons/src/lib/text-button.component.spec.ts @@ -1,10 +1,13 @@ import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; import { TextButtonComponent } from './text-button.component'; +import { NgIconComponent } from '@ng-icons/core'; describe('TextButtonComponent', () => { let spectator: Spectator; const createComponent = createComponentFactory({ component: TextButtonComponent, + declarations: [MockComponent(NgIconComponent)], }); beforeEach(() => { @@ -41,4 +44,18 @@ describe('TextButtonComponent', () => { spectator.detectChanges(); expect(hostElement.getAttribute('tabindex')).toEqual('5'); }); + + it('should update pending class when pending input changes', () => { + // Set the pending input to true and verify the class is added + spectator.setInput('pending', true); + spectator.detectChanges(); + + const hostElement = spectator.element; + expect(hostElement.classList).toContain('ui-text-button__pending'); + + // Set the pending input back to false and verify the class is removed + spectator.setInput('pending', false); + spectator.detectChanges(); + expect(hostElement.classList).not.toContain('ui-text-button__pending'); + }); }); diff --git a/libs/ui/buttons/src/lib/text-button.component.ts b/libs/ui/buttons/src/lib/text-button.component.ts index 6dbca83a3..0ed08fc6a 100644 --- a/libs/ui/buttons/src/lib/text-button.component.ts +++ b/libs/ui/buttons/src/lib/text-button.component.ts @@ -6,27 +6,66 @@ import { ViewEncapsulation, } from '@angular/core'; import { TextButtonColor, TextButtonSize } from './types'; +import { NgIconComponent, provideIcons } from '@ng-icons/core'; +import { isaLoading } from '@isa/icons'; +/** + * A UI text button component that supports customization of size, color, and state. + * + * The component uses reactive signals to dynamically update its CSS classes + * and properties without manual DOM manipulation. + * + * @property size - The size of the button (e.g., 'small', 'medium', 'large'). Default is 'medium'. + * @property sizeClass - A computed CSS class based on the current size, formatted as 'ui-text-button__{size}'. + * @property color - The color theme of the button (e.g., 'normal', 'primary'). Default is 'normal'. + * @property colorClass - A computed CSS class based on the current color, formatted as 'ui-text-button__{color}'. + * @property pending - A boolean flag indicating whether the button is in a loading or pending state. + * @property pendingClass - A computed CSS class that adds a 'pending' style when the button is loading. + * @property tabIndex - The tab index for the button, used to control keyboard navigation order. + * + * @remarks + * This component is designed to be lightweight and flexible, leveraging Angular's + * reactive input and computed signals for seamless updates. + */ @Component({ selector: 'ui-text-button, [uiTextButton]', templateUrl: './text-button.component.html', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, standalone: true, - imports: [], + imports: [NgIconComponent], + providers: [provideIcons({ isaLoading })], host: { - '[class]': '["ui-text-button", sizeClass(), colorClass()]', + '[class]': '["ui-text-button", sizeClass(), colorClass(), pendingClass(), disabledClass()]', '[tabindex]': 'tabIndex()', + '[disabled]': 'disabled()', }, }) export class TextButtonComponent { + /** The size of the button. */ size = input('medium'); + /** A computed CSS class based on the current size. */ sizeClass = computed(() => `ui-text-button__${this.size()}`); + /** The color theme of the button. */ color = input('normal'); + /** A computed CSS class based on the current color. */ colorClass = computed(() => `ui-text-button__${this.color()}`); + /** Indicates whether the button is in a loading or pending state. */ + pending = input(false); + + /** A computed CSS class for the pending state. */ + pendingClass = computed(() => (this.pending() ? 'ui-text-button__pending' : '')); + + /** The tab index for the button. */ tabIndex = input(0); + + /** Indicates whether the button is disabled. */ + disabled = input(false); + + /** A computed CSS class for the disabled state. */ + disabledClass = computed(() => (this.disabled() ? 'disabled' : '')); } diff --git a/libs/ui/input-controls/src/index.ts b/libs/ui/input-controls/src/index.ts index a0701ec32..0c4c28456 100644 --- a/libs/ui/input-controls/src/index.ts +++ b/libs/ui/input-controls/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/checkbox/checkbox.component'; +export * from './lib/core/input-control.directive'; export * from './lib/dropdown/dropdown.component'; export * from './lib/dropdown/dropdown.types'; export * from './lib/text-field/text-field.component'; diff --git a/libs/ui/input-controls/src/input-controls.scss b/libs/ui/input-controls/src/input-controls.scss index 0673e2ceb..440c47cc5 100644 --- a/libs/ui/input-controls/src/input-controls.scss +++ b/libs/ui/input-controls/src/input-controls.scss @@ -1,3 +1,4 @@ @use "./lib/checkbox/checkbox"; @use "./lib/chips/chips"; @use "./lib/dropdown/dropdown"; +@use "./lib/text-field/text-field.scss"; diff --git a/libs/ui/input-controls/src/lib/core/input-control.directive.ts b/libs/ui/input-controls/src/lib/core/input-control.directive.ts new file mode 100644 index 000000000..5ecbe2c8e --- /dev/null +++ b/libs/ui/input-controls/src/lib/core/input-control.directive.ts @@ -0,0 +1,17 @@ +import { Directive, inject, input, OnInit } from '@angular/core'; +import { NgControl } from '@angular/forms'; + +@Directive({ selector: 'input[uiInputControl]', host: { class: 'ui-input-control' } }) +export class InputControlDirective { + readonly control = inject(NgControl, { optional: true, self: true }); + + readonly value = input(); + + getValue(): T | undefined { + if (this.control) { + return this.control.value as T; + } + + return this.value(); + } +} diff --git a/libs/ui/input-controls/src/lib/text-field/text-field.component.scss b/libs/ui/input-controls/src/lib/text-field/_text-field.scss similarity index 58% rename from libs/ui/input-controls/src/lib/text-field/text-field.component.scss rename to libs/ui/input-controls/src/lib/text-field/_text-field.scss index 3f224419b..fc79b6253 100644 --- a/libs/ui/input-controls/src/lib/text-field/text-field.component.scss +++ b/libs/ui/input-controls/src/lib/text-field/_text-field.scss @@ -6,17 +6,14 @@ @apply border border-solid border-isa-neutral-600 rounded-full; @apply bg-white text-isa-neutral-900; - - input { - flex: 1; - appearance: none; - - @apply isa-text-body-2-regular focus:outline-none; - @apply placeholder:text-neutral-500 hover:placeholder:text-neutral-900 focus:placeholder:text-neutral-900; - } } -.ui-text-field__small { +.ui-input-control { + flex: 1; + appearance: none; + + @apply isa-text-body-2-regular focus:outline-none; + @apply placeholder:text-neutral-500 hover:placeholder:text-neutral-900 focus:placeholder:text-neutral-900; } .ui-text-field__medium { diff --git a/libs/ui/input-controls/src/lib/text-field/text-field.component.ts b/libs/ui/input-controls/src/lib/text-field/text-field.component.ts index 371ceaa37..2e299d1a6 100644 --- a/libs/ui/input-controls/src/lib/text-field/text-field.component.ts +++ b/libs/ui/input-controls/src/lib/text-field/text-field.component.ts @@ -1,10 +1,5 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - input, - ViewEncapsulation, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, contentChild, input } from '@angular/core'; +import { InputControlDirective } from '../core/input-control.directive'; export const TextFieldSize = { Small: 'small', @@ -17,8 +12,6 @@ export type TextFieldSize = (typeof TextFieldSize)[keyof typeof TextFieldSize]; @Component({ selector: 'ui-text-field', templateUrl: './text-field.component.html', - styleUrls: ['./text-field.component.scss'], - encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [], @@ -27,6 +20,8 @@ export type TextFieldSize = (typeof TextFieldSize)[keyof typeof TextFieldSize]; }, }) export class TextFieldComponent { + inputControl = contentChild.required(InputControlDirective); + size = input('medium'); sizeClass = computed(() => { diff --git a/package.json b/package.json index 13376730e..a5f1b5bc1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "nx serve isa-app --ssl", - "test": "nx test isa-app", + "test": "npx nx run-many -t test --exclude isa-app", "ci": "npx nx run-many -t test --exclude isa-app -c ci", "build": "nx build isa-app --configuration=development", "build-prod": "nx build isa-app --configuration=production",