mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-27798] Prevent inline menu from opening on the page outside of the viewport (#17664)
* cleanup * prevent inline menu from opening on the page outside of the viewport * update inline menu viewport check to include checks on all sides of the viewport * use VisualViewport when available * update tests
This commit is contained in:
@@ -69,8 +69,8 @@ export type FieldRect = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type InlineMenuPosition = {
|
export type InlineMenuPosition = {
|
||||||
button?: InlineMenuElementPosition;
|
button?: InlineMenuElementPosition | null;
|
||||||
list?: InlineMenuElementPosition;
|
list?: InlineMenuElementPosition | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NewLoginCipherData = {
|
export type NewLoginCipherData = {
|
||||||
|
|||||||
@@ -1424,11 +1424,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* calculates the postion and width for multi-input totp field inline menu
|
* calculates the position and width for multi-input TOTP field inline menu
|
||||||
* @param totpFieldArray - the totp fields used to evaluate the position of the menu
|
* @param totpFieldArray - the TOTP fields used to evaluate the position of the menu
|
||||||
*/
|
*/
|
||||||
private calculateTotpMultiInputMenuBounds(totpFieldArray: AutofillField[]) {
|
private calculateTotpMultiInputMenuBounds(totpFieldArray: AutofillField[]) {
|
||||||
// Filter the fields based on the provided totpfields
|
// Filter the fields based on the provided TOTP fields
|
||||||
const filteredObjects = this.allFieldData.filter((obj) =>
|
const filteredObjects = this.allFieldData.filter((obj) =>
|
||||||
totpFieldArray.some((o) => o.opid === obj.opid),
|
totpFieldArray.some((o) => o.opid === obj.opid),
|
||||||
);
|
);
|
||||||
@@ -1451,8 +1451,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* calculates the postion for multi-input totp field inline button
|
* calculates the position for multi-input TOTP field inline button
|
||||||
* @param totpFieldArray - the totp fields used to evaluate the position of the menu
|
* @param totpFieldArray - the TOTP fields used to evaluate the position of the menu
|
||||||
*/
|
*/
|
||||||
private calculateTotpMultiInputButtonBounds(totpFieldArray: AutofillField[]) {
|
private calculateTotpMultiInputButtonBounds(totpFieldArray: AutofillField[]) {
|
||||||
const filteredObjects = this.allFieldData.filter((obj) =>
|
const filteredObjects = this.allFieldData.filter((obj) =>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
import { AutofillOverlayPort } from "../../../enums/autofill-overlay.enum";
|
import { AutofillOverlayPort } from "../../../enums/autofill-overlay.enum";
|
||||||
import { createPortSpyMock } from "../../../spec/autofill-mocks";
|
import { createPortSpyMock } from "../../../spec/autofill-mocks";
|
||||||
@@ -66,17 +66,38 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO CG - This test is brittle and failing due to how we are calling the private method. This needs to be reworked
|
it("creates an aria alert element if the ariaAlert param is passed to AutofillInlineMenuIframeService", () => {
|
||||||
it.skip("creates an aria alert element if the ariaAlert param is passed", () => {
|
|
||||||
const ariaAlert = "aria alert";
|
|
||||||
jest.spyOn(autofillInlineMenuIframeService as any, "createAriaAlertElement");
|
jest.spyOn(autofillInlineMenuIframeService as any, "createAriaAlertElement");
|
||||||
|
|
||||||
autofillInlineMenuIframeService.initMenuIframe();
|
autofillInlineMenuIframeService.initMenuIframe();
|
||||||
|
|
||||||
expect(autofillInlineMenuIframeService["createAriaAlertElement"]).toHaveBeenCalledWith(
|
expect(autofillInlineMenuIframeService["createAriaAlertElement"]).toHaveBeenCalled();
|
||||||
ariaAlert,
|
expect(autofillInlineMenuIframeService["ariaAlertElement"]).toBeDefined();
|
||||||
|
expect(autofillInlineMenuIframeService["ariaAlertElement"].getAttribute("role")).toBe(
|
||||||
|
"alert",
|
||||||
);
|
);
|
||||||
expect(autofillInlineMenuIframeService["ariaAlertElement"]).toMatchSnapshot();
|
expect(autofillInlineMenuIframeService["ariaAlertElement"].getAttribute("aria-live")).toBe(
|
||||||
|
"polite",
|
||||||
|
);
|
||||||
|
expect(autofillInlineMenuIframeService["ariaAlertElement"].getAttribute("aria-atomic")).toBe(
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not create an aria alert element if the ariaAlert param is not passed to AutofillInlineMenuIframeService", () => {
|
||||||
|
const shadowWithoutAlert = document.createElement("div").attachShadow({ mode: "open" });
|
||||||
|
const serviceWithoutAlert = new AutofillInlineMenuIframeService(
|
||||||
|
shadowWithoutAlert,
|
||||||
|
AutofillOverlayPort.Button,
|
||||||
|
{ height: "0px" },
|
||||||
|
"title",
|
||||||
|
);
|
||||||
|
jest.spyOn(serviceWithoutAlert as any, "createAriaAlertElement");
|
||||||
|
|
||||||
|
serviceWithoutAlert.initMenuIframe();
|
||||||
|
|
||||||
|
expect(serviceWithoutAlert["createAriaAlertElement"]).not.toHaveBeenCalled();
|
||||||
|
expect(serviceWithoutAlert["ariaAlertElement"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("on load of the iframe source", () => {
|
describe("on load of the iframe source", () => {
|
||||||
@@ -200,7 +221,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
sendPortMessage(portSpy, { command: "updateAutofillInlineMenuPosition" });
|
sendPortMessage(portSpy, { command: "updateAutofillInlineMenuPosition" });
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow?.postMessage,
|
||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,7 +237,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
|
|
||||||
expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey);
|
expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey);
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow?.postMessage,
|
||||||
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -234,14 +255,14 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
it("passes the message on to the iframe element", () => {
|
it("passes the message on to the iframe element", () => {
|
||||||
const message = {
|
const message = {
|
||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Light,
|
theme: ThemeTypes.Light,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendPortMessage(portSpy, message);
|
sendPortMessage(portSpy, message);
|
||||||
|
|
||||||
expect(updateElementStylesSpy).not.toHaveBeenCalled();
|
expect(updateElementStylesSpy).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow?.postMessage,
|
||||||
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,18 +270,18 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
window.matchMedia = jest.fn(() => mock<MediaQueryList>({ matches: false }));
|
window.matchMedia = jest.fn(() => mock<MediaQueryList>({ matches: false }));
|
||||||
const message = {
|
const message = {
|
||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.System,
|
theme: ThemeTypes.System,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendPortMessage(portSpy, message);
|
sendPortMessage(portSpy, message);
|
||||||
|
|
||||||
expect(window.matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)");
|
expect(window.matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)");
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow?.postMessage,
|
||||||
).toHaveBeenCalledWith(
|
).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Light,
|
theme: ThemeTypes.Light,
|
||||||
},
|
},
|
||||||
autofillInlineMenuIframeService["extensionOrigin"],
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
@@ -270,18 +291,18 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
window.matchMedia = jest.fn(() => mock<MediaQueryList>({ matches: true }));
|
window.matchMedia = jest.fn(() => mock<MediaQueryList>({ matches: true }));
|
||||||
const message = {
|
const message = {
|
||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.System,
|
theme: ThemeTypes.System,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendPortMessage(portSpy, message);
|
sendPortMessage(portSpy, message);
|
||||||
|
|
||||||
expect(window.matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)");
|
expect(window.matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)");
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow?.postMessage,
|
||||||
).toHaveBeenCalledWith(
|
).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Dark,
|
theme: ThemeTypes.Dark,
|
||||||
},
|
},
|
||||||
autofillInlineMenuIframeService["extensionOrigin"],
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
@@ -290,7 +311,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
it("updates the border to match the `dark` theme", () => {
|
it("updates the border to match the `dark` theme", () => {
|
||||||
const message = {
|
const message = {
|
||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Dark,
|
theme: ThemeTypes.Dark,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendPortMessage(portSpy, message);
|
sendPortMessage(portSpy, message);
|
||||||
@@ -364,6 +385,219 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
autofillInlineMenuIframeService["handleFadeInInlineMenuIframe"],
|
autofillInlineMenuIframeService["handleFadeInInlineMenuIframe"],
|
||||||
).toHaveBeenCalled();
|
).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("closes the inline menu when iframe is outside the viewport (bottom)", () => {
|
||||||
|
const viewportHeight = 800;
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuIframeService["iframe"], "getBoundingClientRect")
|
||||||
|
.mockReturnValue({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 100,
|
||||||
|
bottom: viewportHeight + 1,
|
||||||
|
height: 98,
|
||||||
|
width: 262,
|
||||||
|
} as DOMRect);
|
||||||
|
Object.defineProperty(globalThis.window, "innerHeight", {
|
||||||
|
value: viewportHeight,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis.window, "innerWidth", {
|
||||||
|
value: 1200,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPortMessage(portSpy, {
|
||||||
|
command: "updateAutofillInlineMenuPosition",
|
||||||
|
styles: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the inline menu when iframe is outside the viewport (right)", () => {
|
||||||
|
const viewportWidth = 1200;
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuIframeService["iframe"], "getBoundingClientRect")
|
||||||
|
.mockReturnValue({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: viewportWidth + 1,
|
||||||
|
bottom: 100,
|
||||||
|
height: 98,
|
||||||
|
width: 262,
|
||||||
|
} as DOMRect);
|
||||||
|
Object.defineProperty(globalThis.window, "innerHeight", {
|
||||||
|
value: 800,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis.window, "innerWidth", {
|
||||||
|
value: viewportWidth,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPortMessage(portSpy, {
|
||||||
|
command: "updateAutofillInlineMenuPosition",
|
||||||
|
styles: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the inline menu when iframe is outside the viewport (left)", () => {
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuIframeService["iframe"], "getBoundingClientRect")
|
||||||
|
.mockReturnValue({
|
||||||
|
top: 0,
|
||||||
|
left: -1,
|
||||||
|
right: 0,
|
||||||
|
bottom: 100,
|
||||||
|
height: 98,
|
||||||
|
width: 262,
|
||||||
|
} as DOMRect);
|
||||||
|
Object.defineProperty(globalThis.window, "innerHeight", {
|
||||||
|
value: 800,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis.window, "innerWidth", {
|
||||||
|
value: 1200,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPortMessage(portSpy, {
|
||||||
|
command: "updateAutofillInlineMenuPosition",
|
||||||
|
styles: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the inline menu when iframe is outside the viewport (top)", () => {
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuIframeService["iframe"], "getBoundingClientRect")
|
||||||
|
.mockReturnValue({
|
||||||
|
top: -1,
|
||||||
|
left: 0,
|
||||||
|
right: 100,
|
||||||
|
bottom: 0,
|
||||||
|
height: 98,
|
||||||
|
width: 262,
|
||||||
|
} as DOMRect);
|
||||||
|
Object.defineProperty(globalThis.window, "innerHeight", {
|
||||||
|
value: 800,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis.window, "innerWidth", {
|
||||||
|
value: 1200,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPortMessage(portSpy, {
|
||||||
|
command: "updateAutofillInlineMenuPosition",
|
||||||
|
styles: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows iframe (do not close) when it has no dimensions", () => {
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuIframeService["iframe"], "getBoundingClientRect")
|
||||||
|
.mockReturnValue({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
} as DOMRect);
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis.window, "innerHeight", {
|
||||||
|
value: 800,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis.window, "innerWidth", {
|
||||||
|
value: 1200,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPortMessage(portSpy, {
|
||||||
|
command: "updateAutofillInlineMenuPosition",
|
||||||
|
styles: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses visualViewport when available", () => {
|
||||||
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
jest
|
||||||
|
.spyOn(autofillInlineMenuIframeService["iframe"], "getBoundingClientRect")
|
||||||
|
.mockReturnValue({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 100,
|
||||||
|
bottom: 700,
|
||||||
|
height: 98,
|
||||||
|
width: 262,
|
||||||
|
} as DOMRect);
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis.window, "visualViewport", {
|
||||||
|
value: {
|
||||||
|
height: 600,
|
||||||
|
width: 1200,
|
||||||
|
} as VisualViewport,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis.window, "innerHeight", {
|
||||||
|
value: 800,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis.window, "innerWidth", {
|
||||||
|
value: 1200,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPortMessage(portSpy, {
|
||||||
|
command: "updateAutofillInlineMenuPosition",
|
||||||
|
styles: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the visibility of the iframe", () => {
|
it("updates the visibility of the iframe", () => {
|
||||||
@@ -381,7 +615,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow?.postMessage,
|
||||||
).toHaveBeenCalledWith(
|
).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
command: "updateAutofillInlineMenuColorScheme",
|
command: "updateAutofillInlineMenuColorScheme",
|
||||||
|
|||||||
@@ -282,6 +282,15 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
const styles = this.fadeInTimeout ? Object.assign(position, { opacity: "0" }) : position;
|
const styles = this.fadeInTimeout ? Object.assign(position, { opacity: "0" }) : position;
|
||||||
this.updateElementStyles(this.iframe, styles);
|
this.updateElementStyles(this.iframe, styles);
|
||||||
|
|
||||||
|
const elementHeightCompletelyInViewport = this.isElementCompletelyWithinViewport(
|
||||||
|
this.iframe.getBoundingClientRect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!elementHeightCompletelyInViewport) {
|
||||||
|
this.forceCloseInlineMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.fadeInTimeout) {
|
if (this.fadeInTimeout) {
|
||||||
this.handleFadeInInlineMenuIframe();
|
this.handleFadeInInlineMenuIframe();
|
||||||
}
|
}
|
||||||
@@ -289,6 +298,42 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
this.announceAriaAlert(this.ariaAlert, 2000);
|
this.announceAriaAlert(this.ariaAlert, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if element is completely within the browser viewport.
|
||||||
|
*/
|
||||||
|
private isElementCompletelyWithinViewport(elementPosition: DOMRect) {
|
||||||
|
// An element that lacks size should be considered within the viewport
|
||||||
|
if (!elementPosition.height || !elementPosition.width) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [viewportHeight, viewportWidth] = this.getViewportSize();
|
||||||
|
|
||||||
|
const rightSideIsWithinViewport = (elementPosition.right || 0) <= viewportWidth;
|
||||||
|
const leftSideIsWithinViewport = (elementPosition.left || 0) >= 0;
|
||||||
|
const topSideIsWithinViewport = (elementPosition.top || 0) >= 0;
|
||||||
|
const bottomSideIsWithinViewport = (elementPosition.bottom || 0) <= viewportHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
rightSideIsWithinViewport &&
|
||||||
|
leftSideIsWithinViewport &&
|
||||||
|
topSideIsWithinViewport &&
|
||||||
|
bottomSideIsWithinViewport
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Use Visual Viewport API if available (better for mobile/zoom) */
|
||||||
|
private getViewportSize(): [
|
||||||
|
VisualViewport["height"] | Window["innerHeight"],
|
||||||
|
VisualViewport["width"] | Window["innerWidth"],
|
||||||
|
] {
|
||||||
|
if ("visualViewport" in globalThis.window && globalThis.window.visualViewport) {
|
||||||
|
return [globalThis.window.visualViewport.height, globalThis.window.visualViewport.width];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [globalThis.window.innerHeight, globalThis.window.innerWidth];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the page color scheme meta tag and sends a message to the iframe
|
* Gets the page color scheme meta tag and sends a message to the iframe
|
||||||
* to update its color scheme. Will default to "normal" if the meta tag
|
* to update its color scheme. Will default to "normal" if the meta tag
|
||||||
|
|||||||
@@ -1400,7 +1400,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
this.intersectionObserver = new IntersectionObserver(this.handleFormElementIntersection, {
|
this.intersectionObserver = new IntersectionObserver(this.handleFormElementIntersection, {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: "0px",
|
rootMargin: "0px",
|
||||||
threshold: 1.0,
|
threshold: 0.9999, // Safari doesn't seem to function properly with a threshold of 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user