Files
ISA-Frontend/libs/ui/input-controls
Nino Righi aee63711e4 Merged PR 2066: fix(ui-layout, input-controls-dropdown, oms-return-details): prevent stale sc...
fix(ui-layout, input-controls-dropdown, oms-return-details): prevent stale scroll events from closing dropdown on open

Delay scroll listener registration using requestAnimationFrame when
activating CloseOnScrollDirective. This prevents stale scroll events
still in the event queue from immediately triggering closeOnScroll
when opening the dropdown after scrolling.

Also adds conditional rendering for product format and publication date
in return-details-order-group-item component.

Refs: #5513
2025-12-02 14:02:32 +00:00
..

@isa/ui/input-controls

A comprehensive collection of form input components and directives for Angular applications supporting reactive forms, template-driven forms, and accessibility features.

Overview

The Input Controls library provides a complete suite of reusable form components built on Angular's reactive forms API and Angular CDK. It includes text fields, checkboxes, dropdowns, chips, checklists, listboxes, and inline inputs - all designed to work seamlessly with Angular Forms while maintaining consistent styling and behavior across the ISA application.

Table of Contents

Features

  • Complete form control suite - 8 component types covering common input scenarios
  • Reactive Forms integration - ControlValueAccessor implementation for all form controls
  • Template-driven forms - Full ngModel support for two-way binding
  • Keyboard navigation - Arrow keys, Enter, Escape, and Tab support
  • Accessibility (a11y) - ARIA attributes, screen reader support, keyboard navigation
  • Customizable appearance - Multiple size and appearance variants for each control
  • Validation support - Integration with Angular's built-in validators
  • CDK integration - Built on Angular CDK primitives (listbox, a11y, overlay)
  • Signal-based inputs - Modern Angular signals for reactive property management
  • Type-safe - Full TypeScript support with generic type parameters

Quick Start

1. Import Components

import { Component } from '@angular/core';
import {
  CheckboxComponent,
  DropdownButtonComponent,
  DropdownOptionComponent,
  TextFieldComponent,
  InputControlDirective,
} from '@isa/ui/input-controls';
import { ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-my-form',
  imports: [
    ReactiveFormsModule,
    CheckboxComponent,
    DropdownButtonComponent,
    DropdownOptionComponent,
    TextFieldComponent,
    InputControlDirective,
  ],
  template: '...'
})
export class MyFormComponent {}

2. Basic Text Field

<ui-text-field>
  <input
    type="text"
    uiInputControl
    formControlName="username"
    placeholder="Enter username"
  />
</ui-text-field>

3. Checkbox

<ui-checkbox>
  <input type="checkbox" formControlName="acceptTerms" />
</ui-checkbox>

4. Dropdown

<ui-dropdown [(ngModel)]="selectedOption" label="Select option">
  <ui-dropdown-option [value]="1">Option 1</ui-dropdown-option>
  <ui-dropdown-option [value]="2">Option 2</ui-dropdown-option>
  <ui-dropdown-option [value]="3">Option 3</ui-dropdown-option>
</ui-dropdown>

Core Concepts

Component Categories

The library provides three main categories of form controls:

1. Text Input Controls

  • TextFieldComponent - Standard text input with container, labels, and errors
  • TextareaComponent - Multi-line text input
  • InlineInputComponent - Compact inline text input for space-constrained UIs

2. Selection Controls

  • CheckboxComponent - Single checkbox with bullet or checkbox appearance
  • ChecklistComponent - Multiple checkboxes working as a group
  • DropdownButtonComponent - Select dropdown with keyboard navigation
  • ChipsComponent - Single-select chip group
  • ListboxDirective - CDK listbox wrapper for custom list selections

3. Supporting Components

  • InputControlDirective - Core directive for input field integration
  • TextFieldContainerComponent - Layout container for text fields
  • TextFieldErrorsComponent - Validation error display
  • TextFieldClearComponent - Clear button for text inputs
  • CheckboxLabelDirective - Label styling for checkboxes
  • ChecklistValueDirective - Value binding for checklist items
  • ChipOptionComponent - Individual chip option
  • DropdownOptionComponent - Individual dropdown option
  • ListboxItemDirective - Individual listbox item

Control Value Accessor Pattern

All form controls implement Angular's ControlValueAccessor interface, enabling seamless integration with both reactive and template-driven forms:

export class DropdownButtonComponent<T> implements ControlValueAccessor {
  value = model<T>();
  disabled = model<boolean>(false);

  writeValue(obj: unknown): void { ... }
  registerOnChange(fn: (value: T) => void): void { ... }
  registerOnTouched(fn: () => void): void { ... }
  setDisabledState(isDisabled: boolean): void { ... }
}

Appearance Variants

Checkbox Appearances

const CheckboxAppearance = {
  Bullet: 'bullet',      // Round bullet-style selector
  Checkbox: 'checkbox',  // Traditional square checkbox (default)
} as const;

Text Field Sizes

const TextFieldSize = {
  Small: 'small',
  Medium: 'medium',      // Default
  Large: 'large',
} as const;

API Reference

CheckboxComponent

A customizable checkbox component supporting different visual appearances.

Selector: ui-checkbox

Inputs:

  • appearance: CheckboxAppearance - Visual style ('checkbox' | 'bullet'). Default: 'checkbox'

Usage:

<!-- Default checkbox appearance -->
<ui-checkbox>
  <input type="checkbox" />
</ui-checkbox>

<!-- Bullet appearance -->
<ui-checkbox [appearance]="CheckboxAppearance.Bullet">
  <input type="checkbox" />
</ui-checkbox>

DropdownButtonComponent

A dropdown select component with keyboard navigation and CDK overlay integration.

Selector: ui-dropdown

Inputs:

  • appearance: DropdownAppearance - Visual style. Default: 'accent-outline'
  • label: string - Dropdown label text
  • showSelectedValue: boolean - Show selected option text. Default: true
  • tabIndex: number - Tab index for keyboard navigation. Default: 0
  • id: string - Optional element ID

Outputs:

  • value: ModelSignal<T> - Two-way bindable selected value
  • disabled: ModelSignal<boolean> - Disabled state

Methods:

  • open(): void - Opens the dropdown overlay
  • close(): void - Closes the dropdown overlay
  • select(option, options?): void - Selects an option programmatically

Keyboard Navigation:

  • Arrow Down/Up - Navigate through options
  • Enter - Select highlighted option and close
  • Escape - Close dropdown

Usage:

interface Product {
  id: number;
  name: string;
}

products: Product[] = [
  { id: 1, name: 'Product A' },
  { id: 2, name: 'Product B' }
];
selectedProduct: Product;
<ui-dropdown
  [(value)]="selectedProduct"
  label="Select Product"
  appearance="accent-outline">
  @for (product of products; track product.id) {
    <ui-dropdown-option [value]="product">
      {{ product.name }}
    </ui-dropdown-option>
  }
</ui-dropdown>

ChipsComponent

Single-selection chip group with form integration.

Selector: ui-chips

Outputs:

  • value: ModelSignal<T> - Selected chip value
  • disabled: ModelSignal<boolean> - Disabled state

Methods:

  • select(value: T, options?): void - Selects a chip
  • toggle(value: T, options?): void - Toggles chip selection
  • isSelected(value: T): boolean - Checks if value is selected

Usage:

type Size = 'S' | 'M' | 'L' | 'XL';
selectedSize: Size = 'M';
<ui-chips [(ngModel)]="selectedSize">
  <ui-chip [value]="'S'">Small</ui-chip>
  <ui-chip [value]="'M'">Medium</ui-chip>
  <ui-chip [value]="'L'">Large</ui-chip>
  <ui-chip [value]="'XL'">Extra Large</ui-chip>
</ui-chips>

ChecklistComponent

Multi-select checkbox group returning an array of selected values.

Selector: ui-checklist

Outputs:

  • value: ModelSignal<unknown[]> - Array of selected values

Usage:

selectedFruits: string[] = ['apple', 'banana'];
<ui-checklist [(ngModel)]="selectedFruits">
  <label class="ui-checkbox-label">
    <ui-checkbox>
      <input type="checkbox" [uiChecklistValue]="'apple'">
    </ui-checkbox>
    Apple
  </label>
  <label class="ui-checkbox-label">
    <ui-checkbox>
      <input type="checkbox" [uiChecklistValue]="'banana'">
    </ui-checkbox>
    Banana
  </label>
</ui-checklist>

TextFieldComponent

Container component for text input fields with consistent styling.

Selector: ui-text-field

Inputs:

  • size: TextFieldSize - Field size ('small' | 'medium' | 'large'). Default: 'medium'

Content Projection:

  • Requires InputControlDirective on child input element

Usage:

<ui-text-field size="medium">
  <input
    type="text"
    uiInputControl
    formControlName="email"
    placeholder="Enter email"
  />
</ui-text-field>

InlineInputComponent

Compact inline text input for space-constrained interfaces.

Selector: ui-inline-input

Inputs:

  • size: InlineInputSize - Field size ('small' | 'medium'). Default: 'medium'

Usage:

<ui-inline-input size="small">
  <input type="text" uiInputControl [(ngModel)]="quantity" />
</ui-inline-input>

ListboxDirective

Wrapper directive for Angular CDK listbox with ISA styling.

Selector: [uiListbox]

Inputs (via CDK):

  • value: any - Selected value(s)
  • compareWith: (o1: any, o2: any) => boolean - Comparison function
  • disabled: boolean - Disabled state

Outputs (via CDK):

  • valueChange: EventEmitter - Emits when selection changes

Usage:

<div uiListbox [value]="selectedItem" (valueChange)="handleChange($event)">
  <div uiListboxItem [value]="item1">Item 1</div>
  <div uiListboxItem [value]="item2">Item 2</div>
</div>

Usage Examples

Reactive Form with Validation

import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import {
  TextFieldComponent,
  TextFieldContainerComponent,
  TextFieldErrorsComponent,
  InputControlDirective,
  CheckboxComponent,
  DropdownButtonComponent,
  DropdownOptionComponent
} from '@isa/ui/input-controls';

@Component({
  selector: 'app-user-form',
  imports: [
    ReactiveFormsModule,
    TextFieldComponent,
    TextFieldContainerComponent,
    TextFieldErrorsComponent,
    InputControlDirective,
    CheckboxComponent,
    DropdownButtonComponent,
    DropdownOptionComponent
  ],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <ui-text-field-container>
        <label>Email</label>
        <ui-text-field>
          <input
            type="email"
            uiInputControl
            formControlName="email"
            placeholder="user@example.com"
          />
        </ui-text-field>
        <ui-text-field-errors />
      </ui-text-field-container>

      <ui-dropdown formControlName="role" label="Select Role">
        <ui-dropdown-option [value]="'admin'">Administrator</ui-dropdown-option>
        <ui-dropdown-option [value]="'user'">User</ui-dropdown-option>
      </ui-dropdown>

      <label class="ui-checkbox-label">
        <ui-checkbox>
          <input type="checkbox" formControlName="acceptTerms" />
        </ui-checkbox>
        I accept the terms and conditions
      </label>

      <button type="submit" [disabled]="!userForm.valid">Submit</button>
    </form>
  `
})
export class UserFormComponent {
  #fb = inject(FormBuilder);

  userForm = this.#fb.group({
    email: ['', [Validators.required, Validators.email]],
    role: ['user', Validators.required],
    acceptTerms: [false, Validators.requiredTrue]
  });

  onSubmit() {
    if (this.userForm.valid) {
      console.log('Form submitted:', this.userForm.value);
    }
  }
}

Form Integration

Reactive Forms

All components implement ControlValueAccessor and work with FormControl:

import { FormControl, Validators } from '@angular/forms';

emailControl = new FormControl('', [Validators.required, Validators.email]);
roleControl = new FormControl<string>('user');
agreeControl = new FormControl(false, Validators.requiredTrue);
<ui-text-field>
  <input uiInputControl [formControl]="emailControl" />
</ui-text-field>

<ui-dropdown [formControl]="roleControl" label="Role">...</ui-dropdown>

<ui-checkbox><input [formControl]="agreeControl" /></ui-checkbox>

Template-Driven Forms

Use ngModel for two-way binding:

<ui-text-field>
  <input uiInputControl [(ngModel)]="email" name="email" />
</ui-text-field>

<ui-dropdown [(ngModel)]="role" name="role" label="Role">...</ui-dropdown>

Accessibility

Keyboard Navigation

All components support comprehensive keyboard navigation:

Dropdown:

  • Arrow Down/Up - Navigate through options
  • Enter - Select highlighted option and close
  • Escape - Close dropdown without selection
  • Tab - Move focus to next element

Listbox:

  • Arrow Down/Up - Navigate items
  • Home/End - Jump to first/last item
  • Space/Enter - Select item

Checkbox/Checklist:

  • Space - Toggle checkbox state
  • Tab - Move between checkboxes

ARIA Attributes

Components include proper ARIA attributes for screen readers:

<!-- Dropdown -->
<ui-dropdown
  role="combobox"
  aria-haspopup="listbox"
  [attr.aria-expanded]="isOpen()">
</ui-dropdown>

<!-- Listbox -->
<div uiListbox role="listbox">
  <div uiListboxItem role="option"></div>
</div>

Testing

The library uses Jest with Spectator for testing.

Running Tests

# Run tests for this library
npx nx test ui-input-controls --skip-nx-cache

# Run tests with coverage
npx nx test ui-input-controls --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test ui-input-controls --watch

Test Coverage

The library includes comprehensive unit tests covering:

  • Component rendering - All visual states and variants
  • Form integration - ControlValueAccessor implementation
  • Keyboard navigation - Arrow keys, Enter, Escape, Tab
  • Validation - Error states and validation display
  • Accessibility - ARIA attributes and screen reader support
  • User interactions - Click, keyboard, focus events

Architecture Notes

Design Patterns

1. ControlValueAccessor Pattern

All form controls implement the CVA interface for seamless Angular Forms integration.

Benefits:

  • Works with both reactive and template-driven forms
  • Automatic validation integration
  • Consistent API across all controls

2. Signal-Based Reactivity

Components use Angular signals for reactive state management:

appearance = input<CheckboxAppearance>(CheckboxAppearance.Checkbox);
appearanceClass = computed(() =>
  this.appearance() === CheckboxAppearance.Bullet
    ? 'ui-checkbox__bullet'
    : 'ui-checkbox__checkbox'
);

3. CDK Integration

Leverages Angular CDK for complex interactions:

  • Overlay - Dropdown positioning (CdkConnectedOverlay)
  • A11y - Keyboard navigation (ActiveDescendantKeyManager)
  • Listbox - Accessible list selection (CdkListbox)

Performance Considerations

  1. OnPush Change Detection - All components use OnPush strategy
  2. Signal Reactivity - Computed signals prevent unnecessary re-renders
  3. KeyManager - Efficient keyboard navigation without DOM queries

Dependencies

Required Libraries

  • @angular/core - Angular framework
  • @angular/forms - Forms module
  • @angular/cdk/a11y - Accessibility utilities
  • @angular/cdk/listbox - Listbox primitive
  • @angular/cdk/overlay - Overlay positioning
  • @angular/cdk/collections - SelectionModel
  • @ng-icons/core - Icon system
  • @isa/icons - ISA icon library
  • lodash - Utility functions (isEqual)

Path Alias

Import from: @isa/ui/input-controls

License

Internal ISA Frontend library - not for external distribution.