Ver Fonte

feat(admin-ui): Implement custom dropdown based on CDK Overlay

Relates to #95
Michael Bromley há 6 anos atrás
pai
commit
409bb16b17

+ 17 - 0
admin-ui/src/app/shared/components/dropdown/dropdown-item.directive.ts

@@ -0,0 +1,17 @@
+import { Directive, HostListener } from '@angular/core';
+
+import { DropdownComponent } from './dropdown.component';
+
+@Directive({
+    selector: '[vdrDropdownItem]',
+    // tslint:disable-next-line
+    host: { '[class.dropdown-item]': 'true' },
+})
+export class DropdownItemDirective {
+    constructor(private dropdown: DropdownComponent) {}
+
+    @HostListener('click', ['$event'])
+    onDropdownItemClick(event: any): void {
+        this.dropdown.toggleOpen();
+    }
+}

+ 13 - 0
admin-ui/src/app/shared/components/dropdown/dropdown-menu.component.scss

@@ -0,0 +1,13 @@
+.clear-backdrop {
+    background-color: hotpink;
+}
+
+.dropdown.open > .dropdown-menu {
+    position: relative;
+    top: 0;
+}
+
+:host {
+    opacity: 1;
+    transition: opacity 0.3s;
+}

+ 141 - 0
admin-ui/src/app/shared/components/dropdown/dropdown-menu.component.ts

@@ -0,0 +1,141 @@
+import {
+    ConnectedPosition,
+    HorizontalConnectionPos,
+    Overlay,
+    OverlayRef,
+    PositionStrategy,
+    VerticalConnectionPos,
+} from '@angular/cdk/overlay';
+import { TemplatePortal } from '@angular/cdk/portal';
+import {
+    AfterViewInit,
+    ChangeDetectionStrategy,
+    Component,
+    ContentChild,
+    ElementRef,
+    Input,
+    OnDestroy,
+    OnInit,
+    TemplateRef,
+    ViewChild,
+    ViewContainerRef,
+} from '@angular/core';
+import { Subscription } from 'rxjs';
+
+import { DropdownTriggerDirective } from './dropdown-trigger.directive';
+import { DropdownComponent } from './dropdown.component';
+
+export type DropdownPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
+
+/**
+ * A dropdown menu modelled on the Clarity Dropdown component (https://v1.clarity.design/dropdowns).
+ *
+ * This was created because the Clarity implementation (at this time) does not handle edge detection. Instead
+ * we make use of the Angular CDK's Overlay module to manage the positioning.
+ *
+ * The API of this component (and its related Components & Directives) are based on the Clarity version,
+ * albeit only a subset which is currently used in this application.
+ */
+@Component({
+    selector: 'vdr-dropdown-menu',
+    template: `
+        <ng-template #menu>
+            <div class="dropdown open">
+                <div class="dropdown-menu">
+                    <ng-content></ng-content>
+                </div>
+            </div>
+        </ng-template>
+    `,
+    styleUrls: ['./dropdown-menu.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
+    @Input('vdrPosition') private position: DropdownPosition = 'bottom-left';
+    @ViewChild('menu') private menuTemplate: TemplateRef<any>;
+    private menuPortal: TemplatePortal<any>;
+    private overlayRef: OverlayRef;
+    private backdropClickSub: Subscription;
+
+    constructor(
+        private overlay: Overlay,
+        private viewContainerRef: ViewContainerRef,
+        private dropdown: DropdownComponent,
+    ) {}
+
+    ngOnInit(): void {
+        this.dropdown.onOpenChange(isOpen => {
+            if (isOpen) {
+                this.overlayRef.attach(this.menuPortal);
+            } else {
+                this.overlayRef.detach();
+            }
+        });
+    }
+
+    ngAfterViewInit() {
+        this.overlayRef = this.overlay.create({
+            hasBackdrop: true,
+            backdropClass: 'clear-backdrop',
+            positionStrategy: this.getPositionStrategy(),
+        });
+        this.menuPortal = new TemplatePortal(this.menuTemplate, this.viewContainerRef);
+        this.backdropClickSub = this.overlayRef.backdropClick().subscribe(() => {
+            this.dropdown.toggleOpen();
+        });
+    }
+
+    ngOnDestroy(): void {
+        this.overlayRef.dispose();
+        if (this.backdropClickSub) {
+            this.backdropClickSub.unsubscribe();
+        }
+    }
+
+    private getPositionStrategy(): PositionStrategy {
+        const position: { [K in DropdownPosition]: ConnectedPosition } = {
+            ['top-left']: {
+                originX: 'start',
+                originY: 'top',
+                overlayX: 'start',
+                overlayY: 'bottom',
+            },
+            ['top-right']: {
+                originX: 'end',
+                originY: 'top',
+                overlayX: 'end',
+                overlayY: 'bottom',
+            },
+            ['bottom-left']: {
+                originX: 'start',
+                originY: 'bottom',
+                overlayX: 'start',
+                overlayY: 'top',
+            },
+            ['bottom-right']: {
+                originX: 'end',
+                originY: 'bottom',
+                overlayX: 'end',
+                overlayY: 'top',
+            },
+        };
+
+        const pos = position[this.position];
+
+        return this.overlay
+            .position()
+            .flexibleConnectedTo(this.dropdown.trigger)
+            .withPositions([pos, this.invertPosition(pos)])
+            .withViewportMargin(12)
+            .withPush(true);
+    }
+
+    /** Inverts an overlay position. */
+    private invertPosition(pos: ConnectedPosition): ConnectedPosition {
+        const inverted = { ...pos };
+        inverted.originY = pos.originY === 'top' ? 'bottom' : 'top';
+        inverted.overlayY = pos.overlayY === 'top' ? 'bottom' : 'top';
+
+        return inverted;
+    }
+}

+ 17 - 0
admin-ui/src/app/shared/components/dropdown/dropdown-trigger.directive.ts

@@ -0,0 +1,17 @@
+import { Directive, ElementRef, HostListener } from '@angular/core';
+
+import { DropdownComponent } from './dropdown.component';
+
+@Directive({
+    selector: '[vdrDropdownTrigger]',
+})
+export class DropdownTriggerDirective {
+    constructor(private dropdown: DropdownComponent, private elementRef: ElementRef) {
+        dropdown.setTriggerElement(this.elementRef);
+    }
+
+    @HostListener('click', ['$event'])
+    onDropdownTriggerClick(event: any): void {
+        this.dropdown.toggleOpen();
+    }
+}

+ 1 - 0
admin-ui/src/app/shared/components/dropdown/dropdown.component.html

@@ -0,0 +1 @@
+<ng-content></ng-content>

+ 0 - 0
admin-ui/src/app/shared/components/dropdown/dropdown.component.scss


+ 26 - 0
admin-ui/src/app/shared/components/dropdown/dropdown.component.ts

@@ -0,0 +1,26 @@
+import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core';
+
+@Component({
+    selector: 'vdr-dropdown',
+    templateUrl: './dropdown.component.html',
+    styleUrls: ['./dropdown.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DropdownComponent {
+    private isOpen = false;
+    private onOpenChangeCallbacks: Array<(isOpen: boolean) => void> = [];
+    public trigger: ElementRef;
+
+    toggleOpen() {
+        this.isOpen = !this.isOpen;
+        this.onOpenChangeCallbacks.forEach(fn => fn(this.isOpen));
+    }
+
+    onOpenChange(callback: (isOpen: boolean) => void) {
+        this.onOpenChangeCallbacks.push(callback);
+    }
+
+    setTriggerElement(elementRef: ElementRef) {
+        this.trigger = elementRef;
+    }
+}

+ 10 - 0
admin-ui/src/app/shared/shared.module.ts

@@ -1,3 +1,4 @@
+import { OverlayModule } from '@angular/cdk/overlay';
 import { CommonModule } from '@angular/common';
 import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -21,6 +22,10 @@ import { CustomFieldControlComponent } from './components/custom-field-control/c
 import { CustomerLabelComponent } from './components/customer-label/customer-label.component';
 import { DataTableColumnComponent } from './components/data-table/data-table-column.component';
 import { DataTableComponent } from './components/data-table/data-table.component';
+import { DropdownItemDirective } from './components/dropdown/dropdown-item.directive';
+import { DropdownMenuComponent } from './components/dropdown/dropdown-menu.component';
+import { DropdownTriggerDirective } from './components/dropdown/dropdown-trigger.directive';
+import { DropdownComponent } from './components/dropdown/dropdown.component';
 import { FacetValueChipComponent } from './components/facet-value-chip/facet-value-chip.component';
 import { FacetValueSelectorComponent } from './components/facet-value-selector/facet-value-selector.component';
 import { FormFieldControlDirective } from './components/form-field/form-field-control.directive';
@@ -54,6 +59,7 @@ const IMPORTS = [
     NgSelectModule,
     NgxPaginationModule,
     TranslateModule,
+    OverlayModule,
 ];
 
 const DECLARATIONS = [
@@ -90,6 +96,10 @@ const DECLARATIONS = [
     SimpleDialogComponent,
     TitleInputComponent,
     SentenceCasePipe,
+    DropdownComponent,
+    DropdownMenuComponent,
+    DropdownTriggerDirective,
+    DropdownItemDirective,
 ];
 
 @NgModule({

+ 1 - 0
admin-ui/src/styles/styles.scss

@@ -7,3 +7,4 @@
 @import "theme/theme";
 
 @import "~@ng-select/ng-select/themes/default.theme.css";
+@import '~@angular/cdk/overlay-prebuilt.css';