[PM-9638] Browser V2 Item Details Defects (#10124)

* Item Details Refactored. Created OrgIcon directive, Added screen reader logic, removed excess styling.
This commit is contained in:
Jason Ng 2024-07-23 13:29:46 -04:00 committed by GitHub
parent 6041c460b7
commit 0e0c44b90b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 115 additions and 41 deletions

View File

@ -3850,5 +3850,8 @@
"example": "3" "example": "3"
} }
} }
},
"itemLocation": {
"message": "Item Location"
} }
} }

View File

@ -27,9 +27,10 @@
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon> <app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
<span data-testid="item-name">{{ cipher.name }}</span> <span data-testid="item-name">{{ cipher.name }}</span>
<i <i
class="bwi bwi-sm"
*ngIf="cipher.organizationId" *ngIf="cipher.organizationId"
[ngClass]="cipher.orgIcon" appOrgIcon
[tierType]="cipher.organization.productTierType"
[size]="'small'"
[appA11yTitle]="orgIconTooltip(cipher)" [appA11yTitle]="orgIconTooltip(cipher)"
></i> ></i>
<span slot="secondary">{{ cipher.subTitle }}</span> <span slot="secondary">{{ cipher.subTitle }}</span>

View File

@ -13,6 +13,7 @@ import {
SectionHeaderComponent, SectionHeaderComponent,
TypographyModule, TypographyModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { OrgIconDirective } from "@bitwarden/vault";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { PopupCipherView } from "../../../views/popup-cipher.view"; import { PopupCipherView } from "../../../views/popup-cipher.view";
@ -33,6 +34,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
RouterLink, RouterLink,
ItemCopyActionsComponent, ItemCopyActionsComponent,
ItemMoreOptionsComponent, ItemMoreOptionsComponent,
OrgIconDirective,
], ],
selector: "app-vault-list-items-container", selector: "app-vault-list-items-container",
templateUrl: "vault-list-items-container.component.html", templateUrl: "vault-list-items-container.component.html",

View File

@ -1,5 +1,4 @@
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
@ -21,21 +20,4 @@ export class PopupCipherView extends CipherView {
this.collections = collections; this.collections = collections;
this.organization = organization; this.organization = organization;
} }
/**
* Get the bwi icon for the cipher according to the organization type.
*/
get orgIcon(): "bwi-family" | "bwi-business" | null {
switch (this.organization?.productTierType) {
case ProductTierType.Free:
case ProductTierType.Families:
return "bwi-family";
case ProductTierType.Teams:
case ProductTierType.Enterprise:
case ProductTierType.TeamsStarter:
return "bwi-business";
default:
return null;
}
}
} }

View File

@ -3,36 +3,68 @@
<h2 bitTypography="h6">{{ "itemDetails" | i18n }}</h2> <h2 bitTypography="h6">{{ "itemDetails" | i18n }}</h2>
</bit-section-header> </bit-section-header>
<bit-card> <bit-card>
<div class="tw-pb-2 tw-mb-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"> <div>
<label class="tw-block tw-w-full tw-mb-1 tw-text-xs tw-text-muted tw-select-none"> <label class="tw-block tw-w-full tw-mb-1 tw-text-xs tw-text-muted tw-select-none">
{{ "itemName" | i18n }} {{ "itemName" | i18n }}
</label> </label>
<input readonly bitInput type="text" [value]="cipher.name" aria-readonly="true" /> <input readonly bitInput type="text" [value]="cipher.name" aria-readonly="true" />
</div> </div>
<div *ngIf="cipher.collectionIds || cipher.organizationId || cipher.folderId"> <ul
<p [attr.aria-label]="'itemLocation' | i18n"
*ngIf="!cipher.organizationId" *ngIf="cipher.collectionIds || cipher.organizationId || cipher.folderId"
[ngClass]="{ 'tw-mb-3': cipher.collectionIds }" >
bitTypography="body2" <li
>
<i class="bwi bwi-user bwi-lg bwi-fw"></i> {{ "ownerYou" | i18n }}
</p>
<p
*ngIf="cipher.organizationId && organization" *ngIf="cipher.organizationId && organization"
class="tw-flex tw-list-none"
[ngClass]="{ 'tw-mb-3': cipher.collectionIds }" [ngClass]="{ 'tw-mb-3': cipher.collectionIds }"
bitTypography="body2" bitTypography="body2"
[attr.aria-label]="('owner' | i18n) + organization.name"
> >
<i class="bwi bwi-business bwi-lg bwi-fw"></i> {{ organization.name }} <i
</p> class="tw-pt-1"
<ul *ngIf="cipher.collectionIds && collections" [ngClass]="{ 'tw-mb-2': cipher.folderId }"> appOrgIcon
<p *ngFor="let collection of collections" class="tw-mb-1" bitTypography="body2"> [tierType]="organization.productTierType"
<i class="bwi bwi-collection bwi-lg bwi-fw"></i> {{ collection.name }} [size]="'large'"
</p> [title]="'owner' | i18n"
</ul> ></i>
<p *ngIf="cipher.folderId && folder" bitTypography="body2"> <span aria-hidden="true" class="tw-pl-1 tw-mb-1">
<i class="bwi bwi-folder bwi-lg bwi-fw"></i> {{ folder.name }} {{ organization.name }}
</p> </span>
</div> </li>
<li
class="tw-list-none"
*ngIf="cipher.collectionIds && collections"
[attr.aria-label]="'collection' | i18n"
>
<ul>
<li
*ngFor="let collection of collections; let last = last"
class="tw-flex tw-list-none"
bitTypography="body2"
[ngClass]="{ 'tw-mb-3': last }"
[attr.aria-label]="collection.name"
>
<i
class="bwi bwi-collection bwi-lg tw-pt-1"
aria-hidden="true"
[title]="'collection' | i18n"
></i>
<span aria-hidden="true" class="tw-pl-1 tw-mb-1">
{{ collection.name }}
</span>
</li>
</ul>
</li>
<li
*ngIf="cipher.folderId && folder"
bitTypography="body2"
class="tw-flex tw-list-none"
[attr.aria-label]="('folder' | i18n) + folder.name"
>
<i class="bwi bwi-folder bwi-lg tw-pt-1" aria-hidden="true" [title]="'folder' | i18n"></i>
<span aria-hidden="true" class="tw-pl-1 tw-mb-1">{{ folder.name }} </span>
</li>
</ul>
</bit-card> </bit-card>
</bit-section> </bit-section>

View File

@ -13,6 +13,8 @@ import {
TypographyModule, TypographyModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { OrgIconDirective } from "../../components/org-icon.directive";
@Component({ @Component({
selector: "app-item-details-v2", selector: "app-item-details-v2",
templateUrl: "item-details-v2.component.html", templateUrl: "item-details-v2.component.html",
@ -24,6 +26,7 @@ import {
SectionComponent, SectionComponent,
SectionHeaderComponent, SectionHeaderComponent,
TypographyModule, TypographyModule,
OrgIconDirective,
], ],
}) })
export class ItemDetailsV2Component { export class ItemDetailsV2Component {

View File

@ -0,0 +1,50 @@
import { Directive, ElementRef, HostBinding, Input, Renderer2 } from "@angular/core";
import { ProductTierType } from "@bitwarden/common/billing/enums";
export type OrgIconSize = "default" | "small" | "large";
@Directive({
standalone: true,
selector: "[appOrgIcon]",
})
export class OrgIconDirective {
@Input({ required: true }) tierType: ProductTierType;
@Input() size?: OrgIconSize = "default";
constructor(
private el: ElementRef,
private renderer: Renderer2,
) {
this.renderer.setAttribute(this.el.nativeElement, "aria-hidden", "true");
}
get iconSize(): "bwi-sm" | "bwi-lg" | "" {
switch (this.size) {
case "small":
return "bwi-sm";
case "large":
return "bwi-lg";
default:
return "";
}
}
get orgIcon(): string {
switch (this.tierType) {
case ProductTierType.Free:
case ProductTierType.Families:
return "bwi-family";
case ProductTierType.Teams:
case ProductTierType.Enterprise:
case ProductTierType.TeamsStarter:
return "bwi-business";
default:
return "";
}
}
@HostBinding("class") get classList() {
return ["bwi", this.iconSize, this.orgIcon];
}
}

View File

@ -1,6 +1,7 @@
export { PasswordRepromptService } from "./services/password-reprompt.service"; export { PasswordRepromptService } from "./services/password-reprompt.service";
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service"; export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive"; export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
export { OrgIconDirective } from "./components/org-icon.directive";
export * from "./cipher-view"; export * from "./cipher-view";
export * from "./cipher-form"; export * from "./cipher-form";