feat(main-carousel): enhance image handling for responsive design

This commit is contained in:
2026-09-30 09:00:53 -03:00
parent f70b93922e
commit c851272ebe
5 changed files with 53 additions and 20 deletions

View File

@@ -75,7 +75,7 @@ export interface TenantEvent {
}
export interface WebsiteExtras {
carousel?: string[];
carousel?: (string | { desktop: string; mobile: string })[];
immersiveHeroCarousel?: string[];
immersiveHero?: {
eyebrow: string;

View File

@@ -12,18 +12,21 @@
>
<div class="main-carousel__viewport" aria-live="off">
@for (image of images(); track $index; let index = $index) {
<img
class="main-carousel__image"
[class.main-carousel__image--active]="index === activeIndex()"
[attr.src]="imageSource(image, index)"
[alt]="imageAlt(index)"
[attr.aria-hidden]="index === activeIndex() ? null : 'true'"
loading="eager"
[attr.fetchpriority]="index === activeIndex() ? 'high' : 'low'"
draggable="false"
(load)="handleImageLoad(index)"
(error)="handleImageError(index)"
/>
<picture>
<source media="(max-width: 576px)" [attr.srcset]="mobileImageSource(image, index)" />
<img
class="main-carousel__image"
[class.main-carousel__image--active]="index === activeIndex()"
[attr.src]="imageSource(image, index)"
[alt]="imageAlt(index)"
[attr.aria-hidden]="index === activeIndex() ? null : 'true'"
loading="eager"
[attr.fetchpriority]="index === activeIndex() ? 'high' : 'low'"
draggable="false"
(load)="handleImageLoad(index)"
(error)="handleImageError(index)"
/>
</picture>
}
</div>

View File

@@ -17,9 +17,7 @@
&__viewport {
position: relative;
width: 100%;
aspect-ratio: 4 / 1;
min-height: 9rem;
max-height: 25rem;
aspect-ratio: 355 / 90;
}
&__image {

View File

@@ -18,12 +18,20 @@ class TestHostComponent {
readonly images = ['/images/one.webp', '/images/two.webp', '/images/three.webp'];
}
@Component({
imports: [MainCarouselComponent],
template: `<app-main-carousel [images]="images" />`,
})
class ResponsiveTestHostComponent {
readonly images = [{ desktop: '/images/desktop.webp', mobile: '/images/mobile.webp' }];
}
describe('MainCarouselComponent', () => {
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestHostComponent],
imports: [TestHostComponent, ResponsiveTestHostComponent],
}).compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
@@ -41,6 +49,19 @@ describe('MainCarouselComponent', () => {
expect(images[0].classList).toContain('main-carousel__image--active');
});
it('usa la variante móvil del crop cuando está disponible', () => {
const responsiveFixture = TestBed.createComponent(ResponsiveTestHostComponent);
responsiveFixture.detectChanges();
const picture = responsiveFixture.nativeElement.querySelector('picture') as HTMLPictureElement;
const source = picture.querySelector('source') as HTMLSourceElement;
const image = picture.querySelector('img') as HTMLImageElement;
expect(source.getAttribute('media')).toBe('(max-width: 576px)');
expect(source.getAttribute('srcset')).toBe('/images/mobile.webp');
expect(image.getAttribute('src')).toBe('/images/desktop.webp');
});
it('carga las imágenes secuencialmente y prioriza la imagen visible', () => {
const images = fixture.nativeElement.querySelectorAll(
'.main-carousel__image',

View File

@@ -11,6 +11,9 @@ import {
signal,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import type { WebsiteExtras } from '../../../core/services/tenant.interface';
type CarouselImage = NonNullable<WebsiteExtras['carousel']>[number];
@Component({
selector: 'app-main-carousel',
@@ -26,7 +29,7 @@ export class MainCarouselComponent {
private readonly settledIndexes = signal<ReadonlySet<number>>(new Set());
private firstImageReadyEmitted = false;
readonly images = input.required<readonly string[]>();
readonly images = input.required<readonly CarouselImage[]>();
readonly imageAlts = input<readonly string[]>([]);
readonly autoSlideInterval = input(5000);
readonly ariaLabel = input('Imágenes destacadas');
@@ -91,13 +94,21 @@ export class MainCarouselComponent {
this.paused.set(paused);
}
protected imageSource(image: string, index: number): string | null {
protected imageSource(image: CarouselImage, index: number): string | null {
const shouldLoad =
this.loadedIndexes().has(index) ||
index === this.sequentialLoadIndex() ||
index === this.activeIndex();
return shouldLoad ? image : null;
return shouldLoad ? (typeof image === 'string' ? image : image.desktop) : null;
}
protected mobileImageSource(image: CarouselImage, index: number): string | null {
if (typeof image === 'string' || this.imageSource(image, index) === null) {
return null;
}
return image.mobile;
}
protected handleImageLoad(index: number): void {