From e92817011b859012e61f31f58fc90dc2c2f6b072 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 9 Feb 2026 16:05:19 -0500 Subject: [PATCH] [PM 29531]Remove ts strict ignore in list autofill inline menu list ts (#18738) * fix(autofill): type throttle to preserve handler this/args and return void * fix(autofill): strict TS and defaults for inline menu list, throttle typing, TOTP interval * update snapshots * swap mouse event for event * prevent default does nothing on event --- .../autofill-inline-menu-list.spec.ts.snap | 12 +- .../list/autofill-inline-menu-list.spec.ts | 4 + .../pages/list/autofill-inline-menu-list.ts | 112 +++++++++++------- apps/browser/src/autofill/utils/index.ts | 19 +-- 4 files changed, 90 insertions(+), 57 deletions(-) diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap index 22e3a765666..53a055075fe 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap @@ -716,7 +716,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f stroke-dasharray="78.5" stroke-dashoffset="78.5" stroke-width="3" - style="stroke-dashoffset: NaN;" + style="stroke-dashoffset: 34.033920413889426;" transform="rotate(-90 14.5 14.5)" /> @@ -737,7 +737,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f bittypography="helper" class="totp-sec-span" > - NaN + 17 @@ -2115,7 +2115,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f stroke-dasharray="78.5" stroke-dashoffset="78.5" stroke-width="3" - style="stroke-dashoffset: NaN;" + style="stroke-dashoffset: 34.033920413889426;" transform="rotate(-90 14.5 14.5)" /> @@ -2136,7 +2136,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f bittypography="helper" class="totp-sec-span" > - NaN + 17 @@ -2227,7 +2227,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f stroke-dasharray="78.5" stroke-dashoffset="78.5" stroke-width="3" - style="stroke-dashoffset: NaN;" + style="stroke-dashoffset: 34.033920413889426;" transform="rotate(-90 14.5 14.5)" /> @@ -2248,7 +2248,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f bittypography="helper" class="totp-sec-span" > - NaN + 17 diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index 81bf7240230..1e99ac9df90 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -157,6 +157,8 @@ describe("AutofillInlineMenuList", () => { }); it("creates the view for a totp field", async () => { + jest.spyOn(Date, "now").mockReturnValue(13000); + postWindowMessage( createInitAutofillInlineMenuListMessageMock({ inlineMenuFillType: CipherType.Login, @@ -184,6 +186,8 @@ describe("AutofillInlineMenuList", () => { }); it("renders correctly when there are multiple TOTP elements with username displayed", async () => { + jest.spyOn(Date, "now").mockReturnValue(13000); + const totpCipher1 = createAutofillOverlayCipherDataMock(1, { type: CipherType.Login, login: { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index c680fe4745c..c13c523e30a 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import "@webcomponents/custom-elements"; import "lit/polyfill-support.js"; @@ -33,27 +31,36 @@ import { import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element"; export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { - private inlineMenuListContainer: HTMLDivElement; - private passwordGeneratorContainer: HTMLDivElement; + /** Non-null asserted. Set in initAutofillInlineMenuList before any read. */ + private inlineMenuListContainer!: HTMLDivElement; + /** Non-null asserted. Set in initAutofillInlineMenuList before any read. */ + private passwordGeneratorContainer!: HTMLDivElement; private resizeObserver: ResizeObserver; private eventHandlersMemo: { [key: string]: EventListener } = {}; private ciphers: InlineMenuCipherData[] = []; - private ciphersList: HTMLUListElement; + /** Non-null asserted. Set in buildInlineMenuList before any read. */ + private ciphersList!: HTMLUListElement; private cipherListScrollIsDebounced = false; - private cipherListScrollDebounceTimeout: number | NodeJS.Timeout; + private cipherListScrollDebounceTimeout: number | ReturnType = 0; private currentCipherIndex = 0; - private inlineMenuFillType: InlineMenuFillType; - private showInlineMenuAccountCreation: boolean; - private showPasskeysLabels: boolean; - private newItemButtonElement: HTMLButtonElement; - private passkeysHeadingElement: HTMLLIElement; - private loginHeadingElement: HTMLLIElement; - private lastPasskeysListItem: HTMLLIElement; - private passkeysHeadingHeight: number; - private lastPasskeysListItemHeight: number; - private ciphersListHeight: number; + /** Non-null asserted. Set in initAutofillInlineMenuList from message. */ + private inlineMenuFillType!: InlineMenuFillType; + private showInlineMenuAccountCreation = false; + private showPasskeysLabels = false; + /** Non-null asserted. Set in buildNewItemButton before any read. */ + private newItemButtonElement!: HTMLButtonElement; + /** Conditionally set in buildPasskeysHeadingElements, may be undefined when no passkeys. */ + private passkeysHeadingElement?: HTMLLIElement; + /** Conditionally set in buildPasskeysHeadingElements, may be undefined when no login heading. */ + private loginHeadingElement?: HTMLLIElement; + /** Conditionally set in buildInlineMenuListActionsItem when showPasskeysLabels and passkey cipher. */ + private lastPasskeysListItem?: HTMLLIElement; + private passkeysHeadingHeight = 0; + private lastPasskeysListItemHeight = 0; + private ciphersListHeight = 0; private isPasskeyAuthInProgress = false; - private authStatus: AuthenticationStatus; + private authStatus: AuthenticationStatus = AuthenticationStatus.Locked; + private isInitialized = false; private readonly showCiphersPerPage = 6; private readonly headingBorderClass = "inline-menu-list-heading--bordered"; private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers = @@ -70,6 +77,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { constructor() { super(); + this.resizeObserver = new ResizeObserver(this.handleResizeObserver); this.setupInlineMenuListGlobalListeners(); } @@ -85,11 +93,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { styleSheetUrl, theme, authStatus, - ciphers, + ciphers = [], portKey, - inlineMenuFillType, - showInlineMenuAccountCreation, - showPasskeysLabels, + inlineMenuFillType = CipherType.Login, + showInlineMenuAccountCreation = false, + showPasskeysLabels = false, generatedPassword, showSaveLoginMenu, } = message; @@ -112,6 +120,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { this.resizeObserver.observe(this.inlineMenuListContainer); this.shadowDom.append(linkElement, this.inlineMenuListContainer); + this.isInitialized = true; if (authStatus !== AuthenticationStatus.Unlocked) { this.buildLockedInlineMenu(); @@ -368,7 +377,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { event.target.nextElementSibling ) { (event.target.nextElementSibling as HTMLElement).focus(); - event.target.parentElement.classList.add("remove-outline"); + event.target.parentElement?.classList.add("remove-outline"); return; } }; @@ -409,7 +418,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { event.target.previousElementSibling ) { (event.target.previousElementSibling as HTMLElement).focus(); - event.target.parentElement.classList.remove("remove-outline"); + event.target.parentElement?.classList.remove("remove-outline"); return; } }; @@ -473,8 +482,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields. */ private updateListItems({ - ciphers, - showInlineMenuAccountCreation, + ciphers = [], + showInlineMenuAccountCreation = false, }: UpdateAutofillInlineMenuListCiphersParams) { if (this.isPasskeyAuthInProgress) { return; @@ -655,7 +664,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * scroll listeners that reposition the passkeys and login headings when the user scrolls. */ private setupCipherListScrollListeners() { - const options = { passive: true }; + const options: AddEventListenerOptions = { passive: true }; this.ciphersList.addEventListener(EVENTS.SCROLL, this.updateCiphersListOnScroll, options); if (this.showPasskeysLabels) { this.ciphersList.addEventListener( @@ -673,8 +682,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles updating the list of ciphers when the * user scrolls to the bottom of the list. */ - private updateCiphersListOnScroll = (event: MouseEvent) => { - event.preventDefault(); + private updateCiphersListOnScroll = (event: Event) => { event.stopPropagation(); if (this.cipherListScrollIsDebounced) { @@ -721,8 +729,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * * @param event - The scroll event. */ - private handleThrottledOnScrollEvent = (event: MouseEvent) => { - event.preventDefault(); + private handleThrottledOnScrollEvent = (event: Event) => { event.stopPropagation(); this.updatePasskeysHeadingsOnScroll(this.ciphersList.scrollTop); @@ -754,6 +761,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipherListScrollTop - The current scroll top position of the ciphers list. */ private togglePasskeysHeadingAnchored(cipherListScrollTop: number) { + if (!this.passkeysHeadingElement || !this.lastPasskeysListItem) { + return; + } if (!this.passkeysHeadingHeight) { this.passkeysHeadingHeight = this.passkeysHeadingElement.offsetHeight; } @@ -776,6 +786,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipherListScrollTop - The current scroll top position of the ciphers list. */ private togglePasskeysHeadingBorder(cipherListScrollTop: number) { + if (!this.passkeysHeadingElement) { + return; + } if (cipherListScrollTop < 1) { this.passkeysHeadingElement.classList.remove(this.headingBorderClass); return; @@ -791,6 +804,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipherListScrollTop - The current scroll top position of the ciphers list. */ private toggleLoginHeadingBorder(cipherListScrollTop: number) { + if (!this.loginHeadingElement || !this.lastPasskeysListItem) { + return; + } if (!this.lastPasskeysListItemHeight) { this.lastPasskeysListItemHeight = this.lastPasskeysListItem.offsetHeight; } @@ -884,7 +900,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { ); this.addFillCipherElementAriaDescription(fillCipherElement, cipher); - fillCipherElement.append(cipherIcon, cipherDetailsElement); + fillCipherElement.append(cipherIcon, ...(cipherDetailsElement ? [cipherDetailsElement] : [])); fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher)); fillCipherElement.addEventListener(EVENTS.KEYUP, this.handleFillCipherKeyUpEvent); @@ -1126,7 +1142,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { cipherIcon.appendChild(totpContainer); - const intervalSeconds = cipher.login.totpCodeTimeInterval; + const intervalSeconds = cipher.login.totpCodeTimeInterval ?? 30; const updateCountdown = () => { const epoch = Math.round(Date.now() / 1000); @@ -1266,7 +1282,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { if (this.multipleTotpElements() && username) { const usernameSubtitle = this.buildCipherSubtitleElement(username); - containerElement.appendChild(usernameSubtitle); + if (usernameSubtitle) { + containerElement.appendChild(usernameSubtitle); + } } const totpCodeSpan = document.createElement("span"); @@ -1326,19 +1344,25 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { cipher: InlineMenuCipherData, cipherDetailsElement: HTMLSpanElement, ): HTMLSpanElement { - let rpNameSubtitle: HTMLSpanElement; + const login = cipher.login; + const passkey = login?.passkey; + if (!login || !passkey) { + return cipherDetailsElement; + } - if (cipher.name !== cipher.login.passkey.rpName) { - rpNameSubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.rpName); - if (rpNameSubtitle) { + let rpNameSubtitle: HTMLSpanElement | undefined; + if (cipher.name !== passkey.rpName) { + const element = this.buildCipherSubtitleElement(passkey.rpName); + if (element) { + rpNameSubtitle = element; rpNameSubtitle.prepend(buildSvgDomElement(passkeyIcon)); rpNameSubtitle.classList.add("cipher-subtitle--passkey"); cipherDetailsElement.appendChild(rpNameSubtitle); } } - if (cipher.login.username) { - const usernameSubtitle = this.buildCipherSubtitleElement(cipher.login.username); + if (login.username) { + const usernameSubtitle = this.buildCipherSubtitleElement(login.username); if (usernameSubtitle) { if (!rpNameSubtitle) { usernameSubtitle.prepend(buildSvgDomElement(passkeyIcon)); @@ -1350,7 +1374,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { return cipherDetailsElement; } - const passkeySubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.userName); + const passkeySubtitle = this.buildCipherSubtitleElement(passkey.userName); if (passkeySubtitle) { if (!rpNameSubtitle) { passkeySubtitle.prepend(buildSvgDomElement(passkeyIcon)); @@ -1412,6 +1436,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * If not focused, will check if the button element is focused. */ private checkInlineMenuListFocused() { + if (!this.isInitialized) { + return; + } if (globalThis.document.hasFocus()) { return; } @@ -1450,6 +1477,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * the first cipher button. */ private focusInlineMenuList() { + if (!this.isInitialized) { + return; + } this.inlineMenuListContainer.setAttribute("role", "dialog"); this.inlineMenuListContainer.setAttribute("aria-modal", "true"); @@ -1472,8 +1502,6 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { */ private setupInlineMenuListGlobalListeners() { this.setupGlobalListeners(this.inlineMenuListWindowMessageHandlers); - - this.resizeObserver = new ResizeObserver(this.handleResizeObserver); } /** diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index dc07ca1e258..fa47ddd943b 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -368,20 +368,21 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri /** * Throttles a callback function to run at most once every `limit` milliseconds. * - * @param callback - The callback function to throttle. + * @param callback - The callback function to throttle (must return void). * @param limit - The time in milliseconds to throttle the callback. */ -export function throttle unknown>( - callback: FunctionType, +export function throttle( + callback: (this: TypeContext, ...args: Args) => void, limit: number, -): (this: ThisParameterType, ...args: Parameters) => void { +): (this: TypeContext, ...args: Args) => void { let waitingDelay = false; - return function (this: ThisParameterType, ...args: Parameters) { - if (!waitingDelay) { - callback.apply(this, args); - waitingDelay = true; - globalThis.setTimeout(() => (waitingDelay = false), limit); + return function (this: TypeContext, ...args: Args) { + if (waitingDelay) { + return; } + callback.apply(this, args); + waitingDelay = true; + globalThis.setTimeout(() => (waitingDelay = false), limit); }; }