From 0d4d7ef8ada2f922908c075e57350063edb63a92 Mon Sep 17 00:00:00 2001 From: John Rassa Date: Mon, 7 Oct 2024 08:33:17 -0400 Subject: [PATCH] feat: Rework flyout component * use Angular Animations to animate flyout * support additional placement options * fix alignment issues with nav-bar --- src/app/common/flyout/flyout.component.html | 14 +- src/app/common/flyout/flyout.component.scss | 120 +++++++++++++----- src/app/common/flyout/flyout.component.ts | 72 +++++------ ...c.ts => feedback-flyout.component.spec.ts} | 3 +- .../site-container.component.html | 7 +- .../site-navbar/site-navbar.component.scss | 3 +- 6 files changed, 133 insertions(+), 86 deletions(-) rename src/app/core/feedback/feedback-flyout/{feedback-flyout.spec.ts => feedback-flyout.component.spec.ts} (96%) diff --git a/src/app/common/flyout/flyout.component.html b/src/app/common/flyout/flyout.component.html index 264a57f1..b522314e 100644 --- a/src/app/common/flyout/flyout.component.html +++ b/src/app/common/flyout/flyout.component.html @@ -9,7 +9,15 @@ -
- -
+ @if (placement().startsWith('top') || placement().startsWith('bottom')) { +
+ +
+ } @else { +
+ +
+ } + + diff --git a/src/app/common/flyout/flyout.component.scss b/src/app/common/flyout/flyout.component.scss index 5053cd76..ece338d5 100644 --- a/src/app/common/flyout/flyout.component.scss +++ b/src/app/common/flyout/flyout.component.scss @@ -3,56 +3,87 @@ $flyout-border-top: 8px !default; $flyout-box-shadow: 0 0.25rem 0.5rem rgba(var(--bs-body-bg-rgb), 0.2) !default; -$flyout-right-top: 24px !default; -$flyout-top-top: 20px !default; -$flyout-bottom-bottom: 20px * 2 !default; +$flyout-top: 24px !default; +$flyout-bottom: 24px * 2 !default; :host { position: fixed; right: 0; - top: $flyout-right-top; + top: $flyout-top; @include transition(left 0.15s ease-in-out, right 0.15s ease-in-out); z-index: $zindex-popover; - &[placement='left'] { + &[placement='right-bottom'] { + right: 0; + top: auto; + bottom: $flyout-bottom; + left: var(--nav-open-width); + } + + &[placement='left-top'] + { right: auto; - left: $nav-open-width; + left: var(--nav-open-width); + } + + &[placement='left-bottom'] { + right: auto; + left: var(--nav-open-width); + top: auto; + bottom: $flyout-bottom; } - &[placement='top'] { + &[placement='top-left'] { right: auto; - top: $flyout-top-top; - left: $nav-open-width; + top: $flyout-top; + left: var(--nav-open-width); } - &[placement='bottom'] { + &[placement='top-right'] { + top: $flyout-top; + left: var(--nav-open-width); + } + + &[placement='bottom-left'] { right: auto; top: auto; - bottom: $flyout-bottom-bottom; - left: $nav-open-width; + bottom: $flyout-bottom; + left: var(--nav-open-width); + } + + &[placement='bottom-right'] { + right: 0; + top: auto; + bottom: $flyout-bottom; + left: var(--nav-open-width); } } .navbar-close { - :host[placement='left'], - :host[placement='top'], - :host[placement='bottom'] { - left: $nav-closed-width; + :host[placement='left-top'], + :host[placement='left-bottom'], + :host[placement='top-left'], + :host[placement='bottom-left'] { + left: var(--nav-closed-width); } } .flyout { background: var(--bs-body-bg); - &.flyout-left { + &.flyout-left-top, + &.flyout-left-bottom, { flex-direction: row-reverse; } - &.flyout-top { + + &.flyout-top-left, + &.flyout-top-right { flex-direction: column-reverse; } - &.flyout-bottom { + &.flyout-bottom-left, + &.flyout-bottom-right { flex-direction: column; .flyout-btn { @@ -79,41 +110,62 @@ $flyout-bottom-bottom: 20px * 2 !default; .flyout-btn { position: absolute; - transform: rotate(90deg); - transform-origin: 0 0; - + box-shadow: none; border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius); - .flyout-left & { + .flyout-left-top & { transform: rotate(-90deg); transform-origin: 100% 0; } - .flyout-top & { + .flyout-left-bottom & { + bottom: 0; + transform: translate(100%, 100%) rotate(-90deg); + transform-origin: 0 0; + } + + .flyout-right-top & { + transform: rotate(90deg); + transform-origin: 0 0; + } + + .flyout-right-bottom & { + bottom: 0; + transform: translate(-100%, 100%) rotate(90deg); + transform-origin: top right; + } + + .flyout-top-left & { transform: translateY(100%); } - .flyout-bottom & { + .flyout-top-right & { + right: 0; + transform: translateY(100%); + } + + .flyout-bottom-left & { transform: translateY(-100%); } + + .flyout-bottom-right & { + right: 0; + transform: translateY(-100%) + } + } .flyout-content { @include box-shadow(var(--bs-box-shadow)); overflow: auto; - border-top: $flyout-border-top solid var(--bs-primary); max-height: calc(100vh - 80px); - width: 0; - .flyout-top &, - .flyout-bottom & { - border-top: none; - border-left: $flyout-border-top solid var(--bs-primary); + .flyout-top-left &, + .flyout-top-right &, + .flyout-bottom-left &, + .flyout-bottom-right & { width: auto; - height: 0; max-height: calc(100vh - 100px); } - - @include transition(width 1s ease-in-out, height 1s ease-in-out); } diff --git a/src/app/common/flyout/flyout.component.ts b/src/app/common/flyout/flyout.component.ts index 4253df29..747adc93 100644 --- a/src/app/common/flyout/flyout.component.ts +++ b/src/app/common/flyout/flyout.component.ts @@ -1,58 +1,44 @@ -import { NgClass } from '@angular/common'; -import { - Component, - ElementRef, - Renderer2, - contentChild, - inject, - input, - signal, - viewChild -} from '@angular/core'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { NgClass, NgTemplateOutlet } from '@angular/common'; +import { Component, input, signal } from '@angular/core'; @Component({ selector: 'app-flyout', templateUrl: './flyout.component.html', styleUrls: ['./flyout.component.scss'], standalone: true, - imports: [NgClass] + imports: [NgClass, NgTemplateOutlet], + animations: [ + trigger('flyInOutVertical', [ + state('false', style({ height: '0' })), + transition('false => true', [animate('500ms ease-in', style({ height: '*' }))]), + transition('true => false', [animate('500ms ease-in', style({ height: '0' }))]) + ]), + trigger('flyInOutHorizontal', [ + state('false', style({ width: '0' })), + transition('false => true', [animate('500ms ease-in', style({ width: '*' }))]), + transition('true => false', [animate('500ms ease-in', style({ width: '0' }))]) + ]) + ] }) export class FlyoutComponent { - readonly #renderer = inject(Renderer2); - - readonly container = viewChild.required('flyoutContentContainer'); - readonly content = contentChild.required('flyoutContent'); - readonly label = input(''); - readonly placement = input<'left' | 'right' | 'top' | 'bottom'>('right'); + readonly placement = input< + | 'top' + | 'bottom' + | 'left-top' + | 'left-bottom' + | 'right-top' + | 'right-bottom' + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + >('right-top'); readonly isOpen = signal(false); toggle() { - if (this.content() && this.container()) { - if (this.placement() === 'top' || this.placement() === 'bottom') { - if (this.isOpen()) { - this.#renderer.setStyle(this.container().nativeElement, 'height', 0); - } else { - this.#renderer.setStyle( - this.container().nativeElement, - 'height', - `${this.content().nativeElement.clientHeight}px` - ); - } - } else { - if (this.isOpen()) { - this.#renderer.setStyle(this.container().nativeElement, 'width', 0); - } else { - this.#renderer.setStyle( - this.container().nativeElement, - 'width', - `${this.content().nativeElement.clientWidth}px` - ); - } - } - - this.isOpen.set(!this.isOpen()); - } + this.isOpen.set(!this.isOpen()); } } diff --git a/src/app/core/feedback/feedback-flyout/feedback-flyout.spec.ts b/src/app/core/feedback/feedback-flyout/feedback-flyout.component.spec.ts similarity index 96% rename from src/app/core/feedback/feedback-flyout/feedback-flyout.spec.ts rename to src/app/core/feedback/feedback-flyout/feedback-flyout.component.spec.ts index 26be233e..969a2fbb 100644 --- a/src/app/core/feedback/feedback-flyout/feedback-flyout.spec.ts +++ b/src/app/core/feedback/feedback-flyout/feedback-flyout.component.spec.ts @@ -1,6 +1,7 @@ import { DebugElement, signal } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { APP_CONFIG } from '../../tokens'; @@ -27,7 +28,7 @@ describe('FeedbackFlyoutComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [FeedbackFlyoutComponent], + imports: [FeedbackFlyoutComponent, NoopAnimationsModule], providers: [ { provide: Router, useValue: { url: 'test-url' } }, { provide: APP_CONFIG, useValue: signal(mockConfigObject) }, diff --git a/src/app/core/site-container/site-container.component.html b/src/app/core/site-container/site-container.component.html index 2fdc47c0..b9c2ff58 100644 --- a/src/app/core/site-container/site-container.component.html +++ b/src/app/core/site-container/site-container.component.html @@ -13,6 +13,9 @@
+ @if (isAuthenticated() && showFeedbackFlyout()) { + + } @@ -25,7 +28,3 @@ } - -@if (isAuthenticated() && showFeedbackFlyout()) { - -} diff --git a/src/app/core/site-navbar/site-navbar.component.scss b/src/app/core/site-navbar/site-navbar.component.scss index 92cbdb50..ed9b4aa6 100644 --- a/src/app/core/site-navbar/site-navbar.component.scss +++ b/src/app/core/site-navbar/site-navbar.component.scss @@ -11,9 +11,10 @@ $nav-logo-text-font-size: 1.75rem !default; $nav-popover-width: 192px !default; :host { + --nav-open-width: $nav-open-width; + --nav-closed-width: $nav-closed-width; display: flex; height: 100%; - } .navbar {