1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-21807] Migrate fido components to tailwind (#16645)

* Update CSS class & refactor to use control flow instead of *ngIf directives
* Migrate to Tailwind classes for styling on the use-browser-link component
* Refactor if directive - use control flow instead
* Refactor to leverage Tailwind CSS
This commit is contained in:
Mick Letofsky
2025-10-16 10:08:13 +02:00
committed by GitHub
parent df1dd168dc
commit 96d40ae5c0
4 changed files with 168 additions and 239 deletions

View File

@@ -9,15 +9,17 @@
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
<span data-testid="item-name">
{{ cipher.name }}
<i
*ngIf="cipher.organizationId"
[appA11yTitle]="'shared' | i18n"
class="bwi bwi-collection-shared text-muted"
></i>
@if (cipher.organizationId) {
<i [appA11yTitle]="'shared' | i18n" class="bwi bwi-collection-shared tw-text-muted"></i>
}
</span>
<ng-container slot="secondary">
<div *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</div>
<div *ngIf="cipher.subTitle">{{ cipher.subTitle }}</div>
@if (getSubName(cipher)) {
<div>{{ getSubName(cipher) }}</div>
}
@if (cipher.subTitle) {
<div>{{ cipher.subTitle }}</div>
}
</ng-container>
</button>
</bit-item>

View File

@@ -1,52 +1,24 @@
<ng-container *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
<div class="useBrowserlink">
<button
type="button"
(click)="toggle()"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
aria-haspopup="dialog"
aria-controls="cdk-overlay-container"
>
<span class="text-primary">
@if ((fido2PopoutSessionData$ | async).fallbackSupported) {
<div class="tw-flex tw-items-center tw-justify-center tw-p-2">
<button type="button" [bitMenuTriggerFor]="deviceMenu">
<span bitTypography="body2">
{{ "useDeviceOrHardwareKey" | i18n }}
</span>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</button>
<bit-menu #deviceMenu>
<button type="button" bitMenuItem (click)="abort(false)">
{{ "justOnce" | i18n }}
</button>
<button type="button" bitMenuItem (click)="abort()">
{{ "alwaysForThisSite" | i18n }}
</button>
</bit-menu>
</div>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayPositions]="overlayPosition"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
(backdropClick)="isOpen = false"
(detach)="close()"
>
<div class="box-content">
<div
class="fido2-browser-selector-dropdown"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort(false)">
<span>{{ "justOnce" | i18n }}</span>
</button>
<br />
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort()">
<span>{{ "alwaysForThisSite" | i18n }}</span>
</button>
</div>
</div>
</ng-template>
<div
*ngIf="showOverlay"
class="tw-absolute tw-size-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
></div>
</ng-container>
@if (showOverlay) {
<div
class="tw-absolute tw-size-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
></div>
}
}

View File

@@ -1,8 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { animate, state, style, transition, trigger } from "@angular/animations";
import { A11yModule } from "@angular/cdk/a11y";
import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
@@ -13,6 +10,7 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { MenuModule } from "@bitwarden/components";
import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service";
@@ -20,63 +18,24 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f
@Component({
selector: "app-fido2-use-browser-link",
templateUrl: "fido2-use-browser-link.component.html",
imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule],
animations: [
trigger("transformPanel", [
state(
"void",
style({
opacity: 0,
}),
),
transition(
"void => open",
animate(
"100ms linear",
style({
opacity: 1,
}),
),
),
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
]),
],
imports: [CommonModule, JslibModule, MenuModule],
})
export class Fido2UseBrowserLinkComponent {
showOverlay = false;
isOpen = false;
overlayPosition: ConnectedPosition[] = [
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
offsetY: 5,
},
];
protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
private domainSettingsService: DomainSettingsService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private readonly domainSettingsService: DomainSettingsService,
private readonly platformUtilsService: PlatformUtilsService,
private readonly i18nService: I18nService,
) {}
toggle() {
this.isOpen = !this.isOpen;
}
close() {
this.isOpen = false;
}
/**
* Aborts the current FIDO2 session and fallsback to the browser.
* @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts.
*/
protected async abort(excludeDomain = true) {
this.close();
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (!excludeDomain) {

View File

@@ -1,144 +1,140 @@
<popup-page *ngIf="data$ | async as data">
<popup-header
slot="header"
pageTitle="{{
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
| i18n
}}"
>
<button
*ngIf="showNewPasskeyButton"
bitButton
buttonType="primary"
type="button"
(click)="addCipher()"
slot="end"
@if (data$ | async; as data) {
<popup-page>
<popup-header
slot="header"
pageTitle="{{
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
| i18n
}}"
>
<i class="bwi bwi-plus" aria-hidden="true"></i>
{{ "new" | i18n }}
</button>
</popup-header>
@if (showNewPasskeyButton) {
<button bitButton buttonType="primary" type="button" (click)="addCipher()" slot="end">
<i class="bwi bwi-plus" aria-hidden="true"></i>
{{ "new" | i18n }}
</button>
}
</popup-header>
<div class="tw-p-2">
<bit-section *ngIf="passkeyAction === PasskeyActions.Register">
<bit-search
appAutofocus
autocomplete="off"
id="search"
placeholder="{{ 'searchVault' | i18n }}"
(ngModelChange)="search()"
[(ngModel)]="searchText"
></bit-search>
</bit-section>
<div class="tw-p-2">
@if (passkeyAction === PasskeyActions.Register) {
<bit-section>
<bit-search
appAutofocus
autocomplete="off"
id="search"
placeholder="{{ 'searchVault' | i18n }}"
(ngModelChange)="search()"
[(ngModel)]="searchText"
></bit-search>
</bit-section>
}
<!-- Display when adding a new passkey -->
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest">
<!-- Display when matching ciphers (i.e. same domain, no passkeys) exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<bit-section-header>
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
</bit-section-header>
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
</ng-container>
@switch (data.message.type) {
@case (BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
<bit-section>
@if (displayedCiphers.length > 0) {
<bit-section-header>
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
</bit-section-header>
@for (cipherItem of displayedCiphers; track cipherItem.id) {
<app-fido2-cipher-row
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
}
}
@if (!displayedCiphers.length) {
<bit-no-items [icon]="noResultsIcon">
<ng-container slot="title">{{
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
}}</ng-container>
<ng-container slot="description">{{
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
}}</ng-container>
<!-- Display when no matching ciphers exist -->
<ng-container *ngIf="!displayedCiphers.length">
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
<ng-container slot="title">{{
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
}}</ng-container>
<ng-container slot="description">{{
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
}}</ng-container>
<button
bitButton
buttonType="primary"
slot="button"
type="button"
(click)="hasSearched ? clearSearch() : saveNewLogin()"
[loading]="loading"
>
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
</button>
</bit-no-items>
</ng-container>
</bit-section>
<!-- Display when the passkey being saved already exists -->
<bit-section
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
</div>
</div>
</div>
</bit-section>
<!-- Display when picking a passkey to login with -->
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
<!-- Display when matching ciphers exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<ng-container slot="title">{{ "chooseCipherForPasskeyAuth" | i18n }}</ng-container>
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
</ng-container>
<!-- Display when no matching ciphers exist -->
<ng-container *ngIf="!displayedCiphers.length">
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
<ng-container slot="title">{{
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
}}</ng-container>
<ng-container slot="description">{{
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
}}</ng-container>
<button
bitButton
buttonType="primary"
slot="button"
type="button"
(click)="hasSearched ? clearSearch() : saveNewLogin()"
[loading]="loading"
>
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
</button>
</bit-no-items>
</ng-container>
</bit-section>
<!-- Display when initiating passkey login, but no cooresponding cipher is found in the vault -->
<bit-section
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
</div>
<button type="button" class="btn primary block" (click)="abort(false)">
<span [hidden]="loading">{{ "close" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</bit-section>
<app-fido2-use-browser-link></app-fido2-use-browser-link>
</div>
</popup-page>
<button
bitButton
buttonType="primary"
slot="button"
type="button"
(click)="hasSearched ? clearSearch() : saveNewLogin()"
[loading]="loading"
>
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
</button>
</bit-no-items>
}
</bit-section>
}
@case (BrowserFido2MessageTypes.InformExcludedCredentialRequest) {
<bit-section>
<div class="tw-space-y-4">
<p>{{ "passkeyAlreadyExists" | i18n }}</p>
<div class="tw-divide-y tw-divide-secondary-300">
@for (cipherItem of displayedCiphers; track cipherItem.id) {
<app-fido2-cipher-row
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
}
</div>
</div>
</bit-section>
}
@case (BrowserFido2MessageTypes.PickCredentialRequest) {
<bit-section>
@if (displayedCiphers.length > 0) {
<ng-container slot="title">{{ "chooseCipherForPasskeyAuth" | i18n }}</ng-container>
@for (cipherItem of displayedCiphers; track cipherItem.id) {
<app-fido2-cipher-row
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="handleCipherItemSelect($event)"
></app-fido2-cipher-row>
}
} @else {
<bit-no-items [icon]="noResultsIcon">
<ng-container slot="title">{{
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
}}</ng-container>
<ng-container slot="description">{{
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
}}</ng-container>
<button
bitButton
buttonType="primary"
slot="button"
type="button"
(click)="hasSearched ? clearSearch() : saveNewLogin()"
[loading]="loading"
>
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
</button>
</bit-no-items>
}
</bit-section>
}
@case (BrowserFido2MessageTypes.InformCredentialNotFoundRequest) {
<bit-section>
<div class="tw-space-y-4">
<p>{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
</div>
<button
bitButton
block
buttonType="primary"
type="button"
(click)="abort(false)"
[loading]="loading"
>
{{ "close" | i18n }}
</button>
</bit-section>
}
}
<app-fido2-use-browser-link></app-fido2-use-browser-link>
</div>
</popup-page>
}