1
0
mirror of https://github.com/element-hq/element-web.git synced 2026-10-24 23:19:26 +03:00
Files
element-web/patches/@tanstack__virtual-core@3.patch
renovate[bot] 47a8d087ab Update react (#35174)
* Update react

* Update tanstack patch

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-09-30 13:29:29 +00:00

109 lines
6.8 KiB
Diff

diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
index 039bff8..39d31ff 100644
--- a/dist/esm/index.d.ts
+++ b/dist/esm/index.d.ts
@@ -78,6 +78,12 @@ export interface VirtualizerOptions<TScrollElement extends Element | Window, TIt
initialMeasurementsCache?: Array<VirtualItem>;
lanes?: number;
anchorTo?: ScrollAnchor;
+ /**
+ * With `anchorTo: "end"`, reject an item as the scroll anchor (e.g. a transient
+ * placeholder/sentinel row whose key does not survive the update); anchoring
+ * steps inward to the nearest accepted item. Element-web patch, pending upstream.
+ */
+ isValidAnchorItem?: (item: VirtualItem) => boolean;
followOnAppend?: FollowOnAppend;
scrollEndThreshold?: number;
isScrollingResetDelay?: number;
diff --git a/dist/esm/index.js b/dist/esm/index.js
index 7032b66..104bd75 100644
--- a/dist/esm/index.js
+++ b/dist/esm/index.js
@@ -321,7 +321,24 @@ class Virtualizer {
const didEdgeKeysChange = didCountChange || prevCount > 0 && nextCount > 0 && (merged.getItemKey(0) !== prevFirstKey || merged.getItemKey(nextCount - 1) !== prevLastKey);
if (didEdgeKeysChange) {
edgeKeysChanged = true;
- const item = prevCount > 0 ? this.getVirtualItemForOffset(this.getScrollOffset()) ?? measurements[0] : null;
+ let item = prevCount > 0 ? this.getVirtualItemForOffset(this.getScrollOffset()) ?? measurements[0] : null;
+ // With anchorTo: "end" the scroll is re-pinned by the key of the item
+ // under the current offset. If the consumer rejects that item as an
+ // anchor via isValidAnchorItem — e.g. a transient placeholder/sentinel
+ // row (loading spinner, "load more", skeleton) whose key does not
+ // survive the update — re-pinning by its key would fail and the
+ // prepend would go uncompensated (the list jumps to the edge). Such
+ // rows only sit at the extreme edges, so step one index inward to the
+ // nearest accepted item, whose key persists across the update.
+ const isValidAnchorItem = merged.isValidAnchorItem;
+ if (item && isValidAnchorItem && !isValidAnchorItem(item)) {
+ const step = item.index === 0 ? 1 : -1;
+ let i = item.index;
+ while (i >= 0 && i < measurements.length && measurements[i] && !isValidAnchorItem(measurements[i])) {
+ i += step;
+ }
+ item = measurements[i] ?? item;
+ }
if (item) {
anchor = [item.key, this.getScrollOffset() - item.start];
}
@@ -913,9 +930,19 @@ class Virtualizer {
const itemSize = this.itemSizeCache.get(key) ?? cachedSize;
const delta = size - itemSize;
if (delta !== 0) {
- const wasAtEnd = this.options.anchorTo === "end" && ((_b = this.scrollState) == null ? void 0 : _b.behavior) !== "smooth" && this.getVirtualDistanceFromEnd() <= this.options.scrollEndThreshold;
+ // `this.scrollOffset` only updates when the async scroll event fires, so
+ // within a frame it can be stale in two ways: (a) an earlier resize in the
+ // same ResizeObserver batch already queued `scrollAdjustments`, and (b) a
+ // programmatic scroll (scrollToOffset / the reconcile loop) wrote scrollTop
+ // this frame — recorded synchronously in `_intendedScrollOffset` but not yet
+ // reflected by getScrollOffset(). Evaluating "was the view at the end?" (or
+ // "is this item above the fold?") against the stale offset makes resizes
+ // during a scroll-to-end land uncompensated, stranding the view above the
+ // bottom. Use the synchronous truth when a write is pending.
+ const effectiveScrollOffset = this._intendedScrollOffset ?? (this.getScrollOffset() + this.scrollAdjustments);
+ const wasAtEnd = this.options.anchorTo === "end" && ((_b = this.scrollState) == null ? void 0 : _b.behavior) !== "smooth" && this.getTotalSize() - this.getSize() - effectiveScrollOffset <= this.options.scrollEndThreshold;
const prevTotalSize = wasAtEnd ? this.getTotalSize() : 0;
- const scrollOffsetWithAdj = this.getScrollOffset() + this.scrollAdjustments;
+ const scrollOffsetWithAdj = effectiveScrollOffset;
const isFirstMeasure = !this.itemSizeCache.has(key);
const defaultShouldAdjust = isFirstMeasure ? (
// First measurement: compensate any item whose top sits above the
@@ -928,9 +955,12 @@ class Virtualizer {
// below — e.g. a streaming chat message growing at its bottom)
// changes size *below* the anchor point, so shifting scrollTop by the
// delta would drag the viewport downward on every growth (#1218).
- // Also skip during backward scroll to avoid the "items jump while
- // scrolling up" cascade.
- itemStart + itemSize <= scrollOffsetWithAdj && this.scrollDirection !== "backward"
+ // Upstream also skips this while scrolling backwards, a guard against
+ // corrections firing from a stale scroll offset. The effective-offset
+ // fix above removes the staleness, and keeping the guard meant every
+ // image or link preview that finished loading above the viewport
+ // shoved the view while scrolling up — so the guard is dropped.
+ itemStart + itemSize <= scrollOffsetWithAdj
);
const shouldAdjustScroll = ((_c = this.scrollState) == null ? void 0 : _c.behavior) !== "smooth" && (this.shouldAdjustScrollPositionOnItemSizeChange !== void 0 ? this.shouldAdjustScrollPositionOnItemSizeChange(
// The callback expects a VirtualItem; build one lazily only
@@ -1197,16 +1227,19 @@ class Virtualizer {
this._iosDeferredAdjustment += delta;
return false;
} else {
- const target = this.getScrollOffset() + this.scrollAdjustments + delta;
+ // A programmatic scroll written this frame is not yet visible through
+ // getScrollOffset(); build the correction on the offset that was written.
+ const base = this._intendedScrollOffset ?? this.getScrollOffset();
+ const target = base + this.scrollAdjustments + delta;
const el = this.scrollElement;
const maxAtWrite = el !== null && ("scrollHeight" in el || "document" in el) ? this.getMaxScrollOffset() : null;
this._clampedAdjustment = maxAtWrite !== null && target > maxAtWrite + 0.5 ? { target, maxAtWrite } : null;
- this._scrollToOffset(this.getScrollOffset(), {
+ this._scrollToOffset(base, {
adjustments: this.scrollAdjustments += delta,
behavior
});
if (this.scrollOffset !== null) {
- this.scrollOffset += this.scrollAdjustments;
+ this.scrollOffset = base + this.scrollAdjustments;
if (this.scrollOffset < 0) this.scrollOffset = 0;
this.scrollAdjustments = 0;
}