Files
Basic_theme_configuration_a…/angular/src/app/components/modal/modal.component.ts
T
sayan.kolay b6f2213ccf feat: Add newProjectConfigureHttpApiModule for API configuration and localization
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
2026-08-31 16:18:47 +05:30

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';
}
}