Files
ISA-Frontend/libs/ui/menu/README.md
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

19 KiB

@isa/ui/menu

A lightweight Angular component library providing accessible menu components built on Angular CDK Menu. Part of the ISA Design System.

Overview

The Menu component library delivers reusable, accessible menu components that wrap Angular CDK Menu directives with ISA-specific styling and conventions. It provides a simple API for creating dropdown menus, context menus, and other menu-based UI patterns.

Key Features

  • Angular CDK Integration: Built on @angular/cdk/menu for robust accessibility and keyboard navigation
  • Host Directives Pattern: Leverages Angular host directives for clean, declarative API
  • Minimal Overhead: Thin wrapper around CDK with ISA styling hooks
  • CSS-Based Styling: Uses Tailwind and ISA design tokens via CSS classes
  • Keyboard Navigation: Full keyboard support (Arrow keys, Enter, Escape) via CDK
  • ARIA Compliance: Automatic ARIA attributes for screen readers
  • Standalone Components: Modern Angular standalone architecture

Installation

This library is part of the ISA monorepo and uses path aliases for imports:

import { UiMenu, MenuComponent, MenuItemDirective } from '@isa/ui/menu';

Quick Start

Basic Usage

import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';

@Component({
  selector: 'app-example',
  standalone: true,
  imports: [UiMenu],
  template: `
    <ui-menu>
      <button uiMenuItem>Action 1</button>
      <button uiMenuItem>Action 2</button>
      <button uiMenuItem>Action 3</button>
    </ui-menu>
  `
})
export class ExampleComponent {}

Dropdown Menu Example

import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';

@Component({
  selector: 'app-dropdown-example',
  standalone: true,
  imports: [UiMenu, CdkMenuTrigger],
  template: `
    <button [cdkMenuTriggerFor]="menu">Open Menu</button>

    <ng-template #menu>
      <ui-menu>
        <button uiMenuItem (click)="onEdit()">Edit</button>
        <button uiMenuItem (click)="onDelete()">Delete</button>
        <button uiMenuItem (click)="onShare()">Share</button>
      </ui-menu>
    </ng-template>
  `
})
export class DropdownExampleComponent {
  onEdit() { console.log('Edit clicked'); }
  onDelete() { console.log('Delete clicked'); }
  onShare() { console.log('Share clicked'); }
}

API Reference

MenuComponent

Selector: ui-menu

A container component for menu items that provides accessibility and keyboard navigation.

Host Directives

  • CdkMenu: Provides core menu functionality, keyboard navigation, and ARIA attributes

Host Properties

  • CSS Class: .ui-menu - Applied to the host element for styling

Template

  • Simple content projection: <ng-content></ng-content>

####Configuration

  • Change Detection: Default (inherited from Angular)
  • View Encapsulation: Default
  • Standalone: false (import via UiMenu array)

MenuItemDirective

Selector: [uiMenuItem]

A directive that marks elements as menu items, adding appropriate styling and behavior.

Host Directives

  • CdkMenuItem: Provides menu item functionality, click handling, and ARIA attributes

Host Properties

  • CSS Class: .ui-menu-item - Applied to the host element for styling

Configuration

  • Standalone: false (import via UiMenu array)

UiMenu Import Array

Convenience import for both menu components:

export const UiMenu = [MenuComponent, MenuItemDirective];

Usage:

imports: [UiMenu]  // Imports both MenuComponent and MenuItemDirective

Usage Examples

Example 1: Simple Menu List

import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';

@Component({
  selector: 'app-simple-menu',
  standalone: true,
  imports: [UiMenu],
  template: `
    <ui-menu>
      <button uiMenuItem>Home</button>
      <button uiMenuItem>About</button>
      <button uiMenuItem>Contact</button>
    </ui-menu>
  `
})
export class SimpleMenuComponent {}

Example 2: Context Menu with Icons

import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkContextMenuTrigger } from '@angular/cdk/menu';
import { NgIconComponent } from '@ng-icons/core';

@Component({
  selector: 'app-context-menu',
  standalone: true,
  imports: [UiMenu, CdkContextMenuTrigger, NgIconComponent],
  template: `
    <div [cdkContextMenuTriggerFor]="contextMenu" class="context-area">
      Right-click here
    </div>

    <ng-template #contextMenu>
      <ui-menu>
        <button uiMenuItem>
          <ng-icon name="isaActionCopy"></ng-icon>
          Copy
        </button>
        <button uiMenuItem>
          <ng-icon name="isaActionPaste"></ng-icon>
          Paste
        </button>
        <button uiMenuItem>
          <ng-icon name="isaActionDelete"></ng-icon>
          Delete
        </button>
      </ui-menu>
    </ng-template>
  `
})
export class ContextMenuComponent {}

Example 3: Nested Menus (Submenus)

import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';

@Component({
  selector: 'app-nested-menu',
  standalone: true,
  imports: [UiMenu, CdkMenuTrigger],
  template: `
    <button [cdkMenuTriggerFor]="mainMenu">File</button>

    <ng-template #mainMenu>
      <ui-menu>
        <button uiMenuItem>New File</button>
        <button uiMenuItem [cdkMenuTriggerFor]="openMenu">Open</button>
        <button uiMenuItem>Save</button>
      </ui-menu>
    </ng-template>

    <ng-template #openMenu>
      <ui-menu>
        <button uiMenuItem>Recent Files</button>
        <button uiMenuItem>Browse...</button>
      </ui-menu>
    </ng-template>
  `
})
export class NestedMenuComponent {}

Example 4: Menu with Disabled Items

import { Component, signal } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';

@Component({
  selector: 'app-disabled-menu',
  standalone: true,
  imports: [UiMenu, CdkMenuTrigger],
  template: `
    <button [cdkMenuTriggerFor]="menu">Actions</button>

    <ng-template #menu>
      <ui-menu>
        <button uiMenuItem [disabled]="false">Edit</button>
        <button uiMenuItem [disabled]="!canDelete()">Delete</button>
        <button uiMenuItem [disabled]="false">Share</button>
      </ui-menu>
    </ng-template>
  `
})
export class DisabledMenuComponent {
  canDelete = signal(false);  // Dynamic disabled state
}

Example 5: Menu with Click Handlers

import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';
import { Router } from '@angular/router';

@Component({
  selector: 'app-navigation-menu',
  standalone: true,
  imports: [UiMenu, CdkMenuTrigger],
  template: `
    <button [cdkMenuTriggerFor]="menu">Navigate</button>

    <ng-template #menu>
      <ui-menu>
        <button uiMenuItem (click)="navigateTo('/dashboard')">Dashboard</button>
        <button uiMenuItem (click)="navigateTo('/reports')">Reports</button>
        <button uiMenuItem (click)="navigateTo('/settings')">Settings</button>
        <button uiMenuItem (click)="logout()">Logout</button>
      </ui-menu>
    </ng-template>
  `
})
export class NavigationMenuComponent {
  constructor(private router: Router) {}

  navigateTo(path: string): void {
    this.router.navigate([path]);
  }

  logout(): void {
    // Logout logic
  }
}

Architecture Notes

Component Structure

MenuComponent
├── Host Element (<ui-menu>)
│   ├── Host Directive: CdkMenu
│   │   ├── Keyboard Navigation (Arrow keys, Enter, Escape)
│   │   ├── ARIA Attributes (role="menu")
│   │   └── Focus Management
│   ├── CSS Class: .ui-menu
│   └── <ng-content> (Projects menu items)

MenuItemDirective
├── Host Element ([uiMenuItem])
│   ├── Host Directive: CdkMenuItem
│   │   ├── Click Handling
│   │   ├── ARIA Attributes (role="menuitem")
│   │   └── Keyboard Interaction
│   └── CSS Class: .ui-menu-item

Host Directives Pattern

Why Host Directives?

This library uses Angular's host directives feature introduced in Angular 15 to:

  1. Reuse CDK Logic: Leverage @angular/cdk/menu without re-implementing functionality
  2. Clean API: Hide CDK complexity while exposing ISA-specific styling
  3. Maintainability: CDK updates automatically benefit this library
  4. Type Safety: Full TypeScript support for all CDK features

How It Works:

@Component({
  selector: 'ui-menu',
  template: '<ng-content></ng-content>',
  host: { class: 'ui-menu' },
  hostDirectives: [CdkMenu],  // Applies CdkMenu to this component's host
})
export class MenuComponent {}

When you use <ui-menu>, it automatically:

  • Applies all CdkMenu behaviors (keyboard nav, ARIA, etc.)
  • Adds .ui-menu CSS class for styling
  • Projects child content (menu items)

CDK Menu Features

Automatic Features (via CdkMenu):

  • Keyboard Navigation:

    • / : Navigate menu items
    • Enter / Space: Activate item
    • Escape: Close menu
    • Tab: Move focus out of menu
  • ARIA Attributes:

    • role="menu" on menu container
    • role="menuitem" on menu items
    • aria-disabled on disabled items
    • aria-haspopup for submenus
  • Focus Management:

    • Auto-focus first item on open
    • Circular focus wrapping
    • Focus restoration on close

Styling Architecture

CSS Class Hooks:

// Applied by MenuComponent
.ui-menu {
  // Container styles (padding, background, border, shadow)
}

// Applied by MenuItemDirective
.ui-menu-item {
  // Item styles (padding, hover, focus, active states)
}

// Disabled state (via CDK)
.ui-menu-item[disabled] {
  // Disabled styles (opacity, cursor, pointer-events)
}

Tailwind Integration:

The library provides CSS class hooks that can be styled via:

  • Global SCSS files
  • Tailwind @apply directives
  • Custom Tailwind plugins (ISA menu plugin)

Example Tailwind Configuration:

/* styles.scss */
.ui-menu {
  @apply bg-white shadow-lg rounded-md py-1;
}

.ui-menu-item {
  @apply px-4 py-2 hover:bg-gray-100 cursor-pointer;
}

.ui-menu-item[disabled] {
  @apply opacity-50 cursor-not-allowed;
}

Integration with Angular CDK

Required CDK Directives (imported separately):

import { CdkMenuTrigger } from '@angular/cdk/menu';  // For dropdown triggers
import { CdkContextMenuTrigger } from '@angular/cdk/menu';  // For context menus

CDK Menu Trigger:

<button [cdkMenuTriggerFor]="menuTemplate">Open Menu</button>
<ng-template #menuTemplate>
  <ui-menu>...</ui-menu>
</ng-template>

CDK Context Menu Trigger:

<div [cdkContextMenuTriggerFor]="contextMenuTemplate">Right-click me</div>
<ng-template #contextMenuTemplate>
  <ui-menu>...</ui-menu>
</ng-template>

Performance Characteristics

Optimization Strategies:

  • Lightweight Components: Minimal overhead over CDK base
  • No Template: MenuComponent uses empty template (content projection only)
  • No Change Detection: Default strategy (components are simple wrappers)
  • CDK Optimizations: Inherits CDK's efficient event handling and focus management

Bundle Impact:

  • Component Size: ~2KB (both components combined)
  • CDK Dependency: ~15KB (shared with other CDK features)
  • Total Overhead: Minimal, mostly CDK dependency

Dependencies

Angular Dependencies

Package Version Purpose
@angular/core 20.1.2 Component framework and dependency injection
@angular/cdk/menu 20.1.2 CDK Menu directives for accessibility and keyboard nav

Internal Dependencies

None - this library has no dependencies on other @isa/* libraries.

Peer Dependencies

{
  "peerDependencies": {
    "@angular/core": "^20.0.0",
    "@angular/cdk": "^20.0.0"
  }
}

Testing

Test Configuration

Framework: Jest

Configuration: jest.config.ts (extends workspace defaults)

Running Tests

# Run tests with fresh results
npx nx test ui-menu --skip-nx-cache

# Run tests in watch mode
npx nx test ui-menu --watch

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

Testing Recommendations

Unit Test Coverage

Test menu rendering:

import { TestBed } from '@angular/core/testing';
import { MenuComponent, MenuItemDirective } from '@isa/ui/menu';

describe('MenuComponent', () => {
  it('should render menu with items', () => {
    const fixture = TestBed.createComponent(MenuComponent);
    fixture.detectChanges();

    const menuElement = fixture.nativeElement;
    expect(menuElement.classList.contains('ui-menu')).toBe(true);
  });
});

Test menu item directive:

describe('MenuItemDirective', () => {
  it('should apply ui-menu-item class', () => {
    // Create test component with directive
    const fixture = TestBed.createComponent(TestHostComponent);
    fixture.detectChanges();

    const menuItem = fixture.nativeElement.querySelector('[uiMenuItem]');
    expect(menuItem.classList.contains('ui-menu-item')).toBe(true);
  });
});

Test keyboard navigation (via CDK):

it('should navigate menu items with keyboard', () => {
  const fixture = TestBed.createComponent(MenuComponent);
  fixture.detectChanges();

  const menuElement = fixture.nativeElement;

  // Simulate down arrow
  menuElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));

  // Verify focus moved
  expect(document.activeElement).toBe(firstMenuItem);
});

Integration Test Scenarios

Test with CDK trigger:

import { CdkMenuTrigger } from '@angular/cdk/menu';

@Component({
  template: `
    <button [cdkMenuTriggerFor]="menu" #trigger="cdkMenuTrigger">Open</button>
    <ng-template #menu>
      <ui-menu>
        <button uiMenuItem>Item 1</button>
      </ui-menu>
    </ng-template>
  `
})
class TestHostComponent {}

it('should open menu on trigger click', () => {
  const fixture = TestBed.createComponent(TestHostComponent);
  fixture.detectChanges();

  const triggerButton = fixture.nativeElement.querySelector('button');
  triggerButton.click();
  fixture.detectChanges();

  // Verify menu is open
  const menu = document.querySelector('ui-menu');
  expect(menu).toBeTruthy();
});

Best Practices

1. Use CDK Triggers for Dropdown Menus

// ✅ GOOD: Use CdkMenuTrigger for dropdowns
<button [cdkMenuTriggerFor]="menu">Open Menu</button>
<ng-template #menu>
  <ui-menu>...</ui-menu>
</ng-template>

// ❌ BAD: Manual show/hide logic
<button (click)="showMenu = true">Open Menu</button>
<ui-menu *ngIf="showMenu">...</ui-menu>

2. Always Use MenuItemDirective on Interactive Elements

// ✅ GOOD: Apply directive to buttons
<ui-menu>
  <button uiMenuItem>Action</button>
</ui-menu>

// ❌ BAD: Non-interactive elements
<ui-menu>
  <div uiMenuItem>Action</div>  // Won't receive keyboard events properly
</ui-menu>

3. Leverage CDK Features

// ✅ GOOD: Use CDK for submenus
<button uiMenuItem [cdkMenuTriggerFor]="submenu">More</button>
<ng-template #submenu>
  <ui-menu>...</ui-menu>
</ng-template>

// ❌ BAD: Manual submenu implementation
<button uiMenuItem (click)="toggleSubmenu()">More</button>
<ui-menu *ngIf="submenuOpen">...</ui-menu>

4. Provide ARIA Labels for Icon-Only Items

// ✅ GOOD: Accessible icon-only menu item
<button uiMenuItem aria-label="Delete">
  <ng-icon name="isaActionDelete"></ng-icon>
</button>

// ❌ BAD: No accessible label
<button uiMenuItem>
  <ng-icon name="isaActionDelete"></ng-icon>
</button>

5. Use Semantic HTML

// ✅ GOOD: Use buttons for actions
<ui-menu>
  <button uiMenuItem>Edit</button>
  <button uiMenuItem>Delete</button>
</ui-menu>

// ❌ BAD: Use divs/spans
<ui-menu>
  <div uiMenuItem>Edit</div>
  <span uiMenuItem>Delete</span>
</ui-menu>

6. Handle Menu Item Clicks Properly

// ✅ GOOD: Use click handlers on menu items
<button uiMenuItem (click)="handleAction()">Action</button>

// ❌ BAD: Wrap in extra divs with click handlers
<div (click)="handleAction()">
  <button uiMenuItem>Action</button>
</div>

Common Pitfalls

1. Forgetting CDK Imports

// ❌ ERROR: Missing CdkMenuTrigger import
imports: [UiMenu]

// ✅ CORRECT: Import CDK directives separately
imports: [UiMenu, CdkMenuTrigger]

2. Using Menu Without Template

// ❌ ERROR: Menu needs ng-template with trigger
<ui-menu>
  <button uiMenuItem>Item</button>
</ui-menu>

// ✅ CORRECT: Use with CDK trigger and template
<button [cdkMenuTriggerFor]="menu">Open</button>
<ng-template #menu>
  <ui-menu>
    <button uiMenuItem>Item</button>
  </ui-menu>
</ng-template>

3. Incorrect Selector Usage

// ❌ ERROR: Wrong directive name
<button menuItem>Action</button>

// ✅ CORRECT: Use uiMenuItem
<button uiMenuItem>Action</button>

Migration Guide

From Custom Menu to @isa/ui/menu

Before:

@Component({
  template: `
    <div class="custom-menu">
      <div class="menu-item" (click)="action1()">Action 1</div>
      <div class="menu-item" (click)="action2()">Action 2</div>
    </div>
  `
})

After:

import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';

@Component({
  standalone: true,
  imports: [UiMenu, CdkMenuTrigger],
  template: `
    <button [cdkMenuTriggerFor]="menu">Open Menu</button>
    <ng-template #menu>
      <ui-menu>
        <button uiMenuItem (click)="action1()">Action 1</button>
        <button uiMenuItem (click)="action2()">Action 2</button>
      </ui-menu>
    </ng-template>
  `
})

Benefits:

  • Automatic keyboard navigation
  • ARIA compliance
  • Focus management
  • Consistent styling
  • Less custom code
  • Angular CDK Menu: Official CDK Menu Documentation
  • Host Directives: Angular guide on host directives
  • @isa/ui/buttons: Button components often used with menus
  • ISA Design System: Menu styling guidelines

Support and Contributing

For questions, issues, or contributions related to the Menu components:

  1. Review Angular CDK Menu documentation for advanced features
  2. Check ISA Design System for styling standards
  3. Consult Tailwind configuration for menu styling utilities
  4. Follow Angular standalone component best practices

Changelog

Current Version

Features:

  • Standalone-ready component and directive
  • Angular CDK Menu integration via host directives
  • Minimal API surface (component + directive)
  • Full keyboard navigation and ARIA support
  • Tailwind/ISA design system styling hooks

Testing:

  • Jest configuration
  • Unit tests for component and directive

Package: @isa/ui/menu Path Alias: @isa/ui/menu Entry Point: libs/ui/menu/src/index.ts Selectors: ui-menu, [uiMenuItem] Type: Angular Component Library (CDK Wrapper)