diff --git a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap
index e5bafe34b5..39ca68d912 100644
--- a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap
+++ b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body 1`] = `
+exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body within a shadow root 1`] = `
{
let domElementVisibilityService: DomElementVisibilityService;
let autofillInit: AutofillInit;
let bodyAppendChildSpy: jest.SpyInstance;
+ let postMessageSpy: jest.SpyInstance>;
beforeEach(() => {
jest.useFakeTimers();
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn());
+ jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window);
+ postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
domQueryService = mock();
domElementVisibilityService = new DomElementVisibilityService();
overlayNotificationsContentService = new OverlayNotificationsContentService();
@@ -48,7 +51,7 @@ describe("OverlayNotificationsContentService", () => {
});
it("closes the notification bar if the notification bar type has changed", async () => {
- overlayNotificationsContentService["currentNotificationBarType"] = "add";
+ overlayNotificationsContentService["currentNotificationBarType"] = NotificationType.AddLogin;
const closeNotificationBarSpy = jest.spyOn(
overlayNotificationsContentService as any,
"closeNotificationBar",
@@ -66,7 +69,7 @@ describe("OverlayNotificationsContentService", () => {
expect(closeNotificationBarSpy).toHaveBeenCalled();
});
- it("creates the notification bar elements and appends them to the body", async () => {
+ it("creates the notification bar elements and appends them to the body within a shadow root", async () => {
sendMockExtensionMessage({
command: "openNotificationBar",
data: {
@@ -77,6 +80,13 @@ describe("OverlayNotificationsContentService", () => {
await flushPromises();
expect(overlayNotificationsContentService["notificationBarElement"]).toMatchSnapshot();
+
+ const rootElement = overlayNotificationsContentService["notificationBarRootElement"];
+ expect(bodyAppendChildSpy).toHaveBeenCalledWith(rootElement);
+ expect(rootElement?.tagName).toBe("BIT-NOTIFICATION-BAR-ROOT");
+
+ expect(document.getElementById("bit-notification-bar")).toBeNull();
+ expect(document.querySelector("#bit-notification-bar-iframe")).toBeNull();
});
it("sets up a slide in animation when the notification is fresh", async () => {
@@ -116,6 +126,8 @@ describe("OverlayNotificationsContentService", () => {
});
it("sends an initialization message to the notification bar iframe", async () => {
+ const addEventListenerSpy = jest.spyOn(globalThis, "addEventListener");
+
sendMockExtensionMessage({
command: "openNotificationBar",
data: {
@@ -124,10 +136,7 @@ describe("OverlayNotificationsContentService", () => {
},
});
await flushPromises();
- const postMessageSpy = jest.spyOn(
- overlayNotificationsContentService["notificationBarIframeElement"].contentWindow,
- "postMessage",
- );
+ expect(addEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function));
globalThis.dispatchEvent(
new MessageEvent("message", {
@@ -142,7 +151,6 @@ describe("OverlayNotificationsContentService", () => {
);
await flushPromises();
- expect(postMessageSpy).toHaveBeenCalledTimes(1);
expect(postMessageSpy).toHaveBeenCalledWith(
{
command: "initNotificationBar",
@@ -158,7 +166,7 @@ describe("OverlayNotificationsContentService", () => {
sendMockExtensionMessage({
command: "openNotificationBar",
data: {
- type: "change",
+ type: NotificationType.ChangePassword,
typeData: mock(),
},
});
@@ -242,20 +250,15 @@ describe("OverlayNotificationsContentService", () => {
});
it("sends a message to the notification bar iframe indicating that the save attempt completed", () => {
- jest.spyOn(
- overlayNotificationsContentService["notificationBarIframeElement"].contentWindow,
- "postMessage",
- );
-
sendMockExtensionMessage({
command: "saveCipherAttemptCompleted",
data: { error: undefined },
});
- expect(
- overlayNotificationsContentService["notificationBarIframeElement"].contentWindow
- .postMessage,
- ).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: undefined }, "*");
+ expect(postMessageSpy).toHaveBeenCalledWith(
+ { command: "saveCipherAttemptCompleted", error: undefined },
+ "*",
+ );
});
});
@@ -271,9 +274,10 @@ describe("OverlayNotificationsContentService", () => {
await flushPromises();
});
- it("triggers a closure of the notification bar", () => {
+ it("triggers a closure of the notification bar and cleans up all shadow DOM elements", () => {
overlayNotificationsContentService.destroy();
+ expect(overlayNotificationsContentService["notificationBarRootElement"]).toBeNull();
expect(overlayNotificationsContentService["notificationBarElement"]).toBeNull();
expect(overlayNotificationsContentService["notificationBarIframeElement"]).toBeNull();
});
diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts
index 4e09c3186b..0afa4f1409 100644
--- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts
+++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts
@@ -17,8 +17,10 @@ import {
export class OverlayNotificationsContentService
implements OverlayNotificationsContentServiceInterface
{
+ private notificationBarRootElement: HTMLElement | null = null;
private notificationBarElement: HTMLElement | null = null;
private notificationBarIframeElement: HTMLIFrameElement | null = null;
+ private notificationBarShadowRoot: ShadowRoot | null = null;
private currentNotificationBarType: NotificationType | null = null;
private notificationBarContainerStyles: Partial = {
height: "400px",
@@ -158,12 +160,12 @@ export class OverlayNotificationsContentService
* @private
*/
private openNotificationBar(initData: NotificationBarIframeInitData) {
- if (!this.notificationBarElement && !this.notificationBarIframeElement) {
+ if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
this.createNotificationBarIframeElement(initData);
this.createNotificationBarElement();
this.setupInitNotificationBarMessageListener(initData);
- globalThis.document.body.appendChild(this.notificationBarElement);
+ globalThis.document.body.appendChild(this.notificationBarRootElement);
}
}
@@ -213,15 +215,25 @@ export class OverlayNotificationsContentService
};
/**
- * Creates the container for the notification bar iframe.
+ * Creates the container for the notification bar iframe with shadow DOM.
*/
private createNotificationBarElement() {
if (this.notificationBarIframeElement) {
+ this.notificationBarRootElement = globalThis.document.createElement(
+ "bit-notification-bar-root",
+ );
+
+ this.notificationBarShadowRoot = this.notificationBarRootElement.attachShadow({
+ mode: "closed",
+ delegatesFocus: true,
+ });
+
this.notificationBarElement = globalThis.document.createElement("div");
this.notificationBarElement.id = "bit-notification-bar";
setElementStyles(this.notificationBarElement, this.notificationBarContainerStyles, true);
+ this.notificationBarShadowRoot.appendChild(this.notificationBarElement);
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
}
}
@@ -258,7 +270,7 @@ export class OverlayNotificationsContentService
* @param closedByUserAction - Whether the notification bar was closed by the user.
*/
private closeNotificationBar(closedByUserAction: boolean = false) {
- if (!this.notificationBarElement && !this.notificationBarIframeElement) {
+ if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
return;
}
@@ -267,6 +279,9 @@ export class OverlayNotificationsContentService
this.notificationBarElement.remove();
this.notificationBarElement = null;
+ this.notificationBarShadowRoot = null;
+ this.notificationBarRootElement.remove();
+ this.notificationBarRootElement = null;
const removableNotificationTypes = new Set([
NotificationTypes.Add,