-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(admin-ui): Implement custom dropdown based on CDK Overlay
Relates to #95
- Loading branch information
1 parent
c8539de
commit 409bb16
Showing
9 changed files
with
226 additions
and
0 deletions.
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
admin-ui/src/app/shared/components/dropdown/dropdown-item.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 13 additions & 0 deletions
13
admin-ui/src/app/shared/components/dropdown/dropdown-menu.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 141 additions & 0 deletions
141
admin-ui/src/app/shared/components/dropdown/dropdown-menu.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 17 additions & 0 deletions
17
admin-ui/src/app/shared/components/dropdown/dropdown-trigger.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 change: 1 addition & 0 deletions
1
admin-ui/src/app/shared/components/dropdown/dropdown.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<ng-content></ng-content> |
Empty file.
26 changes: 26 additions & 0 deletions
26
admin-ui/src/app/shared/components/dropdown/dropdown.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters