feat: implement modal service and components for enhanced user interaction, including modal showcase in demo page

This commit is contained in:
2026-07-02 11:24:31 -03:00
parent d31cb65d65
commit b3782a6cc1
15 changed files with 1031 additions and 2 deletions

View File

@@ -0,0 +1,39 @@
<div class="modal-shell" (click)="backdropClick.emit()">
<div
class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-shell__dialog"
[ngClass]="dialogClass()"
(click)="$event.stopPropagation()"
>
<div
#panel
class="modal-content border-0 shadow-lg modal-shell__content"
role="dialog"
aria-modal="true"
[attr.aria-labelledby]="ariaLabelledBy"
tabindex="-1"
>
@if (title() || showCloseButton()) {
<header class="modal-header border-0 modal-shell__header">
@if (title()) {
<h2 class="modal-title modal-shell__title" [id]="titleId">{{ title() }}</h2>
} @else {
<span></span>
}
@if (showCloseButton()) {
<button
type="button"
class="btn-close"
aria-label="Cerrar modal"
(click)="closeRequested.emit()"
></button>
}
</header>
}
<div class="modal-body modal-shell__body">
<ng-content />
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,87 @@
:host {
display: contents;
}
.modal-shell {
position: fixed;
inset: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: rgba(16, 18, 22, 0.48);
backdrop-filter: blur(2px);
}
.modal-shell__dialog {
width: min(100%, 40rem);
margin: 0;
}
.modal-shell__dialog.modal-sm {
width: min(100%, 24rem);
}
.modal-shell__dialog.modal-lg {
width: min(100%, 52rem);
}
.modal-shell__dialog.modal-xl {
width: min(100%, 68rem);
}
.modal-shell__dialog--full {
width: min(100%, 92rem);
height: min(100%, calc(100dvh - 2rem));
}
.modal-shell__content {
max-height: calc(100dvh - 2rem);
border-radius: 1.25rem;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 248, 248, 0.98));
}
.modal-shell__dialog--full .modal-shell__content {
height: 100%;
max-height: 100%;
}
.modal-shell__header {
align-items: flex-start;
padding: 1.25rem 1.25rem 0.75rem;
}
.modal-shell__title {
margin: 0;
font-size: clamp(1.15rem, 1rem + 0.4vw, 1.5rem);
font-weight: 700;
line-height: 1.15;
color: #202020;
}
.modal-shell__body {
padding: 0 1.25rem 1.25rem;
color: #303030;
}
@media (max-width: 576px) {
.modal-shell {
padding: 0.75rem;
align-items: flex-end;
}
.modal-shell__dialog,
.modal-shell__dialog.modal-sm,
.modal-shell__dialog.modal-lg,
.modal-shell__dialog.modal-xl,
.modal-shell__dialog--full {
width: 100%;
}
.modal-shell__content {
max-height: min(100dvh - 1.5rem, 48rem);
border-radius: 1.25rem 1.25rem 0.75rem 0.75rem;
}
}

View File

@@ -0,0 +1,64 @@
import { NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
computed,
input,
output,
viewChild
} from '@angular/core';
import { ModalSize } from '../../../core/services/modal.service';
@Component({
selector: 'app-modal-shell',
imports: [NgClass],
templateUrl: './modal-shell.component.html',
styleUrl: './modal-shell.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalShellComponent {
private readonly panel = viewChild.required<ElementRef<HTMLElement>>('panel');
readonly title = input<string | undefined>();
readonly size = input<ModalSize>('md');
readonly showCloseButton = input(true);
readonly backdropClick = output<void>();
readonly closeRequested = output<void>();
protected readonly dialogClass = computed(() => {
const size = this.size();
return {
'modal-sm': size === 'sm',
'modal-lg': size === 'lg',
'modal-xl': size === 'xl',
'modal-shell__dialog--full': size === 'full'
};
});
protected readonly titleId = `modal-title-${Math.random().toString(36).slice(2, 9)}`;
protected get ariaLabelledBy(): string | null {
return this.title() ? this.titleId : null;
}
focusInitialElement(): void {
const panel = this.panel().nativeElement;
const focusTarget = panel.querySelector<HTMLElement>(
[
'[autofocus]',
'button:not([disabled])',
'[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ')
);
(focusTarget ?? panel).focus();
}
}