1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-24 21:41:33 +01:00

[PM-5189] Implementing a methodology for triggering subframe updates from layout-shift

This commit is contained in:
Cesar Gonzalez 2024-06-13 16:41:32 -05:00
parent a7fa57ce72
commit 2329445d45
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
3 changed files with 68 additions and 18 deletions

View File

@ -64,7 +64,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private inlineMenuPageTranslations: Record<string, string>;
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.

View File

@ -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<AutofillField, string> = 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();
}
}

View File

@ -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);
}
};
}