This commit includes multiple new components and improvements to the UI essentials library: ## New Components Added: - **Accordion**: Expandable/collapsible content panels with full accessibility - **Alert**: Notification and messaging component with variants and dismiss functionality - **Popover**: Floating content containers with positioning and trigger options - **Timeline**: Chronological event display with customizable styling - **Tree View**: Hierarchical data display with expand/collapse functionality - **Toast**: Notification component (previously committed, includes style refinements) ## Component Features: - Full TypeScript implementation with Angular 19+ patterns - Comprehensive SCSS styling using semantic design tokens exclusively - Multiple size variants (sm, md, lg) and color variants (primary, success, warning, danger, info) - Accessibility support with ARIA attributes and keyboard navigation - Responsive design with mobile-first approach - Interactive demos showcasing all component features - Integration with existing design system and routing ## Demo Applications: - Created comprehensive demo components for each new component - Interactive examples with live code demonstrations - Integrated into main demo application routing and navigation ## Documentation: - Added COMPONENT_CREATION_TEMPLATE.md with detailed guidelines - Comprehensive component creation patterns and best practices - Design token usage guidelines and validation rules ## Code Quality: - Follows established Angular and SCSS conventions - Uses only verified semantic design tokens - Maintains consistency with existing component architecture - Comprehensive error handling and edge case management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
534 lines
20 KiB
TypeScript
534 lines
20 KiB
TypeScript
import { Component, ViewChild, ElementRef, signal } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { PopoverComponent } from '../../../../../ui-essentials/src/lib/components/overlays/popover';
|
|
|
|
type PopoverPosition = 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
|
|
type PopoverSize = 'sm' | 'md' | 'lg';
|
|
type PopoverVariant = 'default' | 'elevated' | 'floating' | 'menu' | 'tooltip';
|
|
type PopoverTrigger = 'click' | 'hover' | 'focus' | 'manual';
|
|
|
|
@Component({
|
|
selector: 'ui-popover-demo',
|
|
standalone: true,
|
|
imports: [CommonModule, PopoverComponent],
|
|
template: `
|
|
<div class="popover-demo">
|
|
<!-- Header -->
|
|
<div class="popover-demo__section">
|
|
<h2 class="popover-demo__title">Popover Component</h2>
|
|
<p>A flexible popover component with multiple positioning options, trigger types, and variants. Perfect for dropdowns, tooltips, context menus, and floating content.</p>
|
|
</div>
|
|
|
|
<!-- Size Variants -->
|
|
<div class="popover-demo__section">
|
|
<h3 class="popover-demo__subtitle">Sizes</h3>
|
|
<div class="popover-demo__row">
|
|
@for (size of sizes; track size) {
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
[class]="'popover-demo__trigger-button--' + size"
|
|
#triggerSize
|
|
(click)="togglePopover('size-' + size)">
|
|
{{ size.toUpperCase() }} Size
|
|
</button>
|
|
<ui-popover
|
|
[visible]="getPopoverState('size-' + size)"
|
|
[size]="size"
|
|
[triggerElement]="triggerSize"
|
|
position="bottom"
|
|
trigger="manual"
|
|
(visibleChange)="setPopoverState('size-' + size, $event)">
|
|
<div class="popover-content__tooltip">
|
|
This is a {{ size }} sized popover with sample content to demonstrate the different size options available.
|
|
</div>
|
|
</ui-popover>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Variant Styles -->
|
|
<div class="popover-demo__section">
|
|
<h3 class="popover-demo__subtitle">Variants</h3>
|
|
<div class="popover-demo__row">
|
|
@for (variant of variants; track variant) {
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
#triggerVariant
|
|
(click)="togglePopover('variant-' + variant)">
|
|
{{ variant | titlecase }}
|
|
</button>
|
|
<ui-popover
|
|
[visible]="getPopoverState('variant-' + variant)"
|
|
[variant]="variant"
|
|
[triggerElement]="triggerVariant"
|
|
position="bottom"
|
|
trigger="manual"
|
|
[showArrow]="variant !== 'elevated' && variant !== 'floating'"
|
|
(visibleChange)="setPopoverState('variant-' + variant, $event)">
|
|
<div class="popover-content__tooltip">
|
|
@if (variant === 'tooltip') {
|
|
<div class="popover-content__tooltip">Quick tooltip content</div>
|
|
} @else {
|
|
<div>
|
|
<strong>{{ variant | titlecase }} Variant</strong>
|
|
<p>This demonstrates the {{ variant }} styling variant of the popover component.</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</ui-popover>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Position Examples -->
|
|
<div class="popover-demo__section">
|
|
<h3 class="popover-demo__subtitle">Positions</h3>
|
|
<div class="popover-demo__row popover-demo__row--positions">
|
|
<!-- Top positions -->
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-top-start"
|
|
#triggerTopStart
|
|
(click)="togglePopover('pos-top-start')">
|
|
Top Start
|
|
</button>
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-top"
|
|
#triggerTop
|
|
(click)="togglePopover('pos-top')">
|
|
Top
|
|
</button>
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-top-end"
|
|
#triggerTopEnd
|
|
(click)="togglePopover('pos-top-end')">
|
|
Top End
|
|
</button>
|
|
|
|
<!-- Middle positions -->
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-left"
|
|
#triggerLeft
|
|
(click)="togglePopover('pos-left')">
|
|
Left
|
|
</button>
|
|
<div class="popover-demo__position-center">
|
|
<div style="padding: 20px; border: 2px solid #ddd; border-radius: 8px; background: #f9f9f9;">
|
|
Reference Element
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-right"
|
|
#triggerRight
|
|
(click)="togglePopover('pos-right')">
|
|
Right
|
|
</button>
|
|
|
|
<!-- Bottom positions -->
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-bottom-start"
|
|
#triggerBottomStart
|
|
(click)="togglePopover('pos-bottom-start')">
|
|
Bottom Start
|
|
</button>
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-bottom"
|
|
#triggerBottom
|
|
(click)="togglePopover('pos-bottom')">
|
|
Bottom
|
|
</button>
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__position-bottom-end"
|
|
#triggerBottomEnd
|
|
(click)="togglePopover('pos-bottom-end')">
|
|
Bottom End
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Position Popovers -->
|
|
@for (position of positions; track position.key) {
|
|
<ui-popover
|
|
[visible]="getPopoverState('pos-' + position.key)"
|
|
[position]="position.value"
|
|
[triggerElement]="getPositionTrigger(position.key)"
|
|
trigger="manual"
|
|
(visibleChange)="setPopoverState('pos-' + position.key, $event)">
|
|
<div class="popover-content__tooltip">
|
|
<strong>{{ position.value | titlecase }}</strong>
|
|
<p>Positioned {{ position.value }} relative to trigger.</p>
|
|
</div>
|
|
</ui-popover>
|
|
}
|
|
</div>
|
|
|
|
<!-- Trigger Types -->
|
|
<div class="popover-demo__section">
|
|
<h3 class="popover-demo__subtitle">Trigger Types</h3>
|
|
<div class="popover-demo__row">
|
|
<!-- Click Trigger -->
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
#triggerClick>
|
|
Click Trigger
|
|
</button>
|
|
<ui-popover
|
|
[triggerElement]="triggerClick"
|
|
position="bottom"
|
|
trigger="click">
|
|
<div class="popover-content__menu">
|
|
<button class="menu-item">Action 1</button>
|
|
<button class="menu-item">Action 2</button>
|
|
<button class="menu-item menu-item--divider">Action 3</button>
|
|
<button class="menu-item">Settings</button>
|
|
</div>
|
|
</ui-popover>
|
|
|
|
<!-- Hover Trigger -->
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
#triggerHover>
|
|
Hover Trigger
|
|
</button>
|
|
<ui-popover
|
|
[triggerElement]="triggerHover"
|
|
position="bottom"
|
|
trigger="hover"
|
|
variant="tooltip"
|
|
[hoverDelay]="500">
|
|
<div class="popover-content__tooltip">
|
|
This popover appears on hover with a 500ms delay.
|
|
</div>
|
|
</ui-popover>
|
|
|
|
<!-- Focus Trigger -->
|
|
<input
|
|
type="text"
|
|
class="popover-demo__trigger-button"
|
|
placeholder="Focus Trigger (click to focus)"
|
|
#triggerFocus
|
|
style="text-align: center;">
|
|
<ui-popover
|
|
[triggerElement]="triggerFocus"
|
|
position="bottom"
|
|
trigger="focus">
|
|
<div class="popover-content__form">
|
|
<div class="form-group">
|
|
<label>Help Information</label>
|
|
<p>This popover appears when the input gains focus and helps guide the user.</p>
|
|
</div>
|
|
</div>
|
|
</ui-popover>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Examples -->
|
|
<div class="popover-demo__section">
|
|
<h3 class="popover-demo__subtitle">Advanced Examples</h3>
|
|
|
|
<!-- Menu Dropdown -->
|
|
<div class="popover-demo__row">
|
|
<button
|
|
class="popover-demo__trigger-button popover-demo__trigger-button--primary"
|
|
#triggerMenu
|
|
(click)="togglePopover('menu-dropdown')">
|
|
Menu Dropdown
|
|
</button>
|
|
<ui-popover
|
|
[visible]="getPopoverState('menu-dropdown')"
|
|
[triggerElement]="triggerMenu"
|
|
position="bottom-start"
|
|
variant="menu"
|
|
trigger="manual"
|
|
(visibleChange)="setPopoverState('menu-dropdown', $event)">
|
|
<div class="popover-content__menu">
|
|
<button class="menu-item" (click)="handleMenuAction('new')">📄 New Document</button>
|
|
<button class="menu-item" (click)="handleMenuAction('open')">📂 Open...</button>
|
|
<button class="menu-item" (click)="handleMenuAction('save')">💾 Save</button>
|
|
<button class="menu-item menu-item--divider" (click)="handleMenuAction('export')">📤 Export</button>
|
|
<button class="menu-item" (click)="handleMenuAction('print')">🖨️ Print</button>
|
|
<button class="menu-item menu-item--divider" (click)="handleMenuAction('settings')">⚙️ Settings</button>
|
|
<button class="menu-item" (click)="handleMenuAction('help')">❓ Help</button>
|
|
</div>
|
|
</ui-popover>
|
|
|
|
<!-- Form Popover -->
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
#triggerForm
|
|
(click)="togglePopover('form-popover')">
|
|
Quick Form
|
|
</button>
|
|
<ui-popover
|
|
[visible]="getPopoverState('form-popover')"
|
|
[triggerElement]="triggerForm"
|
|
position="bottom"
|
|
trigger="manual"
|
|
title="Contact Information"
|
|
(visibleChange)="setPopoverState('form-popover', $event)">
|
|
<div class="popover-content__form">
|
|
<div class="form-group">
|
|
<label for="name">Name</label>
|
|
<input type="text" id="name" placeholder="Enter your name">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="email">Email</label>
|
|
<input type="email" id="email" placeholder="Enter your email">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="message">Message</label>
|
|
<textarea id="message" placeholder="Enter your message"></textarea>
|
|
</div>
|
|
</div>
|
|
<div slot="footer">
|
|
<button (click)="handleFormSubmit()">Submit</button>
|
|
<button (click)="setPopoverState('form-popover', false)">Cancel</button>
|
|
</div>
|
|
</ui-popover>
|
|
|
|
<!-- User Card -->
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
#triggerCard
|
|
(click)="togglePopover('user-card')">
|
|
User Profile
|
|
</button>
|
|
<ui-popover
|
|
[visible]="getPopoverState('user-card')"
|
|
[triggerElement]="triggerCard"
|
|
position="bottom"
|
|
variant="elevated"
|
|
trigger="manual"
|
|
(visibleChange)="setPopoverState('user-card', $event)">
|
|
<div class="popover-content__card">
|
|
<div class="card-header">
|
|
<div class="avatar">JD</div>
|
|
<div class="user-info">
|
|
<div class="name">John Doe</div>
|
|
<div class="role">Senior Developer</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-content">
|
|
Full-stack developer with 5+ years of experience in Angular, Node.js, and cloud technologies. Passionate about creating user-friendly interfaces and scalable applications.
|
|
</div>
|
|
<div class="card-actions">
|
|
<button (click)="handleUserAction('message')">Message</button>
|
|
<button class="primary" (click)="handleUserAction('connect')">Connect</button>
|
|
</div>
|
|
</div>
|
|
</ui-popover>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Configuration Options -->
|
|
<div class="popover-demo__section">
|
|
<h3 class="popover-demo__subtitle">Configuration Options</h3>
|
|
|
|
<div class="popover-demo__row">
|
|
<!-- Auto-positioning -->
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
#triggerAutoPos
|
|
(click)="togglePopover('auto-position')"
|
|
style="margin-left: auto; margin-right: 20px;">
|
|
Auto Position (try at edge)
|
|
</button>
|
|
<ui-popover
|
|
[visible]="getPopoverState('auto-position')"
|
|
[triggerElement]="triggerAutoPos"
|
|
position="right"
|
|
[autoPosition]="true"
|
|
trigger="manual"
|
|
(visibleChange)="setPopoverState('auto-position', $event)"
|
|
(positionChange)="handlePositionChange($event)">
|
|
<div class="popover-content__tooltip">
|
|
<strong>Auto-positioning Enabled</strong>
|
|
<p>This popover automatically adjusts its position to stay within the viewport.</p>
|
|
<p><strong>Current Position:</strong> {{ currentAutoPosition() || 'right' }}</p>
|
|
</div>
|
|
</ui-popover>
|
|
|
|
<!-- With Backdrop -->
|
|
<button
|
|
class="popover-demo__trigger-button"
|
|
#triggerBackdrop
|
|
(click)="togglePopover('with-backdrop')">
|
|
With Backdrop
|
|
</button>
|
|
<ui-popover
|
|
[visible]="getPopoverState('with-backdrop')"
|
|
[triggerElement]="triggerBackdrop"
|
|
position="bottom"
|
|
[showBackdrop]="true"
|
|
[backdropClosable]="true"
|
|
[preventBodyScroll]="true"
|
|
trigger="manual"
|
|
(visibleChange)="setPopoverState('with-backdrop', $event)"
|
|
(backdropClicked)="handleBackdropClick()">
|
|
<div class="popover-content__card">
|
|
<div style="text-align: center; padding: 20px;">
|
|
<h4>Modal-like Popover</h4>
|
|
<p>This popover has a backdrop and prevents body scroll. Click the backdrop or press ESC to close.</p>
|
|
<button class="primary" (click)="setPopoverState('with-backdrop', false)">Close</button>
|
|
</div>
|
|
</div>
|
|
</ui-popover>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Usage Statistics -->
|
|
<div class="popover-demo__stats">
|
|
<span>Total Interactions: {{ totalInteractions() }}</span>
|
|
<span>Backdrop Clicks: {{ backdropClicks() }}</span>
|
|
<span>Auto-repositions: {{ autoRepositions() }}</span>
|
|
</div>
|
|
|
|
<!-- Code Example -->
|
|
<div class="popover-demo__code-example">
|
|
<pre><code><!-- Basic Usage -->
|
|
<button #trigger>Click me</button>
|
|
<ui-popover [triggerElement]="trigger" position="bottom" trigger="click">
|
|
<p>Popover content goes here</p>
|
|
</ui-popover>
|
|
|
|
<!-- Advanced Configuration -->
|
|
<ui-popover
|
|
[triggerElement]="myTrigger"
|
|
position="bottom-start"
|
|
variant="menu"
|
|
trigger="click"
|
|
[autoPosition]="true"
|
|
[showArrow]="true"
|
|
(visibleChange)="onPopoverToggle($event)">
|
|
<div class="custom-menu">...</div>
|
|
</ui-popover></code></pre>
|
|
</div>
|
|
</div>
|
|
`,
|
|
styleUrl: './popover-demo.component.scss'
|
|
})
|
|
export class PopoverDemoComponent {
|
|
// Component references
|
|
@ViewChild('triggerTopStart', { read: ElementRef }) triggerTopStart?: ElementRef<HTMLElement>;
|
|
@ViewChild('triggerTop', { read: ElementRef }) triggerTop?: ElementRef<HTMLElement>;
|
|
@ViewChild('triggerTopEnd', { read: ElementRef }) triggerTopEnd?: ElementRef<HTMLElement>;
|
|
@ViewChild('triggerLeft', { read: ElementRef }) triggerLeft?: ElementRef<HTMLElement>;
|
|
@ViewChild('triggerRight', { read: ElementRef }) triggerRight?: ElementRef<HTMLElement>;
|
|
@ViewChild('triggerBottomStart', { read: ElementRef }) triggerBottomStart?: ElementRef<HTMLElement>;
|
|
@ViewChild('triggerBottom', { read: ElementRef }) triggerBottom?: ElementRef<HTMLElement>;
|
|
@ViewChild('triggerBottomEnd', { read: ElementRef }) triggerBottomEnd?: ElementRef<HTMLElement>;
|
|
|
|
// Demo data
|
|
readonly sizes: PopoverSize[] = ['sm', 'md', 'lg'];
|
|
readonly variants: PopoverVariant[] = ['default', 'elevated', 'floating', 'menu', 'tooltip'];
|
|
readonly triggers: PopoverTrigger[] = ['click', 'hover', 'focus', 'manual'];
|
|
|
|
readonly positions = [
|
|
{ key: 'top-start', value: 'top-start' as PopoverPosition },
|
|
{ key: 'top', value: 'top' as PopoverPosition },
|
|
{ key: 'top-end', value: 'top-end' as PopoverPosition },
|
|
{ key: 'left', value: 'left' as PopoverPosition },
|
|
{ key: 'right', value: 'right' as PopoverPosition },
|
|
{ key: 'bottom-start', value: 'bottom-start' as PopoverPosition },
|
|
{ key: 'bottom', value: 'bottom' as PopoverPosition },
|
|
{ key: 'bottom-end', value: 'bottom-end' as PopoverPosition }
|
|
];
|
|
|
|
// State management
|
|
private popoverStates = new Map<string, boolean>();
|
|
|
|
// Statistics
|
|
private _totalInteractions = signal(0);
|
|
private _backdropClicks = signal(0);
|
|
private _autoRepositions = signal(0);
|
|
private _currentAutoPosition = signal<PopoverPosition | null>(null);
|
|
|
|
readonly totalInteractions = this._totalInteractions.asReadonly();
|
|
readonly backdropClicks = this._backdropClicks.asReadonly();
|
|
readonly autoRepositions = this._autoRepositions.asReadonly();
|
|
readonly currentAutoPosition = this._currentAutoPosition.asReadonly();
|
|
|
|
/**
|
|
* Gets the state of a popover by key
|
|
*/
|
|
getPopoverState(key: string): boolean {
|
|
return this.popoverStates.get(key) || false;
|
|
}
|
|
|
|
/**
|
|
* Sets the state of a popover by key
|
|
*/
|
|
setPopoverState(key: string, visible: boolean): void {
|
|
this.popoverStates.set(key, visible);
|
|
if (visible) {
|
|
this._totalInteractions.update(count => count + 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggles a popover state
|
|
*/
|
|
togglePopover(key: string): void {
|
|
const currentState = this.getPopoverState(key);
|
|
this.setPopoverState(key, !currentState);
|
|
}
|
|
|
|
/**
|
|
* Gets the trigger element for position demos
|
|
*/
|
|
getPositionTrigger(position: string): HTMLElement | undefined {
|
|
switch (position) {
|
|
case 'top-start': return this.triggerTopStart?.nativeElement;
|
|
case 'top': return this.triggerTop?.nativeElement;
|
|
case 'top-end': return this.triggerTopEnd?.nativeElement;
|
|
case 'left': return this.triggerLeft?.nativeElement;
|
|
case 'right': return this.triggerRight?.nativeElement;
|
|
case 'bottom-start': return this.triggerBottomStart?.nativeElement;
|
|
case 'bottom': return this.triggerBottom?.nativeElement;
|
|
case 'bottom-end': return this.triggerBottomEnd?.nativeElement;
|
|
default: return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles menu actions
|
|
*/
|
|
handleMenuAction(action: string): void {
|
|
console.log('Menu action:', action);
|
|
this.setPopoverState('menu-dropdown', false);
|
|
// You could implement actual menu logic here
|
|
}
|
|
|
|
/**
|
|
* Handles form submission
|
|
*/
|
|
handleFormSubmit(): void {
|
|
console.log('Form submitted');
|
|
this.setPopoverState('form-popover', false);
|
|
// You could implement actual form logic here
|
|
}
|
|
|
|
/**
|
|
* Handles user profile actions
|
|
*/
|
|
handleUserAction(action: string): void {
|
|
console.log('User action:', action);
|
|
this.setPopoverState('user-card', false);
|
|
// You could implement actual user interaction logic here
|
|
}
|
|
|
|
/**
|
|
* Handles position changes from auto-positioning
|
|
*/
|
|
handlePositionChange(newPosition: PopoverPosition): void {
|
|
console.log('Position changed to:', newPosition);
|
|
this._currentAutoPosition.set(newPosition);
|
|
this._autoRepositions.update(count => count + 1);
|
|
}
|
|
|
|
/**
|
|
* Handles backdrop clicks
|
|
*/
|
|
handleBackdropClick(): void {
|
|
console.log('Backdrop clicked');
|
|
this._backdropClicks.update(count => count + 1);
|
|
}
|
|
} |