mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01: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:
parent
c2e03d2cdc
commit
a4303fac59
@ -19,6 +19,7 @@ export * from "./menu";
|
|||||||
export * from "./multi-select";
|
export * from "./multi-select";
|
||||||
export * from "./navigation";
|
export * from "./navigation";
|
||||||
export * from "./no-items";
|
export * from "./no-items";
|
||||||
|
export * from "./popover";
|
||||||
export * from "./progress";
|
export * from "./progress";
|
||||||
export * from "./radio-button";
|
export * from "./radio-button";
|
||||||
export * from "./search";
|
export * from "./search";
|
||||||
|
150
libs/components/src/popover/default-positions.ts
Normal file
150
libs/components/src/popover/default-positions.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||||
|
|
||||||
|
const ORIGIN_OFFSET_PX = 6;
|
||||||
|
const OVERLAY_OFFSET_PX = 24;
|
||||||
|
|
||||||
|
export type PositionIdentifier =
|
||||||
|
| "right-start"
|
||||||
|
| "right-center"
|
||||||
|
| "right-end"
|
||||||
|
| "left-start"
|
||||||
|
| "left-center"
|
||||||
|
| "left-end"
|
||||||
|
| "below-start"
|
||||||
|
| "below-center"
|
||||||
|
| "below-end"
|
||||||
|
| "above-start"
|
||||||
|
| "above-center"
|
||||||
|
| "above-end";
|
||||||
|
|
||||||
|
export interface DefaultPosition extends ConnectedPosition {
|
||||||
|
id: PositionIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultPositions: DefaultPosition[] = [
|
||||||
|
/**
|
||||||
|
* The order of these positions matters. The Popover component will use
|
||||||
|
* the first position that fits within the viewport.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Popover opens to right of trigger
|
||||||
|
{
|
||||||
|
id: "right-start",
|
||||||
|
offsetX: ORIGIN_OFFSET_PX,
|
||||||
|
offsetY: -OVERLAY_OFFSET_PX,
|
||||||
|
originX: "end",
|
||||||
|
originY: "center",
|
||||||
|
overlayX: "start",
|
||||||
|
overlayY: "top",
|
||||||
|
panelClass: ["bit-popover-right", "bit-popover-right-start"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "right-center",
|
||||||
|
offsetX: ORIGIN_OFFSET_PX,
|
||||||
|
originX: "end",
|
||||||
|
originY: "center",
|
||||||
|
overlayX: "start",
|
||||||
|
overlayY: "center",
|
||||||
|
panelClass: ["bit-popover-right", "bit-popover-right-center"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "right-end",
|
||||||
|
offsetX: ORIGIN_OFFSET_PX,
|
||||||
|
offsetY: OVERLAY_OFFSET_PX,
|
||||||
|
originX: "end",
|
||||||
|
originY: "center",
|
||||||
|
overlayX: "start",
|
||||||
|
overlayY: "bottom",
|
||||||
|
panelClass: ["bit-popover-right", "bit-popover-right-end"],
|
||||||
|
},
|
||||||
|
// ... to left of trigger
|
||||||
|
{
|
||||||
|
id: "left-start",
|
||||||
|
offsetX: -ORIGIN_OFFSET_PX,
|
||||||
|
offsetY: -OVERLAY_OFFSET_PX,
|
||||||
|
originX: "start",
|
||||||
|
originY: "center",
|
||||||
|
overlayX: "end",
|
||||||
|
overlayY: "top",
|
||||||
|
panelClass: ["bit-popover-left", "bit-popover-left-start"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "left-center",
|
||||||
|
offsetX: -ORIGIN_OFFSET_PX,
|
||||||
|
originX: "start",
|
||||||
|
originY: "center",
|
||||||
|
overlayX: "end",
|
||||||
|
overlayY: "center",
|
||||||
|
panelClass: ["bit-popover-left", "bit-popover-left-center"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "left-end",
|
||||||
|
offsetX: -ORIGIN_OFFSET_PX,
|
||||||
|
offsetY: OVERLAY_OFFSET_PX,
|
||||||
|
originX: "start",
|
||||||
|
originY: "center",
|
||||||
|
overlayX: "end",
|
||||||
|
overlayY: "bottom",
|
||||||
|
panelClass: ["bit-popover-left", "bit-popover-left-end"],
|
||||||
|
},
|
||||||
|
// ... below trigger
|
||||||
|
{
|
||||||
|
id: "below-center",
|
||||||
|
offsetY: ORIGIN_OFFSET_PX,
|
||||||
|
originX: "center",
|
||||||
|
originY: "bottom",
|
||||||
|
overlayX: "center",
|
||||||
|
overlayY: "top",
|
||||||
|
panelClass: ["bit-popover-below", "bit-popover-below-center"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "below-start",
|
||||||
|
offsetX: -OVERLAY_OFFSET_PX,
|
||||||
|
offsetY: ORIGIN_OFFSET_PX,
|
||||||
|
originX: "center",
|
||||||
|
originY: "bottom",
|
||||||
|
overlayX: "start",
|
||||||
|
overlayY: "top",
|
||||||
|
panelClass: ["bit-popover-below", "bit-popover-below-start"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "below-end",
|
||||||
|
offsetX: OVERLAY_OFFSET_PX,
|
||||||
|
offsetY: ORIGIN_OFFSET_PX,
|
||||||
|
originX: "center",
|
||||||
|
originY: "bottom",
|
||||||
|
overlayX: "end",
|
||||||
|
overlayY: "top",
|
||||||
|
panelClass: ["bit-popover-below", "bit-popover-below-end"],
|
||||||
|
},
|
||||||
|
// ... above trigger
|
||||||
|
{
|
||||||
|
id: "above-center",
|
||||||
|
offsetY: -ORIGIN_OFFSET_PX,
|
||||||
|
originX: "center",
|
||||||
|
originY: "top",
|
||||||
|
overlayX: "center",
|
||||||
|
overlayY: "bottom",
|
||||||
|
panelClass: ["bit-popover-above", "bit-popover-above-center"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "above-start",
|
||||||
|
offsetX: -OVERLAY_OFFSET_PX,
|
||||||
|
offsetY: -ORIGIN_OFFSET_PX,
|
||||||
|
originX: "center",
|
||||||
|
originY: "top",
|
||||||
|
overlayX: "start",
|
||||||
|
overlayY: "bottom",
|
||||||
|
panelClass: ["bit-popover-above", "bit-popover-above-start"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "above-end",
|
||||||
|
offsetX: OVERLAY_OFFSET_PX,
|
||||||
|
offsetY: -ORIGIN_OFFSET_PX,
|
||||||
|
originX: "center",
|
||||||
|
originY: "top",
|
||||||
|
overlayX: "end",
|
||||||
|
overlayY: "bottom",
|
||||||
|
panelClass: ["bit-popover-above", "bit-popover-above-end"],
|
||||||
|
},
|
||||||
|
];
|
1
libs/components/src/popover/index.ts
Normal file
1
libs/components/src/popover/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./popover.module";
|
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();
|
||||||
|
}
|
||||||
|
}
|
49
libs/components/src/popover/popover.component.css
Normal file
49
libs/components/src/popover/popover.component.css
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
.bit-popover-arrow {
|
||||||
|
@apply tw-absolute tw-z-10 tw-h-4 tw-w-4 tw-rotate-45 tw-border-solid tw-bg-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-right .bit-popover-arrow {
|
||||||
|
@apply tw-left-1 -tw-translate-x-1/2 tw-rounded-bl-sm tw-border-b tw-border-l tw-border-b-secondary-300 tw-border-l-secondary-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-left .bit-popover-arrow {
|
||||||
|
@apply tw-right-1 tw-translate-x-1/2 tw-rounded-tr-sm tw-border-r tw-border-t tw-border-r-secondary-300 tw-border-t-secondary-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-right-start .bit-popover-arrow,
|
||||||
|
.bit-popover-left-start .bit-popover-arrow {
|
||||||
|
@apply tw-top-6 -tw-translate-y-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-right-center .bit-popover-arrow,
|
||||||
|
.bit-popover-left-center .bit-popover-arrow {
|
||||||
|
@apply tw-top-1/2 -tw-translate-y-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-right-end .bit-popover-arrow,
|
||||||
|
.bit-popover-left-end .bit-popover-arrow {
|
||||||
|
@apply tw-bottom-6 tw-translate-y-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-below .bit-popover-arrow {
|
||||||
|
@apply tw-top-1 -tw-translate-y-1/2 tw-rounded-tl-sm tw-border-l tw-border-t tw-border-l-secondary-300 tw-border-t-secondary-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-above .bit-popover-arrow {
|
||||||
|
@apply tw-bottom-1 tw-translate-y-1/2 tw-rounded-br-sm tw-border-b tw-border-r tw-border-b-secondary-300 tw-border-r-secondary-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-below-start .bit-popover-arrow,
|
||||||
|
.bit-popover-above-start .bit-popover-arrow {
|
||||||
|
@apply tw-left-6 -tw-translate-x-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-below-center .bit-popover-arrow,
|
||||||
|
.bit-popover-above-center .bit-popover-arrow {
|
||||||
|
@apply tw-left-1/2 -tw-translate-x-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bit-popover-below-end .bit-popover-arrow,
|
||||||
|
.bit-popover-above-end .bit-popover-arrow {
|
||||||
|
@apply tw-right-6 tw-translate-x-1/2;
|
||||||
|
}
|
26
libs/components/src/popover/popover.component.html
Normal file
26
libs/components/src/popover/popover.component.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<ng-template>
|
||||||
|
<section cdkTrapFocus cdkTrapFocusAutoCapture class="tw-relative" role="dialog" aria-modal="true">
|
||||||
|
<div class="tw-overflow-hidden tw-rounded-md tw-border tw-border-solid tw-border-secondary-300">
|
||||||
|
<div
|
||||||
|
class="tw-relative tw-z-20 tw-w-72 tw-break-words tw-bg-background tw-pb-4 tw-pt-2 tw-text-main"
|
||||||
|
>
|
||||||
|
<div class="tw-mb-1 tw-mr-2 tw-flex tw-items-start tw-justify-between tw-gap-4 tw-pl-4">
|
||||||
|
<h2 class="tw-mb-0 tw-mt-1 tw-text-base tw-font-semibold">
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-close"
|
||||||
|
[attr.title]="'close' | i18n"
|
||||||
|
[attr.aria-label]="'close' | i18n"
|
||||||
|
(click)="closed.emit()"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="tw-px-4">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bit-popover-arrow"></div>
|
||||||
|
</section>
|
||||||
|
</ng-template>
|
18
libs/components/src/popover/popover.component.ts
Normal file
18
libs/components/src/popover/popover.component.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { A11yModule } from "@angular/cdk/a11y";
|
||||||
|
import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core";
|
||||||
|
|
||||||
|
import { IconButtonModule } from "../icon-button/icon-button.module";
|
||||||
|
import { SharedModule } from "../shared/shared.module";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "bit-popover",
|
||||||
|
imports: [A11yModule, IconButtonModule, SharedModule],
|
||||||
|
templateUrl: "./popover.component.html",
|
||||||
|
exportAs: "popoverComponent",
|
||||||
|
})
|
||||||
|
export class PopoverComponent {
|
||||||
|
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||||
|
@Input() title = "";
|
||||||
|
@Output() closed = new EventEmitter();
|
||||||
|
}
|
88
libs/components/src/popover/popover.mdx
Normal file
88
libs/components/src/popover/popover.mdx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import * as stories from "./popover.stories";
|
||||||
|
|
||||||
|
<Meta of={stories} />
|
||||||
|
|
||||||
|
# Popover
|
||||||
|
|
||||||
|
A popover is a page overlay that is triggered by a selecting a button. It displays interactive
|
||||||
|
content.
|
||||||
|
|
||||||
|
Popovers remain actively open until a user dismisses it in one of the following ways:
|
||||||
|
|
||||||
|
- Presses the Esc key
|
||||||
|
- Presses the close "x" button in the Popover
|
||||||
|
- Presses a button within the Popover triggering close
|
||||||
|
- Clicks outside of the Popover
|
||||||
|
|
||||||
|
Popovers are used to provide the user with additional context about an interaction or page. We
|
||||||
|
primarily use popovers when a user clicks on an icon-button with a question icon. This launches a
|
||||||
|
popover that provides the user with in app help text.
|
||||||
|
|
||||||
|
Note: Popovers are not tooltips. Use tooltips to show a short text to respondents when they hover
|
||||||
|
over a word or icon. Use popovers to show a longer text, or when you want to link to an external web
|
||||||
|
page.
|
||||||
|
|
||||||
|
<Primary />
|
||||||
|
|
||||||
|
## Open on Page Load
|
||||||
|
|
||||||
|
A Popover can be set to initially open on page load by setting `[popoverOpen]="true"` on the trigger
|
||||||
|
element, like so:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button [bitPopoverTriggerFor]="myPopover" [popoverOpen]="true">Open Popover</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Positions
|
||||||
|
|
||||||
|
The Popover component uses the following list of default "positions" to determine where to position
|
||||||
|
the Popover overlay.
|
||||||
|
|
||||||
|
1. right-start ---> "Open the Popover to the RIGHT of the trigger and align the START of the Popover
|
||||||
|
with the trigger"
|
||||||
|
2. right-center
|
||||||
|
3. right-end
|
||||||
|
4. left-start
|
||||||
|
5. left-center
|
||||||
|
6. left-end
|
||||||
|
7. below-start
|
||||||
|
8. below-center
|
||||||
|
9. below-end
|
||||||
|
10. above-start
|
||||||
|
11. above-center
|
||||||
|
12. above-end
|
||||||
|
|
||||||
|
The order here matters. If position 1 fits within the viewport, it will be used. If it does not, the
|
||||||
|
Popover component will try position 2, and so forth. This cascading behavior ensures that if the
|
||||||
|
user resizes the screen, the Popover component will find the best way to reposition itself.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Suppose you have a trigger element on the right side of the page. The `right-start` position will
|
||||||
|
not work because there is not enough space to open the Popover to the right. The same is true for
|
||||||
|
`right-center` and `right-end`.
|
||||||
|
|
||||||
|
The first position that "fits" is `left-start`, and therefore that is where the Popover will open.
|
||||||
|
|
||||||
|
<Story of={stories.LeftStart} />
|
||||||
|
|
||||||
|
### Manually Setting a Position
|
||||||
|
|
||||||
|
You can manually set the initial position of the Popover by binding a `[position]` input on the
|
||||||
|
Popover's trigger element, such as:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button [bitPopoverTriggerFor]="myPopover" [position]="'above-end'">Open Popover</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Story of={stories.AboveEnd} />
|
||||||
|
|
||||||
|
Note that if the user resizes the page and the Popover no longer fits in the viewport, the Popover
|
||||||
|
component will fall back to the list of default positions to find the best position.
|
||||||
|
|
||||||
|
To test this out, open the Popopver in the example above and then slowly resize your browser window
|
||||||
|
horizontally to make it smaller. When the Popover no longer fits the `above-end` position, it will
|
||||||
|
jump down below the trigger, using `below-center`, because that is the first position that fits
|
||||||
|
based on the list of default positions.
|
10
libs/components/src/popover/popover.module.ts
Normal file
10
libs/components/src/popover/popover.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { PopoverTriggerForDirective } from "./popover-trigger-for.directive";
|
||||||
|
import { PopoverComponent } from "./popover.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [PopoverComponent, PopoverTriggerForDirective],
|
||||||
|
exports: [PopoverComponent, PopoverTriggerForDirective],
|
||||||
|
})
|
||||||
|
export class PopoverModule {}
|
408
libs/components/src/popover/popover.stories.ts
Normal file
408
libs/components/src/popover/popover.stories.ts
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { ButtonModule } from "../button";
|
||||||
|
import { IconButtonModule } from "../icon-button";
|
||||||
|
import { SharedModule } from "../shared/shared.module";
|
||||||
|
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||||
|
|
||||||
|
import { PopoverTriggerForDirective } from "./popover-trigger-for.directive";
|
||||||
|
import { PopoverModule } from "./popover.module";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Component Library/Popover",
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [PopoverModule, ButtonModule, IconButtonModule, SharedModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useFactory: () => {
|
||||||
|
return new I18nMockService({
|
||||||
|
close: "Close",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: "figma",
|
||||||
|
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1717-15868",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
position: {
|
||||||
|
options: [
|
||||||
|
"right-start",
|
||||||
|
"right-center",
|
||||||
|
"right-end",
|
||||||
|
"left-start",
|
||||||
|
"left-center",
|
||||||
|
"left-end",
|
||||||
|
"below-start",
|
||||||
|
"below-center",
|
||||||
|
"below-end",
|
||||||
|
"above-start",
|
||||||
|
"above-center",
|
||||||
|
"above-end",
|
||||||
|
],
|
||||||
|
control: { type: "select" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
position: "right-start",
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<PopoverTriggerForDirective>;
|
||||||
|
|
||||||
|
const popoverContent = `
|
||||||
|
<bit-popover [title]="'Example Title'" #myPopover>
|
||||||
|
<div>Lorem ipsum dolor <a href="#">adipisicing elit</a>.</div>
|
||||||
|
<ul class="tw-mt-2 tw-mb-0 tw-pl-4">
|
||||||
|
<li>Dolor sit amet consectetur</li>
|
||||||
|
<li>Esse labore veniam tempora</li>
|
||||||
|
<li>Adipisicing elit ipsum <a href="#">iustolaborum</a></li>
|
||||||
|
</ul>
|
||||||
|
<button bitButton class="tw-mt-3" (click)="triggerRef.closePopover()">Close</button>
|
||||||
|
</bit-popover>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Open: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<bit-popover [title]="'Example Title'" #myPopover="popoverComponent">
|
||||||
|
<div>Lorem ipsum dolor <a href="#">adipisicing elit</a>.</div>
|
||||||
|
<ul class="tw-mt-2 tw-mb-0 tw-pl-4">
|
||||||
|
<li>Dolor sit amet consectetur</li>
|
||||||
|
<li>Esse labore veniam tempora</li>
|
||||||
|
<li>Adipisicing elit ipsum <a href="#">iustolaborum</a></li>
|
||||||
|
</ul>
|
||||||
|
</bit-popover>
|
||||||
|
|
||||||
|
<div class="tw-h-40">
|
||||||
|
<div class="cdk-overlay-pane bit-popover-right bit-popover-right-start">
|
||||||
|
<ng-container *ngTemplateOutlet="myPopover.templateRef"></ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InitiallyOpen: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
[popoverOpen]="true"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightStart: Story = {
|
||||||
|
args: {
|
||||||
|
position: "right-start",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightCenter: Story = {
|
||||||
|
args: {
|
||||||
|
position: "right-center",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightEnd: Story = {
|
||||||
|
args: {
|
||||||
|
position: "right-end",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftStart: Story = {
|
||||||
|
args: {
|
||||||
|
position: "left-start",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftCenter: Story = {
|
||||||
|
args: {
|
||||||
|
position: "left-center",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
export const LeftEnd: Story = {
|
||||||
|
args: {
|
||||||
|
position: "left-end",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BelowStart: Story = {
|
||||||
|
args: {
|
||||||
|
position: "below-start",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BelowCenter: Story = {
|
||||||
|
args: {
|
||||||
|
position: "below-center",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BelowEnd: Story = {
|
||||||
|
args: {
|
||||||
|
position: "below-end",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AboveStart: Story = {
|
||||||
|
args: {
|
||||||
|
position: "above-start",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AboveCenter: Story = {
|
||||||
|
args: {
|
||||||
|
position: "above-center",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AboveEnd: Story = {
|
||||||
|
args: {
|
||||||
|
position: "above-end",
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<div class="tw-mt-32 tw-flex tw-justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||||
|
[bitPopoverTriggerFor]="myPopover"
|
||||||
|
#triggerRef="popoverTrigger"
|
||||||
|
[position]="'${args.position}'"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${popoverContent}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
@ -160,6 +160,7 @@
|
|||||||
--tw-ring-offset-color: #002b36;
|
--tw-ring-offset-color: #002b36;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import "./popover/popover.component.css";
|
||||||
@import "./search/search.component.css";
|
@import "./search/search.component.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user