1
0
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:
rr-bw
2023-10-23 09:22:54 -07:00
committed by GitHub
parent c2e03d2cdc
commit a4303fac59
11 changed files with 883 additions and 0 deletions

View 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();
}
}