feat(carousel): adjust item width calculation for better distribution and responsiveness
This commit is contained in:
@@ -1,9 +1,5 @@
|
|||||||
<section class="carousel" role="region" [attr.aria-label]="ariaLabel()">
|
<section class="carousel" role="region" [attr.aria-label]="ariaLabel()">
|
||||||
<div
|
<div class="carousel__frame" [class.carousel__frame--with-controls]="hasMultipleItems()">
|
||||||
class="carousel__frame"
|
|
||||||
[class.carousel__frame--with-controls]="hasMultipleItems()"
|
|
||||||
[style.width.px]="frameWidth()"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
#viewport
|
#viewport
|
||||||
class="carousel__viewport"
|
class="carousel__viewport"
|
||||||
@@ -29,7 +25,7 @@
|
|||||||
[attr.inert]="page.slot === 'current' ? null : ''"
|
[attr.inert]="page.slot === 'current' ? null : ''"
|
||||||
>
|
>
|
||||||
@for (entry of page.items; track trackBy()(entry.index, entry.item)) {
|
@for (entry of page.items; track trackBy()(entry.index, entry.item)) {
|
||||||
<div class="carousel__item">
|
<div class="carousel__item" [style.width.px]="itemWidth()">
|
||||||
<ng-container
|
<ng-container
|
||||||
[ngTemplateOutlet]="itemTemplate()"
|
[ngTemplateOutlet]="itemTemplate()"
|
||||||
[ngTemplateOutletContext]="{ $implicit: entry.item, index: entry.index }"
|
[ngTemplateOutletContext]="{ $implicit: entry.item, index: entry.index }"
|
||||||
@@ -42,7 +38,7 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<div class="carousel__linear-track" [style.gap.px]="resolvedGap()">
|
<div class="carousel__linear-track" [style.gap.px]="resolvedGap()">
|
||||||
@for (item of items(); track trackBy()($index, item); let index = $index) {
|
@for (item of items(); track trackBy()($index, item); let index = $index) {
|
||||||
<div class="carousel__item">
|
<div class="carousel__item" [style.width.px]="itemWidth()">
|
||||||
<ng-container
|
<ng-container
|
||||||
[ngTemplateOutlet]="itemTemplate()"
|
[ngTemplateOutlet]="itemTemplate()"
|
||||||
[ngTemplateOutletContext]="{ $implicit: item, index }"
|
[ngTemplateOutletContext]="{ $implicit: item, index }"
|
||||||
|
|||||||
@@ -79,6 +79,10 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__control {
|
&__control {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ describe('CarouselComponent', () => {
|
|||||||
expect(scrollBy).toHaveBeenCalledTimes(2);
|
expect(scrollBy).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ajusta y centra el viewport para mostrar únicamente elementos completos', () => {
|
it('usa todo el ancho y distribuye el espacio sobrante entre elementos completos', () => {
|
||||||
const host = fixture.nativeElement.querySelector('app-carousel') as HTMLElement;
|
const host = fixture.nativeElement.querySelector('app-carousel') as HTMLElement;
|
||||||
const firstItem = fixture.nativeElement.querySelector('.carousel__item') as HTMLElement;
|
const firstItem = fixture.nativeElement.querySelector('.carousel__item') as HTMLElement;
|
||||||
|
|
||||||
@@ -116,8 +116,11 @@ describe('CarouselComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const frame = fixture.nativeElement.querySelector('.carousel__frame') as HTMLElement;
|
const frame = fixture.nativeElement.querySelector('.carousel__frame') as HTMLElement;
|
||||||
|
const items = fixture.nativeElement.querySelectorAll('.carousel__item');
|
||||||
|
|
||||||
expect(frame.style.width).toBe('512px');
|
expect(frame.style.width).toBe('');
|
||||||
|
expect(items[0].style.width).toBe('269px');
|
||||||
|
expect(items[1].style.width).toBe('269px');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mantiene preparadas las páginas adyacentes y rota al terminar la transición', () => {
|
it('mantiene preparadas las páginas adyacentes y rota al terminar la transición', () => {
|
||||||
|
|||||||
@@ -59,18 +59,11 @@ export class CarouselComponent<T> implements AfterViewInit {
|
|||||||
protected readonly canScrollPrevious = signal(false);
|
protected readonly canScrollPrevious = signal(false);
|
||||||
protected readonly canScrollNext = signal(false);
|
protected readonly canScrollNext = signal(false);
|
||||||
protected readonly hasOverflow = signal(false);
|
protected readonly hasOverflow = signal(false);
|
||||||
protected readonly viewportWidth = signal<number | null>(null);
|
protected readonly itemWidth = signal<number | null>(null);
|
||||||
protected readonly itemsPerPage = signal(Number.MAX_SAFE_INTEGER);
|
protected readonly itemsPerPage = signal(Number.MAX_SAFE_INTEGER);
|
||||||
protected readonly currentPage = signal(0);
|
protected readonly currentPage = signal(0);
|
||||||
protected readonly transitionDirection = signal<-1 | 0 | 1>(0);
|
protected readonly transitionDirection = signal<-1 | 0 | 1>(0);
|
||||||
protected readonly hasMultipleItems = computed(() => this.items().length > 1);
|
protected readonly hasMultipleItems = computed(() => this.items().length > 1);
|
||||||
protected readonly frameWidth = computed(() => {
|
|
||||||
const viewportWidth = this.viewportWidth();
|
|
||||||
|
|
||||||
return viewportWidth === null
|
|
||||||
? null
|
|
||||||
: viewportWidth + (this.hasMultipleItems() ? CarouselComponent.CONTROL_SLOT_WIDTH * 2 : 0);
|
|
||||||
});
|
|
||||||
protected readonly resolvedGap = computed(() => Math.max(this.gap(), 0));
|
protected readonly resolvedGap = computed(() => Math.max(this.gap(), 0));
|
||||||
protected readonly pageCount = computed(() =>
|
protected readonly pageCount = computed(() =>
|
||||||
Math.max(1, Math.ceil(this.items().length / this.itemsPerPage())),
|
Math.max(1, Math.ceil(this.items().length / this.itemsPerPage())),
|
||||||
@@ -234,15 +227,18 @@ export class CarouselComponent<T> implements AfterViewInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!firstItem || availableViewportWidth <= 0) {
|
if (!firstItem || availableViewportWidth <= 0) {
|
||||||
this.viewportWidth.set(null);
|
this.itemWidth.set(null);
|
||||||
this.refreshNavigation();
|
this.refreshNavigation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemWidth = firstItem.getBoundingClientRect().width;
|
const assignedWidth = firstItem.style.width;
|
||||||
|
firstItem.style.width = '';
|
||||||
|
const naturalItemWidth = firstItem.getBoundingClientRect().width;
|
||||||
|
firstItem.style.width = assignedWidth;
|
||||||
|
|
||||||
if (itemWidth <= 0) {
|
if (naturalItemWidth <= 0) {
|
||||||
this.viewportWidth.set(null);
|
this.itemWidth.set(null);
|
||||||
this.refreshNavigation();
|
this.refreshNavigation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -250,10 +246,11 @@ export class CarouselComponent<T> implements AfterViewInit {
|
|||||||
const gap = this.resolvedGap();
|
const gap = this.resolvedGap();
|
||||||
const fittingItems = Math.max(
|
const fittingItems = Math.max(
|
||||||
1,
|
1,
|
||||||
Math.floor((availableViewportWidth + gap) / (itemWidth + gap)),
|
Math.floor((availableViewportWidth + gap) / (naturalItemWidth + gap)),
|
||||||
);
|
);
|
||||||
const visibleItems = Math.min(fittingItems, this.items().length);
|
const visibleItems = Math.min(fittingItems, this.items().length);
|
||||||
const fittedWidth = visibleItems * itemWidth + Math.max(visibleItems - 1, 0) * gap;
|
const distributedItemWidth =
|
||||||
|
(availableViewportWidth - Math.max(visibleItems - 1, 0) * gap) / visibleItems;
|
||||||
|
|
||||||
if (this.itemsPerPage() !== fittingItems) {
|
if (this.itemsPerPage() !== fittingItems) {
|
||||||
this.itemsPerPage.set(fittingItems);
|
this.itemsPerPage.set(fittingItems);
|
||||||
@@ -261,7 +258,7 @@ export class CarouselComponent<T> implements AfterViewInit {
|
|||||||
this.transitionDirection.set(0);
|
this.transitionDirection.set(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewportWidth.set(Math.min(Math.ceil(fittedWidth), availableViewportWidth));
|
this.itemWidth.set(distributedItemWidth);
|
||||||
if (this.circular()) {
|
if (this.circular()) {
|
||||||
const hasOverflow = this.items().length > fittingItems;
|
const hasOverflow = this.items().length > fittingItems;
|
||||||
this.hasOverflow.set(hasOverflow);
|
this.hasOverflow.set(hasOverflow);
|
||||||
|
|||||||
@@ -34,12 +34,13 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
&--carousel {
|
&--carousel {
|
||||||
width: clamp(15rem, 30vw, 18rem);
|
width: 100%;
|
||||||
|
min-width: clamp(15rem, 30vw, 18rem);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--carousel-row {
|
&--carousel-row {
|
||||||
width: clamp(18rem, 70vw, 32rem);
|
min-width: clamp(18rem, 70vw, 32rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user