1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

Merge branch 'feature/organization-domain-claiming' into feature/SG-680-create-domain-verification-comp

This commit is contained in:
Jared Snider
2022-12-12 12:02:38 -05:00
263 changed files with 12257 additions and 1061 deletions

View File

@@ -1,6 +1,7 @@
import { Directive, HostListener, Input, OnDestroy, Optional } from "@angular/core";
import { BehaviorSubject, finalize, Subject, takeUntil, tap } from "rxjs";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
@@ -23,7 +24,8 @@ export class BitActionDirective implements OnDestroy {
constructor(
private buttonComponent: ButtonLikeAbstraction,
@Optional() private validationService?: ValidationService
@Optional() private validationService?: ValidationService,
@Optional() private logService?: LogService
) {}
get loading() {
@@ -44,7 +46,12 @@ export class BitActionDirective implements OnDestroy {
this.loading = true;
functionToObservable(this.handler)
.pipe(
tap({ error: (err: unknown) => this.validationService?.showError(err) }),
tap({
error: (err: unknown) => {
this.logService?.error(`Async action exception: ${err}`);
this.validationService?.showError(err);
},
}),
finalize(() => (this.loading = false)),
takeUntil(this.destroy$)
)

View File

@@ -2,6 +2,7 @@ import { Directive, Input, OnDestroy, OnInit, Optional } from "@angular/core";
import { FormGroupDirective } from "@angular/forms";
import { BehaviorSubject, catchError, filter, of, Subject, switchMap, takeUntil } from "rxjs";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { FunctionReturningAwaitable, functionToObservable } from "../utils/function-to-observable";
@@ -24,7 +25,8 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
constructor(
private formGroupDirective: FormGroupDirective,
@Optional() validationService?: ValidationService
@Optional() validationService?: ValidationService,
@Optional() logService?: LogService
) {
formGroupDirective.ngSubmit
.pipe(
@@ -39,6 +41,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
return awaitable.pipe(
catchError((err: unknown) => {
logService?.error(`Async submit exception: ${err}`);
validationService?.showError(err);
return of(undefined);
})

View File

@@ -3,6 +3,7 @@ import { action } from "@storybook/addon-actions";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { delay, of } from "rxjs";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { ButtonModule } from "../button";
@@ -68,6 +69,12 @@ export default {
showError: action("ValidationService.showError"),
} as Partial<ValidationService>,
},
{
provide: LogService,
useValue: {
error: action("LogService.error"),
} as Partial<LogService>,
},
],
}),
],

View File

@@ -7,7 +7,7 @@ type SizeTypes = "large" | "default" | "small";
const SizeClasses: Record<SizeTypes, string[]> = {
large: ["tw-h-16", "tw-w-16"],
default: ["tw-h-12", "tw-w-12"],
default: ["tw-h-10", "tw-w-10"],
small: ["tw-h-7", "tw-w-7"],
};

View File

@@ -43,7 +43,6 @@ class ExampleComponent {
export default {
title: "Component Library/Form/Checkbox",
component: ExampleComponent,
decorators: [
moduleMetadata({
declarations: [ExampleComponent],
@@ -68,10 +67,6 @@ export default {
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4",
},
},
args: {
checked: false,
disabled: false,
},
} as Meta;
const DefaultTemplate: Story<ExampleComponent> = (args: ExampleComponent) => ({
@@ -80,6 +75,17 @@ const DefaultTemplate: Story<ExampleComponent> = (args: ExampleComponent) => ({
});
export const Default = DefaultTemplate.bind({});
Default.parameters = {
docs: {
source: {
code: template,
},
},
};
Default.args = {
checked: false,
disabled: false,
};
const CustomTemplate: Story = (args) => ({
props: args,
@@ -100,5 +106,6 @@ const CustomTemplate: Story = (args) => ({
</div>
`,
});
CustomTemplate.args = {};
export const Custom = CustomTemplate.bind({});

View File

@@ -18,7 +18,7 @@ import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
DialogComponent,
SimpleDialogComponent,
],
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent],
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent, DialogCloseDirective],
providers: [DialogService],
})
export class DialogModule {}

View File

@@ -9,7 +9,7 @@ export class DialogComponent {
@Input() dialogSize: "small" | "default" | "large" = "default";
private _disablePadding: boolean;
@Input() set disablePadding(value: boolean) {
@Input() set disablePadding(value: boolean | string) {
this._disablePadding = coerceBooleanProperty(value);
}
get disablePadding() {

View File

@@ -1,4 +1,11 @@
export type InputTypes = "text" | "password" | "number" | "datetime-local" | "email" | "checkbox";
export type InputTypes =
| "text"
| "password"
| "number"
| "datetime-local"
| "email"
| "checkbox"
| "search";
export abstract class BitFormFieldControl {
ariaDescribedBy: string;

View File

@@ -5,6 +5,7 @@ export * from "./banner";
export * from "./button";
export * from "./callout";
export * from "./checkbox";
export * from "./color-password";
export * from "./dialog";
export * from "./form-field";
export * from "./icon-button";
@@ -12,8 +13,8 @@ export * from "./icon";
export * from "./link";
export * from "./menu";
export * from "./multi-select";
export * from "./tabs";
export * from "./navigation";
export * from "./table";
export * from "./tabs";
export * from "./toggle-group";
export * from "./color-password";
export * from "./utils/i18n-mock.service";

View File

@@ -0,0 +1 @@
export * from "./navigation.module";

View File

@@ -0,0 +1,47 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
/**
* Base class used in `NavGroupComponent` and `NavItemComponent`
*/
@Directive()
export abstract class NavBaseComponent {
/**
* Text to display in main content
*/
@Input() text: string;
/**
* `aria-label` for main content
*/
@Input() ariaLabel: string;
/**
* Optional icon, e.g. `"bwi-collection"`
*/
@Input() icon: string;
/**
* Route to be passed to internal `routerLink`
*/
@Input() route: string | any[];
/**
* If this item is used within a tree, set `variant` to `"tree"`
*/
@Input() variant: "default" | "tree" = "default";
/**
* Depth level when nested inside of a `'tree'` variant
*/
@Input() treeDepth = 0;
/**
* If `true`, do not change styles when nav item is active.
*/
@Input() hideActiveStyles = false;
/**
* Fires when main content is clicked
*/
@Output() mainContentClicked: EventEmitter<MouseEvent> = new EventEmitter();
}

View File

@@ -0,0 +1 @@
<div class="tw-h-px tw-w-full tw-bg-secondary-300"></div>

View File

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

View File

@@ -0,0 +1,46 @@
<!-- This a higher order component that composes `NavItemComponent` -->
<bit-nav-item
[text]="text"
[icon]="icon"
[route]="route"
[variant]="variant"
(mainContentClicked)="toggle()"
[treeDepth]="treeDepth"
(mainContentClicked)="mainContentClicked.emit()"
[ariaLabel]="ariaLabel"
>
<ng-template #button>
<button
class="tw-ml-auto"
[bitIconButton]="
open ? 'bwi-chevron-up' : variant === 'tree' ? 'bwi-angle-right' : 'bwi-angle-down'
"
[buttonType]="'main'"
(click)="toggle($event)"
size="small"
[title]="'toggleCollapse' | i18n"
aria-haspopup="true"
[attr.aria-expanded]="open.toString()"
[attr.aria-controls]="contentId"
[attr.aria-label]="['toggleCollapse' | i18n, text].join(' ')"
></button>
</ng-template>
<!-- Show toggle to the left for trees otherwise to the right -->
<ng-container slot-start *ngIf="variant === 'tree'">
<ng-container *ngTemplateOutlet="button"></ng-container>
</ng-container>
<ng-container slot-end *ngIf="variant !== 'tree'">
<ng-container *ngTemplateOutlet="button"></ng-container>
</ng-container>
</bit-nav-item>
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
<div
*ngIf="open"
[attr.id]="contentId"
[attr.aria-label]="[text, 'submenu' | i18n].join(' ')"
role="group"
>
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,62 @@
import {
AfterContentInit,
Component,
ContentChildren,
EventEmitter,
Input,
Output,
QueryList,
} from "@angular/core";
import { NavBaseComponent } from "./nav-base.component";
import { NavItemComponent } from "./nav-item.component";
@Component({
selector: "bit-nav-group",
templateUrl: "./nav-group.component.html",
})
export class NavGroupComponent extends NavBaseComponent implements AfterContentInit {
@ContentChildren(NavGroupComponent, {
descendants: true,
})
nestedGroups!: QueryList<NavGroupComponent>;
@ContentChildren(NavItemComponent, {
descendants: true,
})
nestedItems!: QueryList<NavItemComponent>;
/**
* UID for `[attr.aria-controls]`
*/
protected contentId = Math.random().toString(36).substring(2);
/**
* Is `true` if the expanded content is visible
*/
@Input()
open = false;
@Output()
openChange = new EventEmitter<boolean>();
protected toggle(event?: MouseEvent) {
event?.stopPropagation();
this.open = !this.open;
}
/**
* - For any nested NavGroupComponents or NavItemComponents, increment the `treeDepth` by 1.
*/
private initNestedStyles() {
if (this.variant !== "tree") {
return;
}
[...this.nestedGroups, ...this.nestedItems].forEach((navGroupOrItem) => {
navGroupOrItem.treeDepth += 1;
});
}
ngAfterContentInit(): void {
this.initNestedStyles();
}
}

View File

@@ -0,0 +1,74 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { SharedModule } from "../shared/shared.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { NavGroupComponent } from "./nav-group.component";
import { NavigationModule } from "./navigation.module";
export default {
title: "Component Library/Nav/Nav Group",
component: NavGroupComponent,
decorators: [
moduleMetadata({
imports: [SharedModule, RouterTestingModule, NavigationModule],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
submenu: "submenu",
toggleCollapse: "toggle collapse",
});
},
},
],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642",
},
},
} as Meta;
export const Default: Story<NavGroupComponent> = (args) => ({
props: args,
template: `
<bit-nav-group text="Hello World (Anchor)" [route]="['']" icon="bwi-filter" [open]="true">
<bit-nav-item text="Child A" route="#" icon="bwi-filter"></bit-nav-item>
<bit-nav-item text="Child B" route="#"></bit-nav-item>
<bit-nav-item text="Child C" route="#" icon="bwi-filter"></bit-nav-item>
</bit-nav-group>
<bit-nav-group text="Lorem Ipsum (Button)" icon="bwi-filter">
<bit-nav-item text="Child A" icon="bwi-filter"></bit-nav-item>
<bit-nav-item text="Child B"></bit-nav-item>
<bit-nav-item text="Child C" icon="bwi-filter"></bit-nav-item>
</bit-nav-group>
`,
});
export const Tree: Story<NavGroupComponent> = (args) => ({
props: args,
template: `
<bit-nav-group text="Tree example" icon="bwi-collection" [open]="true">
<bit-nav-group text="Level 1 - with children (empty)" route="#" icon="bwi-collection" variant="tree"></bit-nav-group>
<bit-nav-item text="Level 1 - no childen" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
<bit-nav-group text="Level 1 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
<bit-nav-group text="Level 2 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
<bit-nav-item text="Level 3 - no childen, no icon" route="#" variant="tree"></bit-nav-item>
<bit-nav-group text="Level 3 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
<bit-nav-item text="Level 4 - no childen, no icon" route="#" variant="tree"></bit-nav-item>
</bit-nav-group>
</bit-nav-group>
<bit-nav-group text="Level 2 - with children (empty)" route="#" icon="bwi-collection" variant="tree" [open]="true"></bit-nav-group>
<bit-nav-item text="Level 2 - no childen" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
</bit-nav-group>
<bit-nav-item text="Level 1 - no childen" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
</bit-nav-group>
`,
});

View File

@@ -0,0 +1,79 @@
<div
class="tw-relative"
[ngClass]="[
showActiveStyles ? 'tw-bg-background-alt4' : 'tw-bg-background-alt3',
fvwStyles$ | async
]"
>
<div
[ngStyle]="{
'padding-left': (variant === 'tree' ? 2.5 : 1) + treeDepth * 1.5 + 'rem'
}"
class="tw-relative tw-flex tw-items-center tw-pr-4"
[ngClass]="[variant === 'tree' ? 'tw-py-1' : 'tw-py-2']"
>
<div
#slotStart
class="[&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*]:!tw-text-alt2 [&>*:hover]:!tw-border-text-alt2"
>
<ng-content select="[slot-start]"></ng-content>
</div>
<!-- Default content for #slotStart (for consistent sizing) -->
<div
*ngIf="slotStart.childElementCount === 0"
[ngClass]="{
'tw-w-0': variant !== 'tree'
}"
>
<button
class="tw-invisible"
[bitIconButton]="'bwi-angle-down'"
size="small"
aria-hidden="true"
></button>
</div>
<ng-container *ngIf="route; then isAnchor; else isButton"></ng-container>
<!-- Main content of `NavItem` -->
<ng-template #anchorAndButtonContent>
<i class="bwi bwi-fw tw-text-alt2 tw-mx-1 {{ icon }}"></i
><span [ngClass]="showActiveStyles ? 'tw-font-bold' : 'tw-font-semibold'">{{ text }}</span>
</ng-template>
<!-- Show if a value was passed to `this.to` -->
<ng-template #isAnchor>
<!-- The `fvw` class passes focus to `this.focusVisibleWithin$` -->
<!-- The following `class` field should match the `#isButton` class field below -->
<a
class="fvw tw-w-full tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-border-none tw-bg-transparent tw-p-0 tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&>:not(.bwi)]:hover:tw-underline"
[routerLink]="route"
[attr.aria-label]="ariaLabel || text"
routerLinkActive
[routerLinkActiveOptions]="rlaOptions"
[ariaCurrentWhenActive]="'page'"
(isActiveChange)="setActive($event)"
(click)="mainContentClicked.emit()"
>
<ng-container *ngTemplateOutlet="anchorAndButtonContent"></ng-container>
</a>
</ng-template>
<!-- Show if `this.to` is falsy -->
<ng-template #isButton>
<!-- Class field should match `#isAnchor` class field above -->
<button
class="fvw tw-w-full tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-border-none tw-bg-transparent tw-p-0 tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&>:not(.bwi)]:hover:tw-underline"
(click)="mainContentClicked.emit()"
>
<ng-container *ngTemplateOutlet="anchorAndButtonContent"></ng-container>
</button>
</ng-template>
<div
class="tw-flex tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*]:!tw-text-alt2 [&>*:hover]:!tw-border-text-alt2"
>
<ng-content select="[slot-end]"></ng-content>
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
import { Component, HostListener } from "@angular/core";
import { IsActiveMatchOptions } from "@angular/router";
import { BehaviorSubject, map } from "rxjs";
import { NavBaseComponent } from "./nav-base.component";
@Component({
selector: "bit-nav-item",
templateUrl: "./nav-item.component.html",
})
export class NavItemComponent extends NavBaseComponent {
/**
* Is `true` if `to` matches the current route
*/
private _active = false;
protected setActive(isActive: boolean) {
this._active = isActive;
}
protected get showActiveStyles() {
return this._active && !this.hideActiveStyles;
}
protected readonly rlaOptions: IsActiveMatchOptions = {
paths: "exact",
queryParams: "exact",
fragment: "ignored",
matrixParams: "ignored",
};
/**
* The design spec calls for the an outline to wrap the entire element when the template's anchor/button has :focus-visible.
* Usually, we would use :focus-within for this. However, that matches when a child element has :focus instead of :focus-visible.
*
* Currently, the browser does not have a pseudo selector that combines these two, e.g. :focus-visible-within (WICG/focus-visible#151)
* To make our own :focus-visible-within functionality, we use event delegation on the host and manually check if the focus target (denoted with the .fvw class) matches :focus-visible. We then map that state to some styles, so the entire component can have an outline.
*/
protected focusVisibleWithin$ = new BehaviorSubject(false);
protected fvwStyles$ = this.focusVisibleWithin$.pipe(
map((value) => (value ? "tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-text-alt2" : ""))
);
@HostListener("focusin", ["$event.target"])
onFocusIn(target: HTMLElement) {
this.focusVisibleWithin$.next(target.matches(".fvw:focus-visible"));
}
@HostListener("focusout")
onFocusOut() {
this.focusVisibleWithin$.next(false);
}
}

View File

@@ -0,0 +1,93 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { IconButtonModule } from "../icon-button";
import { NavItemComponent } from "./nav-item.component";
import { NavigationModule } from "./navigation.module";
export default {
title: "Component Library/Nav/Nav Item",
component: NavItemComponent,
decorators: [
moduleMetadata({
declarations: [],
imports: [RouterTestingModule, IconButtonModule, NavigationModule],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642",
},
},
} as Meta;
const Template: Story<NavItemComponent> = (args: NavItemComponent) => ({
props: args,
template: `
<bit-nav-item text="${args.text}" [route]="['']" icon="${args.icon}"></bit-nav-item>
`,
});
export const Default = Template.bind({});
Default.args = {
text: "Hello World",
icon: "bwi-filter",
};
export const WithoutIcon = Template.bind({});
WithoutIcon.args = {
text: "Hello World",
icon: "",
};
export const WithoutRoute: Story<NavItemComponent> = (args: NavItemComponent) => ({
props: args,
template: `
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
`,
});
export const WithChildButtons: Story<NavItemComponent> = (args: NavItemComponent) => ({
props: args,
template: `
<bit-nav-item text="Hello World" [route]="['']" icon="bwi-collection">
<button
slot-start
class="tw-ml-auto"
[bitIconButton]="'bwi-clone'"
[buttonType]="'contrast'"
size="small"
aria-label="option 1"
></button>
<button
slot-end
class="tw-ml-auto"
[bitIconButton]="'bwi-pencil-square'"
[buttonType]="'contrast'"
size="small"
aria-label="option 2"
></button>
<button
slot-end
class="tw-ml-auto"
[bitIconButton]="'bwi-check'"
[buttonType]="'contrast'"
size="small"
aria-label="option 3"
></button>
</bit-nav-item>
`,
});
export const MultipleItemsWithDivider: Story<NavItemComponent> = (args: NavItemComponent) => ({
props: args,
template: `
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
<bit-nav-divider></bit-nav-divider>
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
`,
});

View File

@@ -0,0 +1,18 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { IconButtonModule } from "../icon-button/icon-button.module";
import { SharedModule } from "../shared/shared.module";
import { NavDividerComponent } from "./nav-divider.component";
import { NavGroupComponent } from "./nav-group.component";
import { NavItemComponent } from "./nav-item.component";
@NgModule({
imports: [CommonModule, SharedModule, IconButtonModule, OverlayModule, RouterModule],
declarations: [NavDividerComponent, NavGroupComponent, NavItemComponent],
exports: [NavDividerComponent, NavGroupComponent, NavItemComponent],
})
export class NavigationModule {}

View File

@@ -21,6 +21,8 @@ export const Table = (args) => (
{Row("background")}
{Row("background-alt")}
{Row("background-alt2")}
{Row("background-alt3")}
{Row("background-alt4")}
</tbody>
<tbody>
{Row("primary-300")}

View File

@@ -24,7 +24,7 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit, OnDestr
fragment: "ignored",
};
@Input() route: string;
@Input() route: string | any[];
@Input() disabled = false;
@HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) {

View File

@@ -4,6 +4,8 @@
--color-background: 255 255 255;
--color-background-alt: 251 251 251;
--color-background-alt2: 23 92 219;
--color-background-alt3: 18 82 163;
--color-background-alt4: 13 60 119;
--color-primary-300: 103 149 232;
--color-primary-500: 23 93 220;
@@ -45,6 +47,8 @@
--color-background: 31 36 46;
--color-background-alt: 22 28 38;
--color-background-alt2: 47 52 61;
--color-background-alt3: 47 52 61;
--color-background-alt4: 16 18 21;
--color-primary-300: 23 93 220;
--color-primary-500: 106 153 240;

View File

@@ -56,6 +56,8 @@ module.exports = {
DEFAULT: rgba("--color-background"),
alt: rgba("--color-background-alt"),
alt2: rgba("--color-background-alt2"),
alt3: rgba("--color-background-alt3"),
alt4: rgba("--color-background-alt4"),
},
},
textColor: {
@@ -83,6 +85,9 @@ module.exports = {
"50vw": "50vw",
"75vw": "75vw",
},
minWidth: {
52: "13rem",
},
maxWidth: ({ theme }) => ({
...theme("width"),
"90vw": "90vw",