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:
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user