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:
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user