feat(carousel): enhance carousel layout and control positioning for improved responsiveness
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user