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

[PM-23689] Setup Extension Video tweaks (#15620)

* add transitions for overlay and top border of video

* refactor video container class for readability

* update max width for setup-extension page

* tweak sizes of videos for larger viewports

* fix opacity never changing

* remove complex interval transitions
This commit is contained in:
Nick Krantz
2025-07-17 08:50:07 -05:00
committed by GitHub
parent 67d3035aa5
commit 6843e273b8
5 changed files with 73 additions and 30 deletions

View File

@@ -610,7 +610,7 @@ const routes: Routes = [
data: { data: {
hideCardWrapper: true, hideCardWrapper: true,
hideIcon: true, hideIcon: true,
maxWidth: "3xl", maxWidth: "4xl",
} satisfies AnonLayoutWrapperData, } satisfies AnonLayoutWrapperData,
children: [ children: [
{ {

View File

@@ -46,10 +46,10 @@
The first video is relatively positioned to force the layout and spacing of the videos. The first video is relatively positioned to force the layout and spacing of the videos.
--> -->
<div <div
class="tw-mx-auto tw-w-[15rem] tw-mb-8 tw-relative md:tw-grid md:tw-gap-10 md:tw-w-auto md:tw-grid-rows-1 md:tw-grid-cols-3 md:tw-justify-center md:tw-justify-items-center" class="tw-mx-auto tw-w-[15rem] tw-mb-8 tw-relative md:tw-grid md:tw-gap-10 md:tw-w-auto md:tw-grid-rows-1 md:tw-grid-cols-3 md:tw-justify-center md:tw-justify-items-center xl:tw-gap-12"
[attr.aria-label]="'setupExtensionContentAlt' | i18n" [attr.aria-label]="'setupExtensionContentAlt' | i18n"
> >
<div class="tw-relative tw-w-[15rem] tw-max-w-full tw-aspect-[0.807]"> <div [ngClass]="videoContainerClass">
<div <div
*ngIf="!allVideosLoaded" *ngIf="!allVideosLoaded"
class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg" class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg"
@@ -58,9 +58,8 @@
<ng-container *ngTemplateOutlet="newLoginItem"></ng-container> <ng-container *ngTemplateOutlet="newLoginItem"></ng-container>
</div> </div>
<div <!-- Use `tw-relative` on the first video container to maintain the proper spacing -->
class="tw-absolute tw-left-0 tw-top-0 tw-opacity-0 md:tw-opacity-100 md:tw-relative tw-w-[15rem] tw-max-w-full tw-aspect-[0.807]" <div [ngClass]="videoContainerClass" class="tw-relative">
>
<div <div
*ngIf="!allVideosLoaded" *ngIf="!allVideosLoaded"
class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg" class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg"
@@ -69,9 +68,7 @@
<ng-container *ngTemplateOutlet="browserExtensionEasyAccess"></ng-container> <ng-container *ngTemplateOutlet="browserExtensionEasyAccess"></ng-container>
</div> </div>
<div <div [ngClass]="videoContainerClass">
class="tw-absolute tw-left-0 tw-top-0 tw-opacity-0 md:tw-opacity-100 md:tw-relative tw-w-[15rem] tw-max-w-full tw-aspect-[0.807]"
>
<div <div
*ngIf="!allVideosLoaded" *ngIf="!allVideosLoaded"
class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg" class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg"

View File

@@ -21,7 +21,14 @@ describe("AddExtensionVideosComponent", () => {
HTMLMediaElement.prototype.play = play; HTMLMediaElement.prototype.play = play;
beforeEach(async () => { beforeEach(async () => {
window.matchMedia = jest.fn().mockReturnValue(false); Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation(() => ({
matches: false,
addListener() {},
removeListener() {},
})),
});
play.mockClear(); play.mockClear();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@@ -126,45 +133,34 @@ describe("AddExtensionVideosComponent", () => {
thirdVideo = component["videoElements"].get(2)!.nativeElement; thirdVideo = component["videoElements"].get(2)!.nativeElement;
}); });
it("starts the video sequence when all videos are loaded", fakeAsync(() => { it("starts the video sequence when all videos are loaded", () => {
tick();
expect(firstVideo.play).toHaveBeenCalled(); expect(firstVideo.play).toHaveBeenCalled();
})); });
it("plays videos in sequence", fakeAsync(() => {
tick(); // let first video play
it("plays videos in sequence", () => {
play.mockClear(); play.mockClear();
firstVideo.onended!(new Event("ended")); // trigger next video firstVideo.onended!(new Event("ended")); // trigger next video
tick();
expect(secondVideo.play).toHaveBeenCalledTimes(1); expect(secondVideo.play).toHaveBeenCalledTimes(1);
play.mockClear(); play.mockClear();
secondVideo.onended!(new Event("ended")); // trigger next video secondVideo.onended!(new Event("ended")); // trigger next video
tick();
expect(thirdVideo.play).toHaveBeenCalledTimes(1); expect(thirdVideo.play).toHaveBeenCalledTimes(1);
})); });
it("doesn't play videos again when the user prefers no motion", fakeAsync(() => { it("doesn't play videos again when the user prefers no motion", () => {
component["prefersReducedMotion"] = true; component["prefersReducedMotion"] = true;
tick();
firstVideo.onended!(new Event("ended")); firstVideo.onended!(new Event("ended"));
tick();
secondVideo.onended!(new Event("ended")); secondVideo.onended!(new Event("ended"));
tick();
play.mockClear(); play.mockClear();
thirdVideo.onended!(new Event("ended")); // trigger first video again thirdVideo.onended!(new Event("ended")); // trigger first video again
tick();
expect(play).toHaveBeenCalledTimes(0); expect(play).toHaveBeenCalledTimes(0);
})); });
}); });
}); });

View File

@@ -17,6 +17,11 @@ export class AddExtensionVideosComponent {
private document = inject(DOCUMENT); private document = inject(DOCUMENT);
/** CSS variable name tied to the video overlay */
private cssOverlayVariable = "--overlay-opacity";
/** CSS variable name tied to the video border */
private cssBorderVariable = "--border-opacity";
/** Current viewport size */ /** Current viewport size */
protected variant: "mobile" | "desktop" = "desktop"; protected variant: "mobile" | "desktop" = "desktop";
@@ -26,6 +31,15 @@ export class AddExtensionVideosComponent {
/** True when the user prefers reduced motion */ /** True when the user prefers reduced motion */
protected prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; protected prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/** CSS classes for the video container, pulled into the class only for readability. */
protected videoContainerClass = [
"tw-absolute tw-left-0 tw-top-0 tw-w-[15rem] tw-opacity-0 md:tw-opacity-100 md:tw-relative lg:tw-w-[17rem] tw-max-w-full tw-aspect-[0.807]",
`[${this.cssOverlayVariable}:0.7] after:tw-absolute after:tw-top-0 after:tw-left-0 after:tw-size-full after:tw-bg-primary-100 after:tw-content-[''] after:tw-rounded-lg after:tw-opacity-[--overlay-opacity]`,
`[${this.cssBorderVariable}:0] before:tw-absolute before:tw-top-0 before:tw-left-0 before:tw-w-full before:tw-h-2 before:tw-bg-primary-600 before:tw-content-[''] before:tw-rounded-t-lg before:tw-opacity-[--border-opacity]`,
"after:tw-transition-opacity after:tw-duration-400 after:tw-ease-linear",
"before:tw-transition-opacity before:tw-duration-400 before:tw-ease-linear",
].join(" ");
/** Returns true when all videos are loaded */ /** Returns true when all videos are loaded */
get allVideosLoaded(): boolean { get allVideosLoaded(): boolean {
return this.numberOfLoadedVideos >= 3; return this.numberOfLoadedVideos >= 3;
@@ -97,12 +111,14 @@ export class AddExtensionVideosComponent {
const video = this.videoElements.toArray()[index].nativeElement; const video = this.videoElements.toArray()[index].nativeElement;
video.onended = () => { video.onended = () => {
void this.startVideoSequence(index + 1); void this.startVideoSequence(index + 1);
void this.addPausedStyles(video);
}; };
this.mobileTransitionIn(index); this.mobileTransitionIn(index);
// Set muted via JavaScript, browsers are respecting autoplay consistently over just the HTML attribute // Browsers are not respecting autoplay consistently with just the HTML attribute, set via JavaScript as well.
video.muted = true; video.muted = true;
this.addPlayingStyles(video);
await video.play(); await video.play();
} }
@@ -143,4 +159,36 @@ export class AddExtensionVideosComponent {
element.style.transition = transition ? "opacity 0.5s linear" : ""; element.style.transition = transition ? "opacity 0.5s linear" : "";
element.style.opacity = "1"; element.style.opacity = "1";
} }
/**
* Add styles to the video that is moving to the paused/completed state.
* Fade in the overlay and fade out the border.
*/
private addPausedStyles(video: HTMLVideoElement): void {
const parentElement = video.parentElement;
if (!parentElement) {
return;
}
// The border opacity transitions from 1 to 0 based on the percent complete.
parentElement.style.setProperty(this.cssBorderVariable, "0");
// The opacity transitions from 0 to 0.7 based on the percent complete.
parentElement.style.setProperty(this.cssOverlayVariable, "0.7");
}
/**
* Add styles to the video that is moving to the playing state.
* Fade out the overlay and fade in the border.
*/
private addPlayingStyles(video: HTMLVideoElement): void {
const parentElement = video.parentElement;
if (!parentElement) {
return;
}
// The border opacity transitions from 0 to 1 based on the percent complete.
parentElement.style.setProperty(this.cssBorderVariable, "1");
// The opacity transitions from 0.7 to 0 based on the percent complete.
parentElement.style.setProperty(this.cssOverlayVariable, "0");
}
} }

View File

@@ -23,7 +23,7 @@ import { AnonLayoutBitwardenShield } from "../icon/logos";
import { SharedModule } from "../shared"; import { SharedModule } from "../shared";
import { TypographyModule } from "../typography"; import { TypographyModule } from "../typography";
export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl"; export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
@Component({ @Component({
selector: "auth-anon-layout", selector: "auth-anon-layout",
@@ -74,6 +74,8 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
return "tw-max-w-2xl"; return "tw-max-w-2xl";
case "3xl": case "3xl":
return "tw-max-w-3xl"; return "tw-max-w-3xl";
case "4xl":
return "tw-max-w-4xl";
} }
} }