feat: implement global loading state with interceptor and component
This commit is contained in:
@@ -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>
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user