feat: implement global loading state with interceptor and component

This commit is contained in:
2026-07-29 11:03:35 -03:00
parent 1fb9789abf
commit 125d9137fb
11 changed files with 354 additions and 3 deletions

View File

@@ -0,0 +1,11 @@
@if (isLoading()) {
<div
class="global-loading"
role="status"
aria-live="polite"
aria-label="Cargando"
>
<span class="global-loading__spinner" aria-hidden="true"></span>
<span class="visually-hidden">Cargando...</span>
</div>
}

View File

@@ -0,0 +1,31 @@
.global-loading {
position: fixed;
inset: 0;
z-index: 3000;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.64);
backdrop-filter: blur(2px);
cursor: wait;
}
.global-loading__spinner {
width: 3rem;
height: 3rem;
border: 0.25rem solid rgba(32, 32, 32, 0.12);
border-top-color: var(--tenant-primary, #6376f3);
border-radius: 50%;
animation: global-loading-spin 0.75s linear infinite;
}
@keyframes global-loading-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.global-loading__spinner {
animation-duration: 1.5s;
}
}

View File

@@ -0,0 +1,54 @@
import '@angular/compiler';
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { afterEach, describe, expect, it } from 'vitest';
import { GlobalLoadingService } from '../../../core/services/global-loading/global-loading.service';
import { GlobalLoadingComponent } from './global-loading.component';
describe('GlobalLoadingComponent', () => {
afterEach(() => {
TestBed.resetTestingModule();
});
it('renders an accessible overlay while the app is loading', async () => {
const loadingState = signal(true);
await TestBed.configureTestingModule({
imports: [GlobalLoadingComponent],
providers: [
{
provide: GlobalLoadingService,
useValue: { isLoading: loadingState.asReadonly() }
}
]
}).compileComponents();
const fixture = TestBed.createComponent(GlobalLoadingComponent);
fixture.detectChanges();
const overlay = fixture.nativeElement.querySelector('.global-loading');
expect(overlay).not.toBeNull();
expect(overlay.getAttribute('role')).toBe('status');
expect(overlay.getAttribute('aria-label')).toBe('Cargando');
});
it('does not render the overlay while the app is idle', async () => {
const loadingState = signal(false);
await TestBed.configureTestingModule({
imports: [GlobalLoadingComponent],
providers: [
{
provide: GlobalLoadingService,
useValue: { isLoading: loadingState.asReadonly() }
}
]
}).compileComponents();
const fixture = TestBed.createComponent(GlobalLoadingComponent);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.global-loading')).toBeNull();
});
});

View File

@@ -0,0 +1,15 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { GlobalLoadingService } from '../../../core/services/global-loading/global-loading.service';
@Component({
selector: 'app-global-loading',
templateUrl: './global-loading.component.html',
styleUrl: './global-loading.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GlobalLoadingComponent {
private readonly loadingService = inject(GlobalLoadingService);
protected readonly isLoading = this.loadingService.isLoading;
}