1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 19:23:52 +00:00

[PM-22180] Setup Extension Videos (#15419)

* remove placeholder image

* add videos for setup extension

* add support for mobile viewports

* add mobile/responsiveness for setup extension page

* add videos from `assets.bitwarden.com`

* align with figma for borders and shadow

* make text responsive for setup headings

* remove period

* add tests

* add tests for video sequence

* force font weight on `h2`

* add 8px to bottom margin of video container
This commit is contained in:
Nick Krantz
2025-07-07 08:56:31 -05:00
committed by GitHub
parent 03a7530f8b
commit 2e03b8cbac
9 changed files with 450 additions and 17 deletions

View File

@@ -0,0 +1,146 @@
import { CommonModule, DOCUMENT } from "@angular/common";
import { Component, ViewChildren, QueryList, ElementRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { debounceTime, fromEvent } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@Component({
selector: "vault-add-extension-videos",
templateUrl: "./add-extension-videos.component.html",
imports: [CommonModule, JslibModule],
})
export class AddExtensionVideosComponent {
@ViewChildren("video", { read: ElementRef }) protected videoElements!: QueryList<
ElementRef<HTMLVideoElement>
>;
private document = inject(DOCUMENT);
/** Current viewport size */
protected variant: "mobile" | "desktop" = "desktop";
/** Number of videos that have loaded and are ready to play */
protected numberOfLoadedVideos = 0;
/** True when the user prefers reduced motion */
protected prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/** Returns true when all videos are loaded */
get allVideosLoaded(): boolean {
return this.numberOfLoadedVideos >= 3;
}
constructor() {
fromEvent(window, "resize")
.pipe(takeUntilDestroyed(), debounceTime(25))
.subscribe(() => this.onResize());
}
/** Resets the video states based on the viewport width changes */
onResize(): void {
const oldVariant = this.variant;
this.variant = this.document.documentElement.clientWidth < 768 ? "mobile" : "desktop";
// When the viewport changes from desktop to mobile, hide all videos except the one that is playing.
if (this.variant !== oldVariant && this.variant === "mobile") {
this.videoElements.forEach((video) => {
if (video.nativeElement.paused) {
this.hideElement(video.nativeElement.parentElement!);
} else {
this.showElement(video.nativeElement.parentElement!);
}
});
}
// When the viewport changes from mobile to desktop, show all videos.
if (this.variant !== oldVariant && this.variant === "desktop") {
this.videoElements.forEach((video) => {
this.showElement(video.nativeElement.parentElement!);
});
}
}
/**
* Increment the number of loaded videos.
* When all videos are loaded, start the first one.
*/
protected onVideoLoad() {
this.numberOfLoadedVideos = this.numberOfLoadedVideos + 1;
if (this.allVideosLoaded) {
void this.startVideoSequence(0);
}
}
/** Recursive method to start the video sequence. */
private async startVideoSequence(i: number): Promise<void> {
let index = i;
const endOfVideos = index >= this.videoElements.length;
// When the user prefers reduced motion, don't play the videos more than once
if (endOfVideos && this.prefersReducedMotion) {
return;
}
// When the last of the videos has played, loop back to the start
if (endOfVideos) {
this.videoElements.forEach((video) => {
// Reset all videos to the start
video.nativeElement.currentTime = 0;
});
// Loop back to the first video
index = 0;
}
const video = this.videoElements.toArray()[index].nativeElement;
video.onended = () => {
void this.startVideoSequence(index + 1);
};
this.mobileTransitionIn(index);
// Set muted via JavaScript, browsers are respecting autoplay consistently over just the HTML attribute
video.muted = true;
await video.play();
}
/** For mobile viewports, fades the current video out and the next video in. */
private mobileTransitionIn(index: number): void {
// When the viewport is above the tablet breakpoint, all videos are shown at once.
// No transition is needed.
if (this.isAboveTabletBreakpoint()) {
return;
}
const currentParent = this.videoElements.toArray()[index].nativeElement.parentElement!;
const previousIndex = index === 0 ? this.videoElements.length - 1 : index - 1;
const previousParent = this.videoElements.toArray()[previousIndex].nativeElement.parentElement!;
// Fade out the previous video
this.hideElement(previousParent, true);
// Fade in the current video
this.showElement(currentParent, true);
}
/** Returns true when the viewport width is 768px or above. */
private isAboveTabletBreakpoint(): boolean {
const width = this.document.documentElement.clientWidth;
return width >= 768;
}
/** Visually hides the given element. */
private hideElement(element: HTMLElement, transition = false): void {
element.style.transition = transition ? "opacity 0.5s linear" : "";
element.style.opacity = "0";
}
/** Visually shows the given element. */
private showElement(element: HTMLElement, transition = false): void {
element.style.transition = transition ? "opacity 0.5s linear" : "";
element.style.opacity = "1";
}
}