mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Enhance UI components with new input control directive and styling.
- ✨ **Feature**: Added InputControlDirective for better input handling - 🎨 **Style**: Updated button and text-field styles for loading states - 🛠️ **Refactor**: Improved button component structure and disabled state handling - 📚 **Docs**: Updated code style guidelines with new control flow syntax
This commit is contained in:
@@ -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<UiButtonComponentInputs> = {
|
||||
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: `<ui-button ${argsToTemplate(args, { exclude: ['disabled'] })} ${args.disabled ? 'disabled' : ''}>Button</ui-button>`,
|
||||
template: `<button uiButton ${argsToTemplate(args)}>Button</button>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
@@ -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<UiIconButtonComponentInputs> = {
|
||||
],
|
||||
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: `<ui-icon-button ${argsToTemplate(args, { exclude: ['disabled', 'icon'] })} ${args.disabled ? 'disabled' : ''}>
|
||||
template: `<button uiIconButton ${argsToTemplate(args, { exclude: ['icon'] })} >
|
||||
<ng-icon name="${args.icon}"></ng-icon>
|
||||
</ui-icon-button>`,
|
||||
</button>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
@@ -19,21 +19,37 @@ const meta: Meta<InfoButtonComponentInputs> = {
|
||||
}),
|
||||
],
|
||||
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: `<ui-info-button ${argsToTemplate(args, { exclude: ['disabled', 'icon'] })} ${args.disabled ? 'disabled' : ''}>
|
||||
template: `<button uiInfoButton ${argsToTemplate(args, { exclude: ['icon'] })} >
|
||||
<span uiInfoButtonLabel>${args.label}</span>
|
||||
<ng-icon name="${args.icon}" uiInfoButtonIcon></ng-icon>
|
||||
</ui-info-button>`,
|
||||
</button>`,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -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<UiTextButtonComponentInputs> = {
|
||||
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: `<ui-text-button ${argsToTemplate(args, { exclude: ['disabled'] })} ${args.disabled ? 'disabled' : ''}>Button</ui-text-button>`,
|
||||
template: `<button uiTextButton ${argsToTemplate(args)}>Button</button>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<TextButtonComponent>;
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -6,12 +6,20 @@
|
||||
<div class="text-right">
|
||||
<ui-text-field size="small" class="min-w-[22.875rem]">
|
||||
<input
|
||||
class="isa-text-body-2-bold"
|
||||
uiInputControl
|
||||
class="isa-text-body-2-bold placeholder:isa-text-body-2-bold"
|
||||
placeholder="EAN eingeben / scannen"
|
||||
type="text"
|
||||
[formControl]="control"
|
||||
/>
|
||||
<button class="px-0" uiTextButton size="small" color="strong" (click)="check()">
|
||||
<button
|
||||
class="px-0"
|
||||
uiTextButton
|
||||
size="small"
|
||||
color="strong"
|
||||
(click)="check()"
|
||||
[pending]="checking()"
|
||||
>
|
||||
Prüfen
|
||||
</button>
|
||||
</ui-text-field>
|
||||
@@ -25,8 +33,8 @@
|
||||
<div class="flex flex-row gap-4">
|
||||
<img class="w-[3.375rem]" sharedProductImage [ean]="p.ean" [alt]="p.name" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="isa-text-body-2-bold">{{ p.contributors }}</div>
|
||||
<div class="text-isa-secondary-900 isa-text-body-2-regular">{{ p.name }}</div>
|
||||
<div class="isa-text-body-2-bold text-isa-secondary-900">{{ p.contributors }}</div>
|
||||
<div class="text-isa-neutral-900 isa-text-body-2-regular">{{ p.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@if (!pending()) {
|
||||
<ng-content></ng-content>
|
||||
} @else {
|
||||
<ng-icon class="ui-button__spinner" size="1.5rem" name="isaLoading"></ng-icon>
|
||||
<ng-content></ng-content>
|
||||
|
||||
@if (pending()) {
|
||||
<div class="ui-button__spinner-container">
|
||||
<ng-icon class="ui-button__spinner" size="1.5rem" name="isaLoading"></ng-icon>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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<ButtonComponent>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<ButtonSize>('medium');
|
||||
|
||||
/** A computed CSS class based on the current size. */
|
||||
sizeClass = computed(() => `ui-button__${this.size()}`);
|
||||
|
||||
/** The color theme of the button. */
|
||||
color = input<ButtonColor>('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<boolean>(false);
|
||||
|
||||
/** A computed CSS class for the pending state. */
|
||||
pendingClass = computed(() => (this.pending() ? 'ui-button__pending' : ''));
|
||||
|
||||
/** The tab index for the button. */
|
||||
tabIndex = input<number>(0);
|
||||
|
||||
/** Indicates whether the button is disabled. */
|
||||
disabled = input<boolean>(false);
|
||||
|
||||
/** A computed CSS class for the disabled state. */
|
||||
disabledClass = computed(() => (this.disabled() ? 'disabled' : ''));
|
||||
}
|
||||
|
||||
@@ -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<IconButtonComponent>;
|
||||
@@ -7,7 +9,7 @@ describe('IconButtonComponent', () => {
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: IconButtonComponent,
|
||||
imports: [], // additional imports if needed
|
||||
declarations: [MockComponent(NgIconComponent)],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -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<boolean>(false);
|
||||
|
||||
tabIndex = input<number>(0);
|
||||
|
||||
/** Indicates whether the button is disabled. */
|
||||
disabled = input<boolean>(false);
|
||||
|
||||
/** A computed CSS class for the disabled state. */
|
||||
disabledClass = computed(() => (this.disabled() ? 'disabled' : ''));
|
||||
}
|
||||
|
||||
@@ -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<InfoButtonComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: InfoButtonComponent,
|
||||
shallow: true,
|
||||
declarations: [MockComponent(NgIconComponent)],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -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<boolean>(false);
|
||||
|
||||
pendingClass = computed(() => (this.pending() ? 'ui-info-button--pending' : ''));
|
||||
|
||||
tabIndex = input<number>(0);
|
||||
|
||||
/** Indicates whether the button is disabled. */
|
||||
disabled = input<boolean>(false);
|
||||
|
||||
/** A computed CSS class for the disabled state. */
|
||||
disabledClass = computed(() => (this.disabled() ? 'disabled' : ''));
|
||||
}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
<ng-content></ng-content>
|
||||
|
||||
@if (pending()) {
|
||||
<div class="ui-text-button__spinner-container">
|
||||
<ng-icon class="ui-text-button__spinner" size="1.5rem" name="isaLoading"></ng-icon>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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<TextButtonComponent>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<TextButtonSize>('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<TextButtonColor>('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<boolean>(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<number>(0);
|
||||
|
||||
/** Indicates whether the button is disabled. */
|
||||
disabled = input<boolean>(false);
|
||||
|
||||
/** A computed CSS class for the disabled state. */
|
||||
disabledClass = computed(() => (this.disabled() ? 'disabled' : ''));
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@use "./lib/checkbox/checkbox";
|
||||
@use "./lib/chips/chips";
|
||||
@use "./lib/dropdown/dropdown";
|
||||
@use "./lib/text-field/text-field.scss";
|
||||
|
||||
@@ -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<T> {
|
||||
readonly control = inject(NgControl, { optional: true, self: true });
|
||||
|
||||
readonly value = input<T>();
|
||||
|
||||
getValue(): T | undefined {
|
||||
if (this.control) {
|
||||
return this.control.value as T;
|
||||
}
|
||||
|
||||
return this.value();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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<TextFieldSize>('medium');
|
||||
|
||||
sizeClass = computed(() => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user