diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 3313d3405c..8b8ec47e89 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -98,7 +98,9 @@ export type OverlayBackgroundExtensionMessageHandlers = { updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; checkIsFieldCurrentlyFilling: () => boolean; getAutofillInlineMenuVisibility: () => void; - + closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkAutofillInlineMenuFocused: () => void; + focusAutofillInlineMenuList: () => void; updateAutofillInlineMenuPosition: ({ message, sender, @@ -141,14 +143,10 @@ export type OverlayContentScriptPortMessageHandlers = { updateFocusedFieldData: ({ message, port }: PortOnMessageHandlerParams) => void; updateIsFieldCurrentlyFocused: ({ message }: PortMessageParam) => void; openAutofillInlineMenu: () => void; - closeAutofillInlineMenu: ({ message, port }: PortOnMessageHandlerParams) => void; - checkAutofillInlineMenuFocused: () => void; - focusAutofillInlineMenuList: () => void; }; export type InlineMenuButtonPortMessageHandlers = { [key: string]: CallableFunction; - closeAutofillInlineMenu: ({ message, port }: PortOnMessageHandlerParams) => void; triggerDelayedAutofillInlineMenuClosure: ({ port }: PortConnectionParam) => void; autofillInlineMenuButtonClicked: ({ port }: PortConnectionParam) => void; autofillInlineMenuBlurred: () => void; @@ -158,7 +156,6 @@ export type InlineMenuButtonPortMessageHandlers = { export type InlineMenuListPortMessageHandlers = { [key: string]: CallableFunction; - closeAutofillInlineMenu: ({ message, port }: PortOnMessageHandlerParams) => void; checkAutofillInlineMenuButtonFocused: () => void; autofillInlineMenuBlurred: () => void; unlockVault: ({ port }: PortConnectionParam) => void; diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index fd8aeb5f0f..e9d5795fbe 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -82,7 +82,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(), - + closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message), + checkAutofillInlineMenuFocused: () => this.checkInlineMenuFocused(), + focusAutofillInlineMenuList: () => this.focusInlineMenuList(), updateAutofillInlineMenuPosition: ({ message, sender }) => this.updateInlineMenuPosition(message, sender), toggleAutofillInlineMenuHidden: ({ message, sender }) => @@ -109,12 +111,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { updateFocusedFieldData: ({ message, port }) => this.setFocusedFieldData(message, port), updateIsFieldCurrentlyFocused: ({ message }) => this.updateIsFieldCurrentlyFocused(message), openAutofillInlineMenu: () => this.openInlineMenu(false), - closeAutofillInlineMenu: ({ message, port }) => this.closeInlineMenu(port.sender, message), - checkAutofillInlineMenuFocused: () => this.checkInlineMenuFocused(), - focusAutofillInlineMenuList: () => this.focusInlineMenuList(), }; private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { - closeAutofillInlineMenu: ({ message, port }) => this.closeInlineMenu(port.sender, message), triggerDelayedAutofillInlineMenuClosure: ({ port }) => this.triggerDelayedInlineMenuClosure(), autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port), autofillInlineMenuBlurred: () => this.checkInlineMenuListFocused(), @@ -123,7 +121,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { updateAutofillInlineMenuColorScheme: () => this.updateInlineMenuButtonColorScheme(), }; private readonly inlineMenuListPortMessageHandlers: InlineMenuListPortMessageHandlers = { - closeAutofillInlineMenu: ({ message, port }) => this.closeInlineMenu(port.sender, message), checkAutofillInlineMenuButtonFocused: () => this.checkInlineMenuButtonFocused(), autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(), unlockVault: ({ port }) => this.unlockVault(port), diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts index 997836dd36..a8d4c7354d 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts @@ -1,7 +1,7 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; import { ThemeType } from "@bitwarden/common/platform/enums"; -import { setElementStyles } from "../../../utils"; +import { sendExtensionMessage, setElementStyles } from "../../../utils"; import { BackgroundPortMessageHandlers, AutofillInlineMenuIframeService as AutofillInlineMenuIframeServiceInterface, @@ -10,6 +10,7 @@ import { export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframeServiceInterface { private readonly setElementStyles = setElementStyles; + private readonly sendExtensionMessage = sendExtensionMessage; private port: chrome.runtime.Port | null = null; private portKey: string; private iframeMutationObserver: MutationObserver; @@ -304,7 +305,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe * mutation observer is triggered excessively. */ private forceCloseInlineMenu() { - void this.port.postMessage({ command: "closeAutofillInlineMenu", forceClose: true }); + void this.sendExtensionMessage("closeAutofillInlineMenu", { forceClose: true }); } private handleFadeInInlineMenuIframe() { diff --git a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts index 853778ed97..cceeddbac8 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts @@ -44,7 +44,6 @@ export type AutofillOverlayContentExtensionMessage = { overlayElement?: AutofillOverlayElementType; focusedFieldData?: FocusedFieldData; isFieldCurrentlyFocused?: boolean; - forceCloseInlineMenu?: boolean; } & OverlayAddNewItemMessage; export interface AutofillOverlayContentService { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 0b77f7e051..da9e7f6200 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -905,6 +905,17 @@ describe("AutofillOverlayContentService", () => { }); }); + it("clears the user interaction timeout", async () => { + jest.useFakeTimers(); + const clearTimeoutSpy = jest.spyOn(globalThis, "clearTimeout"); + autofillOverlayContentService["userInteractionEventTimeout"] = setTimeout(jest.fn(), 123); + + globalThis.dispatchEvent(new Event(EVENTS.SCROLL)); + await flushPromises(); + + expect(clearTimeoutSpy).toHaveBeenCalledWith(expect.anything()); + }); + it("removes the overlay completely if the field is not focused", async () => { jest.useFakeTimers(); jest @@ -1684,6 +1695,14 @@ describe("AutofillOverlayContentService", () => { autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement; }); + it("clears the user interaction event timeout", () => { + jest.spyOn(autofillOverlayContentService as any, "clearUserInteractionEventTimeout"); + + autofillOverlayContentService.destroy(); + + expect(autofillOverlayContentService["clearUserInteractionEventTimeout"]).toHaveBeenCalled(); + }); + it("de-registers all global event listeners", () => { jest.spyOn(globalThis.document, "removeEventListener"); jest.spyOn(globalThis, "removeEventListener"); diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 4f1e27babe..af1ae6aaa6 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -54,8 +54,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ private focusableElements: FocusableElement[] = []; private mostRecentlyFocusedField: ElementWithOpId; private focusedFieldData: FocusedFieldData; + private userInteractionEventTimeout: number | NodeJS.Timeout; + private recalculateSubFrameOffsetsTimeout: number | NodeJS.Timeout; private closeInlineMenuOnRedirectTimeout: number | NodeJS.Timeout; private focusInlineMenuListTimeout: number | NodeJS.Timeout; + private closeInlineMenuOnFilledFieldTimeout: number | NodeJS.Timeout; private eventHandlersMemo: { [key: string]: EventListener } = {}; private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = { openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message), @@ -204,7 +207,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.mostRecentlyFocusedField?.blur(); if (isClosingInlineMenu) { - this.sendPortMessage("closeAutofillInlineMenu"); + void this.sendExtensionMessage("closeAutofillInlineMenu"); } } @@ -249,7 +252,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ if (direction === RedirectFocusDirection.Current) { this.focusMostRecentlyFocusedField(); this.closeInlineMenuOnRedirectTimeout = globalThis.setTimeout( - () => this.sendPortMessage("closeAutofillInlineMenu"), + () => void this.sendExtensionMessage("closeAutofillInlineMenu"), 100, ); return; @@ -347,7 +350,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.sendPortMessage("updateIsFieldCurrentlyFocused", { isFieldCurrentlyFocused: false, }); - this.sendPortMessage("checkAutofillInlineMenuFocused"); + void this.sendExtensionMessage("checkAutofillInlineMenuFocused"); }; /** @@ -361,7 +364,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ private handleFormFieldKeyupEvent = async (event: KeyboardEvent) => { const eventCode = event.code; if (eventCode === "Escape") { - this.sendPortMessage("closeAutofillInlineMenu", { + void this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true, }); return; @@ -391,13 +394,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); this.openInlineMenu({ isOpeningFullInlineMenu: true }); this.focusInlineMenuListTimeout = globalThis.setTimeout( - () => this.sendPortMessage("focusAutofillInlineMenuList"), + () => this.sendExtensionMessage("focusAutofillInlineMenuList"), 125, ); return; } - this.sendPortMessage("focusAutofillInlineMenuList"); + void this.sendExtensionMessage("focusAutofillInlineMenuList"); } /** @@ -427,7 +430,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.storeModifiedFormElement(formFieldElement); if (await this.hideInlineMenuListOnFilledField(formFieldElement)) { - this.sendPortMessage("closeAutofillInlineMenu", { + void this.sendExtensionMessage("closeAutofillInlineMenu", { overlayElement: AutofillOverlayElement.List, forceCloseInlineMenu: true, }); @@ -511,6 +514,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.sendPortMessage("updateIsFieldCurrentlyFocused", { isFieldCurrentlyFocused: true, }); + if (this.userInteractionEventTimeout) { + this.clearUserInteractionEventTimeout(); + void this.toggleInlineMenuHidden(false, true); + void this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); + } const initiallyFocusedField = this.mostRecentlyFocusedField; await this.updateMostRecentlyFocusedField(formFieldElement); @@ -519,7 +529,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ (initiallyFocusedField !== this.mostRecentlyFocusedField && (await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement))) ) { - this.sendPortMessage("closeAutofillInlineMenu", { + await this.sendExtensionMessage("closeAutofillInlineMenu", { overlayElement: AutofillOverlayElement.List, forceCloseInlineMenu: true, }); @@ -1007,7 +1017,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } this.unsetMostRecentlyFocusedField(); - this.sendPortMessage("closeAutofillInlineMenu", { + void this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true, }); }; @@ -1125,6 +1135,32 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.port.postMessage({ command, ...message }); } + /** + * Clears the user interaction event timeout. This is used to ensure that + * the overlay is not repositioned while the user is interacting with it. + */ + private clearUserInteractionEventTimeout() { + if (this.userInteractionEventTimeout) { + globalThis.clearTimeout(this.userInteractionEventTimeout); + this.userInteractionEventTimeout = null; + } + } + + private clearCloseInlineMenuOnFilledFieldTimeout() { + if (this.closeInlineMenuOnFilledFieldTimeout) { + globalThis.clearTimeout(this.closeInlineMenuOnFilledFieldTimeout); + } + } + + /** + * Clears the timeout that facilitates recalculating the sub frame offsets. + */ + private clearRecalculateSubFrameOffsetsTimeout() { + if (this.recalculateSubFrameOffsetsTimeout) { + globalThis.clearTimeout(this.recalculateSubFrameOffsetsTimeout); + } + } + private clearFocusInlineMenuListTimeout() { if (this.focusInlineMenuListTimeout) { globalThis.clearTimeout(this.focusInlineMenuListTimeout); @@ -1138,6 +1174,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } private clearAllTimeouts() { + this.clearUserInteractionEventTimeout(); + this.clearCloseInlineMenuOnFilledFieldTimeout(); + this.clearRecalculateSubFrameOffsetsTimeout(); this.clearFocusInlineMenuListTimeout(); this.clearCloseInlineMenuOnRedirectTimeout(); }