mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +00:00
SM-310 [] Secrets (#3355)
* [SM-63] Secrets List overview (#3239) The purpose of this PR is to create a new component for the Secrets Manager project where all the secrets associated to a specific organization ID can be viewed. * [SM-63] Secrets List overview (#3239) The purpose of this PR is to create a new component for the Secrets Manager project where all the secrets associated to a specific organization ID can be viewed. * [SM-63] Display dates based off Figma (#3358) * Display dates based off Figma * Swapping date to medium format * [SM-185] Use feature flags for secrets (#3409) * Fix SM lint errors (#3526) * Fix SM lint errors * Update bitwarden_license/bit-web/src/app/sm/secrets/secrets.component.ts Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * [SM-65] Create/Edit Secrets Dialog (#3376) The purpose of this PR is to add a Create/Edit Secrets dialog component. * [SM-198] Empty Secrets View (#3585) * SM-198 Empty Secrets View * [SM-64] Soft delete secrets (#3549) * Soft delete secrets * SM-95-ProjectList (#3508) * Adding project list and creating a shared module for secrets * updates to style , temporarily using secrets results until API portion is completed * removing non project related options from the list, updting api call to call projects now * Adding view project option from drop down * Changes requested by Thomas * Changes requested by Thomas * suggested fixes * fixes after merge from master * Adding decrypting to project list * Update bitwarden_license/bit-web/src/app/sm/shared/sm-shared.module.ts Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * Update bitwarden_license/bit-web/src/app/sm/projects/project.service.ts Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * Update bitwarden_license/bit-web/src/app/sm/projects/project.service.ts Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * fix to projectRequest so name is type EncString instead of string * lint + prettier fixes * Oscar's suggestions - Removing this. from projectList * updating to use bitIconButton * Updating to use BitIconButton Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * Fix double edit secret dialog (#3645) * Fix typescript errors on secrets init (#3649) * Resolve breaking changes * Remove unecessary class * SM-198 Update empty list text. (#3652) * [SM-267] Minor visual fixes (#3673) * SM-96: Add/Edit Project for SM (#3616) * SM-96: Initial add for Add/Edit project * Update secrets.module.ts * Small fixes based on PR comments * SM-96: Small fixes + fix new project creation * Fully fix create / edit project * SM-96: Update toast text * Remove message with exclamation * SM-96: Fix broken build * SM-96: Remove disabled on save buttons for SM dialogs & switch to early exits * SM-96: Run linter * [SM-186] Service Accounts - Overview (#3653) * SM-186 Service Accounts Overview * Remove duplicate titles (#3659) * [SM-187] Create Service Account Dialog (#3710) * SM-187 Create Service Account Dialog * Fix renamed paths * SM Modal Updates (#3776) * Add type=button to cancel button on sm dialogs * Update new secret/project modal titles to match design * Add loading spinner for project and secret edit modals * Add max length to project name * Use Tailwind CSS class instead of custom and remove click handler * Fix spinner * Add buttonType=primary to project dialog save button * Fix loading change for secret dialog and use tw-text-center Co-authored-by: Hinton <hinton@users.noreply.github.com> * [SM-113] Delete Projects Dialog (#3777) * SM-113 Add Delete Projects Dialog * [SM-306] Migrate secrets dialog to async form (#3849) * [SM-310] Prepare secrets manager for merge to master (#3885) * Remove Built In Validator on Project Delete (#3909) Handle all Project Delete validation through custom validator * [SM-312] Mark all inputs as touched when submitting (#3920) * Use new icon for no item (#3917) * Create navigation component (#3881) * [SM-150] Project / Secret mapping (#3912) * wip * removing added file * updates * handling projects and secrets mapping in UI * moving files and fixing errors * Update bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets-list.component.html Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * Decrypt the name * fixing the secrets-list.component bug * renaming file and view name * lint fixes * removing secret with projects list response, and other misc name changes * Adding back things I shouldnt have deleted * Update bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret-with-projects-list.response.ts Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * updating button (#3779) * [SM-300] Access Tokens (#3955) * [SM-301] fix: associate labels with inputs (#4058) * fix: wrap input in label * fix: update all label in projects and service accounts * [SM-196] Create Access Token Dialog (#4095) * Add create access token dialog * Use ServiceAccountView for access token creation * Set version to readonly for access token * DRY up Expiration Date & bug fix * Break out expiration options component * Move expiration-options to layout; Match FIGMA * Create Generic Key generator * Add getByServiceAccountId * Change to use keyMaterial and not the full key * Use access token id, not service account * Remove generic key generator * Swap to service account name placeholder * Swap ExpirationOptions to a CVA * No longer masking according to FIGMA * Remove schema comment * Code review updates * Update required logic and approach * Move ExpirationOptionsComponent into access Co-authored-by: Hinton <hinton@users.noreply.github.com> * SM-99: Individual Project / Secrets Tab (#4011) Co-authored-by: Hinton <hinton@users.noreply.github.com> * Fixes for the demo (#4159) * [SM-360] Add support for never expiring access tokens (#4150) * Add support for never expiring access tokens * Render performance fixes * Update bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * [SM-360] Fix access token display dialog for never expiring tokens (#4164) * Fix access token display dialog * Add disableClose to access token display dialog * [SM-299] Add license checks (#4078) * [SM-69] feature: create org-switcher, bit-nav-item, bit-nav-group, bit-nav-divider (#4073) * feat: create nav-item, nav-group, org-switcher * add tree variant; add stories; move to component library * render button if no link is present * fix routerLinkActive; add template comments; fix styles * update storybook stories * rename to route * a11y fixes * update stories * simplify tree nesting * rename nav-base component * add divider; finish org-switcher; add overview page skeleton * add nav-divider story * code review * rename components to CL naming scheme * fix iconButton focus color * apply code review changes * fix strict template route param * add ariaLabel input; update org-switcher a11y * add two way binding for nav-group open state; update stories * add toggle control to org-switcher * [SM-310] Disable Secrets Manager in QA (#4199) Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Thomas Avery <tavery@bitwarden.com> Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com> Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com> Co-authored-by: Will Martin <contact@willmartian.com>
This commit is contained in:
@@ -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"],
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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";
|
||||
|
||||
1
libs/components/src/navigation/index.ts
Normal file
1
libs/components/src/navigation/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./navigation.module";
|
||||
47
libs/components/src/navigation/nav-base.component.ts
Normal file
47
libs/components/src/navigation/nav-base.component.ts
Normal 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();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<div class="tw-h-px tw-w-full tw-bg-secondary-300"></div>
|
||||
7
libs/components/src/navigation/nav-divider.component.ts
Normal file
7
libs/components/src/navigation/nav-divider.component.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "bit-nav-divider",
|
||||
templateUrl: "./nav-divider.component.html",
|
||||
})
|
||||
export class NavDividerComponent {}
|
||||
46
libs/components/src/navigation/nav-group.component.html
Normal file
46
libs/components/src/navigation/nav-group.component.html
Normal 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>
|
||||
62
libs/components/src/navigation/nav-group.component.ts
Normal file
62
libs/components/src/navigation/nav-group.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
74
libs/components/src/navigation/nav-group.stories.ts
Normal file
74
libs/components/src/navigation/nav-group.stories.ts
Normal 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>
|
||||
`,
|
||||
});
|
||||
79
libs/components/src/navigation/nav-item.component.html
Normal file
79
libs/components/src/navigation/nav-item.component.html
Normal 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>
|
||||
48
libs/components/src/navigation/nav-item.component.ts
Normal file
48
libs/components/src/navigation/nav-item.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
93
libs/components/src/navigation/nav-item.stories.ts
Normal file
93
libs/components/src/navigation/nav-item.stories.ts
Normal 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>
|
||||
`,
|
||||
});
|
||||
18
libs/components/src/navigation/navigation.module.ts
Normal file
18
libs/components/src/navigation/navigation.module.ts
Normal 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 {}
|
||||
@@ -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")}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user