Modal Dialog
Build a flexible modal dialog component with backdrop, animations, and focus management
What We're Building
Component Code
modal-dialog.ts
import { bindable, IEventAggregator } from 'aurelia';
import { resolve } from '@aurelia/kernel';
import { queueTask } from '@aurelia/runtime';
import { IPlatform } from '@aurelia/runtime-html';
export class ModalDialog {
@bindable open = false;
@bindable closeOnBackdropClick = true;
@bindable closeOnEscape = true;
@bindable size: 'small' | 'medium' | 'large' | 'full' = 'medium';
private platform = resolve(IPlatform);
private element?: HTMLElement;
private modalElement?: HTMLElement;
private previousActiveElement?: HTMLElement;
private focusableElements: HTMLElement[] = [];
openChanged(newValue: boolean) {
if (newValue) {
this.onOpen();
} else {
this.onClose();
}
}
attaching(initiator: HTMLElement) {
this.element = initiator;
this.modalElement = this.element.querySelector('[data-modal]') as HTMLElement;
}
detaching() {
// Clean up if modal is still open
if (this.open) {
this.cleanupModal();
}
}
closeModal() {
this.open = false;
}
handleBackdropClick(event: MouseEvent) {
// Only close if clicking the backdrop itself, not content inside
if (this.closeOnBackdropClick && event.target === event.currentTarget) {
this.closeModal();
}
}
handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape' && this.closeOnEscape) {
event.preventDefault();
this.closeModal();
return;
}
// Tab key focus trap
if (event.key === 'Tab') {
this.handleTabKey(event);
}
}
private onOpen() {
// Store currently focused element to return focus later
this.previousActiveElement = document.activeElement as HTMLElement;
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Wait for DOM to render, then focus first element
queueTask(() => {
this.updateFocusableElements();
this.focusFirstElement();
});
}
private onClose() {
this.cleanupModal();
}
private cleanupModal() {
// Restore body scroll
document.body.style.overflow = '';
// Return focus to element that opened the modal
if (this.previousActiveElement) {
this.previousActiveElement.focus();
this.previousActiveElement = undefined;
}
}
private updateFocusableElements() {
if (!this.modalElement) return;
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'textarea:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
this.focusableElements = Array.from(
this.modalElement.querySelectorAll(focusableSelectors)
) as HTMLElement[];
}
private focusFirstElement() {
const firstFocusable = this.focusableElements[0];
if (firstFocusable) {
firstFocusable.focus();
}
}
private handleTabKey(event: KeyboardEvent) {
if (this.focusableElements.length === 0) return;
const firstElement = this.focusableElements[0];
const lastElement = this.focusableElements[this.focusableElements.length - 1];
const activeElement = document.activeElement as HTMLElement;
if (event.shiftKey) {
// Shift + Tab: Move backwards
if (activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
} else {
// Tab: Move forwards
if (activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
}
}modal-dialog.html
modal-dialog.css
Usage Examples
Basic Modal
Confirmation Dialog
Form Modal
Full-Screen Modal
Testing
Accessibility Features
Enhancements
1. Add Transition Animations
2. Add Confirmation Before Close
3. Add Modal Service
Best Practices
Summary
Last updated
Was this helpful?