feat: Implement SampleAppServiceTests for application service testing feat: Create newProjectConfigureApplicationTests project for application layer tests feat: Establish newProjectConfigureDomainTests project for domain layer tests feat: Introduce Entity Framework Core tests for the newProjectConfigure application feat: Develop console test application for HTTP API client demonstration feat: Implement FakeCurrentPrincipalAccessor for testing purposes chore: Set up test base classes and data seed contributors for consistent testing environment
129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
import {
|
|
Component,
|
|
ElementRef,
|
|
EventEmitter,
|
|
HostListener,
|
|
Input,
|
|
OnChanges,
|
|
OnDestroy,
|
|
Output,
|
|
SimpleChanges,
|
|
} from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { A11yModule } from '@angular/cdk/a11y';
|
|
|
|
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
export type ModalVariant = 'default' | 'primary' | 'danger' | 'warning' | 'success' | 'info';
|
|
export type ModalPosition = 'center' | 'top';
|
|
|
|
/**
|
|
* Professional, reusable modal dialog component.
|
|
*
|
|
* Fully integrated with the application theme (supports Light/Dark mode via CSS tokens).
|
|
* Uses @angular/cdk's A11yModule (cdkTrapFocus) for keyboard accessibility and focus trapping.
|
|
*
|
|
* Features:
|
|
* - Responsive sizing: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
|
|
* - Semantic variants: 'default' | 'primary' | 'danger' | 'warning' | 'success' | 'info'
|
|
* - Title, subtitle, and badge icon support
|
|
* - Custom content projection via [modal-header], [modal-body], and [modal-footer]
|
|
* - Smooth backdrop fade and panel scale transitions
|
|
* - Accessible Esc and backdrop click dismissing
|
|
* - Automatic background scroll-lock management
|
|
*/
|
|
|
|
@Component({
|
|
selector: 'app-modal',
|
|
imports: [CommonModule, A11yModule],
|
|
templateUrl: './modal.component.html',
|
|
styleUrl: './modal.component.scss',
|
|
})
|
|
export class ModalComponent implements OnChanges, OnDestroy {
|
|
@Input() isOpen = false;
|
|
@Input() title = '';
|
|
@Input() subtitle?: string;
|
|
@Input() iconClass?: string;
|
|
@Input() variant: ModalVariant = 'default';
|
|
@Input() size: ModalSize = 'md';
|
|
@Input() position: ModalPosition = 'center';
|
|
@Input() scrollable = true;
|
|
@Input() closeOnBackdropClick = true;
|
|
@Input() closeOnEsc = true;
|
|
@Input() showCloseButton = true;
|
|
/** Set false when you don't project any modal-footer content to omit the bottom action bar. */
|
|
@Input() showFooter = true;
|
|
|
|
@Output() closed = new EventEmitter<void>();
|
|
@Output() opened = new EventEmitter<void>();
|
|
|
|
private previouslyFocusedElement: HTMLElement | null = null;
|
|
private originalBodyOverflow = '';
|
|
|
|
constructor(private hostRef: ElementRef<HTMLElement>) {}
|
|
|
|
ngOnChanges(changes: SimpleChanges): void {
|
|
if (!changes['isOpen']) {
|
|
return;
|
|
}
|
|
|
|
if (this.isOpen) {
|
|
this.previouslyFocusedElement = document.activeElement as HTMLElement;
|
|
this.originalBodyOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = 'hidden';
|
|
this.opened.emit();
|
|
} else {
|
|
this.unlockBodyScroll();
|
|
this.previouslyFocusedElement?.focus?.();
|
|
}
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (this.isOpen) {
|
|
this.unlockBodyScroll();
|
|
}
|
|
}
|
|
|
|
private unlockBodyScroll(): void {
|
|
document.body.style.overflow = this.originalBodyOverflow || '';
|
|
}
|
|
|
|
@HostListener('document:keydown.escape')
|
|
onEscKey(): void {
|
|
if (this.isOpen && this.closeOnEsc) {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
onBackdropClick(): void {
|
|
if (this.closeOnBackdropClick) {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
close(): void {
|
|
this.closed.emit();
|
|
}
|
|
|
|
get sizeClass(): string {
|
|
switch (this.size) {
|
|
case 'sm':
|
|
return 'modal-panel--sm';
|
|
case 'lg':
|
|
return 'modal-panel--lg';
|
|
case 'xl':
|
|
return 'modal-panel--xl';
|
|
case '2xl':
|
|
return 'modal-panel--2xl';
|
|
case 'full':
|
|
return 'modal-panel--full';
|
|
default:
|
|
return 'modal-panel--md';
|
|
}
|
|
}
|
|
|
|
get positionClass(): string {
|
|
return this.position === 'top' ? 'items-start pt-12 md:pt-16' : 'items-center';
|
|
}
|
|
}
|
|
|