feat(carousel): enhance carousel layout and control positioning for improved responsiveness

This commit is contained in:
2026-07-23 12:29:08 -03:00
parent ca1cacc9ea
commit 4f444fd757
4 changed files with 52 additions and 14 deletions

View File

@@ -1,5 +1,9 @@
<section class="carousel" role="region" [attr.aria-label]="ariaLabel()"> <section class="carousel" role="region" [attr.aria-label]="ariaLabel()">
<div class="carousel__frame" [style.width.px]="viewportWidth()"> <div
class="carousel__frame"
[class.carousel__frame--with-controls]="hasMultipleItems()"
[style.width.px]="frameWidth()"
>
<div <div
#viewport #viewport
class="carousel__viewport" class="carousel__viewport"

View File

@@ -8,11 +8,23 @@
&__frame { &__frame {
position: relative; position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: center;
width: 100%; width: 100%;
margin-inline: auto; margin-inline: auto;
&--with-controls {
grid-template-columns: 3rem minmax(0, 1fr) 3rem;
.carousel__viewport {
grid-column: 2;
}
}
} }
&__viewport { &__viewport {
grid-row: 1;
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
overscroll-behavior-inline: contain; overscroll-behavior-inline: contain;
@@ -70,19 +82,25 @@
} }
&__control { &__control {
position: absolute; grid-row: 1;
top: 50%; justify-self: center;
display: grid; display: grid;
width: 2.25rem; width: 2.25rem;
height: 2.25rem; height: 2.25rem;
padding: 0; padding: 0;
color: #fff; color: var(--color-primary, #6376f3);
cursor: pointer; cursor: pointer;
background: rgb(0 0 0 / 55%); background: transparent;
border: 0; border: 0;
border-radius: 50%;
place-items: center; place-items: center;
transform: translateY(-50%); transition:
color 150ms ease,
transform 150ms ease;
&:hover:not(:disabled) {
color: color-mix(in srgb, var(--color-primary, #6376f3) 78%, #000);
transform: scale(1.12);
}
&:disabled { &:disabled {
cursor: default; cursor: default;
@@ -96,11 +114,11 @@
} }
&--previous { &--previous {
left: 0.5rem; grid-column: 1;
} }
&--next { &--next {
right: 0.5rem; grid-column: 3;
} }
} }
} }

View File

@@ -117,7 +117,7 @@ describe('CarouselComponent', () => {
const frame = fixture.nativeElement.querySelector('.carousel__frame') as HTMLElement; const frame = fixture.nativeElement.querySelector('.carousel__frame') as HTMLElement;
expect(frame.style.width).toBe('632px'); expect(frame.style.width).toBe('512px');
}); });
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', () => {
@@ -129,7 +129,7 @@ describe('CarouselComponent', () => {
Object.defineProperty(host, 'clientWidth', { Object.defineProperty(host, 'clientWidth', {
configurable: true, configurable: true,
value: 250, value: 366,
}); });
firstItem.getBoundingClientRect = vi.fn( firstItem.getBoundingClientRect = vi.fn(
() => () =>

View File

@@ -39,6 +39,8 @@ interface CarouselPage<T> {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class CarouselComponent<T> implements AfterViewInit { export class CarouselComponent<T> implements AfterViewInit {
private static readonly CONTROL_SLOT_WIDTH = 48;
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly platformId = inject(PLATFORM_ID); private readonly platformId = inject(PLATFORM_ID);
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef); private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
@@ -62,6 +64,13 @@ export class CarouselComponent<T> implements AfterViewInit {
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())),
@@ -219,8 +228,12 @@ export class CarouselComponent<T> implements AfterViewInit {
const viewport = this.viewport().nativeElement; const viewport = this.viewport().nativeElement;
const firstItem = viewport.querySelector<HTMLElement>('.carousel__item'); const firstItem = viewport.querySelector<HTMLElement>('.carousel__item');
const availableWidth = this.host.nativeElement.clientWidth; const availableWidth = this.host.nativeElement.clientWidth;
const availableViewportWidth = Math.max(
availableWidth - (this.hasMultipleItems() ? CarouselComponent.CONTROL_SLOT_WIDTH * 2 : 0),
0,
);
if (!firstItem || availableWidth <= 0) { if (!firstItem || availableViewportWidth <= 0) {
this.viewportWidth.set(null); this.viewportWidth.set(null);
this.refreshNavigation(); this.refreshNavigation();
return; return;
@@ -235,7 +248,10 @@ export class CarouselComponent<T> implements AfterViewInit {
} }
const gap = this.resolvedGap(); const gap = this.resolvedGap();
const fittingItems = Math.max(1, Math.floor((availableWidth + gap) / (itemWidth + gap))); const fittingItems = Math.max(
1,
Math.floor((availableViewportWidth + gap) / (itemWidth + 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 fittedWidth = visibleItems * itemWidth + Math.max(visibleItems - 1, 0) * gap;
@@ -245,7 +261,7 @@ export class CarouselComponent<T> implements AfterViewInit {
this.transitionDirection.set(0); this.transitionDirection.set(0);
} }
this.viewportWidth.set(Math.min(Math.ceil(fittedWidth), availableWidth)); this.viewportWidth.set(Math.min(Math.ceil(fittedWidth), availableViewportWidth));
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);