feat(modal): add simple modal component with open and close functionality

This commit is contained in:
2026-07-03 09:00:49 -03:00
parent bedb74ebd5
commit 3697c4b97c
8 changed files with 234 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ import { firstValueFrom } from 'rxjs';
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
import { SimpleModalComponent } from '../../shared/components/simple-modal/simple-modal.component';
import {
MODAL_DATA,
ModalRef,
@@ -212,6 +213,41 @@ describe('ModalService', () => {
showCloseButton: false
});
});
it('opens the simple modal with default button label', () => {
service.openSimple({
title: 'Aviso',
content: 'Este es un aviso simple.'
});
const activeModal = service.activeModal();
expect(activeModal?.component).toBe(SimpleModalComponent);
expect(activeModal?.config).toEqual({
title: 'Aviso',
data: {
content: 'Este es un aviso simple.',
buttonLabel: 'Entendido'
},
size: 'md',
closeOnBackdrop: true,
closeOnEscape: true,
showCloseButton: true
});
});
it('maps the simple modal close result to undefined', async () => {
const result$ = service.openSimple({
title: 'Aviso',
content: 'Este es un aviso simple.'
});
const activeModal = service.activeModal();
const resultPromise = firstValueFrom(result$);
activeModal?.ref.close();
await expect(resultPromise).resolves.toBeUndefined();
});
});
@Component({

View File

@@ -2,6 +2,7 @@ import { InjectionToken, Injectable, Type, signal } from '@angular/core';
import { Observable, Subject, map } from 'rxjs';
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
import { SimpleModalComponent } from '../../shared/components/simple-modal/simple-modal.component';
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
export type ModalDismissReason =
@@ -43,6 +44,17 @@ export interface ConfirmModalConfig
cancelLabel?: string;
}
export interface SimpleModalData {
content: string;
buttonLabel: string;
}
export interface SimpleModalConfig
extends Omit<ModalConfig<SimpleModalData>, 'data'> {
content: string;
buttonLabel?: string;
}
export interface ActiveModalState<TResult = unknown, TData = unknown> {
component: Type<unknown>;
config: NormalizedModalConfig<TData>;
@@ -158,6 +170,19 @@ export class ModalService {
);
}
openSimple(config: SimpleModalConfig): Observable<void> {
return this.openSimpleRef(config).afterClosed$.pipe(
map(() => undefined)
);
}
openSimpleRef(config: SimpleModalConfig): ModalRef<void> {
return this.open(
SimpleModalComponent,
this.buildSimpleModalConfig(config)
);
}
private close<TResult>(ref: ModalRef<TResult>, result?: TResult): void {
if (this.activeModalState()?.ref !== ref) {
return;
@@ -207,4 +232,22 @@ export class ModalService {
}
};
}
private buildSimpleModalConfig(
config: SimpleModalConfig
): ModalConfig<SimpleModalData> {
const {
content,
buttonLabel = 'Entendido',
...modalConfig
} = config;
return {
...modalConfig,
data: {
content,
buttonLabel
}
};
}
}

View File

@@ -91,6 +91,9 @@
<app-button variant="danger-secondary" (click)="openWideModal()">
Abrir modal ancho
</app-button>
<app-button (click)="openSimpleModal()">
Abrir simple modal
</app-button>
</div>
<div class="modal-showcase__result" data-testid="modal-last-result">
{{ lastModalResult }}

View File

@@ -226,6 +226,16 @@ export class ReutilizablesTestPageComponent {
});
}
protected openSimpleModal(): void {
this.modalService.openSimple({
title: 'Mensaje del sistema',
content: 'Este es un mensaje simple del sistema que no requiere confirmación.',
buttonLabel: 'Entendido'
}).subscribe(() => {
this.lastModalResult = 'Simple modal cerrado';
});
}
private openConfirmModal(config: Parameters<ModalService['openConfirm']>[0]): void {
this.modalService.openConfirm(config).subscribe((confirmed) => {
this.lastModalResult = `Resultado: ${confirmed}`;

View File

@@ -0,0 +1,9 @@
<div class="simple-modal">
<p class="simple-modal__content">{{ data.content }}</p>
<div class="simple-modal__actions">
<app-button (click)="close()">
{{ data.buttonLabel }}
</app-button>
</div>
</div>

View File

@@ -0,0 +1,19 @@
.simple-modal {
display: grid;
gap: 1.5rem;
justify-items: center;
text-align: center;
}
.simple-modal__content {
margin: 0;
color: #666666;
line-height: 1.5;
}
.simple-modal__actions {
display: flex;
justify-content: center;
gap: 0.75rem;
flex-wrap: wrap;
}

View File

@@ -0,0 +1,90 @@
import '@angular/compiler';
import { TestBed, getTestBed } from '@angular/core/testing';
import {
BrowserTestingModule,
platformBrowserTesting
} from '@angular/platform-browser/testing';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import {
SimpleModalData,
MODAL_DATA,
ModalRef
} from '../../../core/services/modal.service';
import { SimpleModalComponent } from './simple-modal.component';
describe('SimpleModalComponent', () => {
const data: SimpleModalData = {
content: 'Este es un mensaje simple.',
buttonLabel: 'Entendido'
};
beforeAll(() => {
try {
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting()
);
} catch {
// Test environment may already be initialized by another setup entrypoint.
}
});
afterEach(() => {
TestBed.resetTestingModule();
});
it('renders the configured content and button label', async () => {
await TestBed.configureTestingModule({
imports: [SimpleModalComponent],
providers: [
{
provide: MODAL_DATA,
useValue: data
},
{
provide: ModalRef,
useValue: {
close: vi.fn()
}
}
]
}).compileComponents();
const fixture = TestBed.createComponent(SimpleModalComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain(data.content);
expect(element.textContent).toContain(data.buttonLabel);
});
it('closes when clicking the primary button', async () => {
const closeSpy = vi.fn();
await TestBed.configureTestingModule({
imports: [SimpleModalComponent],
providers: [
{
provide: MODAL_DATA,
useValue: data
},
{
provide: ModalRef,
useValue: {
close: closeSpy
}
}
]
}).compileComponents();
const fixture = TestBed.createComponent(SimpleModalComponent);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(closeSpy).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
SimpleModalData,
MODAL_DATA,
ModalRef
} from '../../../core/services/modal.service';
import { ButtonComponent } from '../button/button.component';
@Component({
selector: 'app-simple-modal',
imports: [ButtonComponent],
templateUrl: './simple-modal.component.html',
styleUrl: './simple-modal.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SimpleModalComponent {
protected readonly data = inject<SimpleModalData>(MODAL_DATA);
private readonly modalRef = inject<ModalRef<void>>(ModalRef);
protected close(): void {
this.modalRef.close();
}
}