1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

Move to libs

This commit is contained in:
Hinton
2022-06-03 16:24:40 +02:00
parent 28d15bfe2a
commit d7492e3cf3
878 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
export * from "./menu.module";
export * from "./menu.component";
export * from "./menu-trigger-for.directive";
export * from "./menu-item.directive";
export * from "./menu-divider.component";

View File

@@ -0,0 +1,4 @@
<div
class="tw-border-solid tw-border-0 tw-border-t tw-border-t-secondary-500 tw-my-2"
role="separator"
></div>

View File

@@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "bit-menu-divider",
templateUrl: "./menu-divider.component.html",
})
export class MenuDividerComponent {}

View File

@@ -0,0 +1,36 @@
import { FocusableOption } from "@angular/cdk/a11y";
import { Directive, ElementRef, HostBinding } from "@angular/core";
@Directive({
selector: "[bitMenuItem]",
})
export class MenuItemDirective implements FocusableOption {
@HostBinding("class") classList = [
"tw-block",
"tw-py-1",
"tw-px-4",
"!tw-text-main",
"!tw-no-underline",
"tw-cursor-pointer",
"tw-border-none",
"tw-bg-background",
"tw-text-left",
"hover:tw-bg-secondary-100",
"focus:tw-bg-secondary-100",
"focus:tw-z-50",
"focus:tw-outline-none",
"focus:tw-ring",
"focus:tw-ring-offset-2",
"focus:tw-ring-primary-700",
"active:!tw-ring-0",
"active:!tw-ring-offset-0",
];
@HostBinding("attr.role") role = "menuitem";
@HostBinding("tabIndex") tabIndex = "-1";
constructor(private elementRef: ElementRef) {}
focus() {
this.elementRef.nativeElement.focus();
}
}

View File

@@ -0,0 +1,119 @@
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
import {
Directive,
ElementRef,
HostBinding,
HostListener,
Input,
OnDestroy,
ViewContainerRef,
} from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { filter, mergeWith } from "rxjs/operators";
import { MenuComponent } from "./menu.component";
@Directive({
selector: "[bitMenuTriggerFor]",
})
export class MenuTriggerForDirective implements OnDestroy {
@HostBinding("attr.aria-expanded") isOpen = false;
@HostBinding("attr.aria-haspopup") hasPopup = "menu";
@HostBinding("attr.role") role = "button";
@Input("bitMenuTriggerFor") menu: MenuComponent;
private overlayRef: OverlayRef;
private defaultMenuConfig: OverlayConfig = {
panelClass: "bit-menu-panel",
hasBackdrop: true,
backdropClass: "cdk-overlay-transparent-backdrop",
scrollStrategy: this.overlay.scrollStrategies.reposition(),
positionStrategy: this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
},
{
originX: "end",
originY: "bottom",
overlayX: "end",
overlayY: "top",
},
])
.withLockedPosition(true)
.withFlexibleDimensions(false)
.withPush(false),
};
private closedEventsSub: Subscription;
private keyDownEventsSub: Subscription;
constructor(
private elementRef: ElementRef<HTMLElement>,
private viewContainerRef: ViewContainerRef,
private overlay: Overlay
) {}
@HostListener("click") toggleMenu() {
this.isOpen ? this.destroyMenu() : this.openMenu();
}
ngOnDestroy() {
this.disposeAll();
}
private openMenu() {
if (this.menu == null) {
throw new Error("Cannot find bit-menu element");
}
this.isOpen = true;
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
const templatePortal = new TemplatePortal(this.menu.templateRef, this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
if (event?.key === "Tab") {
// Required to ensure tab order resumes correctly
this.elementRef.nativeElement.focus();
}
this.destroyMenu();
});
this.keyDownEventsSub = this.overlayRef
.keydownEvents()
.subscribe((event: KeyboardEvent) => this.menu.keyManager.onKeydown(event));
}
private destroyMenu() {
if (this.overlayRef == null || !this.isOpen) {
return;
}
this.isOpen = false;
this.disposeAll();
}
private getClosedEvents(): Observable<any> {
const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef
.keydownEvents()
.pipe(filter((event: KeyboardEvent) => event.key === "Escape" || event.key === "Tab"));
const backdrop = this.overlayRef.backdropClick();
const menuClosed = this.menu.closed;
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
}
private disposeAll() {
this.closedEventsSub?.unsubscribe();
this.overlayRef?.dispose();
this.keyDownEventsSub?.unsubscribe();
}
}

View File

@@ -0,0 +1,9 @@
<ng-template>
<div
(click)="closed.emit()"
class="tw-flex tw-flex-col tw-bg-background tw-border tw-border-solid tw-rounded tw-border-secondary-500 tw-bg-clip-padding tw-py-2 tw-shrink-0"
role="menu"
>
<ng-content></ng-content>
</div>
</ng-template>

View File

@@ -0,0 +1,77 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuModule } from "./index";
describe("Menu", () => {
let fixture: ComponentFixture<TestApp>;
const getMenuTriggerDirective = () => {
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
return buttonDebugElement.injector.get(MenuTriggerForDirective);
};
// The overlay is created outside the root debugElement, so we need to query its parent
const getBitMenuPanel = () => document.querySelector(".bit-menu-panel");
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MenuModule],
declarations: [TestApp],
});
TestBed.compileComponents();
fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
})
);
it("should open when the trigger is clicked", async () => {
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
(buttonDebugElement.nativeElement as HTMLButtonElement).click();
expect(getBitMenuPanel()).toBeTruthy();
});
it("should close when the trigger is clicked", () => {
getMenuTriggerDirective().toggleMenu();
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
(buttonDebugElement.nativeElement as HTMLButtonElement).click();
expect(getBitMenuPanel()).toBeFalsy();
});
it("should close when a menu item is clicked", () => {
getMenuTriggerDirective().toggleMenu();
(document.querySelector("#item1") as HTMLAnchorElement).click();
expect(getBitMenuPanel()).toBeFalsy();
});
it("should close when the backdrop is clicked", () => {
getMenuTriggerDirective().toggleMenu();
(document.querySelector(".cdk-overlay-backdrop") as HTMLAnchorElement).click();
expect(getBitMenuPanel()).toBeFalsy();
});
});
@Component({
selector: "test-app",
template: `
<button type="button" [bitMenuTriggerFor]="testMenu" class="testclass">Open menu</button>
<bit-menu #testMenu>
<a id="item1" bitMenuItem>Item 1</a>
<a id="item2" bitMenuItem>Item 2</a>
</bit-menu>
`,
})
class TestApp {}

View File

@@ -0,0 +1,30 @@
import { FocusKeyManager } from "@angular/cdk/a11y";
import {
Component,
Output,
TemplateRef,
ViewChild,
EventEmitter,
ContentChildren,
QueryList,
AfterContentInit,
} from "@angular/core";
import { MenuItemDirective } from "./menu-item.directive";
@Component({
selector: "bit-menu",
templateUrl: "./menu.component.html",
exportAs: "menuComponent",
})
export class MenuComponent implements AfterContentInit {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
@Output() closed = new EventEmitter<void>();
@ContentChildren(MenuItemDirective, { descendants: true })
menuItems: QueryList<MenuItemDirective>;
keyManager: FocusKeyManager<MenuItemDirective>;
ngAfterContentInit() {
this.keyManager = new FocusKeyManager(this.menuItems).withWrap();
}
}

View File

@@ -0,0 +1,15 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { MenuDividerComponent } from "./menu-divider.component";
import { MenuItemDirective } from "./menu-item.directive";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuComponent } from "./menu.component";
@NgModule({
imports: [CommonModule, OverlayModule],
declarations: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent],
exports: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent],
})
export class MenuModule {}

View File

@@ -0,0 +1,69 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../button/button.module";
import { MenuDividerComponent } from "./menu-divider.component";
import { MenuItemDirective } from "./menu-item.directive";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuComponent } from "./menu.component";
export default {
title: "Jslib/Menu",
component: MenuTriggerForDirective,
decorators: [
moduleMetadata({
declarations: [
MenuTriggerForDirective,
MenuComponent,
MenuItemDirective,
MenuDividerComponent,
],
imports: [OverlayModule, ButtonModule],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17952",
},
},
} as Meta;
const Template: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
props: args,
template: `
<bit-menu #myMenu="menuComponent">
<a href="#" bitMenuItem>Anchor link</a>
<a href="#" bitMenuItem>Another link</a>
<button type="button" bitMenuItem>Button</button>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem>Button after divider</button>
</bit-menu>
<div class="tw-h-40">
<div class="cdk-overlay-pane bit-menu-panel">
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container>
</div>
</div>
`,
});
const TemplateWithButton: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
props: args,
template: `
<div class="tw-h-40">
<button bitButton buttonType="secondary" [bitMenuTriggerFor]="myMenu">Open menu</button>
</div>
<bit-menu #myMenu>
<a href="#" bitMenuItem>Anchor link</a>
<a href="#" bitMenuItem>Another link</a>
<button type="button" bitMenuItem>Button</button>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem>Button after divider</button>
</bit-menu>`,
});
export const OpenMenu = Template.bind({});
export const ClosedMenu = TemplateWithButton.bind({});