mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
[PM-2452] Popover Component (#5889)
* setup popover component template and basic story * add a11y features * add multiple positions for the popover * add stories for open right and left * prevent panel from hugging edges of screen * fix typo * add popover arrow depending on position * add buttons to stories * add figma preview * move toward directive approach * add all positions * add header input * add close functionality * make standalone component * add a11y import * add all stories * add story controls/args * add module of standalone components * gracefully handle text wrap and align close button to top for longer headings * update semantic html * add story for open state * use bitIconButton * adjust styles * add public close method * setup walkthrough mode * add walkthrough mode * revert to before walkthrough service added * add triggerRef to stories * change property name * add Escape key to close events * add initially open state * add docs * minor reformatting --------- Co-authored-by: William Martin <contact@willmartian.com>
This commit is contained in:
131
libs/components/src/popover/popover-trigger-for.directive.ts
Normal file
131
libs/components/src/popover/popover-trigger-for.directive.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
|
||||
import { TemplatePortal } from "@angular/cdk/portal";
|
||||
import {
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
OnDestroy,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { Observable, Subscription, filter, mergeWith } from "rxjs";
|
||||
|
||||
import { defaultPositions } from "./default-positions";
|
||||
import { PopoverComponent } from "./popover.component";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitPopoverTriggerFor]",
|
||||
standalone: true,
|
||||
exportAs: "popoverTrigger",
|
||||
})
|
||||
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
@Input()
|
||||
@HostBinding("attr.aria-expanded")
|
||||
popoverOpen = false;
|
||||
|
||||
@Input("bitPopoverTriggerFor")
|
||||
popover: PopoverComponent;
|
||||
|
||||
@Input("position")
|
||||
position: string;
|
||||
|
||||
private overlayRef: OverlayRef;
|
||||
private closedEventsSub: Subscription;
|
||||
|
||||
get positions() {
|
||||
if (!this.position) {
|
||||
return defaultPositions;
|
||||
}
|
||||
|
||||
const preferredPosition = defaultPositions.find((position) => position.id === this.position);
|
||||
|
||||
if (preferredPosition) {
|
||||
return [preferredPosition, ...defaultPositions];
|
||||
}
|
||||
|
||||
return defaultPositions;
|
||||
}
|
||||
|
||||
get defaultPopoverConfig(): OverlayConfig {
|
||||
return {
|
||||
hasBackdrop: true,
|
||||
backdropClass: "cdk-overlay-transparent-backdrop",
|
||||
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
||||
positionStrategy: this.overlay
|
||||
.position()
|
||||
.flexibleConnectedTo(this.elementRef)
|
||||
.withPositions(this.positions)
|
||||
.withLockedPosition(true)
|
||||
.withFlexibleDimensions(false)
|
||||
.withPush(true),
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef<HTMLElement>,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private overlay: Overlay
|
||||
) {}
|
||||
|
||||
@HostListener("click")
|
||||
togglePopover() {
|
||||
if (this.popoverOpen) {
|
||||
this.closePopover();
|
||||
} else {
|
||||
this.openPopover();
|
||||
}
|
||||
}
|
||||
|
||||
private openPopover() {
|
||||
this.popoverOpen = true;
|
||||
this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
|
||||
|
||||
const templatePortal = new TemplatePortal(this.popover.templateRef, this.viewContainerRef);
|
||||
|
||||
this.overlayRef.attach(templatePortal);
|
||||
this.closedEventsSub = this.getClosedEvents().subscribe(() => {
|
||||
this.destroyPopover();
|
||||
});
|
||||
}
|
||||
|
||||
private getClosedEvents(): Observable<any> {
|
||||
const detachments = this.overlayRef.detachments();
|
||||
const escKey = this.overlayRef
|
||||
.keydownEvents()
|
||||
.pipe(filter((event: KeyboardEvent) => event.key === "Escape"));
|
||||
const backdrop = this.overlayRef.backdropClick();
|
||||
const popoverClosed = this.popover.closed;
|
||||
|
||||
return detachments.pipe(mergeWith(escKey, backdrop, popoverClosed));
|
||||
}
|
||||
|
||||
private destroyPopover() {
|
||||
if (this.overlayRef == null || !this.popoverOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.popoverOpen = false;
|
||||
this.disposeAll();
|
||||
}
|
||||
|
||||
private disposeAll() {
|
||||
this.closedEventsSub?.unsubscribe();
|
||||
this.overlayRef?.dispose();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.popoverOpen) {
|
||||
this.openPopover();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.disposeAll();
|
||||
}
|
||||
|
||||
closePopover() {
|
||||
this.destroyPopover();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user