1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +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: {
hideCardWrapper: true,
hideIcon: true,
maxWidth: "3xl",
maxWidth: "4xl",
} satisfies AnonLayoutWrapperData,
children: [
{

View File

@@ -46,10 +46,10 @@
The first video is relatively positioned to force the layout and spacing of the videos.
-->
<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"
>
<div class="tw-relative tw-w-[15rem] tw-max-w-full tw-aspect-[0.807]">
<div [ngClass]="videoContainerClass">
<div
*ngIf="!allVideosLoaded"
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>
</div>
<div
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]"
>
<!-- Use `tw-relative` on the first video container to maintain the proper spacing -->
<div [ngClass]="videoContainerClass" class="tw-relative">
<div
*ngIf="!allVideosLoaded"
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>
</div>
<div
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">
<div
*ngIf="!allVideosLoaded"
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;
beforeEach(async () => {
window.matchMedia = jest.fn().mockReturnValue(false);
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation(() => ({
matches: false,
addListener() {},
removeListener() {},
})),
});
play.mockClear();
await TestBed.configureTestingModule({
@@ -126,45 +133,34 @@ describe("AddExtensionVideosComponent", () => {
thirdVideo = component["videoElements"].get(2)!.nativeElement;
});
it("starts the video sequence when all videos are loaded", fakeAsync(() => {
tick();
it("starts the video sequence when all videos are loaded", () => {
expect(firstVideo.play).toHaveBeenCalled();
}));
it("plays videos in sequence", fakeAsync(() => {
tick(); // let first video play
});
it("plays videos in sequence", () => {
play.mockClear();
firstVideo.onended!(new Event("ended")); // trigger next video
tick();
expect(secondVideo.play).toHaveBeenCalledTimes(1);
play.mockClear();
secondVideo.onended!(new Event("ended")); // trigger next video
tick();
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;
tick();
firstVideo.onended!(new Event("ended"));
tick();
secondVideo.onended!(new Event("ended"));
tick();
play.mockClear();
thirdVideo.onended!(new Event("ended")); // trigger first video again
tick();
expect(play).toHaveBeenCalledTimes(0);
}));
});
});
});

View File

@@ -17,6 +17,11 @@ export class AddExtensionVideosComponent {
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 */
protected variant: "mobile" | "desktop" = "desktop";
@@ -26,6 +31,15 @@ export class AddExtensionVideosComponent {
/** True when the user prefers reduced motion */
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 */
get allVideosLoaded(): boolean {
return this.numberOfLoadedVideos >= 3;
@@ -97,12 +111,14 @@ export class AddExtensionVideosComponent {
const video = this.videoElements.toArray()[index].nativeElement;
video.onended = () => {
void this.startVideoSequence(index + 1);
void this.addPausedStyles(video);
};
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;
this.addPlayingStyles(video);
await video.play();
}
@@ -143,4 +159,36 @@ export class AddExtensionVideosComponent {
element.style.transition = transition ? "opacity 0.5s linear" : "";
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 { TypographyModule } from "../typography";
export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl";
export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
@Component({
selector: "auth-anon-layout",
@@ -74,6 +74,8 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
return "tw-max-w-2xl";
case "3xl":
return "tw-max-w-3xl";
case "4xl":
return "tw-max-w-4xl";
}
}