tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 53600157c00452353a56c0710addf1d0f46f7563
parent 1986f706869fa42f62d8146a5f5cdec1b477760a
Author: Dimi <dlee@mozilla.com>
Date:   Wed, 15 Oct 2025 12:42:39 +0000

Bug 1991651 - Use scroll position to determine visible items during scrolling r=mtigley,credential-management-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D268508

Diffstat:
Mtoolkit/components/satchel/megalist/content/MegalistAlpha.mjs | 5+++++
Mtoolkit/components/satchel/megalist/content/components/password-card/password-card.css | 8++++++++
Mtoolkit/components/satchel/megalist/content/components/virtual-passwords-list/virtual-passwords-list.mjs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 75 insertions(+), 0 deletions(-)

diff --git a/toolkit/components/satchel/megalist/content/MegalistAlpha.mjs b/toolkit/components/satchel/megalist/content/MegalistAlpha.mjs @@ -420,6 +420,10 @@ export class MegalistAlpha extends MozLitElement { }; renderList() { + const scroller = this.shadowRoot.querySelector( + ".sidebar-panel-scrollable-content" + ); + return this.records.length ? html` <virtual-passwords-list @@ -460,6 +464,7 @@ export class MegalistAlpha extends MozLitElement { } }} .items=${this.records} + .scroller=${scroller} .version=${this.listVersion} .itemHeightEstimate=${PasswordCard.DEFAULT_PASSWORD_CARD_HEIGHT} .heightCalculator=${this.heightCalculator} diff --git a/toolkit/components/satchel/megalist/content/components/password-card/password-card.css b/toolkit/components/satchel/megalist/content/components/password-card/password-card.css @@ -3,6 +3,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ :host { + /** + * Use 1px instead of 0.5px because the virtual list relies on accurate layout + * measurements to pre-compute item positions. Sub-pixel borders (e.g. 0.5px) + * are rendered differently across devices due to DPR rounding, which leads to + * inconsistent height calculations and causes incorrect virtualization offsets. + */ + --sidebar-box-border-width: 1px; + display: flex; flex-direction: column; background-color: var(--sidebar-box-background); diff --git a/toolkit/components/satchel/megalist/content/components/virtual-passwords-list/virtual-passwords-list.mjs b/toolkit/components/satchel/megalist/content/components/virtual-passwords-list/virtual-passwords-list.mjs @@ -5,6 +5,12 @@ import { html, repeat } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + /** * TODO: This code is duplicated from fxview-tab-list.mjs. * The duplication is intentional to keep the variable-height changes @@ -13,6 +19,9 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; * shared component. */ class VirtualPasswordsList extends MozLitElement { + #scroller = null; + #onScrollEndTimer = null; + static properties = { items: { type: Array }, template: { type: Function }, @@ -54,8 +63,17 @@ class VirtualPasswordsList extends MozLitElement { this.isVisible = false; this.version = 0; + // Ignore IntersectionObserver callbacks during scrolling. + // Rapid scrolling causes frequent visibility changes, which can trigger + // excessive IO callbacks and hurt performance. While scrolling, we rely + // on scroll position to determine which items should be rendered instead. + this.ignoreIO = false; + this.intersectionObserver = new IntersectionObserver( ([entry]) => { + if (this.ignoreIO) { + return; + } this.isVisible = entry.isIntersecting; }, { root: this.ownerDocument } @@ -141,6 +159,50 @@ class VirtualPasswordsList extends MozLitElement { } } + get scroller() { + return this.#scroller; + } + + set scroller(element) { + if (this.isSubList || this.#scroller === element) { + return; + } + + if (this.#scroller) { + this.#scroller.removeEventListener("scroll", this.onScroll); + } + + this.#scroller = element; + this.#scroller.addEventListener("scroll", () => this.onScroll()); + } + + onScroll() { + if (!this.children.length) { + return; + } + + if (this.#onScrollEndTimer) { + // reset the timer + lazy.clearTimeout(this.#onScrollEndTimer); + } else { + Array.from(this.children).forEach(child => (child.ignoreIO = true)); + } + + this.#onScrollEndTimer = lazy.setTimeout(() => { + Array.from(this.children).forEach(child => (child.ignoreIO = false)); + this.#onScrollEndTimer = null; + }, 1000); + + const index = parseInt( + this.scroller.scrollTop / this.children[0].clientHeight, + 10 + ); + + for (let i = 0; i < this.children.length; i++) { + this.children[i].isVisible = i <= index + 1 && i >= index - 1; + } + } + recalculateAfterWindowResize() { this.maxRenderCountEstimate = Math.max( 40,