1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-14426] At-risk password Getting Started Carousel (#13383)

* [PM-14426] Add hideIcon input to simple dialog component

* [PM-14426] Introduce dark-image-source.directive.ts

* [PM-14426] Tweaks to the Vault Carousel component
- Create a Carousel NgModule so that the carousel component and carousel slide component are exported
- Update barrel files
- Adjust min height calculation logic to wait for ;hidden slides to finish rendering before calculating height

* [PM-14426] Introduce at risk password getting started carousel component and images

* [PM-14426] Refactor at-risk-password-page.service.ts to use the same state definition for banner and carousel dismissal

* [PM-14426] Open the getting started carousel on page load

* [PM-14426] Add tests

* [PM-14426] Use booleanAttribute

* [PM-14426] Fix failing type checking
This commit is contained in:
Shane Melton
2025-02-26 13:24:35 -08:00
committed by GitHub
parent 9aee5f16c4
commit b9ebf0704a
22 changed files with 334 additions and 30 deletions

View File

@@ -20,6 +20,6 @@
></vault-carousel-button>
</div>
<div class="tw-absolute tw-invisible" #tempSlideContainer *ngIf="minHeight === null">
<ng-template [cdkPortalOutlet] #tempSlideOutlet></ng-template>
<ng-template cdkPortalOutlet></ng-template>
</div>
</section>

View File

@@ -8,12 +8,12 @@ import {
ContentChildren,
ElementRef,
EventEmitter,
inject,
Input,
Output,
QueryList,
ViewChild,
ViewChildren,
inject,
} from "@angular/core";
import { ButtonModule } from "@bitwarden/components";
@@ -89,7 +89,7 @@ export class VaultCarouselComponent implements AfterViewInit {
this.slideChange.emit(index);
}
ngAfterViewInit(): void {
async ngAfterViewInit() {
this.keyManager = new FocusKeyManager(this.carouselButtons)
.withHorizontalOrientation("ltr")
.withWrap()
@@ -98,7 +98,7 @@ export class VaultCarouselComponent implements AfterViewInit {
// Set the first carousel button as active, this avoids having to double tab the arrow keys on initial focus.
this.keyManager.setFirstItemActive();
this.setMinHeightOfCarousel();
await this.setMinHeightOfCarousel();
}
/**
@@ -106,7 +106,7 @@ export class VaultCarouselComponent implements AfterViewInit {
* Render each slide in a temporary portal outlet to get the height of each slide
* and store the tallest slide height.
*/
private setMinHeightOfCarousel() {
private async setMinHeightOfCarousel() {
// Store the height of the carousel button element.
const heightOfButtonsPx = this.carouselButtonWrapper.nativeElement.offsetHeight;
@@ -121,13 +121,14 @@ export class VaultCarouselComponent implements AfterViewInit {
// to determine the height of the first slide.
let tallestSlideHeightPx = containerHeight - heightOfButtonsPx;
this.slides.forEach((slide, index) => {
// Skip the first slide, the height is accounted for above.
if (index === this.selectedIndex) {
return;
for (let i = 0; i < this.slides.length; i++) {
if (i === this.selectedIndex) {
continue;
}
this.tempSlideOutlet.attach(this.slides.get(i)!.content);
this.tempSlideOutlet.attach(slide.content);
// Wait for the slide to render. Otherwise, the previous slide may not have been removed from the DOM yet.
await new Promise(requestAnimationFrame);
// Store the height of the current slide if is larger than the current stored height;
if (this.tempSlideContainer.nativeElement.offsetHeight > tallestSlideHeightPx) {
@@ -136,8 +137,7 @@ export class VaultCarouselComponent implements AfterViewInit {
// cleanup the outlet
this.tempSlideOutlet.detach();
});
}
// Set the min height of the entire carousel based on the largest slide.
this.minHeight = `${tallestSlideHeightPx + heightOfButtonsPx}px`;
this.changeDetectorRef.detectChanges();

View File

@@ -0,0 +1,10 @@
import { NgModule } from "@angular/core";
import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component";
import { VaultCarouselComponent } from "./carousel.component";
@NgModule({
imports: [VaultCarouselComponent, VaultCarouselSlideComponent],
exports: [VaultCarouselComponent, VaultCarouselSlideComponent],
})
export class VaultCarouselModule {}

View File

@@ -1 +1 @@
export { VaultCarouselComponent } from "./carousel.component";
export { VaultCarouselModule } from "./carousel.module";

View File

@@ -0,0 +1,62 @@
import {
DestroyRef,
Directive,
ElementRef,
HostBinding,
inject,
input,
OnInit,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { combineLatest, Observable } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { Theme } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
/**
* Directive that will switch the image source based on the currently applied theme.
*
* @example
* ```html
* <img src="light-image.png" appDarkImgSrc="dark-image.png" />
* ```
*/
@Directive({
selector: "[appDarkImgSrc]",
standalone: true,
})
export class DarkImageSourceDirective implements OnInit {
private themeService = inject(ThemeStateService);
private systemTheme$: Observable<Theme> = inject(SYSTEM_THEME_OBSERVABLE);
private el = inject(ElementRef<HTMLElement>);
private destroyRef = inject(DestroyRef);
/**
* The image source to use when the light theme is applied. Automatically assigned the value
* of the `<img>` src attribute.
*/
protected lightImgSrc: string | undefined;
/**
* The image source to use when the dark theme is applied.
*/
darkImgSrc = input.required<string>({ alias: "appDarkImgSrc" });
@HostBinding("attr.src") src: string | undefined;
ngOnInit() {
// Set the light image source from the element's current src attribute
this.lightImgSrc = this.el.nativeElement.getAttribute("src");
// Update the image source based on the active theme
combineLatest([this.themeService.selectedTheme$, this.systemTheme$])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([theme, systemTheme]) => {
const appliedTheme = theme === "system" ? systemTheme : theme;
const isDark =
appliedTheme === "dark" || appliedTheme === "nord" || appliedTheme === "solarizedDark";
this.src = isDark ? this.darkImgSrc() : this.lightImgSrc;
});
}
}

View File

@@ -4,6 +4,7 @@ export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
export { OrgIconDirective } from "./components/org-icon.directive";
export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive";
export { DarkImageSourceDirective } from "./components/dark-image-source.directive";
export * from "./utils/observable-utilities";
@@ -21,6 +22,7 @@ export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-de
export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component";
export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component";
export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component";
export * from "./components/carousel";
export * as VaultIcons from "./icons";