Files
Lorenz Hilpert 2b5da00249 feat(checkout): add reward order confirmation feature with schema migrations
- 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
2025-10-21 14:28:52 +02:00
..

@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

  • 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>
<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
  • 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 name
  • iconSize: 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 tabIndex input

Screen Reader Support

  • Icon buttons include aria-label with 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:

  1. 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);
  }
});
  1. 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();
}
  1. Lifecycle Management:
ngOnDestroy(): void {
  this.clearDismissTimer(); // Prevent memory leaks
}

Performance Considerations

  1. OnPush Change Detection: All components use ChangeDetectionStrategy.OnPush for optimal performance
  2. Signal-Based Updates: Computed properties only recalculate when dependencies change
  3. Minimal Re-renders: Host bindings update only affected attributes
  4. 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.