mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-13 13:49:37 +01:00
[PM-7683] Fix dynamic item defects (#9575)
* [PM-8639] Add data-testid attribute for test automation * [PM-8669] Add autofill aria label * [PM-8674] Show autofill menu options for card/identities when not in the autofill suggestion list * [PM-8635] Hide menu items when copy cipher field directive is disabled * [PM-8636] Disable copy menu dropdown when no items available to copy * [CL-309] Add title override to bitBadge * [PM-8669] Update menu-item directive disabled input * [PM-7683] Fix race condition for remainingCiphers$ * [PM-7683] Use strict equality check
This commit is contained in:
parent
7c16410c86
commit
6687ef5978
@ -3323,16 +3323,6 @@
|
|||||||
"clearFiltersOrTryAnother": {
|
"clearFiltersOrTryAnother": {
|
||||||
"message": "Clear filters or try another search term"
|
"message": "Clear filters or try another search term"
|
||||||
},
|
},
|
||||||
"copyInfoLabel": {
|
|
||||||
"message": "Copy info, $ITEMNAME$",
|
|
||||||
"description": "Aria label for a button that opens a menu with options to copy information from an item.",
|
|
||||||
"placeholders": {
|
|
||||||
"itemname": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "Secret Item"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"copyInfoTitle": {
|
"copyInfoTitle": {
|
||||||
"message": "Copy info - $ITEMNAME$",
|
"message": "Copy info - $ITEMNAME$",
|
||||||
"description": "Title for a button that opens a menu with options to copy information from an item.",
|
"description": "Title for a button that opens a menu with options to copy information from an item.",
|
||||||
@ -3343,16 +3333,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copyNoteLabel": {
|
|
||||||
"message": "Copy Note, $ITEMNAME$",
|
|
||||||
"description": "Aria label for a button copies a note to the clipboard.",
|
|
||||||
"placeholders": {
|
|
||||||
"itemname": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "Secret Note Item"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"copyNoteTitle": {
|
"copyNoteTitle": {
|
||||||
"message": "Copy Note - $ITEMNAME$",
|
"message": "Copy Note - $ITEMNAME$",
|
||||||
"description": "Title for a button copies a note to the clipboard.",
|
"description": "Title for a button copies a note to the clipboard.",
|
||||||
@ -3393,6 +3373,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"autofillTitle": {
|
||||||
|
"message": "Auto-fill - $ITEMNAME$",
|
||||||
|
"description": "Title for a button that auto-fills a login item.",
|
||||||
|
"placeholders": {
|
||||||
|
"itemname": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Secret Item"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"noValuesToCopy": {
|
||||||
|
"message": "No values to copy"
|
||||||
|
},
|
||||||
"assignCollections": {
|
"assignCollections": {
|
||||||
"message": "Assign collections"
|
"message": "Assign collections"
|
||||||
},
|
},
|
||||||
|
@ -3,8 +3,10 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
size="small"
|
size="small"
|
||||||
[attr.aria-label]="'copyInfoLabel' | i18n: cipher.name"
|
[appA11yTitle]="
|
||||||
[title]="'copyInfoTitle' | i18n: cipher.name"
|
hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
|
||||||
|
"
|
||||||
|
[disabled]="!hasLoginValues"
|
||||||
[bitMenuTriggerFor]="loginOptions"
|
[bitMenuTriggerFor]="loginOptions"
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #loginOptions>
|
<bit-menu #loginOptions>
|
||||||
@ -25,8 +27,10 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
size="small"
|
size="small"
|
||||||
[attr.aria-label]="'copyInfoLabel' | i18n: cipher.name"
|
[appA11yTitle]="
|
||||||
[title]="'copyInfoTitle' | i18n: cipher.name"
|
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
|
||||||
|
"
|
||||||
|
[disabled]="!hasCardValues"
|
||||||
[bitMenuTriggerFor]="cardOptions"
|
[bitMenuTriggerFor]="cardOptions"
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #cardOptions>
|
<bit-menu #cardOptions>
|
||||||
@ -44,8 +48,10 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
size="small"
|
size="small"
|
||||||
[attr.aria-label]="'copyInfoLabel' | i18n: cipher.name"
|
[appA11yTitle]="
|
||||||
[title]="'copyInfoTitle' | i18n: cipher.name"
|
hasIdentityValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
|
||||||
|
"
|
||||||
|
[disabled]="!hasIdentityValues"
|
||||||
[bitMenuTriggerFor]="identityOptions"
|
[bitMenuTriggerFor]="identityOptions"
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #identityOptions>
|
<bit-menu #identityOptions>
|
||||||
@ -69,8 +75,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
size="small"
|
size="small"
|
||||||
[attr.aria-label]="'copyNoteLabel' | i18n: cipher.name"
|
[appA11yTitle]="
|
||||||
[title]="'copyNoteTitle' | i18n: cipher.name"
|
hasSecureNoteValue ? ('copyNoteTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
|
||||||
|
"
|
||||||
appCopyField="secureNote"
|
appCopyField="secureNote"
|
||||||
[cipher]="cipher"
|
[cipher]="cipher"
|
||||||
></button>
|
></button>
|
||||||
|
@ -25,5 +25,28 @@ export class ItemCopyActionsComponent {
|
|||||||
|
|
||||||
protected CipherType = CipherType;
|
protected CipherType = CipherType;
|
||||||
|
|
||||||
|
get hasLoginValues() {
|
||||||
|
return (
|
||||||
|
!!this.cipher.login.hasTotp || !!this.cipher.login.password || !!this.cipher.login.username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasCardValues() {
|
||||||
|
return !!this.cipher.card.code || !!this.cipher.card.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasIdentityValues() {
|
||||||
|
return (
|
||||||
|
!!this.cipher.identity.fullAddressForCopy ||
|
||||||
|
!!this.cipher.identity.email ||
|
||||||
|
!!this.cipher.identity.username ||
|
||||||
|
!!this.cipher.identity.phone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasSecureNoteValue() {
|
||||||
|
return !!this.cipher.notes;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
[bitMenuTriggerFor]="moreOptions"
|
[bitMenuTriggerFor]="moreOptions"
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #moreOptions>
|
<bit-menu #moreOptions>
|
||||||
<ng-container *ngIf="isLogin && !hideLoginOptions">
|
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
|
||||||
<ng-container *ngIf="autofillAllowed$ | async">
|
<ng-container *ngIf="autofillAllowed$ | async">
|
||||||
<button type="button" bitMenuItem>
|
<button type="button" bitMenuItem>
|
||||||
{{ "autofill" | i18n }}
|
{{ "autofill" | i18n }}
|
||||||
|
@ -33,11 +33,11 @@ export class ItemMoreOptionsComponent {
|
|||||||
cipher: CipherView;
|
cipher: CipherView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag to hide the login specific menu options. Used for login items that are
|
* Flag to hide the autofill menu options. Used for items that are
|
||||||
* already in the autofill list suggestion.
|
* already in the autofill list suggestion.
|
||||||
*/
|
*/
|
||||||
@Input({ transform: booleanAttribute })
|
@Input({ transform: booleanAttribute })
|
||||||
hideLoginOptions: boolean;
|
hideAutofillOptions: boolean;
|
||||||
|
|
||||||
protected autofillAllowed$ = this.vaultPopupItemsService.autofillAllowed$;
|
protected autofillAllowed$ = this.vaultPopupItemsService.autofillAllowed$;
|
||||||
|
|
||||||
@ -55,8 +55,11 @@ export class ItemMoreOptionsComponent {
|
|||||||
return this.cipher.edit;
|
return this.cipher.edit;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isLogin() {
|
/**
|
||||||
return this.cipher.type === CipherType.Login;
|
* Determines if the cipher can be autofilled.
|
||||||
|
*/
|
||||||
|
get canAutofill() {
|
||||||
|
return [CipherType.Login, CipherType.Card, CipherType.Identity].includes(this.cipher.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
get favoriteText() {
|
get favoriteText() {
|
||||||
@ -67,7 +70,7 @@ export class ItemMoreOptionsComponent {
|
|||||||
* Determines if the login cipher can be launched in a new browser tab.
|
* Determines if the login cipher can be launched in a new browser tab.
|
||||||
*/
|
*/
|
||||||
get canLaunch() {
|
get canLaunch() {
|
||||||
return this.isLogin && this.cipher.login.canLaunch;
|
return this.cipher.type === CipherType.Login && this.cipher.login.canLaunch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||||
>
|
>
|
||||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
{{ cipher.name }}
|
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-sm"
|
class="bwi bwi-sm"
|
||||||
*ngIf="cipher.organizationId"
|
*ngIf="cipher.organizationId"
|
||||||
@ -36,12 +36,20 @@
|
|||||||
</a>
|
</a>
|
||||||
<ng-container slot="end">
|
<ng-container slot="end">
|
||||||
<bit-item-action *ngIf="showAutofillButton">
|
<bit-item-action *ngIf="showAutofillButton">
|
||||||
<button type="button" bitBadge variant="primary">{{ "autoFill" | i18n }}</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
bitBadge
|
||||||
|
variant="primary"
|
||||||
|
[title]="'autofillTitle' | i18n: cipher.name"
|
||||||
|
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||||
|
>
|
||||||
|
{{ "autoFill" | i18n }}
|
||||||
|
</button>
|
||||||
</bit-item-action>
|
</bit-item-action>
|
||||||
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
||||||
<app-item-more-options
|
<app-item-more-options
|
||||||
[cipher]="cipher"
|
[cipher]="cipher"
|
||||||
[hideLoginOptions]="showAutofillButton"
|
[hideAutofillOptions]="showAutofillButton"
|
||||||
></app-item-more-options>
|
></app-item-more-options>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
|
@ -2,6 +2,7 @@ import { inject, Injectable, NgZone } from "@angular/core";
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
distinctUntilKeyChanged,
|
distinctUntilKeyChanged,
|
||||||
from,
|
from,
|
||||||
@ -176,7 +177,12 @@ export class VaultPopupItemsService {
|
|||||||
* Ciphers are sorted by name.
|
* Ciphers are sorted by name.
|
||||||
*/
|
*/
|
||||||
remainingCiphers$: Observable<PopupCipherView[]> = this.favoriteCiphers$.pipe(
|
remainingCiphers$: Observable<PopupCipherView[]> = this.favoriteCiphers$.pipe(
|
||||||
withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$),
|
concatMap(
|
||||||
|
(
|
||||||
|
favoriteCiphers, // concatMap->of is used to make withLatestFrom lazy to avoid race conditions with autoFillCiphers$
|
||||||
|
) =>
|
||||||
|
of(favoriteCiphers).pipe(withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$)),
|
||||||
|
),
|
||||||
map(([favoriteCiphers, ciphers, autoFillCiphers]) =>
|
map(([favoriteCiphers, ciphers, autoFillCiphers]) =>
|
||||||
ciphers.filter(
|
ciphers.filter(
|
||||||
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
|
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
|
||||||
|
@ -51,10 +51,18 @@ export class BadgeDirective implements FocusableElement {
|
|||||||
.concat(this.hasHoverEffects ? hoverStyles[this.variant] : [])
|
.concat(this.hasHoverEffects ? hoverStyles[this.variant] : [])
|
||||||
.concat(this.truncate ? ["tw-truncate", this.maxWidthClass] : []);
|
.concat(this.truncate ? ["tw-truncate", this.maxWidthClass] : []);
|
||||||
}
|
}
|
||||||
@HostBinding("attr.title") get title() {
|
@HostBinding("attr.title") get titleAttr() {
|
||||||
|
if (this.title !== undefined) {
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
return this.truncate ? this.el.nativeElement.textContent.trim() : null;
|
return this.truncate ? this.el.nativeElement.textContent.trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional override for the automatic badge title when truncating.
|
||||||
|
*/
|
||||||
|
@Input() title?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant, sets the background color of the badge.
|
* Variant, sets the background color of the badge.
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FocusableOption } from "@angular/cdk/a11y";
|
import { FocusableOption } from "@angular/cdk/a11y";
|
||||||
import { Component, ElementRef, HostBinding } from "@angular/core";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
|
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "[bitMenuItem]",
|
selector: "[bitMenuItem]",
|
||||||
@ -32,6 +33,11 @@ export class MenuItemDirective implements FocusableOption {
|
|||||||
];
|
];
|
||||||
@HostBinding("attr.role") role = "menuitem";
|
@HostBinding("attr.role") role = "menuitem";
|
||||||
@HostBinding("tabIndex") tabIndex = "-1";
|
@HostBinding("tabIndex") tabIndex = "-1";
|
||||||
|
@HostBinding("attr.disabled") get disabledAttr() {
|
||||||
|
return this.disabled || null; // native disabled attr must be null when false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input({ transform: coerceBooleanProperty }) disabled?: boolean = false;
|
||||||
|
|
||||||
constructor(private elementRef: ElementRef) {}
|
constructor(private elementRef: ElementRef) {}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Directive, HostBinding, HostListener, Input, OnChanges } from "@angular/core";
|
import { Directive, HostBinding, HostListener, Input, OnChanges, Optional } from "@angular/core";
|
||||||
|
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { MenuItemDirective } from "@bitwarden/components";
|
||||||
import { CopyAction, CopyCipherFieldService } from "@bitwarden/vault";
|
import { CopyAction, CopyCipherFieldService } from "@bitwarden/vault";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,6 +10,8 @@ import { CopyAction, CopyCipherFieldService } from "@bitwarden/vault";
|
|||||||
*
|
*
|
||||||
* Automatically disables the host element if the field to copy is not available or null.
|
* Automatically disables the host element if the field to copy is not available or null.
|
||||||
*
|
*
|
||||||
|
* If the host element is a menu item, it will be hidden when disabled.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```html
|
* ```html
|
||||||
* <button appCopyField="username" [cipher]="cipher">Copy Username</button>
|
* <button appCopyField="username" [cipher]="cipher">Copy Username</button>
|
||||||
@ -27,11 +30,23 @@ export class CopyCipherFieldDirective implements OnChanges {
|
|||||||
|
|
||||||
@Input({ required: true }) cipher: CipherView;
|
@Input({ required: true }) cipher: CipherView;
|
||||||
|
|
||||||
constructor(private copyCipherFieldService: CopyCipherFieldService) {}
|
constructor(
|
||||||
|
private copyCipherFieldService: CopyCipherFieldService,
|
||||||
|
@Optional() private menuItemDirective?: MenuItemDirective,
|
||||||
|
) {}
|
||||||
|
|
||||||
@HostBinding("attr.disabled")
|
@HostBinding("attr.disabled")
|
||||||
protected disabled: boolean | null = null;
|
protected disabled: boolean | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the element if it is disabled and is a menu item.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@HostBinding("class.tw-hidden")
|
||||||
|
private get hidden() {
|
||||||
|
return this.disabled && this.menuItemDirective;
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener("click")
|
@HostListener("click")
|
||||||
async copy() {
|
async copy() {
|
||||||
const value = this.getValueToCopy();
|
const value = this.getValueToCopy();
|
||||||
@ -49,6 +64,11 @@ export class CopyCipherFieldDirective implements OnChanges {
|
|||||||
(this.action === "totp" && !(await this.copyCipherFieldService.totpAllowed(this.cipher)))
|
(this.action === "totp" && !(await this.copyCipherFieldService.totpAllowed(this.cipher)))
|
||||||
? true
|
? true
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// If the directive is used on a menu item, update the menu item to prevent keyboard navigation
|
||||||
|
if (this.menuItemDirective) {
|
||||||
|
this.menuItemDirective.disabled = this.disabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getValueToCopy() {
|
private getValueToCopy() {
|
||||||
|
Loading…
Reference in New Issue
Block a user