mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merged PR 2047: feat(carousel): convert to transformX with touch support and card animations
feat(carousel): convert to transformX with touch support and card animations - Convert carousel from scroll-based to translate3d() transform positioning - Add touch/swipe support with direct DOM manipulation for smooth 60fps performance - Add mouse drag support for desktop navigation - Implement hardware-accelerated transforms with will-change optimization - Add disabled input to prevent navigation when needed - Fix bounds calculation to use parent viewport width - Add card stacking animation with translateY and rotation effects - Remove shadow for cards beyond 3rd position in stacked mode - Update tests with 4 new disabled state test cases (17 tests total) Refs #5499 Related work items: #5499
This commit is contained in:
committed by
Nino Righi
parent
7f1cdf880f
commit
38de927c4e
@@ -1 +1,4 @@
|
||||
/* Customer card styles - using Tailwind, no additional CSS needed */
|
||||
/* Customer card styles - using Tailwind, no additional CSS needed */
|
||||
:host {
|
||||
@apply rounded-2xl overflow-hidden;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- Card container: 337×213px, rounded-2xl, shadow -->
|
||||
<div
|
||||
class="relative flex h-[14.8125rem] w-[21.0625rem] flex-col overflow-hidden rounded-2xl bg-isa-black shadow-card"
|
||||
class="relative flex h-[14.8125rem] w-[21.0625rem] flex-col bg-isa-black"
|
||||
[attr.data-what]="'customer-card'"
|
||||
[attr.data-which]="card().code"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive({ selector: '[crmCardStackContainer]' })
|
||||
export class CardStackContainerDirective implements AfterViewInit {
|
||||
readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||
|
||||
readonly centerX = signal(0);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.centerX.set(this.getHorizontalCenter());
|
||||
}
|
||||
|
||||
getHorizontalCenter(): number {
|
||||
const el = this.elementRef.nativeElement;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.left + rect.width / 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
computed,
|
||||
Directive,
|
||||
ElementRef,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { CardStackContainerDirective } from './card-stack-container.directive';
|
||||
|
||||
@Directive({
|
||||
selector: '[crmCardStackDistance]',
|
||||
host: {
|
||||
'[style.--distance-to-center.px]': 'distanceToCenter()',
|
||||
},
|
||||
})
|
||||
export class CardStackDistanceDirective implements AfterViewInit {
|
||||
readonly container = inject(CardStackContainerDirective, { host: true });
|
||||
readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||
|
||||
distanceToCenter = computed(() => {
|
||||
const containerCenterX = this.container.centerX();
|
||||
const centerX = this.centerX();
|
||||
return containerCenterX - centerX;
|
||||
});
|
||||
|
||||
centerX = signal(0);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.centerX.set(this.getHorizontalCenter());
|
||||
}
|
||||
|
||||
getHorizontalCenter(): number {
|
||||
const el = this.elementRef.nativeElement;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.left + rect.width / 2;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,68 @@
|
||||
/* Carousel container styles - using Tailwind and ui-carousel, no additional CSS needed */
|
||||
|
||||
/* Keyframes for card entrance animation */
|
||||
@keyframes cardEntrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframes for unstacking animation */
|
||||
@keyframes unstack {
|
||||
from {
|
||||
transform: translateX(var(--distance-to-center));
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
crm-customer-card {
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); /* Ease-out-back for slight overshoot */
|
||||
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.25);
|
||||
|
||||
/* Initial entrance animation with staggered delay */
|
||||
animation: cardEntrance 0.4s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
animation-delay: calc(var(--card-index, 0) * 80ms);
|
||||
}
|
||||
|
||||
/* Unstacked state (default) */
|
||||
crm-customer-card {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Stacked state transformations */
|
||||
.stacked crm-customer-card {
|
||||
transform: translateX(var(--distance-to-center));
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(1) {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(2) {
|
||||
transform: translateX(calc(var(--distance-to-center) - 3.6rem))
|
||||
translateY(1rem) rotate(-2.538deg);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(3) {
|
||||
transform: translateX(calc(var(--distance-to-center) + 3.6rem))
|
||||
translateY(1rem) rotate(2.538deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(n + 4) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Respect user's motion preferences */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
crm-customer-card {
|
||||
animation: none;
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<div class="relative" data-what="customer-cards-carousel">
|
||||
<!-- Carousel with navigation arrows -->
|
||||
<ui-carousel
|
||||
[gap]="'1rem'"
|
||||
[class.stacked]="stacked()"
|
||||
[gap]="stacked() ? '0rem' : '1rem'"
|
||||
[arrowAutoHide]="true"
|
||||
[padding]="'0.75rem 0.5rem'"
|
||||
crmCardStackContainer
|
||||
[disabled]="stacked()"
|
||||
>
|
||||
@for (card of sortedCards(); track card.code) {
|
||||
<crm-customer-card [card]="card" (cardLocked)="cardLocked.emit()" />
|
||||
@for (card of sortedCards(); track card.code; let idx = $index) {
|
||||
<crm-customer-card
|
||||
[card]="card"
|
||||
(cardLocked)="cardLocked.emit()"
|
||||
crmCardStackDistance
|
||||
[style.--card-index]="idx"
|
||||
/>
|
||||
}
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { Component, computed, input, output, signal } from '@angular/core';
|
||||
import { BonusCardInfo } from '@isa/crm/data-access';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
import { CustomerCardComponent } from '../customer-card';
|
||||
import { CardStackContainerDirective } from './card-stack-container.directive';
|
||||
import { CardStackDistanceDirective } from './card-stack-distance.directive';
|
||||
|
||||
/**
|
||||
* Carousel container for displaying multiple customer loyalty cards.
|
||||
@@ -23,11 +25,21 @@ import { CustomerCardComponent } from '../customer-card';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'crm-customer-cards-carousel',
|
||||
imports: [CarouselComponent, CustomerCardComponent],
|
||||
imports: [
|
||||
CarouselComponent,
|
||||
CustomerCardComponent,
|
||||
CardStackDistanceDirective,
|
||||
CardStackContainerDirective,
|
||||
],
|
||||
templateUrl: './customer-cards-carousel.component.html',
|
||||
styleUrl: './customer-cards-carousel.component.css',
|
||||
host: {
|
||||
'(click)': 'unStack()',
|
||||
},
|
||||
})
|
||||
export class CustomerCardsCarouselComponent {
|
||||
stacked = signal(true);
|
||||
|
||||
/**
|
||||
* All bonus cards to display in carousel.
|
||||
*/
|
||||
@@ -51,4 +63,8 @@ export class CustomerCardsCarouselComponent {
|
||||
return a.isActive ? -1 : 1;
|
||||
});
|
||||
});
|
||||
|
||||
unStack(): void {
|
||||
this.stacked.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user