feat(main-carousel): enhance image handling for responsive design
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
>
|
>
|
||||||
<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) {
|
||||||
|
<picture>
|
||||||
|
<source media="(max-width: 576px)" [attr.srcset]="mobileImageSource(image, index)" />
|
||||||
<img
|
<img
|
||||||
class="main-carousel__image"
|
class="main-carousel__image"
|
||||||
[class.main-carousel__image--active]="index === activeIndex()"
|
[class.main-carousel__image--active]="index === activeIndex()"
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
(load)="handleImageLoad(index)"
|
(load)="handleImageLoad(index)"
|
||||||
(error)="handleImageError(index)"
|
(error)="handleImageError(index)"
|
||||||
/>
|
/>
|
||||||
|
</picture>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user