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:
@@ -610,7 +610,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
hideCardWrapper: true,
|
||||
hideIcon: true,
|
||||
maxWidth: "3xl",
|
||||
maxWidth: "4xl",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user