Browse Source

Finalize component

pull/2394/head
Thomas 2 years ago
parent
commit
c941fc0fb6
  1. 64
      apps/client/src/app/pages/landing/landing-page.html
  2. 6
      apps/client/src/app/pages/landing/landing-page.module.ts
  3. 16
      libs/ui/src/lib/carousel/_carousel-theme.scss.
  4. 22
      libs/ui/src/lib/carousel/carousel-item.directive.ts
  5. 34
      libs/ui/src/lib/carousel/carousel.component.html
  6. 34
      libs/ui/src/lib/carousel/carousel.component.scss
  7. 147
      libs/ui/src/lib/carousel/carousel.component.ts
  8. 16
      libs/ui/src/lib/carousel/carousel.html
  9. 14
      libs/ui/src/lib/carousel/carousel.module.ts
  10. 37
      libs/ui/src/lib/carousel/carousel.scss
  11. 144
      libs/ui/src/lib/carousel/carousel.ts
  12. 1
      libs/ui/src/lib/carousel/index.ts

64
apps/client/src/app/pages/landing/landing-page.html

@ -320,49 +320,37 @@
<div class="row my-5">
<div class="col-12">
<h2 class="h4 mb-1 text-center" i18n>
<h2 class="h4 mb-3 text-center" i18n>
What our <strong>users</strong> are saying
</h2>
</div>
<!-- <div *ngFor="let testimonial of testimonials" class="col-md-6">
<div class="d-flex flex-row py-3">
<div class="d-flex justify-content-center">
<gf-logo
class="mr-3 mt-2 pt-1"
size="medium"
[showLabel]="false"
></gf-logo>
</div>
<div>
<div>{{ testimonial.quote }}</div>
<div class="mt-2 text-muted">
<a *ngIf="testimonial.url" target="_blank" [href]="testimonial.url"
>{{ testimonial.author }}</a
>
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span>, {{
testimonial.country }}
<div class="col-md-8 offset-md-2">
<gf-carousel [aria-label]="'Testimonials'">
<div *ngFor="let testimonial of testimonials" gf-carousel-item>
<div class="d-flex px-3">
<gf-logo
class="mr-3 mt-2 pt-1"
size="medium"
[showLabel]="false"
></gf-logo>
<div>
<div>{{ testimonial.quote }}</div>
<div class="mt-2 text-muted">
<a
*ngIf="testimonial.url"
target="_blank"
[href]="testimonial.url"
>{{ testimonial.author }}</a
>
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span>,
{{ testimonial.country }}
</div>
</div>
</div>
</div>
</div>
</div> -->
<gf-carousel [aria-label]="'Featured components'">
<div *ngFor="let testimonial of testimonials" carousel-item>
<gf-logo
class="mr-3 mt-2 pt-1"
size="medium"
[showLabel]="false"
></gf-logo>
<div class="mt-2 text-muted">
<a *ngIf="testimonial.url" target="_blank" [href]="testimonial.url"
>{{ testimonial.author }}</a
>
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span>, {{
testimonial.country }}
</div>
</div>
</gf-carousel>
</gf-carousel>
</div>
</div>
<div *ngIf="hasPermissionForSubscription" class="row my-5">

6
apps/client/src/app/pages/landing/landing-page.module.ts

@ -4,18 +4,18 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfCarouselModule } from '@ghostfolio/ui/carousel';
import { GfLogoModule } from '@ghostfolio/ui/logo';
import { GfValueModule } from '@ghostfolio/ui/value';
import { CarouselItem } from '@ghostfolio/ui/carousel/carousel-item.directive';
import { Carousel } from '@ghostfolio/ui/carousel/carousel';
import { LandingPageRoutingModule } from './landing-page-routing.module';
import { LandingPageComponent } from './landing-page.component';
@NgModule({
declarations: [Carousel, CarouselItem, LandingPageComponent],
declarations: [LandingPageComponent],
imports: [
CommonModule,
GfCarouselModule,
GfLogoModule,
GfValueModule,
GfWorldMapChartModule,

16
libs/ui/src/lib/carousel/_carousel-theme.scss.

@ -1,16 +0,0 @@
@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)
}
}

22
libs/ui/src/lib/carousel/carousel-item.directive.ts

@ -1,14 +1,16 @@
import { FocusableOption } from "@angular/cdk/a11y";
import { ElementRef, HostBinding } from "@angular/core";
import { FocusableOption } from '@angular/cdk/a11y';
import { Directive, ElementRef, HostBinding } from '@angular/core';
@Directive({
selector: '[gf-carousel-item]'
})
export class CarouselItem implements FocusableOption {
@HostBinding('attr.role') readonly role = 'listitem';
@HostBinding('tabindex') tabindex = '-1';
export class CarouselItem implements FocusableOption {
@HostBinding('attr.role') readonly role = 'listitem';
@HostBinding('tabindex') tabindex = '-1';
public constructor(readonly element: ElementRef<HTMLElement>) {}
constructor(readonly element: ElementRef<HTMLElement>) {}
focus(): void {
this.element.nativeElement.focus({preventScroll: true});
}
public focus(): void {
this.element.nativeElement.focus({ preventScroll: true });
}
}

34
libs/ui/src/lib/carousel/carousel.component.html

@ -0,0 +1,34 @@
<button
*ngIf="this.showPrevArrow"
aria-hidden="true"
aria-label="previous"
class="carousel-nav carousel-nav-prev no-min-width position-absolute"
mat-stroked-button
tabindex="-1"
(click)="previous()"
>
<ion-icon name="chevron-back-outline"></ion-icon>
</button>
<div
#contentWrapper
class="overflow-hidden"
role="region"
(keyup)="onKeydown($event)"
>
<div #list class="d-flex carousel-content" role="list" tabindex="0">
<ng-content></ng-content>
</div>
</div>
<button
*ngIf="this.showNextArrow"
aria-hidden="true"
aria-label="next"
class="carousel-nav carousel-nav-next no-min-width position-absolute"
mat-stroked-button
tabindex="-1"
(click)="next()"
>
<ion-icon name="chevron-forward-outline"></ion-icon>
</button>

34
libs/ui/src/lib/carousel/carousel.component.scss

@ -0,0 +1,34 @@
:host {
display: block;
position: relative;
::ng-deep {
[gf-carousel-item] {
flex-shrink: 0;
width: 100%;
}
}
button {
top: 50%;
transform: translateY(-50%);
&.carousel-nav-prev {
left: -50px;
}
&.carousel-nav-next {
right: -50px;
}
}
.carousel-content {
flex-direction: row;
outline: none;
transition: transform 0.5s ease-in-out;
.animations-disabled & {
transition: none;
}
}
}

147
libs/ui/src/lib/carousel/carousel.component.ts

@ -0,0 +1,147 @@
import { FocusKeyManager } from '@angular/cdk/a11y';
import { LEFT_ARROW, RIGHT_ARROW, TAB } from '@angular/cdk/keycodes';
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ContentChildren,
ElementRef,
HostBinding,
Inject,
Input,
Optional,
QueryList,
ViewChild
} from '@angular/core';
import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
import { CarouselItem } from './carousel-item.directive';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-carousel',
styleUrls: ['./carousel.component.scss'],
templateUrl: './carousel.component.html'
})
export class CarouselComponent implements AfterContentInit {
@ContentChildren(CarouselItem) public items!: QueryList<CarouselItem>;
@HostBinding('class.animations-disabled')
public readonly animationsDisabled: boolean;
@Input('aria-label') public ariaLabel: string | undefined;
@ViewChild('list') public list!: ElementRef<HTMLElement>;
public showPrevArrow = false;
public showNextArrow = true;
private index = 0;
private keyManager!: FocusKeyManager<CarouselItem>;
private position = 0;
public constructor(
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationsModule?: string
) {
this.animationsDisabled = animationsModule === 'NoopAnimations';
}
public ngAfterContentInit() {
this.keyManager = new FocusKeyManager<CarouselItem>(this.items);
}
public next() {
for (let i = this.index; i < this.items.length; i++) {
if (this.isOutOfView(i)) {
this.index = i;
this.scrollToActiveItem();
break;
}
}
}
public 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();
}
}
}
public previous() {
for (let i = this.index; i > -1; i--) {
if (this.isOutOfView(i)) {
this.index = i;
this.scrollToActiveItem();
break;
}
}
}
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
);
}
private scrollToActiveItem() {
if (!this.isOutOfView(this.index)) {
return;
}
const itemsArray = this.items.toArray();
let targetItemIndex = this.index;
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;
}
}
}
private updateItemTabIndices() {
this.items.forEach((item: CarouselItem) => {
if (this.keyManager != null) {
item.tabindex = item === this.keyManager.activeItem ? '0' : '-1';
}
});
}
}

16
libs/ui/src/lib/carousel/carousel.html

@ -1,16 +0,0 @@
<button (click)="previous()" *ngIf="this.showPrevArrow" aria-hidden="true" tabindex="-1"
class="docs-carousel-nav docs-carousel-nav-prev" mat-mini-fab aria-label="previous">
<mat-icon>navigate_before</mat-icon>
</button>
<div #contentWrapper (keyup)="onKeydown($event)"
class="docs-carousel-content-wrapper" role="region">
<div class="docs-carousel-content" role="list" tabindex="0" #list>
<ng-content></ng-content>
</div>
</div>
<button (click)="next()" *ngIf="this.showNextArrow" aria-hidden="true" tabindex="-1"
class="docs-carousel-nav docs-carousel-nav-next" mat-mini-fab aria-label="next">
<mat-icon>navigate_next</mat-icon>
</button>

14
libs/ui/src/lib/carousel/carousel.module.ts

@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { CarouselItem } from './carousel-item.directive';
import { CarouselComponent } from './carousel.component';
@NgModule({
declarations: [CarouselComponent, CarouselItem],
exports: [CarouselComponent, CarouselItem],
imports: [CommonModule, MatButtonModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfCarouselModule {}

37
libs/ui/src/lib/carousel/carousel.scss

@ -1,37 +0,0 @@
gf-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;
}

144
libs/ui/src/lib/carousel/carousel.ts

@ -1,144 +0,0 @@
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';
import { CarouselItem } from './carousel-item.directive';
@Component({
selector: 'gf-carousel',
imports: [
NgIf,
MatButtonModule,
MatIconModule,
],
})
export class Carousel implements AfterContentInit {
@Input('aria-label') ariaLabel: string | undefined;
@ContentChildren(CarouselItem) items!: QueryList<CarouselItem>;
@ViewChild('list') list!: ElementRef<HTMLElement>;
@HostBinding('class.animations-disabled') readonly animationsDisabled: boolean;
position = 0;
showPrevArrow = false;
showNextArrow = true;
index = 0;
private _keyManager!: FocusKeyManager<CarouselItem>;
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<CarouselItem>(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
);
}
}

1
libs/ui/src/lib/carousel/index.ts

@ -0,0 +1 @@
export * from './carousel.module';
Loading…
Cancel
Save