1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00: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
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 inlineMenuPageTranslations: Record<string, string>;
private inlineMenuFadeInTimeout: number | NodeJS.Timeout; private inlineMenuFadeInTimeout: number | NodeJS.Timeout;
private updateInlineMenuPositionTimeout: number | NodeJS.Timeout; private updateInlineMenuPositionTimeout: number | NodeJS.Timeout;
private isReflowUpdatingSubFrames: boolean = false;
private delayedCloseTimeout: number | NodeJS.Timeout; private delayedCloseTimeout: number | NodeJS.Timeout;
private focusedFieldData: FocusedFieldData; private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFocused: boolean = false;
@@ -414,9 +413,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return; return;
} }
if (this.updateInlineMenuPositionTimeout) { this.clearUpdateInlineMenuPositionTimeout();
clearTimeout(this.updateInlineMenuPositionTimeout);
}
await this.rebuildSubFrameOffsets(sender); 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 * Handles cleanup when an overlay element is closed. Disconnects
* the list and button ports and sets them to null. * the list and button ports and sets them to null.

View File

@@ -17,7 +17,12 @@ import {
} from "../enums/autofill-overlay.enum"; } from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field"; import AutofillField from "../models/autofill-field";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import { elementIsFillableFormField, getAttributeBoolean, sendExtensionMessage } from "../utils"; import {
elementIsFillableFormField,
getAttributeBoolean,
sendExtensionMessage,
throttle,
} from "../utils";
import { import {
AutofillOverlayContentExtensionMessageHandlers, AutofillOverlayContentExtensionMessageHandlers,
@@ -43,7 +48,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
private focusedFieldData: FocusedFieldData; private focusedFieldData: FocusedFieldData;
private userInteractionEventTimeout: number | NodeJS.Timeout; private userInteractionEventTimeout: number | NodeJS.Timeout;
private recalculateSubFrameOffsetsTimeout: 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 autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
private eventHandlersMemo: { [key: string]: EventListener } = {}; private eventHandlersMemo: { [key: string]: EventListener } = {};
private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = { private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
@@ -778,6 +784,45 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; 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 * Sets up event listeners that facilitate repositioning
* the overlay elements on scroll or resize. * the overlay elements on scroll or resize.
@@ -787,17 +832,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
capture: true, capture: true,
}); });
globalThis.addEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); 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.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
this.setupPageReflowEventListeners();
this.setOverlayRepositionEventListeners(); this.setOverlayRepositionEventListeners();
}; };
@@ -1173,7 +1208,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.handleVisibilityChangeEvent, this.handleVisibilityChangeEvent,
); );
globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
this.performanceObserver?.disconnect(); this.reflowPerformanceObserver?.disconnect();
this.reflowMutationObserver?.disconnect();
this.removeOverlayRepositionEventListeners(); this.removeOverlayRepositionEventListeners();
} }
} }

View File

@@ -300,3 +300,14 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri
return element.getAttribute(attributeName); 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);
}
};
}