[PM-6546] Fix issue with blurring of elements after autofill occurs (#8153)
* [PM-6546] Fix issue with blurring of elements after autofill occurs * [PM-6546] Implementing a methodology where Firefox browsers render the overlay UI within a div element rather than custom web component
This commit is contained in:
parent
952b71f4da
commit
3953318c28
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<CSSStyleDeclaration>,
|
||||
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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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<ElementWithOpId<FormFieldElement>> = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 = `<input type="text" id="username" value="${elementValue}"/>`;
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue