commit 6a0b366f7333c6d76cd663a63d50642450bffb46
parent fb2b1473c009464d91fe6c3d98ecf0bba2649b0f
Author: Dimi <dlee@mozilla.com>
Date: Mon, 17 Nov 2025 08:54:33 +0000
Bug 1900391 - Port virtual list support changes from Contextual Password Manager to Firefox View r=kcochrane,sclements,fxview-reviewers
This patch introduces the following changes:
- Adds support for variable-height items by allowing clients to pass a 'heightCalculator' function.
- Introduces a 'version' property that clients can update to force a virtual list refresh when item content changes.
- Exposes a configurable 'minimalRenderCount'.
- Emits the custom event 'virtual-list-ready' to notify clients when the list has completed its initial full render.
Differential Revision: https://phabricator.services.mozilla.com/D265792
Diffstat:
2 files changed, 77 insertions(+), 10 deletions(-)
diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs
@@ -326,6 +326,7 @@ export class FxviewTabListBase extends MozLitElement {
lazy.virtualListEnabledPref,
() => html`
<virtual-list
+ .itemHeightEstimate=${FXVIEW_ROW_HEIGHT_PX}
.activeIndex=${this.activeIndex}
.items=${this.tabItems}
.template=${this.itemTemplate}
@@ -813,12 +814,27 @@ export class VirtualList extends MozLitElement {
template: { type: Function },
activeIndex: { type: Number },
itemOffset: { type: Number },
- maxRenderCountEstimate: { type: Number, state: true },
+
+ // For fixed-height lists, set `itemHeightEstimate` to the fixed height.
+ // For variable-height lists, set `itemHeightEstimate` to the minimum possible item height,
+ // and provide a `heightCalculator` function to compute the total height of the list.
+ // In variable-height lists, sublists are still divided based on a fixed number of items,
+ // determined by the minimum possible item height.
itemHeightEstimate: { type: Number, state: true },
+ heightCalculator: { type: Function },
+
+ // Minimum number of items to render in each sublist. Used only if the
+ // height-based calculation yields fewer items.
+ minimalRenderCount: { type: Number },
+
+ maxRenderCountEstimate: { type: Number, state: true },
isAlwaysVisible: { type: Boolean },
isVisible: { type: Boolean, state: true },
isSubList: { type: Boolean },
pinnedTabsIndexOffset: { type: Number },
+
+ // Used to force re-rendering of list items
+ version: { type: Number },
};
createRenderRoot() {
@@ -832,13 +848,15 @@ export class VirtualList extends MozLitElement {
this.pinnedTabsIndexOffset = 0;
this.items = [];
this.subListItems = [];
- this.itemHeightEstimate = FXVIEW_ROW_HEIGHT_PX;
- this.maxRenderCountEstimate = Math.max(
- 40,
- 2 * Math.ceil(window.innerHeight / this.itemHeightEstimate)
- );
+
+ this.itemHeightEstimate = 0;
+ this.heightCalculator = items => this.itemHeightEstimate * items.length;
+ this.minimalRenderCount = 40;
+ this.maxRenderCountEstimate = this.minimalRenderCount;
this.isSubList = false;
this.isVisible = false;
+ this.version = 0;
+
this.intersectionObserver = new IntersectionObserver(
([entry]) => {
this.isVisible = entry.isIntersecting;
@@ -859,6 +877,18 @@ export class VirtualList extends MozLitElement {
);
}
});
+
+ this.parentChildAddedObserver = new MutationObserver(mutations => {
+ for (const m of mutations) {
+ if (m.type != "childList") {
+ return;
+ }
+ if (this.children.length == this.subListItems.length) {
+ this.waitForSublistUpdated();
+ this.parentChildAddedObserver.disconnect();
+ }
+ }
+ });
}
disconnectedCallback() {
@@ -866,6 +896,20 @@ export class VirtualList extends MozLitElement {
this.intersectionObserver.disconnect();
this.childResizeObserver.disconnect();
this.selfResizeObserver.disconnect();
+ this.parentChildAddedObserver.disconnect();
+ }
+
+ async waitForSublistUpdated() {
+ if (this.isSubList) {
+ return;
+ }
+ await Promise.all([...this.children].map(e => e.updateComplete));
+ this.dispatchEvent(
+ new CustomEvent("virtual-list-ready", {
+ bubbles: true,
+ composed: true,
+ })
+ );
}
triggerIntersectionObserver() {
@@ -902,7 +946,7 @@ export class VirtualList extends MozLitElement {
recalculateAfterWindowResize() {
this.maxRenderCountEstimate = Math.max(
- 40,
+ this.minimalRenderCount,
2 * Math.ceil(window.innerHeight / this.itemHeightEstimate)
);
}
@@ -910,6 +954,18 @@ export class VirtualList extends MozLitElement {
firstUpdated() {
this.intersectionObserver.observe(this);
this.selfResizeObserver.observe(this);
+
+ if (!this.isSubList) {
+ if (
+ this.subListItems.length &&
+ this.children.length == this.subListItems.length
+ ) {
+ this.waitForSublistUpdated();
+ } else {
+ this.parentChildAddedObserver.observe(this, { childList: true });
+ }
+ }
+
if (this.isSubList && this.children[0]) {
this.childResizeObserver.observe(this.children[0]);
}
@@ -919,6 +975,14 @@ export class VirtualList extends MozLitElement {
this.updateListHeight(changedProperties);
if (changedProperties.has("items") && !this.isSubList) {
this.triggerIntersectionObserver();
+ } else if (
+ changedProperties.has("itemHeightEstimate") ||
+ changedProperties.has("minimalRenderCount")
+ ) {
+ this.maxRenderCountEstimate = Math.max(
+ this.minimalRenderCount,
+ 2 * Math.ceil(window.innerHeight / this.itemHeightEstimate)
+ );
}
}
@@ -930,7 +994,7 @@ export class VirtualList extends MozLitElement {
this.style.height =
this.isAlwaysVisible || this.isVisible
? "auto"
- : `${this.items.length * this.itemHeightEstimate}px`;
+ : `${this.heightCalculator(this.items)}px`;
}
}
@@ -941,8 +1005,11 @@ export class VirtualList extends MozLitElement {
subListTemplate = (data, i) => {
return html`<virtual-list
.template=${this.template}
+ .version=${this.version}
.items=${data}
.itemHeightEstimate=${this.itemHeightEstimate}
+ .heightCalculator=${this.heightCalculator}
+ .minimalRenderCount=${this.minimalRenderCount}
.itemOffset=${i * this.maxRenderCountEstimate +
this.pinnedTabsIndexOffset}
.isAlwaysVisible=${i ==
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js
@@ -53,8 +53,8 @@ add_task(async function test_max_render_count_on_win_resize() {
await TestUtils.waitForCondition(
() =>
rootVirtualList.updateComplete &&
- rootVirtualList.maxRenderCountEstimate < initialMaxRenderCount,
- `Max render count ${rootVirtualList.maxRenderCountEstimate} is not less than initial max render count ${initialMaxRenderCount}`
+ rootVirtualList.maxRenderCountEstimate == initialMaxRenderCount,
+ `Max render count ${rootVirtualList.maxRenderCountEstimate} is not equal to the initial max render count ${initialMaxRenderCount}`
);
const newMaxRenderCount = rootVirtualList.maxRenderCountEstimate;