From 2329445d4501b22d9d81d6ef672ca7ae4148f550 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Thu, 13 Jun 2024 16:41:32 -0500 Subject: [PATCH] [PM-5189] Implementing a methodology for triggering subframe updates from layout-shift --- .../autofill/background/overlay.background.ts | 11 ++-- .../autofill-overlay-content.service.ts | 64 +++++++++++++++---- apps/browser/src/autofill/utils/index.ts | 11 ++++ 3 files changed, 68 insertions(+), 18 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 931824515e5..5d1cf35453d 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -64,7 +64,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { private inlineMenuPageTranslations: Record; private inlineMenuFadeInTimeout: number | NodeJS.Timeout; private updateInlineMenuPositionTimeout: number | NodeJS.Timeout; - private isReflowUpdatingSubFrames: boolean = false; private delayedCloseTimeout: number | NodeJS.Timeout; private focusedFieldData: FocusedFieldData; private isFieldCurrentlyFocused: boolean = false; @@ -414,9 +413,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - if (this.updateInlineMenuPositionTimeout) { - clearTimeout(this.updateInlineMenuPositionTimeout); - } + this.clearUpdateInlineMenuPositionTimeout(); await this.rebuildSubFrameOffsets(sender); @@ -583,6 +580,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { } } + private clearUpdateInlineMenuPositionTimeout() { + if (this.updateInlineMenuPositionTimeout) { + clearTimeout(this.updateInlineMenuPositionTimeout); + } + } + /** * Handles cleanup when an overlay element is closed. Disconnects * the list and button ports and sets them to null. 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 3eeec2422e1..c1df9124e06 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -17,7 +17,12 @@ import { } from "../enums/autofill-overlay.enum"; import AutofillField from "../models/autofill-field"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; -import { elementIsFillableFormField, getAttributeBoolean, sendExtensionMessage } from "../utils"; +import { + elementIsFillableFormField, + getAttributeBoolean, + sendExtensionMessage, + throttle, +} from "../utils"; import { AutofillOverlayContentExtensionMessageHandlers, @@ -43,7 +48,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ private focusedFieldData: FocusedFieldData; private userInteractionEventTimeout: number | NodeJS.Timeout; private recalculateSubFrameOffsetsTimeout: number | NodeJS.Timeout; - private performanceObserver: PerformanceObserver; + private reflowPerformanceObserver: PerformanceObserver; + private reflowMutationObserver: MutationObserver; private autofillFieldKeywordsMap: WeakMap = new WeakMap(); private eventHandlersMemo: { [key: string]: EventListener } = {}; private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = { @@ -778,6 +784,45 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; } + private setupPageReflowEventListeners() { + if ("PerformanceObserver" in window && "LayoutShift" in window) { + this.reflowPerformanceObserver = new PerformanceObserver( + throttle(this.updateSubFrameOffsetsFromLayoutShiftEvent.bind(this), 10), + ); + this.reflowPerformanceObserver.observe({ type: "layout-shift", buffered: true }); + + return; + } + + this.reflowMutationObserver = new MutationObserver( + throttle(this.updateSubFrameOffsetsFromDomMutationEvent.bind(this), 10), + ); + this.reflowMutationObserver.observe(globalThis.document.documentElement, { + childList: true, + subtree: true, + attributes: true, + }); + } + + private updateSubFrameOffsetsFromLayoutShiftEvent = (list: any) => { + const entries: any[] = list.getEntries(); + for (let index = 0; index < entries.length; index++) { + const entry = entries[index]; + if (entry.sources?.length) { + this.clearUserInteractionEventTimeout(); + this.clearRecalculateSubFrameOffsetsTimeout(); + void this.sendExtensionMessage("updateSubFrameOffsetsForReflowEvent"); + return; + } + } + }; + + private updateSubFrameOffsetsFromDomMutationEvent = async () => { + this.clearUserInteractionEventTimeout(); + this.clearRecalculateSubFrameOffsetsTimeout(); + void this.sendExtensionMessage("updateSubFrameOffsetsForReflowEvent"); + }; + /** * Sets up event listeners that facilitate repositioning * the overlay elements on scroll or resize. @@ -787,17 +832,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ capture: true, }); globalThis.addEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); - this.performanceObserver = new PerformanceObserver((list) => { - const entries: any = list.getEntries(); - for (let index = 0; index < entries.length; index++) { - const entry = entries[index]; - if (entry.sources?.length > 0) { - void this.sendExtensionMessage("updateSubFrameOffsetsForReflowEvent"); - return; - } - } - }); - this.performanceObserver.observe({ type: "layout-shift", buffered: true }); } /** @@ -941,6 +975,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent); globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); + this.setupPageReflowEventListeners(); this.setOverlayRepositionEventListeners(); }; @@ -1173,7 +1208,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.handleVisibilityChangeEvent, ); globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); - this.performanceObserver?.disconnect(); + this.reflowPerformanceObserver?.disconnect(); + this.reflowMutationObserver?.disconnect(); this.removeOverlayRepositionEventListeners(); } } diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index bbb1e2c76f2..ffbcc995b2a 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -300,3 +300,14 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri return element.getAttribute(attributeName); } + +export function throttle(callback: () => void, limit: number) { + let waitingDelay = false; + return function (...args: unknown[]) { + if (!waitingDelay) { + callback.apply(this, args); + waitingDelay = true; + globalThis.setTimeout(() => (waitingDelay = false), limit); + } + }; +}