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

787 lines
19 KiB
Markdown

# @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:
```typescript
import { UiMenu, MenuComponent, MenuItemDirective } from '@isa/ui/menu';
```
## Quick Start
### Basic Usage
```typescript
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
```typescript
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:
```typescript
export const UiMenu = [MenuComponent, MenuItemDirective];
```
**Usage**:
```typescript
imports: [UiMenu] // Imports both MenuComponent and MenuItemDirective
```
## Usage Examples
### Example 1: Simple Menu List
```typescript
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
```typescript
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)
```typescript
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
```typescript
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
```typescript
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**:
```typescript
@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**:
```scss
// 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**:
```css
/* 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):
```typescript
import { CdkMenuTrigger } from '@angular/cdk/menu'; // For dropdown triggers
import { CdkContextMenuTrigger } from '@angular/cdk/menu'; // For context menus
```
**CDK Menu Trigger**:
```typescript
<button [cdkMenuTriggerFor]="menuTemplate">Open Menu</button>
<ng-template #menuTemplate>
<ui-menu>...</ui-menu>
</ng-template>
```
**CDK Context Menu Trigger**:
```typescript
<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
```json
{
"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
```bash
# 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**:
```typescript
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**:
```typescript
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):
```typescript
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**:
```typescript
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
```typescript
// ✅ 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
```typescript
// ✅ 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
```typescript
// ✅ 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
```typescript
// ✅ 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
```typescript
// ✅ 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
```typescript
// ✅ 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
```typescript
// ❌ ERROR: Missing CdkMenuTrigger import
imports: [UiMenu]
// ✅ CORRECT: Import CDK directives separately
imports: [UiMenu, CdkMenuTrigger]
```
### 2. Using Menu Without Template
```typescript
// ❌ 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
```typescript
// ❌ 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**:
```typescript
@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**:
```typescript
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
## Related Documentation
- **Angular CDK Menu**: [Official CDK Menu Documentation](https://material.angular.io/cdk/menu/overview)
- **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)