From 0ba6dcccf69ced121aa29ae8190bddc228c2a125 Mon Sep 17 00:00:00 2001 From: Subhajit Ghosh Date: Tue, 26 Sep 2023 05:15:06 +0000 Subject: [PATCH] Created carousel component for testimonials --- .../ui/src/lib/carousel/_carousel-theme.scss. | 16 ++ libs/ui/src/lib/carousel/carousel.html | 16 ++ libs/ui/src/lib/carousel/carousel.scss | 37 ++++ libs/ui/src/lib/carousel/carousel.ts | 162 ++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 libs/ui/src/lib/carousel/_carousel-theme.scss. create mode 100644 libs/ui/src/lib/carousel/carousel.html create mode 100644 libs/ui/src/lib/carousel/carousel.scss create mode 100644 libs/ui/src/lib/carousel/carousel.ts diff --git a/libs/ui/src/lib/carousel/_carousel-theme.scss. b/libs/ui/src/lib/carousel/_carousel-theme.scss. new file mode 100644 index 000000000..4c28229a4 --- /dev/null +++ b/libs/ui/src/lib/carousel/_carousel-theme.scss. @@ -0,0 +1,16 @@ +@use 'sass:map'; +@use '@angular/material' as mat; + +@mixin theme($theme) { + $primary: map.get($theme, primary); + $accent: map.get($theme, accent); + $warn: map.get($theme, warn); + $background: map.get($theme, background); + $foreground: map.get($theme, foreground); + + .docs-carousel-nav.mat-mdc-mini-fab { + background: mat.get-color-from-palette($background, background); + color: mat.get-color-from-palette($foreground, secondary-text) + } + +} \ No newline at end of file diff --git a/libs/ui/src/lib/carousel/carousel.html b/libs/ui/src/lib/carousel/carousel.html new file mode 100644 index 000000000..efc91781c --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.html @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/libs/ui/src/lib/carousel/carousel.scss b/libs/ui/src/lib/carousel/carousel.scss new file mode 100644 index 000000000..15a2b0daa --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.scss @@ -0,0 +1,37 @@ +app-carousel { + display: block; + position: relative; + } + + .docs-carousel-content { + display: flex; + flex-direction: row; + outline: none; + transition: transform 0.5s ease-in-out; + + .animations-disabled & { + transition: none; + } + } + + .docs-carousel-content-wrapper { + overflow: hidden; + } + + [carousel-item] { + flex-shrink: 0; + } + + button.docs-carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + } + + .docs-carousel-nav-prev { + left: -50px; + } + + .docs-carousel-nav-next { + right: -50px; + } \ No newline at end of file diff --git a/libs/ui/src/lib/carousel/carousel.ts b/libs/ui/src/lib/carousel/carousel.ts new file mode 100644 index 000000000..a025c6142 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.ts @@ -0,0 +1,162 @@ +import { + AfterContentInit, + Component, + ContentChildren, + Directive, + ElementRef, + HostBinding, + Inject, + Input, + Optional, + QueryList, + ViewChild, + ViewEncapsulation, + } from '@angular/core'; + import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y'; + import {LEFT_ARROW, RIGHT_ARROW, TAB} from '@angular/cdk/keycodes'; + import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; + import {MatIconModule} from '@angular/material/icon'; + import {MatButtonModule} from '@angular/material/button'; + import {NgIf} from '@angular/common'; + + @Directive({ + selector: '[carousel-item]', + standalone: true, + }) + export class CarouselItem implements FocusableOption { + @HostBinding('attr.role') readonly role = 'listitem'; + @HostBinding('tabindex') tabindex = '-1'; + + constructor(readonly element: ElementRef) {} + + focus(): void { + this.element.nativeElement.focus({preventScroll: true}); + } + } + + @Component({ + selector: 'app-carousel', + templateUrl: './carousel.html', + styleUrls: ['./carousel.scss'], + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [ + NgIf, + MatButtonModule, + MatIconModule, + ], + }) + export class Carousel implements AfterContentInit { + @Input('aria-label') ariaLabel: string | undefined; + @ContentChildren(CarouselItem) items!: QueryList; + @ViewChild('list') list!: ElementRef; + @HostBinding('class.animations-disabled') readonly animationsDisabled: boolean; + position = 0; + showPrevArrow = false; + showNextArrow = true; + index = 0; + private _keyManager!: FocusKeyManager; + + onKeydown({keyCode}: KeyboardEvent) { + const manager = this._keyManager; + const previousActiveIndex = manager.activeItemIndex; + + if (keyCode === LEFT_ARROW) { + manager.setPreviousItemActive(); + } else if (keyCode === RIGHT_ARROW) { + manager.setNextItemActive(); + } else if (keyCode === TAB && !manager.activeItem) { + manager.setFirstItemActive(); + } + + if (manager.activeItemIndex != null && manager.activeItemIndex !== previousActiveIndex) { + this.index = manager.activeItemIndex; + this._updateItemTabIndices(); + + if (this._isOutOfView(this.index)) { + this._scrollToActiveItem(); + } + } + } + + constructor(@Optional() @Inject(ANIMATION_MODULE_TYPE) animationsModule?: string) { + this.animationsDisabled = animationsModule === 'NoopAnimations'; + } + + ngAfterContentInit(): void { + this._keyManager = new FocusKeyManager(this.items); + } + + /** Goes to the next set of items */ + next() { + for (let i = this.index; i < this.items.length; i++) { + if (this._isOutOfView(i)) { + this.index = i; + this._scrollToActiveItem(); + break; + } + } + } + + /** Goes to the previous set of items. */ + previous() { + for (let i = this.index; i > -1; i--) { + if (this._isOutOfView(i)) { + this.index = i; + this._scrollToActiveItem(); + break; + } + } + } + + /** Updates the `tabindex` of each of the items based on their active state. */ + private _updateItemTabIndices() { + this.items.forEach((item: CarouselItem) => { + if (this._keyManager != null) { + item.tabindex = item === this._keyManager.activeItem ? '0' : '-1'; + } + }); + } + + /** Scrolls an item into the viewport. */ + private _scrollToActiveItem() { + if (!this._isOutOfView(this.index)) { + return; + } + + const itemsArray = this.items.toArray(); + let targetItemIndex = this.index; + + // Only shift the carousel by one if we're going forwards. This + // looks better compared to moving the carousel by an entire page. + if (this.index > 0 && !this._isOutOfView(this.index - 1)) { + targetItemIndex = itemsArray.findIndex((_, i) => !this._isOutOfView(i)) + 1; + } + + this.position = itemsArray[targetItemIndex].element.nativeElement.offsetLeft; + this.list.nativeElement.style.transform = `translateX(-${this.position}px)`; + this.showPrevArrow = this.index > 0; + this.showNextArrow = false; + + for (let i = itemsArray.length - 1; i > -1; i--) { + if (this._isOutOfView(i, 'end')) { + this.showNextArrow = true; + break; + } + } + } + + /** Checks whether an item at a specific index is outside of the viewport. */ + private _isOutOfView(index: number, side?: 'start' | 'end') { + const {offsetWidth, offsetLeft} = this.items.toArray()[index].element.nativeElement; + + if ((!side || side === 'start') && offsetLeft - this.position < 0) { + return true; + } + + return ( + (!side || side === 'end') && + offsetWidth + offsetLeft - this.position > this.list.nativeElement.clientWidth + ); + } + } \ No newline at end of file