diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts index 8106f2698d..5cba4e0c0e 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts @@ -1,8 +1,15 @@ import AutofillOverlayButtonIframe from "./autofill-overlay-button-iframe"; -import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; describe("AutofillOverlayButtonIframe", () => { - window.customElements.define("autofill-overlay-button-iframe", AutofillOverlayButtonIframe); + window.customElements.define( + "autofill-overlay-button-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayButtonIframe(this); + } + }, + ); afterAll(() => { jest.clearAllMocks(); @@ -13,7 +20,7 @@ describe("AutofillOverlayButtonIframe", () => { const iframe = document.querySelector("autofill-overlay-button-iframe"); - expect(iframe).toBeInstanceOf(AutofillOverlayButtonIframe); - expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement); + expect(iframe).toBeInstanceOf(HTMLElement); + expect(iframe.shadowRoot).toBeDefined(); }); }); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts index 813d805470..4f5d64b3cb 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts @@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum"; import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; class AutofillOverlayButtonIframe extends AutofillOverlayIframeElement { - constructor() { + constructor(element: HTMLElement) { super( + element, "overlay/button.html", AutofillOverlayPort.Button, { diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts index 7af5d973a9..71f7a290de 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts @@ -4,7 +4,21 @@ import AutofillOverlayIframeService from "./autofill-overlay-iframe.service"; jest.mock("./autofill-overlay-iframe.service"); describe("AutofillOverlayIframeElement", () => { - window.customElements.define("autofill-overlay-iframe", AutofillOverlayIframeElement); + window.customElements.define( + "autofill-overlay-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayIframeElement( + this, + "overlay/button.html", + "overlay/button", + { background: "transparent", border: "none" }, + "bitwardenOverlayButton", + ); + } + }, + ); afterAll(() => { jest.clearAllMocks(); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts index 209834410f..ed61c1eb8f 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts @@ -1,16 +1,15 @@ import AutofillOverlayIframeService from "./autofill-overlay-iframe.service"; -class AutofillOverlayIframeElement extends HTMLElement { +class AutofillOverlayIframeElement { constructor( + element: HTMLElement, iframePath: string, portName: string, initStyles: Partial, iframeTitle: string, ariaAlert?: string, ) { - super(); - - const shadow: ShadowRoot = this.attachShadow({ mode: "closed" }); + const shadow: ShadowRoot = element.attachShadow({ mode: "closed" }); const autofillOverlayIframeService = new AutofillOverlayIframeService( iframePath, portName, diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts index 5ba39eb002..ec89697e75 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts @@ -1,8 +1,15 @@ -import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; import AutofillOverlayListIframe from "./autofill-overlay-list-iframe"; describe("AutofillOverlayListIframe", () => { - window.customElements.define("autofill-overlay-list-iframe", AutofillOverlayListIframe); + window.customElements.define( + "autofill-overlay-list-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayListIframe(this); + } + }, + ); afterAll(() => { jest.clearAllMocks(); @@ -13,7 +20,7 @@ describe("AutofillOverlayListIframe", () => { const iframe = document.querySelector("autofill-overlay-list-iframe"); - expect(iframe).toBeInstanceOf(AutofillOverlayListIframe); - expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement); + expect(iframe).toBeInstanceOf(HTMLElement); + expect(iframe.shadowRoot).toBeDefined(); }); }); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts index b60b618e4e..23df658154 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts @@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum"; import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; class AutofillOverlayListIframe extends AutofillOverlayIframeElement { - constructor() { + constructor(element: HTMLElement) { super( + element, "overlay/list.html", AutofillOverlayPort.List, { 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 8926f5b298..9f3ffea142 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 @@ -877,6 +877,44 @@ describe("AutofillOverlayContentService", () => { sender: "autofillOverlayContentService", }); }); + + it("builds the overlay elements as custom web components if the user's browser is not Firefox", () => { + let namesIndex = 0; + const customNames = ["op-autofill-overlay-button", "op-autofill-overlay-list"]; + + jest + .spyOn(autofillOverlayContentService as any, "generateRandomCustomElementName") + .mockImplementation(() => { + if (namesIndex > 1) { + return ""; + } + const customName = customNames[namesIndex]; + namesIndex++; + + return customName; + }); + autofillOverlayContentService["isFirefoxBrowser"] = false; + + autofillOverlayContentService.openAutofillOverlay(); + + expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLElement); + expect(autofillOverlayContentService["overlayButtonElement"].tagName).toEqual( + customNames[0].toUpperCase(), + ); + expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLElement); + expect(autofillOverlayContentService["overlayListElement"].tagName).toEqual( + customNames[1].toUpperCase(), + ); + }); + + it("builds the overlay elements as `div` elements if the user's browser is Firefox", () => { + autofillOverlayContentService["isFirefoxBrowser"] = true; + + autofillOverlayContentService.openAutofillOverlay(); + + expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLDivElement); + expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLDivElement); + }); }); describe("focusMostRecentOverlayField", () => { 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 2cf063a5ba..79abdc3938 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -30,6 +30,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte isOverlayCiphersPopulated = false; pageDetailsUpdateRequired = false; autofillOverlayVisibility: number; + private isFirefoxBrowser = + globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || + globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; + private readonly generateRandomCustomElementName = generateRandomCustomElementName; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; private formFieldElements: Set> = new Set([]); @@ -593,6 +597,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private updateOverlayButtonPosition() { if (!this.overlayButtonElement) { this.createAutofillOverlayButton(); + this.updateCustomElementDefaultStyles(this.overlayButtonElement); } if (!this.isOverlayButtonVisible) { @@ -613,6 +618,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private updateOverlayListPosition() { if (!this.overlayListElement) { this.createAutofillOverlayList(); + this.updateCustomElementDefaultStyles(this.overlayListElement); } if (!this.isOverlayListVisible) { @@ -765,11 +771,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - const customElementName = generateRandomCustomElementName(); - globalThis.customElements?.define(customElementName, AutofillOverlayButtonIframe); - this.overlayButtonElement = globalThis.document.createElement(customElementName); + if (this.isFirefoxBrowser) { + this.overlayButtonElement = globalThis.document.createElement("div"); + new AutofillOverlayButtonIframe(this.overlayButtonElement); - this.updateCustomElementDefaultStyles(this.overlayButtonElement); + return; + } + + const customElementName = this.generateRandomCustomElementName(); + globalThis.customElements?.define( + customElementName, + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayButtonIframe(this); + } + }, + ); + this.overlayButtonElement = globalThis.document.createElement(customElementName); } /** @@ -781,11 +800,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - const customElementName = generateRandomCustomElementName(); - globalThis.customElements?.define(customElementName, AutofillOverlayListIframe); - this.overlayListElement = globalThis.document.createElement(customElementName); + if (this.isFirefoxBrowser) { + this.overlayListElement = globalThis.document.createElement("div"); + new AutofillOverlayListIframe(this.overlayListElement); - this.updateCustomElementDefaultStyles(this.overlayListElement); + return; + } + + const customElementName = this.generateRandomCustomElementName(); + globalThis.customElements?.define( + customElementName, + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayListIframe(this); + } + }, + ); + this.overlayListElement = globalThis.document.createElement(customElementName); } /** diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index c63d25c364..5ea1284d1b 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -553,17 +553,30 @@ describe("InsertAutofillContentService", () => { insertAutofillContentService as any, "simulateUserMouseClickAndFocusEventInteractions", ); + jest.spyOn(targetInput, "blur"); insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0"); expect( insertAutofillContentService["collectAutofillContentService"].getAutofillFieldElementByOpid, ).toBeCalledWith("__0"); + expect(targetInput.blur).not.toHaveBeenCalled(); expect( insertAutofillContentService["simulateUserMouseClickAndFocusEventInteractions"], ).toHaveBeenCalledWith(targetInput, true); expect(elementEventCount).toEqual(expectedElementEventCount); }); + + it("blurs the element if it is currently the active element before simulating click and focus events", () => { + const targetInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + targetInput.opid = "__0"; + targetInput.focus(); + jest.spyOn(targetInput, "blur"); + + insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0"); + + expect(targetInput.blur).toHaveBeenCalled(); + }); }); describe("insertValueIntoField", () => { @@ -710,7 +723,7 @@ describe("InsertAutofillContentService", () => { }); describe("triggerPostInsertEventsOnElement", () => { - it("triggers simulated event interactions and blurs the element after", () => { + it("triggers simulated event interactions", () => { const elementValue = "test"; document.body.innerHTML = ``; const element = document.getElementById("username") as FillableFormFieldElement; @@ -726,7 +739,6 @@ describe("InsertAutofillContentService", () => { expect(insertAutofillContentService["simulateInputElementChangedEvent"]).toHaveBeenCalledWith( element, ); - expect(element.blur).toHaveBeenCalled(); expect(element.value).toBe(elementValue); }); }); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index c5b763d77d..dd14cadfa7 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -185,11 +185,18 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf /** * Handles finding an element by opid and triggering click and focus events on the element. - * @param {string} opid - * @private + * To ensure that we trigger a blur event correctly on a filled field, we first check if the + * element is already focused. If it is, we blur the element before focusing on it again. + * + * @param {string} opid - The opid of the element to focus on. */ private handleFocusOnFieldByOpidAction(opid: string) { const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); + + if (document.activeElement === element) { + element.blur(); + } + this.simulateUserMouseClickAndFocusEventInteractions(element, true); } @@ -282,7 +289,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf } this.simulateInputElementChangedEvent(element); - element.blur(); } /** @@ -379,10 +385,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf element.dispatchEvent(new Event(simulatedInputEvents[index], { bubbles: true })); } } - - private nodeIsElement(node: Node): node is HTMLElement { - return node.nodeType === Node.ELEMENT_NODE; - } } export default InsertAutofillContentService;