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 { export interface WebsiteExtras {
carousel?: string[]; carousel?: (string | { desktop: string; mobile: string })[];
immersiveHeroCarousel?: string[]; immersiveHeroCarousel?: string[];
immersiveHero?: { immersiveHero?: {
eyebrow: string; eyebrow: string;

View File

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

View File

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

View File

@@ -18,12 +18,20 @@ class TestHostComponent {
readonly images = ['/images/one.webp', '/images/two.webp', '/images/three.webp']; 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', () => { describe('MainCarouselComponent', () => {
let fixture: ComponentFixture<TestHostComponent>; let fixture: ComponentFixture<TestHostComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TestHostComponent], imports: [TestHostComponent, ResponsiveTestHostComponent],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(TestHostComponent); fixture = TestBed.createComponent(TestHostComponent);
@@ -41,6 +49,19 @@ describe('MainCarouselComponent', () => {
expect(images[0].classList).toContain('main-carousel__image--active'); 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', () => { it('carga las imágenes secuencialmente y prioriza la imagen visible', () => {
const images = fixture.nativeElement.querySelectorAll( const images = fixture.nativeElement.querySelectorAll(
'.main-carousel__image', '.main-carousel__image',

View File

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