- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
25 KiB
@isa/ui/buttons
A comprehensive button component library for Angular applications providing five specialized button components with consistent styling, loading states, and accessibility features.
Overview
The UI Buttons library provides a complete suite of button components for the ISA application, each optimized for specific use cases. All components leverage Angular signals for reactive state management, support customizable sizes and colors, and follow the ISA design system. The library includes standard buttons, text/link-style buttons, icon-only buttons, info/help buttons, and stateful buttons with success/error states and auto-dismiss functionality.
Table of Contents
- Features
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Styling and Customization
- Accessibility
- Testing
- Architecture Notes
Features
- Five specialized button components - Standard, text, icon, info, and stateful buttons
- Reactive signal-based state - All inputs and computed properties use Angular signals
- Loading states - Built-in pending/loading indicators with spinners
- Customizable sizing - Small, medium, and large sizes for all button types
- Theme support - Multiple color schemes (primary, secondary, brand, tertiary, neutral)
- Stateful feedback - Success/error states with configurable auto-dismiss
- Disabled state handling - Proper disabled styling and ARIA attributes
- Keyboard navigation - Full keyboard accessibility with tab index support
- OnPush change detection - Optimized performance with minimal re-renders
- No encapsulation - View encapsulation disabled for flexible styling
- Animation support - Smooth transitions for stateful button state changes
Quick Start
1. Import Components
import { Component } from '@angular/core';
import {
ButtonComponent,
TextButtonComponent,
IconButtonComponent,
InfoButtonComponent,
StatefulButtonComponent
} from '@isa/ui/buttons';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [
ButtonComponent,
TextButtonComponent,
IconButtonComponent,
InfoButtonComponent,
StatefulButtonComponent
],
template: '...'
})
export class MyComponent {}
2. Use Standard Button
<button uiButton color="primary" size="large" [pending]="isLoading">
Submit Form
</button>
3. Use Text Button (Link Style)
<button uiTextButton color="strong" (click)="handleClick()">
Learn More
</button>
4. Use Icon Button
<button uiIconButton
name="isaActionEdit"
color="primary"
size="large"
[pending]="isSaving">
</button>
5. Use Info Button
<button uiInfoButton [disabled]="false">
<ng-icon name="isaActionInfo"></ng-icon>
</button>
6. Use Stateful Button
<ui-stateful-button
[(state)]="buttonState"
defaultContent="Save Changes"
successContent="Changes Saved!"
errorContent="Save Failed"
errorAction="Retry"
color="primary"
[dismiss]="3000"
(clicked)="handleSave()"
(action)="handleRetry()">
</ui-stateful-button>
Core Concepts
Button Component Types
The library provides five specialized button components, each optimized for specific use cases:
1. ButtonComponent (Standard Button)
- Purpose: Primary action buttons for forms, dialogs, and main interactions
- Selector:
ui-button, [uiButton] - Key Features:
- Customizable size (small, medium, large)
- Four color themes (primary, secondary, brand, tertiary)
- Loading spinner overlay in pending state
- Preserves button dimensions during loading
- Disabled state support
2. TextButtonComponent (Link-Style Button)
- Purpose: Secondary actions, navigation links, and lightweight interactions
- Selector:
ui-text-button, [uiTextButton] - Key Features:
- Text-based styling (no background)
- Three color themes (normal, strong, subtle)
- Pending state with inline spinner
- Smaller footprint than standard buttons
3. IconButtonComponent (Icon-Only Button)
- Purpose: Compact actions, toolbar buttons, and icon-based interactions
- Selector:
ui-icon-button, [uiIconButton] - Key Features:
- Icon-only display with configurable icon name
- Five color themes (primary, secondary, brand, tertiary, neutral)
- Three sizes with responsive icon sizing
- Automatic loading icon replacement
- Enhanced ARIA attributes for accessibility
4. InfoButtonComponent (Help/Info Button)
- Purpose: Help buttons, tooltips triggers, and informational actions
- Selector:
ui-info-button, [uiInfoButton] - Key Features:
- Fixed styling optimized for info icons
- Pending and disabled states
- Minimal visual footprint
- Designed for use with tooltip directives
5. StatefulButtonComponent (Success/Error States)
- Purpose: Form submissions, save actions, and operations requiring feedback
- Selector:
ui-stateful-button - Key Features:
- Three states: default, success, error
- Configurable content for each state
- Auto-dismiss with configurable timeout
- Width animations during state transitions
- Optional error action button
- Two-way binding for state management
Signal-Based Reactivity
All button components use Angular signals for reactive state management:
// Input signals
size = input<ButtonSize>('medium');
color = input<ButtonColor>('primary');
pending = input<boolean>(false);
disabled = input<boolean>(false);
// Computed signals
sizeClass = computed(() => `ui-button__${this.size()}`);
colorClass = computed(() => `ui-button__${this.color()}`);
pendingClass = computed(() => this.pending() ? 'ui-button__pending' : '');
disabledClass = computed(() => this.disabled() ? 'disabled' : '');
Host Binding Pattern
Components use Angular host binding for dynamic CSS classes:
host: {
'[class]': '["ui-button", sizeClass(), colorClass(), pendingClass(), disabledClass()]',
'[tabindex]': 'tabIndex()',
'[disabled]': 'disabled()',
}
Loading State Handling
All button components support loading states with different rendering strategies:
- Standard Button: Overlay spinner preserving button dimensions
- Text Button: Inline spinner replacing content
- Icon Button: Replaces icon with loading icon
- Info Button: Overlay spinner
- Stateful Button: Inherits from standard button behavior
API Reference
ButtonComponent
Standard button component with customizable appearance and loading state.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
size |
ButtonSize |
'medium' |
Button size: 'small', 'medium', or 'large' |
color |
ButtonColor |
'primary' |
Color theme: 'primary', 'secondary', 'brand', or 'tertiary' |
pending |
boolean |
false |
Shows loading spinner when true |
disabled |
boolean |
false |
Disables button when true |
tabIndex |
number |
0 |
Tab index for keyboard navigation |
Example
<button uiButton
color="brand"
size="large"
[pending]="isLoading"
[disabled]="!isValid"
(click)="handleSubmit()">
Submit Form
</button>
TextButtonComponent
Link-style button for secondary actions and navigation.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
size |
TextButtonSize |
'medium' |
Button size: 'small', 'medium', or 'large' |
color |
TextButtonColor |
'normal' |
Color theme: 'normal', 'strong', or 'subtle' |
pending |
boolean |
false |
Shows loading spinner when true |
disabled |
boolean |
false |
Disables button when true |
tabIndex |
number |
0 |
Tab index for keyboard navigation |
Example
<button uiTextButton
color="strong"
size="medium"
(click)="handleCancel()">
Cancel
</button>
IconButtonComponent
Icon-only button for compact actions and toolbars.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
name |
string |
undefined |
Icon name from @isa/icons |
size |
IconButtonSize |
'large' |
Button size: 'small', 'medium', or 'large' |
color |
IconButtonColor |
'primary' |
Color theme: 'primary', 'secondary', 'brand', 'tertiary', or 'neutral' |
pending |
boolean |
false |
Shows loading icon when true |
disabled |
boolean |
false |
Disables button when true |
tabIndex |
number |
0 |
Tab index for keyboard navigation |
Computed Properties
iconName: Returns 'isaLoading' when pending, otherwise returns the input nameiconSize: Returns computed icon size ('1rem', '1.25rem', or '1.5rem') based on button size
Example
<button uiIconButton
name="isaActionEdit"
color="primary"
size="large"
[pending]="isSaving"
[disabled]="!canEdit"
(click)="handleEdit()">
</button>
InfoButtonComponent
Help/info button optimized for tooltips and informational content.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
pending |
boolean |
false |
Shows loading spinner when true |
disabled |
boolean |
false |
Disables button when true |
tabIndex |
number |
0 |
Tab index for keyboard navigation |
Example
<button uiInfoButton
[disabled]="false"
(click)="showHelp()">
<ng-icon name="isaActionInfo"></ng-icon>
</button>
StatefulButtonComponent
Button with success/error states and auto-dismiss functionality.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
defaultContent |
string |
required | Content displayed in default state |
successContent |
string |
required | Content displayed in success state |
errorContent |
string |
required | Content displayed in error state |
errorAction |
string |
undefined |
Optional action button text in error state |
defaultWidth |
string |
'100%' |
Button width in default state |
successWidth |
string |
'100%' |
Button width in success state |
errorWidth |
string |
'100%' |
Button width in error state |
dismiss |
number |
5000 |
Auto-dismiss timeout in milliseconds (0 to disable) |
color |
ButtonColor |
'primary' |
Color theme |
size |
ButtonSize |
'medium' |
Button size |
pending |
boolean |
false |
Shows loading spinner when true |
disabled |
boolean |
false |
Disables button when true |
Outputs
| Output | Type | Description |
|---|---|---|
clicked |
void |
Emitted when button is clicked in default state |
action |
void |
Emitted when error action button is clicked |
Two-Way Binding
| Property | Type | Description |
|---|---|---|
state |
StatefulButtonState |
Current state: 'default', 'success', or 'error' |
Example
import { Component, signal } from '@angular/core';
import { StatefulButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-save-form',
standalone: true,
imports: [StatefulButtonComponent],
template: `
<ui-stateful-button
[(state)]="saveState"
defaultContent="Save Changes"
successContent="Changes Saved Successfully!"
errorContent="Failed to Save Changes"
errorAction="Retry"
color="primary"
size="large"
[dismiss]="3000"
[pending]="isSaving()"
(clicked)="handleSave()"
(action)="handleRetry()">
</ui-stateful-button>
`
})
export class SaveFormComponent {
saveState = signal<'default' | 'success' | 'error'>('default');
isSaving = signal(false);
async handleSave(): Promise<void> {
this.isSaving.set(true);
try {
await this.saveFormData();
this.saveState.set('success');
} catch (error) {
this.saveState.set('error');
} finally {
this.isSaving.set(false);
}
}
handleRetry(): void {
this.saveState.set('default');
this.handleSave();
}
}
Usage Examples
Standard Button with Loading State
import { Component, signal } from '@angular/core';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-submit-form',
standalone: true,
imports: [ButtonComponent],
template: `
<button uiButton
color="primary"
size="large"
[pending]="isSubmitting()"
[disabled]="!isFormValid()"
(click)="handleSubmit()">
Submit Application
</button>
`
})
export class SubmitFormComponent {
isSubmitting = signal(false);
isFormValid = signal(true);
async handleSubmit(): Promise<void> {
this.isSubmitting.set(true);
try {
await this.submitForm();
} finally {
this.isSubmitting.set(false);
}
}
}
Text Button for Navigation
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { TextButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-navigation-links',
standalone: true,
imports: [TextButtonComponent],
template: `
<nav>
<button uiTextButton color="strong" (click)="navigateHome()">
Home
</button>
<button uiTextButton color="normal" (click)="navigateAbout()">
About
</button>
<button uiTextButton color="subtle" (click)="navigateHelp()">
Help
</button>
</nav>
`
})
export class NavigationLinksComponent {
constructor(private router: Router) {}
navigateHome(): void {
this.router.navigate(['/']);
}
navigateAbout(): void {
this.router.navigate(['/about']);
}
navigateHelp(): void {
this.router.navigate(['/help']);
}
}
Icon Button Toolbar
import { Component, signal } from '@angular/core';
import { IconButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-editor-toolbar',
standalone: true,
imports: [IconButtonComponent],
template: `
<div class="toolbar">
<button uiIconButton
name="isaActionEdit"
color="primary"
size="medium"
[disabled]="!canEdit()"
(click)="handleEdit()">
</button>
<button uiIconButton
name="isaActionDelete"
color="tertiary"
size="medium"
[disabled]="!canDelete()"
(click)="handleDelete()">
</button>
<button uiIconButton
name="isaActionSave"
color="brand"
size="medium"
[pending]="isSaving()"
(click)="handleSave()">
</button>
</div>
`
})
export class EditorToolbarComponent {
canEdit = signal(true);
canDelete = signal(true);
isSaving = signal(false);
handleEdit(): void {
console.log('Edit clicked');
}
handleDelete(): void {
console.log('Delete clicked');
}
async handleSave(): Promise<void> {
this.isSaving.set(true);
try {
await this.saveDocument();
} finally {
this.isSaving.set(false);
}
}
}
Mixed Button Types in Dialog
import { Component, signal } from '@angular/core';
import {
ButtonComponent,
TextButtonComponent,
IconButtonComponent
} from '@isa/ui/buttons';
@Component({
selector: 'app-confirmation-dialog',
standalone: true,
imports: [ButtonComponent, TextButtonComponent, IconButtonComponent],
template: `
<div class="dialog">
<div class="dialog-header">
<h2>Confirm Deletion</h2>
<button uiIconButton
name="isaActionClose"
color="neutral"
size="small"
(click)="handleClose()">
</button>
</div>
<div class="dialog-content">
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
</div>
<div class="dialog-actions">
<button uiTextButton
color="subtle"
(click)="handleCancel()">
Cancel
</button>
<button uiButton
color="tertiary"
[pending]="isDeleting()"
(click)="handleDelete()">
Delete Item
</button>
</div>
</div>
`
})
export class ConfirmationDialogComponent {
isDeleting = signal(false);
async handleDelete(): Promise<void> {
this.isDeleting.set(true);
try {
await this.deleteItem();
this.handleClose();
} finally {
this.isDeleting.set(false);
}
}
handleCancel(): void {
this.handleClose();
}
handleClose(): void {
// Close dialog logic
}
private async deleteItem(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
Styling and Customization
Size Classes
All button components (except InfoButton) support three sizes:
export const ButtonSize = {
Small: 'small',
Medium: 'medium',
Large: 'large',
} as const;
CSS classes applied: ui-button__small, ui-button__medium, ui-button__large
Color Themes
Standard Button Colors
export const ButtonColor = {
Primary: 'primary', // Default blue theme
Secondary: 'secondary', // Gray theme
Brand: 'brand', // ISA brand color
Tertiary: 'tertiary', // Red/warning theme
} as const;
CSS classes applied: ui-button__primary, ui-button__secondary, etc.
Text Button Colors
export const TextButtonColor = {
Normal: 'normal', // Standard text color
Strong: 'strong', // Bold/emphasized text
Subtle: 'subtle', // Muted text
} as const;
Icon Button Colors
export const IconButtonColor = {
Primary: 'primary',
Secondary: 'secondary',
Brand: 'brand',
Tertiary: 'tertiary',
Neutral: 'neutral', // Additional neutral theme
} as const;
State Classes
All buttons apply these state-based classes:
- Pending:
ui-button__pending,ui-text-button__pending,ui-info-button--pending - Disabled:
disabled(applied to all button types)
Custom Styling
Override styles using specific class selectors:
// Custom primary button styling
.ui-button.ui-button__primary {
background-color: custom-color;
border-radius: 8px;
}
// Custom text button hover
.ui-text-button.ui-text-button__strong:hover {
text-decoration: underline;
}
// Custom icon button sizing
.ui-icon-button.ui-icon-button__large {
width: 48px;
height: 48px;
}
Accessibility
All button components implement comprehensive accessibility features:
ARIA Attributes
Standard, Text, and Info Buttons
[disabled]: Applied via host binding- Proper button semantics via
<button>element - Tab index support for keyboard navigation
Icon Button (Enhanced ARIA)
host: {
'[attr.aria-disabled]': 'disabled()',
'[attr.aria-label]': 'name()', // Icon name as label
'[attr.aria-busy]': 'pending()', // Loading state
'[attr.role]': 'pending() ? "progressbar" : "button"'
}
Keyboard Navigation
All buttons support:
- Tab: Focus navigation
- Enter/Space: Trigger click event
- Custom tab index: Configurable via
tabIndexinput
Screen Reader Support
- Icon buttons include
aria-labelwith icon name - Loading states announce via
aria-busy - Disabled states properly conveyed via
aria-disabled - Stateful buttons maintain role semantics during state changes
Testing
The library uses Jest for testing.
Running Tests
# Run tests for this library
npx nx test ui-buttons --skip-nx-cache
# Run tests with coverage
npx nx test ui-buttons --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-buttons --watch
Test Coverage
All components include comprehensive unit tests covering:
- Input signals - Size, color, pending, disabled states
- Computed properties - CSS class generation
- Host bindings - Dynamic class and attribute application
- Event handling - Click events, state changes (stateful button)
- Loading states - Spinner rendering and icon replacement
- Accessibility - ARIA attributes and keyboard navigation
- State management - Stateful button state transitions and auto-dismiss
Architecture Notes
Component Architecture
The library follows a consistent architecture pattern across all button types:
ButtonComponent (Standalone)
├─→ Signal-based inputs (size, color, pending, disabled, tabIndex)
├─→ Computed properties (CSS classes)
├─→ Host bindings (dynamic attributes and classes)
├─→ Template (content projection + conditional spinner)
└─→ ViewEncapsulation.None (flexible styling)
Design Patterns
1. Host Binding for Dynamic Styling
All components use host binding to apply CSS classes:
host: {
'[class]': '["ui-button", sizeClass(), colorClass(), pendingClass(), disabledClass()]',
'[tabindex]': 'tabIndex()',
'[disabled]': 'disabled()',
}
Benefits:
- No manual DOM manipulation required
- Reactive updates via signals
- Type-safe class application
- Optimized change detection
2. Signal-Based Reactivity
Components leverage Angular signals for all reactive state:
// Input signals
size = input<ButtonSize>('medium');
color = input<ButtonColor>('primary');
pending = input<boolean>(false);
// Computed signals
sizeClass = computed(() => `ui-button__${this.size()}`);
pendingClass = computed(() => this.pending() ? 'ui-button__pending' : '');
Benefits:
- Fine-grained reactivity
- Automatic dependency tracking
- Minimal re-renders with OnPush
- Clear data flow
3. ViewEncapsulation.None
All components disable view encapsulation:
encapsulation: ViewEncapsulation.None
Rationale:
- Allows global ISA design system styles to apply
- Enables custom theming and overrides
- Simplifies CSS class naming (no shadow DOM)
- Consistent with ISA application styling strategy
4. Content Projection
Standard, text, and info buttons use content projection:
<ng-content></ng-content>
@if (pending()) {
<div class="ui-button__spinner-container">
<ng-icon name="isaLoading"></ng-icon>
</div>
}
Benefits:
- Flexible button content
- Supports text, icons, and mixed content
- Preserves button dimensions during loading
Stateful Button Architecture
The StatefulButtonComponent uses a more complex architecture:
StatefulButtonComponent
├─→ Extends ButtonComponent functionality
├─→ State management (default, success, error)
├─→ Auto-dismiss timer with cleanup
├─→ Width animations via AnimationBuilder
├─→ Effect-based state watching
├─→ Two-way state binding with model()
└─→ Conditional error action button
Key Implementation Details:
- State Effect with Auto-Dismiss:
effect(() => {
const currentState = this.state();
const dismissTimeout = this.dismiss();
this.clearDismissTimer();
if (dismissTimeout && (currentState === 'success' || currentState === 'error')) {
this.dismissTimer = setTimeout(() => {
this.changeState('default');
}, dismissTimeout);
}
});
- Width Animations:
#makeWidthAnimation(width: string): void {
const animation = this.#animationBuilder.build([
style({ width: '*' }),
animate('250ms', style({ width }))
]);
const player = animation.create(this.buttonElement().nativeElement);
player.play();
}
- Lifecycle Management:
ngOnDestroy(): void {
this.clearDismissTimer(); // Prevent memory leaks
}
Performance Considerations
- OnPush Change Detection: All components use
ChangeDetectionStrategy.OnPushfor optimal performance - Signal-Based Updates: Computed properties only recalculate when dependencies change
- Minimal Re-renders: Host bindings update only affected attributes
- Animation Performance: StatefulButton uses AnimationBuilder for performant width transitions
Known Architectural Considerations
1. ViewEncapsulation Strategy (Low Priority)
Current State:
- All components use
ViewEncapsulation.None - Relies on BEM-like class naming for style isolation
Consideration:
- Could enable encapsulation with CSS custom properties
- Would require design system refactoring
Impact: Low - current approach works well with ISA design system
2. Stateful Button Complexity (Medium Priority)
Current State:
- Combines multiple concerns (state, animations, timers)
- Complex lifecycle management
Consideration:
- Could extract animation logic to separate service
- Could use directive composition for state management
- Timer management could be centralized
Impact: Medium - component works well but is less maintainable than simpler buttons
Dependencies
Required Libraries
@angular/core- Angular framework@angular/animations- Animations (StatefulButton only)@ng-icons/core- Icon rendering@isa/icons- ISA icon library (loading icon)
Optional Dependencies
@angular/forms- For use with reactive forms (stateful button state binding)
Path Alias
Import from: @isa/ui/buttons
Project Configuration
- Project Name:
ui-buttons - Prefix:
ui - Testing: Jest
- Source Root:
libs/ui/buttons/src
License
Internal ISA Frontend library - not for external distribution.