From 81b81e264126d254b2c085706731b697470d02a2 Mon Sep 17 00:00:00 2001 From: Kebechet Date: Tue, 3 Nov 2026 20:08:00 +0100 Subject: [PATCH 1/2] Bump virtual-core to 3.19.11 and drop the three stopgaps it fixes virtual-core 3.19.9 stops a touch fling before a command on Android too (https://github.com/TanStack/virtual/pull/1347), and 3.19.11 negates the IsRtl scroll write (https://github.com/TanStack/virtual/pull/1291) and computes getOffsetForIndex from the current measurements (https://github.com/TanStack/virtual/pull/1363). The interop's fling stop with its window input listeners, its scrollToFn negation and the refresh before getOffsetForIndex and scrollToIndex go; kept, the negation would have applied the RTL fix twice and sent every write back to the start. The other refreshMeasurements calls stay: they cover the wrapper's own cache writes. The tests that pinned the stopgaps now pin virtual-core's behaviour. One case changes: with a classic scrollbar the fling cannot be stopped, and virtual-core holds the command until the fling ends instead of writing it at once. The interop's private-API surface (_willUpdate, _didMount, itemSizeCache, itemSizeCacheVersion, pendingMin) is unchanged in 3.19.11, and so is VirtualizerOptions. --- README.md | 18 ++-- .../Blazor.TanStack.VirtualCore.csproj | 8 +- .../wwwroot/adaptive-virtualize-interop.js | 98 +------------------ .../wwwroot/tanstack-virtual-core.js | 6 +- .../InteropModuleTests.cs | 15 +-- .../OptionCoverageTests.cs | 5 +- 6 files changed, 31 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index e49ec0b..b0e54c0 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Holding the trailing side instead means a reversal is never worse than a symmetr It is a plain `Func` rather than an `EventCallback` on purpose: `EventCallback` calls `StateHasChanged` on the receiver after the handler returns, which on a page with a debounced search input re-renders the bound input with a stale value and clobbers in-flight keystrokes. -## Coverage vs. @tanstack/virtual-core 3.19.8 +## Coverage vs. @tanstack/virtual-core 3.19.11 **Complete.** Every `VirtualizerOptions` member is reachable, and every `Virtualizer` member that means something to a .NET caller is surfaced. @@ -184,24 +184,18 @@ This is a real coupling: a virtual-core release could change them without it bei ## Stopgaps for upstream fixes -Behaviour that belongs in virtual-core, carried here only until the upstream fix ships: +Behaviour that belongs in virtual-core is carried here only until the upstream fix ships, and listed here with the upstream PR and the test that pins it. There are none in this release: the three that 3.19.8 carried - the Android touch-fling stop (https://github.com/TanStack/virtual/pull/1347), the negated `IsRtl` scroll write (https://github.com/TanStack/virtual/pull/1291) and the measurement refresh before `getOffsetForIndex` (https://github.com/TanStack/virtual/pull/1363) - shipped in virtual-core 3.19.9 and 3.19.11. Their tests stay, and now pin virtual-core's own behaviour. -| Behaviour | Upstream | Stopgap | -|---|---|---| -| A `ScrollToIndex`, `ScrollToOffset`, `ScrollToEnd` or `ScrollBy` issued by code while a touch fling is still moving the list overshoots its target on Android: Chrome carries the fling on from the written position. virtual-core stops the fling first only on iOS 27. | https://github.com/TanStack/virtual/pull/1347 (issue https://github.com/TanStack/virtual/issues/1346) | `runScrollCommand` in `wwwroot/adaptive-virtualize-interop.js` hides the scroll axis's overflow for two frames before the write. Pinned by `ScrollToIndex_WhileATouchFlingIsMovingTheList_StopsTheMomentumBeforeWriting`. | -| Scroll commands in a horizontal `IsRtl` list go nowhere: virtual-core reads `scrollLeft` negated, but writes the offset back positive, and a right-to-left container clamps that to its start. | https://github.com/TanStack/virtual/pull/1291 (issue https://github.com/TanStack/virtual/issues/1065) | `buildVirtualizerOptions` in `wwwroot/adaptive-virtualize-interop.js` wraps `scrollToFn` to negate the write. Pinned by `IsRtl_ScrollToIndex_BringsTheItemOnScreen`, which fails once virtual-core negates the write itself and this negates it a second time. | -| `getOffsetForIndex` answers from the layout before the latest size change until something else reads the items. | https://github.com/TanStack/virtual/pull/1363 (issue https://github.com/TanStack/virtual/issues/1361) | `getOffsetForIndex`, `takeSnapshot` and `getSnapshot` call `refreshMeasurements()` first. The refresh before `getOffsetForIndex` goes with the fix; the others stay, because they cover this wrapper's own direct cache writes as well. Pinned by `GetOffsetForIndex_RightAfterARowAboveChanges_UsesTheSizesTheVirtualizerUses`. | - -⚠️ **Remove the stopgap together with the bump to the virtual-core release that contains the fix.** Until then it also runs on iOS, where it is not needed and costs its two frames, because telling the platforms apart would mean copying virtual-core's private user-agent checks. +⚠️ **Remove a stopgap together with the bump to the virtual-core release that contains the fix.** Kept past it, a stopgap applies the fix a second time - the `IsRtl` negation would have sent every write back to the start. ## Vendored library provenance | | | |---|---| -| Version | @tanstack/virtual-core 3.19.8 | +| Version | @tanstack/virtual-core 3.19.11 | | File | `wwwroot/tanstack-virtual-core.js` | -| Source | jsDelivr rollup bundle of npm `@tanstack/virtual-core@3.19.8` (`+esm`) | -| SHA-256 | `52c91d57d1bf1bd22bea2c3a64cd4cfcceea1808b747cbfd3bbbc13ea0ca487a` | +| Source | jsDelivr rollup bundle of npm `@tanstack/virtual-core@3.19.11` (`+esm`) | +| SHA-256 | `a293372e09c2a90fb1d5d275e5ccd4ded1c65e1e78f3157ee1cce4b2a2e6b995` | ⚠️ **Deliberately the bundled build, not the npm tarball's `dist/esm/index.js`.** diff --git a/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj b/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj index 67b5b02..cc49b5c 100644 --- a/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj +++ b/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj @@ -26,8 +26,12 @@ icon.png blazor;virtualization;virtual-scroll;tanstack;variable-height;list;performance - 3.19.8.4 - 3.19.8.4: the rows an Items change moves are no longer painted hidden, and InvalidateItemSize keeps the size a row is rendered at. + 3.19.11 + 3.19.11: @tanstack/virtual-core 3.19.8 -> 3.19.11, and the three stopgaps it makes redundant are gone. + +virtual-core now does itself what this package did in its place: it stops a touch fling before a ScrollToIndex, ScrollToOffset or ScrollToEnd issued during it on Android too, negates the scroll write of a horizontal IsRtl list, and computes GetOffsetForIndex - and the first write of ScrollToIndex - from the current measurements. Behaviour that changes with it: with a classic scrollbar that takes layout space (a touchscreen laptop), a command issued mid-fling now waits for the fling to end, about 150 ms after its last frame, instead of being written at once; a ScrollBy issued mid-fling is written at once without stopping the fling, because its target is relative to where the fling is; and under IsRtl in window-scroller mode the write is no longer negated, which matches how virtual-core reads the window's offset. Also from virtual-core: rows that a render inside a ResizeObserver callback mounts are observed from the next frame, and a scroll offset left past the end of a list that just shrank keeps a full rendered range instead of only the last row. + +3.19.8.4: the rows an Items change moves are no longer painted hidden, and InvalidateItemSize keeps the size a row is rendered at. setOptions and setItems now go out from OnParametersSet, in that order, ahead of the render that shows the change, instead of from OnAfterRenderAsync after it. That render draws the rows an Items change moves hidden until the JS side holds the set, and on a WebView bridge the set used to arrive a round trip later: on a Samsung A53 every row below an expanding group was painted hidden for up to 220 ms. It now arrives first, and the rows are placed and shown before the frame is painted. InvalidateItemSize reads a rendered row's size off the row instead of dropping it, so a set arriving before the row's content changes no longer moves the rows below to the estimate; the ResizeObserver takes the new size when the content changes, and a row that is not rendered, or any row under UseCachedMeasurements, still falls back to the estimate. A row of the replaced item set that resizes before the render lands keeps its new size under its own key instead of losing it. diff --git a/src/Blazor.TanStack.VirtualCore/wwwroot/adaptive-virtualize-interop.js b/src/Blazor.TanStack.VirtualCore/wwwroot/adaptive-virtualize-interop.js index 61add46..0afaca4 100644 --- a/src/Blazor.TanStack.VirtualCore/wwwroot/adaptive-virtualize-interop.js +++ b/src/Blazor.TanStack.VirtualCore/wwwroot/adaptive-virtualize-interop.js @@ -47,8 +47,6 @@ function compact(source) { return result; } -const nextFrame = () => new Promise(resolve => requestAnimationFrame(() => resolve())); - /** * The key of the item a row renders. Contract with AdaptiveVirtualize.razor, which writes it on every * row; syncRenderedRows and the resize measurement read it to tell whether a row belongs to the item @@ -94,22 +92,6 @@ class AdaptiveVirtualizeInstance { // past the item count - so the measure pass after that render reads these off the rows. this.keysToRemeasure = new Set(); - // What runScrollCommand needs to know about the user's input. Listened for on the window, so a - // tap on a button outside the list counts too - that tap is how a jump gets issued mid-fling. - this.touchActive = false; - this.lastInputWasTouch = false; - this.momentumStop = null; - this.inputListeners = { - pointerdown: event => { this.lastInputWasTouch = event.pointerType === 'touch'; }, - wheel: () => { this.lastInputWasTouch = false; }, - touchstart: () => { this.touchActive = true; }, - touchend: () => { this.touchActive = false; }, - touchcancel: () => { this.touchActive = false; }, - }; - for (const [type, listener] of Object.entries(this.inputListeners)) { - window.addEventListener(type, listener, { capture: true, passive: true }); - } - this.virtualizer = new Virtualizer(this.buildVirtualizerOptions()); this.virtualizer._willUpdate(); @@ -223,16 +205,6 @@ class AdaptiveVirtualizeInstance { resolved.scrollToFn = elementScroll; } - // virtual-core negates scrollLeft when it reads the offset under isRtl, but writes the offset - // back as is - and in a right-to-left container scrollLeft runs from 0 down to negative - // values, so a positive write clamps to the start and every scroll command goes nowhere. - // Negating it on the way out matches the read. - if (passThrough.isRtl && passThrough.horizontal) { - const write = resolved.scrollToFn; - resolved.scrollToFn = (offset, { adjustments = 0, behavior }, instance) => - write(-(offset + adjustments), { behavior }, instance); - } - // Enums cross interop as their C# names; virtual-core wants its own lowercase literals. if (laneAssignmentMode) { resolved.laneAssignmentMode = laneAssignmentMode.toLowerCase(); @@ -706,76 +678,19 @@ class AdaptiveVirtualizeInstance { } scrollToIndex(index, align, behavior) { - return this.runScrollCommand(() => { - // virtual-core aims the write through getOffsetForIndex, which reads measurementsCache. - this.refreshMeasurements(); - this.virtualizer.scrollToIndex(index, { align, behavior }); - }); + this.virtualizer.scrollToIndex(index, { align, behavior }); } scrollToOffset(offset, align, behavior) { - return this.runScrollCommand(() => this.virtualizer.scrollToOffset(offset, { align, behavior })); + this.virtualizer.scrollToOffset(offset, { align, behavior }); } scrollToEnd(behavior) { - return this.runScrollCommand(() => this.virtualizer.scrollToEnd({ behavior })); + this.virtualizer.scrollToEnd({ behavior }); } scrollBy(delta, behavior) { - return this.runScrollCommand(() => this.virtualizer.scrollBy(delta, { behavior })); - } - - /** - * Runs a scroll command, first stopping a touch fling that is still moving the list. Android - * Chrome carries a fling on after a programmatic scroll write, so a jump issued mid-fling lands - * wherever the momentum takes it instead of on its target. virtual-core stops a fling before a - * command only on iOS 27; this stands in for the rest until - * https://github.com/TanStack/virtual/pull/1347 ships, and goes away with it. - * - * Hiding the scroll axis's overflow for two frames is what stops the momentum - the same trick - * virtual-core uses on iOS 27. It runs on iOS too, where it is not needed (before 27 the write - * stops the fling itself, and on 27 virtual-core does) and costs those two frames: telling the - * platforms apart would take copying virtual-core's private user-agent checks. Its own - * _stopMomentum is not reused: it is private, and it drops the callback of a command superseded - * mid-stop, which would leave that command's caller awaiting forever. Without a touch there is - * no fling to stop, and on a desktop hiding the overflow would also hide a classic scrollbar and - * reflow the list, so the write goes out at once. - */ - async runScrollCommand(command) { - const isTouchFling = this.lastInputWasTouch && !this.touchActive - && this.virtualizer.isScrolling === true; - if (isTouchFling) { - // Shared by every command issued during the same stop, so none of them reads back the - // "hidden" another one set as the value to restore. - this.momentumStop ??= this.stopMomentum()?.finally(() => { this.momentumStop = null; }) ?? null; - if (this.momentumStop) { - await this.momentumStop; - if (!this.cleanupMount) { - return; - } - } - } - - command(); - } - - /** Hides the scroll axis's overflow for two frames; null when it had to be put back at once. */ - stopMomentum() { - const element = this.isWindowScroller ? this.findScrollContainer(this.rootElement) : this.scrollContainer; - const property = this.options.horizontal ? 'overflow-x' : 'overflow-y'; - const { style } = element; - const value = style.getPropertyValue(property); - const priority = style.getPropertyPriority(property); - const crossSize = () => this.options.horizontal ? element.clientHeight : element.clientWidth; - const crossSizeBefore = crossSize(); - style.setProperty(property, 'hidden', 'important'); - // A classic scrollbar takes layout space, so hiding it would reflow the list for two frames - // (a touchscreen laptop). Put it back before anything paints and write at once, as before. - if (crossSize() !== crossSizeBefore) { - style.setProperty(property, value, priority); - return null; - } - return nextFrame().then(nextFrame).then(() => style.setProperty(property, value, priority)); + this.virtualizer.scrollBy(delta, { behavior }); } cancelScroll() { @@ -801,8 +716,6 @@ class AdaptiveVirtualizeInstance { } getOffsetForIndex(index, align) { - // virtual-core computes the offset from measurementsCache. - this.refreshMeasurements(); // Upstream returns [offset, alignment]; only the offset is meaningful to a caller. const result = this.virtualizer.getOffsetForIndex(index, align); return Array.isArray(result) ? result[0] : result; @@ -863,9 +776,6 @@ class AdaptiveVirtualizeInstance { this.containerObserver?.disconnect(); this.rowObserver?.disconnect(); clearTimeout(this.pendingNotification); - for (const [type, listener] of Object.entries(this.inputListeners)) { - window.removeEventListener(type, listener, { capture: true }); - } if (this.cleanupMount) { try { this.cleanupMount(); diff --git a/src/Blazor.TanStack.VirtualCore/wwwroot/tanstack-virtual-core.js b/src/Blazor.TanStack.VirtualCore/wwwroot/tanstack-virtual-core.js index 17ed1ac..dd34902 100644 --- a/src/Blazor.TanStack.VirtualCore/wwwroot/tanstack-virtual-core.js +++ b/src/Blazor.TanStack.VirtualCore/wwwroot/tanstack-virtual-core.js @@ -1,8 +1,8 @@ /** * Bundled by jsDelivr using Rollup v4.62.2 and esbuild v0.28.1. - * Original file: /npm/@tanstack/virtual-core@3.19.8/dist/esm/index.js + * Original file: /npm/@tanstack/virtual-core@3.19.11/dist/esm/index.js * * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files */ -function J(o){return typeof o=="object"?o.key:o}function Z(o,r){const t=o.length;return new Proxy(o,{get(e,s,i){if(typeof s=="string"){const l=s.charCodeAt(0);if(l>=48&&l<=57){const n=+s;if(Number.isInteger(n)&&n>=0&&ne[c]!==a))&&(e=n,s=r(...n),t?.onChange&&!(i&&t.skipInitialOnChange)&&t.onChange(s),i=!1),s}return l.updateDeps=n=>{e=n},l}function B(o,r){if(o===void 0)throw new Error(`Unexpected undefined${r?`: ${r}`:""}`);return o}const w=(o,r)=>Math.abs(o-r)<1.01,K=(o,r,t)=>{let e;return Object.assign(function(...s){o.clearTimeout(e),e=o.setTimeout(()=>r.apply(this,s),t)},{cancel:()=>{o.clearTimeout(e)}})};function tt({offset:o,isScrolling:r,elementMax:t,holdOffset:e,scrollOffset:s,pendingWrite:i,clampedMaxAtWrite:l}){var n;return r&&i===null&&o===s?"spurious":i!==null&&Math.abs(o-i.target)<1.5?"readBack":i!==null&&!i.smooth&&t!==null&&i.target>t+e+.5&&Math.abs(o-(t+e))<1.5||i!==null&&!i.smooth&&((n=i.lowMaxes)!=null&&n.some(h=>Math.abs(o-h)<1.5))?"clampedReadBack":l!==null&&Math.abs(o-l)<1.5?"clampedEcho":"external"}const et=o=>o==="smooth"||o==="smoothFinishing";function st({kind:o,offset:r,command:t,target:e}){return o!=="spurious"&&o!=="readBack"&&o!=="clampedReadBack"&&t?.index!=null&&t.phase!=="queued"&&!et(t.phase)&&!w(r,t.lastTargetOffset)&&!w(r,e())&&o!=="clampedEcho"}const D=40;function it(o){const r=o[o.length-1];if(r===void 0)return!1;let t=o.length-2;for(;t>=0&&r.t-o[t].t=0&&e.t-o[s].t=0&&Math.abs(l)>=2*Math.abs(n)&&Math.abs(l)-Math.abs(n)>=.8}function nt(o){const r=o[o.length-1];if(r===void 0)return!1;let t=o.length-2;for(;t>=0&&r.t-o[t].t=.05}let I;const W=()=>{if(I!==void 0)return I;if(typeof navigator>"u")return I=!1;if(/iP(hone|od|ad)/.test(navigator.userAgent))return I=!0;const o=navigator.maxTouchPoints;return I=navigator.platform==="MacIntel"&&o!==void 0&&o>0};let k;const lt=()=>k!==void 0?k:k=W()&&typeof CSS<"u"&&typeof CSS.supports=="function"&&CSS.supports("overflow-anchor: auto"),ot=()=>{I=void 0,k=void 0},A={capture:!0,passive:!0};class rt{constructor(r){this.host=r,this._touch=null,this.inMotion=()=>this._touch!==null&&this._touch.phase!=="lateFling",this.dragging=()=>{var t;return((t=this._touch)==null?void 0:t.phase)==="down"&&this._touch.moved},this.tail=()=>{var t;return((t=this._touch)==null?void 0:t.phase)==="tail"?this._touch.moved?"fling":"tap":null},this.onScroll=({isScrolling:t,ownReadBack:e,moved:s,external:i})=>{var l,n,h;return t&&((l=this._touch)==null?void 0:l.phase)==="tail"?this._openTail(this._touch.moved||!e,!1):((n=this._touch)==null?void 0:n.phase)==="lateFling"&&(this.host.now()>this._touch.until?this._touch=null:i&&t&&s&&this._openTail(!0,!1)),!e&&t&&s&&((h=this._touch)==null?void 0:h.phase)==="down"?(this._touch.moved=!0,!0):!1},this.touchStart=()=>{this.end(),this._touch={phase:"down",moved:!1}},this.touchEnd=()=>{var t;const e=((t=this._touch)==null?void 0:t.phase)==="down"&&this._touch.moved;this.end(),this._openTail(e,e)},this.end=()=>{var t;const e=this.host.getWindow();((t=this._touch)==null?void 0:t.phase)==="tail"&&e!=null&&e.clearTimeout(this._touch.timerId),this._touch=null},this.listen=t=>{const e=()=>this.touchStart(),s=()=>this.touchEnd();return t.addEventListener("touchstart",e,A),t.addEventListener("touchend",s,A),t.addEventListener("touchcancel",s,A),()=>{t.removeEventListener("touchstart",e,A),t.removeEventListener("touchend",s,A),t.removeEventListener("touchcancel",s,A),this.end()}},this._openTail=(t,e)=>{const s=this.host.getWindow();if(!W()||s==null)return;this.end();const i={phase:"tail",moved:t,awaitingFling:e,timerId:s.setTimeout(()=>{if(this.host.tailStillMoving()){this._openTail(i.moved,i.awaitingFling);return}this._touch=i.awaitingFling?{phase:"lateFling",until:this.host.now()+1e3}:null,this.host.onTailClosed()},150)};this._touch=i}}get touch(){return this._touch}}const H=o=>{const{clientWidth:r,clientHeight:t}=o;return{width:r,height:t}},q=o=>{const r=new Set;return{schedule:t=>{const e=o.requestAnimationFrame(()=>{r.delete(e),t()});r.add(e)},cancel:()=>{r.forEach(t=>o.cancelAnimationFrame(t)),r.clear()}}},N=o=>o,G=o=>{const r=o.overscan*Math.max(o.lanes??1,1),t=Math.max(o.startIndex-r,0),s=Math.min(o.endIndex+r,o.count-1)-t+1,i=new Array(s);for(let l=0;l{const t=o.scrollElement;if(!t)return;const e=o.targetWindow;if(!e)return;const s=n=>{const{width:h,height:a}=n;r({width:Math.round(h),height:Math.round(a)})};if(s(H(t)),!e.ResizeObserver)return()=>{};const i=q(e),l=new e.ResizeObserver(()=>{const n=()=>{s(H(t))};o.options.useAnimationFrameWithResizeObserver?i.schedule(n):n()});return l.observe(t,{box:"content-box"}),()=>{l.unobserve(t),i.cancel()}},P={passive:!0},at=(o,r)=>{const t=o.scrollElement;if(!t)return;const e=()=>{r({width:t.innerWidth,height:t.innerHeight})};return e(),t.addEventListener("resize",e,P),()=>{t.removeEventListener("resize",e)}},ct=typeof window>"u"?!0:"onscrollend"in window,U=(o,r,t)=>{const e=o.scrollElement;if(!e)return;const s=o.targetWindow;if(!s)return;const i=o.options.useScrollendEvent&&ct;let l=0;const n=i?null:K(s,()=>r(t(e),!1),o.options.isScrollingResetDelay),h=u=>()=>{l=t(e),n?.(),r(l,u)},a=h(!0),c=h(!1);return e.addEventListener("scroll",a,P),i&&e.addEventListener("scrollend",c,P),()=>{e.removeEventListener("scroll",a),i&&e.removeEventListener("scrollend",c),n?.cancel()}},ut=(o,r)=>U(o,r,t=>{const{horizontal:e,isRtl:s}=o.options;return e?t.scrollLeft*(s&&-1||1):t.scrollTop}),ft=(o,r)=>U(o,r,t=>o.options.horizontal?t.scrollX:t.scrollY),X=(o,r,t)=>{if(t.options.useCachedMeasurements){const e=t.indexFromElement(o),s=t.options.getItemKey(e);return t.itemSizeCache.get(s)??t.options.estimateSize(e)}if(r?.borderBoxSize){const e=r.borderBoxSize[0];if(e)return Math.round(e[t.options.horizontal?"inlineSize":"blockSize"])}if(!r){const e=t.indexFromElement(o),s=t.options.getItemKey(e),i=t.itemSizeCache.get(s);if(i!==void 0)return i}return o[t.options.horizontal?"offsetWidth":"offsetHeight"]},Y=(o,{adjustments:r=0,behavior:t},e)=>{var s,i;(i=(s=e.scrollElement)==null?void 0:s.scrollTo)==null||i.call(s,{[e.options.horizontal?"left":"top"]:o+r,behavior:t})},dt=Y,mt=Y,$=(o,r)=>o==="smooth"?r?"smoothLanded":"smooth":"instant",L=o=>o?.phase==="smooth"||o?.phase==="smoothLanded";function gt(o,r,t,e){if(r===0)return!1;const s=e(0),i=new Set;let l=0;for(;l=r)return!1;for(let h=0;hthis.targetWindow,now:()=>this.now(),tailStillMoving:()=>{const t=this.scrollState;if(t?.phase!=="queued"||this.pendingWrite!==null)return!1;const e=this._readElementOffset();return e===null||this._lastElementOffset===null||Math.abs(e-this._lastElementOffset)<1?!1:(this._lastElementOffset=e,t.samples=[...t.samples??[],{t:this.now(),offset:e}],!0)},onTailClosed:()=>{const t=this.scrollState;t?.phase==="queued"&&nt(t.samples??[])&&(this.scrollState=null),this._flushHoldIfReady(!0),this._writeAwaitingCommand(),this._retryClampedAdjustment()}}),this._lastElementOffset=null,this.pendingWrite=null,this._clampedAdjustment=null,this.elementsCache=new Map,this._resizeBatch=null,this.now=()=>{var t,e,s;return((s=(e=(t=this.targetWindow)==null?void 0:t.performance)==null?void 0:e.now)==null?void 0:s.call(e))??Date.now()},this.observer=(()=>{let t=null,e=null;const s=()=>{if(t)return t;const l=this.targetWindow;if(!l||!l.ResizeObserver)return null;const n=e=q(l);return t=new l.ResizeObserver(h=>{const a=()=>{const c={sync:!1};this._resizeBatch=c;try{h.forEach(i)}finally{this._resizeBatch=null,c.notify&&(this.notify(c.sync),this._noteClampMax(),this._flushHoldIfReady(!0),this._retryClampedAdjustment())}};this.options.useAnimationFrameWithResizeObserver?n.schedule(a):a()})},i=l=>{const n=l.target,h=this.indexFromElement(n);if(!n.isConnected){this.observer.unobserve(n);for(const[a,c]of this.elementsCache)if(c===n){this.elementsCache.delete(a);break}return}this.isIndexInRange(h)&&this.shouldMeasureDuringScroll(h)&&this.resizeItem(h,this.options.measureElement(n,l,this))};return{disconnect:()=>{var l;(l=s())==null||l.disconnect(),e?.cancel(),t=null,e=null},observe:l=>{var n;return(n=s())==null?void 0:n.observe(l,{box:"border-box"})},unobserve:l=>{var n;return(n=s())==null?void 0:n.unobserve(l)}}})(),this.range=null,this.setOptions=t=>{var e;const s={debug:!1,initialOffset:0,overscan:1,paddingStart:0,paddingEnd:0,scrollPaddingStart:0,scrollPaddingEnd:0,horizontal:!1,getItemKey:N,rangeExtractor:G,onChange:()=>{},measureElement:X,initialRect:{width:0,height:0},scrollMargin:0,gap:0,indexAttribute:"data-index",initialMeasurementsCache:[],lanes:1,anchorTo:"start",followOnAppend:!1,scrollEndThreshold:1,isScrollingResetDelay:150,enabled:!0,isRtl:!1,useScrollendEvent:!1,useAnimationFrameWithResizeObserver:!1,laneAssignmentMode:"estimate",useCachedMeasurements:!1};for(const c in t){const u=t[c];u!==void 0&&(s[c]=u)}const i=this.options;let l=null,n=null,h=!1;if(i!==void 0&&i.enabled&&s.enabled&&s.anchorTo==="end"&&this.scrollElement!==null){const c=i.count,u=s.count,O=this.getMeasurements(),m=((e=this._singleLaneMeasurements)==null?void 0:e.items)??O,p=f=>J(m[f]),g=c>0?p(0):null,y=c>0?p(c-1):null;if(u!==c||c>0&&u>0&&(s.getItemKey(0)!==g||s.getItemKey(u-1)!==y)){h=!0;const f=c>0?this.getVirtualItemForOffset(this.getScrollOffset())??O[0]:null;f&&(l=[f.key,this.getScrollOffset()-f.start]);const M=s.followOnAppend===!0?"auto":s.followOnAppend||null;M&&u>0&&this.isAtEnd(i.scrollEndThreshold)&&(c===0||s.getItemKey(u-1)!==y)&&(u>c||gt(c,u,p,s.getItemKey))&&(n=M)}}this.options=s,h&&(this.pendingMin=0,this.itemSizeCacheVersion++);let a=null;if(l&&this.scrollOffset!==null&&!this._awaitingCommandReadBack()){const[c,u]=l,O=this.getMeasurements(),{count:m,getItemKey:p}=this.options;let g=0;for(;g{var e,s;(s=(e=this.options).onChange)==null||s.call(e,this,t)},this.maybeNotify=E(()=>(this.calculateRange(),[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]),t=>{this.notify(t)},{key:!1,debug:()=>this.options.debug,initialDeps:[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]}),this.cleanup=()=>{var t;this.unsubs.filter(Boolean).forEach(e=>e()),this.unsubs=[],this.observer.disconnect(),this.rafId!=null&&this.targetWindow&&(this.targetWindow.cancelAnimationFrame(this.rafId),this.rafId=null),this.scrollState=null,(t=this._momentumStop)==null||t.restore(),this._momentumStop=null,this.isScrolling=!1,this.scrollDirection=null,this._holdOffset=0,this._reportedOverscroll=!1,this._clampedAdjustment=null,this.pendingWrite=null,this.scrollElement=null,this.targetWindow=null},this._didMount=()=>()=>{this.cleanup()},this._willUpdate=()=>{var t;const e=this.options.enabled?this.options.getScrollElement():null;if(this.scrollElement!==e){if(this.cleanup(),!e){this.maybeNotify();return}this.scrollElement=e,this.scrollElement&&"ownerDocument"in this.scrollElement?this.targetWindow=this.scrollElement.ownerDocument.defaultView:this.targetWindow=((t=this.scrollElement)==null?void 0:t.window)??null,this.elementsCache.forEach(i=>{this.observer.observe(i)}),this.unsubs.push(this.options.observeElementRect(this,i=>{this.scrollRect=i,this.maybeNotify()})),this.unsubs.push(this.options.observeElementOffset(this,(i,l)=>{var n,h,a,c;const u=l?this._elementMaxScrollOffset():null,O=this._reportedOverscroll;this._reportedOverscroll=i<0||u!==null&&i>u+1;const m=i;this._lastElementOffset=m,i=this._toLayoutOffset(i);const p=this._classifyScrollEvent(i,l,u);if(p==="spurious")return;p==="readBack"?i=this.pendingWrite.target:p==="clampedReadBack"&&((n=this.scrollState)==null?void 0:n.index)!=null&&ithis.getScrollStateTarget(this.scrollState)})&&(this.scrollState=null),this.pendingWrite=null,this._clampedAdjustment!==null&&Math.abs(i-this._clampedAdjustment.maxAtWrite)>=1.5&&(this._clampedAdjustment=null);const g=this.getScrollOffset(),y=this.scrollDirection;this.scrollDirection=l?g===i?this.scrollDirection:gd-f.t<=200),{t:d,offset:m}],(y!==null&&this.scrollDirection!==y||it(S.samples))&&(this.scrollState=null)}this._iosGesture.onScroll({isScrolling:l,ownReadBack:p==="readBack",moved:i!==g,external:p==="external"&&((h=this.scrollState)==null?void 0:h.phase)!=="smooth"&&((a=this.scrollState)==null?void 0:a.phase)!=="smoothFinishing"})&&((c=this.scrollState)==null?void 0:c.phase)==="queued"&&(this.scrollState=null),this._flushHoldIfReady(!0),this.scrollState&&this.scheduleScrollReconcile(),this.maybeNotify()})),"addEventListener"in this.scrollElement&&this.unsubs.push(this._iosGesture.listen(this.scrollElement)),this._scrollToOffset(this.getScrollOffset())}this._noteClampMax();const s=this.pendingScrollAnchor;this.pendingScrollAnchor=null,s&&this.scrollElement&&this.options.enabled&&(s.kind==="anchor"?this._canWriteScroll()?L(this.scrollState)&&!w(this.getScrollOffset()-s.delta,this.scrollState.lastTargetOffset)||(this._followOffsetTarget(s.delta),this._scrollToOffset(this.getScrollOffset())):(this._holdOffset+=s.delta,this._followOffsetTarget(s.delta),this.notify(!1)):this.scrollToEnd({behavior:s.behavior})),this._flushHoldIfReady(!1),this._retryClampedAdjustment()},this._classifyScrollEvent=(t,e,s)=>{var i;return tt({offset:t,isScrolling:e,elementMax:s,holdOffset:this._holdOffset,scrollOffset:this.scrollOffset,pendingWrite:this.pendingWrite,clampedMaxAtWrite:((i=this._clampedAdjustment)==null?void 0:i.maxAtWrite)??null})},this._retryClampedAdjustment=()=>{if(this._clampedAdjustment===null||!this.scrollElement||!this.options.enabled||!this._canWriteScroll())return;const{target:t,maxAtWrite:e}=this._clampedAdjustment,s=this.getMaxScrollOffset();s>e+.5&&(this._clampedAdjustment=t>s+.5?{target:t,maxAtWrite:s}:null,this._scrollToOffset(t))},this._writeAwaitingCommand=()=>{const t=this.scrollState;if(t===null||t.phase!=="queued")return;const{behavior:e}=t;this.getMeasurements();const s=this.getScrollStateTarget(t);t.lastTargetOffset=s,this._clampedAdjustment=null,t.phase=$(e,t.index!=null&&w(s,this.getScrollOffset())),this._scrollToOffset(s,{behavior:e}),this.scheduleScrollReconcile()},this._readElementOffset=()=>{const t=this.scrollElement;if(t==null)return null;const{horizontal:e,isRtl:s}=this.options;return typeof t.scrollTop=="number"?e?t.scrollLeft*(s&&-1||1):t.scrollTop:typeof t.scrollY=="number"?e?t.scrollX:t.scrollY:null},this._canWriteScroll=()=>!(W()&&this._iosGesture.inMotion())&&!this._reportedOverscroll,this._flushHoldIfReady=t=>{var e;if(this._holdOffset===0||!this.scrollElement||!this.options.enabled||!this._canWriteScroll()||((e=this.scrollState)==null?void 0:e.phase)==="smooth")return;this._holdOffset=0,this.scrollOffset!==null&&this.scrollOffset<0&&(this.scrollOffset=0),this.notify(t);const s=this.getCommandWrittenOffset()??this.getScrollOffset();this._clampedAdjustment=this._clampedBy(s),this._scrollToOffset(s)},this.rafId=null,this.getSize=()=>this.options.enabled?(this.scrollRect=this.scrollRect??this.options.initialRect,this.scrollRect[this.options.horizontal?"width":"height"]):(this.scrollRect=null,0),this.getScrollOffset=()=>this.options.enabled?(this.scrollOffset=this.scrollOffset??(typeof this.options.initialOffset=="function"?this.options.initialOffset():this.options.initialOffset),this.scrollOffset):(this.scrollOffset=null,0),this.getMeasurementOptions=E(()=>[this.options.count,this.options.paddingStart,this.options.scrollMargin,this.options.getItemKey,this.options.enabled,this.options.lanes,this.options.laneAssignmentMode,this.options.gap],(t,e,s,i,l,n,h,a)=>(this.prevLanes!==void 0&&this.prevLanes!==n&&(this.lanesChangedFlag=!0),this.prevLanes=n,this.pendingMin=null,{count:t,paddingStart:e,scrollMargin:s,getItemKey:i,enabled:l,lanes:n,laneAssignmentMode:h,gap:a}),{key:!1}),this.isIndexInRange=t=>t>=0&&t[this.getMeasurementOptions(),this.itemSizeCacheVersion],({count:t,paddingStart:e,scrollMargin:s,getItemKey:i,enabled:l,lanes:n,laneAssignmentMode:h,gap:a},c)=>{var u;const O=this.itemSizeCache;if(!l)return this.measurementsCache=[],this._singleLaneMeasurements=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),[];if(this.laneAssignments.size>t)for(const d of this.laneAssignments.keys())d>=t&&this.laneAssignments.delete(d);this.lanesChangedFlag&&(this.lanesChangedFlag=!1,this.lanesSettling=!0,this.measurementsCache=[],this._singleLaneMeasurements=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),this.pendingMin=null),this.measurementsCache.length===0&&!this.lanesSettling&&(this.measurementsCache=this.options.initialMeasurementsCache,this.measurementsCache.forEach(d=>{this.itemSizeCache.set(d.key,d.size)}));const m=this.lanesSettling?0:this.pendingMin??0;if(this.pendingMin=null,this.lanesSettling&&this.measurementsCache.length===t&&(this.lanesSettling=!1),n===1){const d=t*2;let f=(u=this._singleLaneMeasurements)==null?void 0:u.flat;if(!f||f.length0&&_.set(f.subarray(0,m*2)),f=_}const M=m===0?new Array(t):this._singleLaneMeasurements.items.slice();let v;if(m===0)v=e+s;else{const _=m-1;v=f[_*2]+f[_*2+1]+a}for(let _=m;_1){v=M;const F=g[v],x=F!==void 0?p[F]:void 0;b=x?x.end+a:e+s}else if(S===n){let F=0,x=y[0],V=g[0];for(let C=1;Cthis.options.debug}),this.calculateRange=E(()=>[this.getMeasurements(),this.getSize(),this.getScrollOffset(),this.options.lanes],(t,e,s,i)=>t.length===0||e===0?(this.range=null,null):(this.range=_t(t,e,s,i,i===1&&this._singleLaneMeasurements!==null?this._singleLaneMeasurements.flat:null),this.range),{key:!1,debug:()=>this.options.debug}),this.getVirtualIndexes=E(()=>{let t=null,e=null;const s=this.calculateRange();return s&&(t=s.startIndex,e=s.endIndex),this.maybeNotify.updateDeps([this.isScrolling,t,e]),[this.options.rangeExtractor,this.options.overscan,this.options.count,t,e,this.options.lanes]},(t,e,s,i,l,n)=>i===null||l===null?[]:t({startIndex:i,endIndex:l,overscan:e,count:s,lanes:n}),{key:!1,debug:()=>this.options.debug}),this.indexFromElement=t=>{const e=this.options.indexAttribute,s=t.getAttribute(e);return s?parseInt(s,10):(console.warn(`Missing attribute name '${e}={index}' on measured element.`),-1)},this.shouldMeasureDuringScroll=t=>{var e;if(!L(this.scrollState))return!0;const s=this.scrollState.index??((e=this.getVirtualItemForOffset(this.scrollState.lastTargetOffset))==null?void 0:e.index);if(s!==void 0&&this.range){const i=Math.max(this.options.overscan,Math.ceil((this.range.endIndex-this.range.startIndex)/2)),l=Math.max(0,s-i),n=Math.min(this.options.count-1,s+i);return t>=l&&t<=n}return!0},this.measureElement=t=>{if(!t){this.elementsCache.forEach((l,n)=>{l.isConnected||(this.observer.unobserve(l),this.elementsCache.delete(n))});return}const e=this.indexFromElement(t);if(!this.isIndexInRange(e))return;const s=this.options.getItemKey(e),i=this.elementsCache.get(s);i!==t&&(i&&this.observer.unobserve(i),this.observer.observe(t),this.elementsCache.set(s,t)),t.isConnected&&(!this.isScrolling||this.scrollState)&&this.shouldMeasureDuringScroll(e)&&this.resizeItem(e,this.options.measureElement(t,void 0,this))},this.resizeItem=(t,e)=>{var s,i,l;if(!this.isIndexInRange(t))return;(s=this._resizeBatch)!=null&&s.sync&&this.getMeasurements();let n,h,a;const c=(i=this._singleLaneMeasurements)==null?void 0:i.flat;if(this.options.lanes===1&&c!=null)a=this.options.getItemKey(t),h=c[t*2],n=c[t*2+1];else{const m=this.measurementsCache[t];if(!m)return;a=m.key,h=m.start,n=m.size}const u=this.itemSizeCache.get(a)??n,O=e-u;if(O!==0){const m=this.options.anchorTo==="end"&&!L(this.scrollState)&&this.getVirtualDistanceFromEnd()<=this.options.scrollEndThreshold,p=m?this._getLayoutTotalSize():0,g=this.getScrollOffset(),S=!this.itemSizeCache.has(a)?h[this.getVirtualIndexes(),this.getMeasurements(),this._holdOffset],(t,e,s)=>{const i=[];for(let l=0,n=t.length;lthis.options.debug}),this.getVirtualItemForOffset=t=>{var e;const s=this.getMeasurements();if(s.length===0)return;const i=(e=this._singleLaneMeasurements)==null?void 0:e.flat,l=this.options.lanes===1&&i!=null,n=Q(0,s.length-1,l?h=>i[h*2]:h=>B(s[h]).start,t);return B(s[n])},this._noteClampMax=()=>{if(!this._awaitingCommandReadBack())return;const t=this._elementMaxScrollOffset();if(t===null)return;const e=this.pendingWrite,s=Math.max(t,0)+this._holdOffset;if(s>=e.target)return;const i=e.lowMaxes??(e.lowMaxes=[]);i[i.length-1]!==s&&i.push(s)},this.getMaxScrollOffset=()=>(this._elementMaxScrollOffset()??0)+this._holdOffset,this._toLayoutOffset=t=>{var e;let s=t+this._holdOffset;return((e=this.pendingScrollAnchor)==null?void 0:e.kind)==="anchor"&&(s+=this.pendingScrollAnchor.delta),s},this._elementMaxScrollOffset=()=>{const t=this.scrollElement;if(t===null)return null;if("scrollHeight"in t)return this.options.horizontal?t.scrollWidth-t.clientWidth:t.scrollHeight-t.clientHeight;if("document"in t){const e=t.document.documentElement;return this.options.horizontal?e.scrollWidth-t.innerWidth:e.scrollHeight-t.innerHeight}return null},this._momentumStop=null,this._stopMomentum=t=>{const e=this.scrollElement,s=this.targetWindow;if(e===null||s===null)return;const i="document"in e?e.document.scrollingElement:e;if(!i?.style)return;if(this._momentumStop){this._momentumStop.done=t;return}const{style:l}=i,n=this.options.horizontal?"overflow-x":"overflow-y",h=l.getPropertyValue(n),a=l.getPropertyPriority(n);l.setProperty(n,"hidden","important");const c={restore:()=>l.getPropertyValue(n)!=="hidden"||l.getPropertyPriority(n)!=="important"?!1:(l.setProperty(n,h,a),!0),done:t};this._momentumStop=c,s.requestAnimationFrame(()=>s.requestAnimationFrame(()=>{this._momentumStop===c&&(this._momentumStop=null,c.restore()&&c.done())}))},this._clampedBy=t=>{const e=this._elementMaxScrollOffset();if(e===null)return null;const s=e+this._holdOffset;return t>s+.5?{target:t,maxAtWrite:s}:null},this.getVirtualDistanceFromEnd=()=>Math.max(this._getLayoutTotalSize()+this.options.scrollMargin-this.getSize()-this.getScrollOffset(),0),this.getDistanceFromEnd=()=>Math.max(this.getMaxScrollOffset()-this.getScrollOffset(),0),this.isAtEnd=(t=this.options.scrollEndThreshold)=>this.getDistanceFromEnd()<=t,this.getOffsetForAlignment=(t,e,s=0)=>{if(!this.scrollElement)return 0;const i=this.getSize(),l=this.getScrollOffset();e==="auto"&&(e=t>=l+i?"end":"start"),e==="center"?t+=(s-i)/2:e==="end"&&(t-=i);const n=this.getMaxScrollOffset();return Math.max(Math.min(n,t),0)},this.getOffsetForIndex=(t,e="auto")=>{t=Math.max(0,Math.min(t,this.options.count-1));const s=this.getSize(),i=this.getScrollOffset(),l=this.measurementsCache[t];if(!l)return;if(e==="auto")if(l.end>=i+s-this.options.scrollPaddingEnd)e="end";else if(l.start<=i+this.options.scrollPaddingStart)e="start";else return[i,e];if(e==="end"&&t===this.options.count-1){const h=this.getMaxScrollOffset(),a=h-this.options.paddingEnd+this.options.scrollPaddingEnd;return[Math.max(Math.min(a,h),0),e]}const n=e==="end"?l.end+this.options.scrollPaddingEnd:l.start-this.options.scrollPaddingStart;return[this.getOffsetForAlignment(n,e,l.size),e]},this.scrollToOffset=(t,{align:e="start",behavior:s="auto"}={})=>{this._startScroll({index:null,align:e},this.getOffsetForAlignment(t,e),s,"absolute")},this.scrollToIndex=(t,{align:e="auto",behavior:s="auto"}={})=>{t=Math.max(0,Math.min(t,this.options.count-1));const i=this.getOffsetForIndex(t,e);if(!i)return;const[l,n]=i;this._startScroll({index:t,align:n},l,s,"absolute")},this.scrollBy=(t,{behavior:e="auto"}={})=>{const s=this.getOffsetForAlignment((this.getCommandWrittenOffset()??this.getScrollOffset())+t,"start");this._startScroll({index:null,align:"start"},s,e,"relative")},this.scrollToEnd=({behavior:t="auto"}={})=>{if(this.options.count>0){this._startScroll({index:this.options.count-1,align:"end",toEnd:!0},Math.max(this.getMaxScrollOffset(),0),t,"absolute");return}this.scrollToOffset(Math.max(this.getTotalSize()-this.getSize(),0),{behavior:t})},this.cancelScroll=()=>{this.rafId!=null&&this.targetWindow&&(this.targetWindow.cancelAnimationFrame(this.rafId),this.rafId=null),this.scrollState=null},this.getTotalSize=()=>Math.max(this._getLayoutTotalSize()-this._holdOffset,0),this._getLayoutTotalSize=()=>{var t,e;const s=this.getMeasurements();let i;if(s.length===0)i=this.options.paddingStart;else if(this.options.lanes===1){const l=s.length-1,n=(t=this._singleLaneMeasurements)==null?void 0:t.flat;n!=null?i=n[l*2]+n[l*2+1]:i=((e=s[l])==null?void 0:e.end)??0}else{const l=Array(this.options.lanes).fill(null);let n=s.length-1;for(;n>=0&&l.some(h=>h===null);){const h=s[n];l[h.lane]===null&&(l[h.lane]=h.end),n--}i=Math.max(...l.filter(h=>h!==null))}return Math.max(i-this.options.scrollMargin+this.options.paddingEnd,0)},this.takeSnapshot=()=>{const t=[];if(this.itemSizeCache.size===0)return t;const e=this.getMeasurements();for(const s of e)s&&this.itemSizeCache.has(s.key)&&t.push({index:s.index,key:s.key,start:s.start,size:s.size,end:s.end,lane:s.lane});return t},this._scrollToOffset=(t,{adjustments:e,behavior:s}={})=>{const i={target:t+(e??0),smooth:s==="smooth"};return this.pendingWrite=i,this.options.scrollToFn(t-this._holdOffset,{behavior:s,adjustments:e},this),i},this.getCommandWrittenOffset=()=>this.scrollState!==null&&this.pendingWrite!==null&&!this.pendingWrite.smooth?this.pendingWrite.target:null,this._awaitingCommandReadBack=()=>{const t=this.getCommandWrittenOffset();return!(W()&&this._iosGesture.dragging())&&t!==null&&!w(t,this.getScrollOffset())},this._followOffsetTarget=t=>{this.scrollState!==null&&this.scrollState.index==null&&(this.scrollState.lastTargetOffset+=t)},this.measure=()=>{this.pendingMin=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),this.itemSizeCacheVersion++,this.notify(!1)},this.setOptions(r)}get scrollAdjustments(){return 0}applyScrollAdjustment(r,t){if(r===0)return!1;if(this._followOffsetTarget(r),this._canWriteScroll()){const e=this.getScrollOffset();this._clampedAdjustment=this._clampedBy(e+r);const s=this._scrollToOffset(e,{adjustments:r,behavior:t});return this.scrollOffset!==null&&(this.pendingWrite===s&&(this.scrollOffset+=r),this.scrollOffset<0&&(this.scrollOffset=0)),!0}else return this._holdOffset+=r,this.scrollOffset!==null&&(this.scrollOffset+=r),!0}scheduleScrollReconcile(){if(!this.targetWindow){this.scrollState=null;return}this.rafId==null&&(this.rafId=this.targetWindow.requestAnimationFrame(()=>{this.rafId=null,this.reconcileScroll()}))}getScrollStateTarget(r){const t=r.toEnd?[Math.max(this.getMaxScrollOffset(),0)]:r.index!=null?this.getOffsetForIndex(r.index,r.align):void 0;return t?t[0]:r.lastTargetOffset}reconcileScroll(){if(!this.scrollState||!this.scrollElement)return;if(this.now()-this.scrollState.startedAt>5e3){this.scrollState=null;return}if(this.scrollState.phase==="queued"){this.scheduleScrollReconcile();return}this.getMeasurements();const e=this.getScrollStateTarget(this.scrollState),s=this.scrollState.index==null?1:this.options.useAnimationFrameWithResizeObserver?3:2,i=e!==this.scrollState.lastTargetOffset;if(!i&&w(e,this.getScrollOffset())){if(this.scrollState.phase==="smooth"?this.scrollState.phase="smoothLanded":this.scrollState.phase==="smoothFinishing"&&(this.scrollState.phase="instant"),this.scrollState.stableFrames++,this.scrollState.stableFrames>=s){this.getScrollOffset()!==e&&this._scrollToOffset(e,{behavior:"auto"}),this.scrollState=null;return}}else if(this.scrollState.stableFrames=0,i){const l=this.getSize()||600,n=Math.abs(e-this.getScrollOffset()),h=L(this.scrollState)&&n>l;this.scrollState.lastTargetOffset=e,h?this.scrollState.phase="smooth":this.scrollState.phase==="smooth"?this.scrollState.phase="smoothFinishing":this.scrollState.phase==="smoothLanded"&&(this.scrollState.phase="instant"),this._scrollToOffset(e,{behavior:h?"smooth":"auto"})}this.scheduleScrollReconcile()}_startScroll(r,t,e,s){this.pendingScrollAnchor=null,this._clampedAdjustment=null;const i=this._iosGesture.tail();if(s==="absolute"&&i!==null){if(i==="fling"&<()){this._stopMomentum(()=>{const l=this.scrollState;l?.phase!=="queued"||this._iosGesture.tail()!=="fling"||this._reportedOverscroll||(this._iosGesture.end(),this._flushHoldIfReady(!0),this._writeAwaitingCommand())}),this.scrollState={...r,startedAt:this.now(),lastTargetOffset:t,stableFrames:0,phase:"queued",behavior:e,samples:[]},this.scheduleScrollReconcile();return}this._iosGesture.end()}this.scrollState={...r,startedAt:this.now(),lastTargetOffset:t,stableFrames:0,behavior:e,phase:$(e,r.index!=null&&w(t,this.getScrollOffset()))},this._scrollToOffset(t,{behavior:e}),this.scheduleScrollReconcile()}}const Q=(o,r,t,e)=>{for(;o<=r;){const s=(o+r)/2|0,i=t(s);if(ie)r=s-1;else return s}return o>0?o-1:0};function St(o,r,t){let e=0;for(;e<=r;){const s=(e+r)/2|0,i=o[s*2];if(it)r=s-1;else return s}return e>0?e-1:0}function _t(o,r,t,e,s){const i=o.length-1;if(o.length<=e)return{startIndex:0,endIndex:i};if(e===1&&s!==null){const a=St(s,i,t);let c=a;const u=t+r;for(;co[a].start,t),h=n;if(e===1)for(;h1){const a=Array(e).fill(0);for(;hu=0&&c.some(u=>u>=t);){const u=o[n];c[u.lane]=u.start,n--}n=Math.max(0,n-n%e),h=Math.min(i,h+(e-1-h%e))}return{startIndex:n,endIndex:h}}export{pt as Virtualizer,ot as _resetIOSDetectionForTests,w as approxEqual,K as debounce,N as defaultKeyExtractor,G as defaultRangeExtractor,mt as elementScroll,X as measureElement,E as memo,B as notUndefined,ut as observeElementOffset,ht as observeElementRect,ft as observeWindowOffset,at as observeWindowRect,dt as windowScroll}; -//# sourceMappingURL=/sm/231f777dce3a9f082c79e6b4946a7c321e34de4007f0ca665c2efe1c101895bc.map \ No newline at end of file +function J(l){return typeof l=="object"?l.key:l}function Z(l,r){const t=l.length;return new Proxy(l,{get(e,s,i){if(typeof s=="string"){const o=s.charCodeAt(0);if(o>=48&&o<=57){const n=+s;if(Number.isInteger(n)&&n>=0&&ne[c]!==a))&&(e=n,s=r(...n),t?.onChange&&!(i&&t.skipInitialOnChange)&&t.onChange(s),i=!1),s}return o.updateDeps=n=>{e=n},o}function B(l,r){if(l===void 0)throw new Error(`Unexpected undefined${r?`: ${r}`:""}`);return l}const w=(l,r)=>Math.abs(l-r)<1.01,K=(l,r,t)=>{let e;return Object.assign(function(...s){l.clearTimeout(e),e=l.setTimeout(()=>r.apply(this,s),t)},{cancel:()=>{l.clearTimeout(e)}})};function tt({offset:l,isScrolling:r,elementMax:t,holdOffset:e,scrollOffset:s,pendingWrite:i,clampedMaxAtWrite:o}){var n;return r&&i===null&&l===s?"spurious":i!==null&&Math.abs(l-i.target)<1.5?"readBack":i!==null&&!i.smooth&&t!==null&&i.target>t+e+.5&&Math.abs(l-(t+e))<1.5||i!==null&&!i.smooth&&((n=i.lowMaxes)!=null&&n.some(h=>Math.abs(l-h)<1.5))?"clampedReadBack":o!==null&&Math.abs(l-o)<1.5?"clampedEcho":"external"}const et=l=>l==="smooth"||l==="smoothFinishing";function st({kind:l,offset:r,command:t,target:e}){return l!=="spurious"&&l!=="readBack"&&l!=="clampedReadBack"&&t?.index!=null&&t.phase!=="queued"&&!et(t.phase)&&!w(r,t.lastTargetOffset)&&!w(r,e())&&l!=="clampedEcho"}const D=40;function it(l){const r=l[l.length-1];if(r===void 0)return!1;let t=l.length-2;for(;t>=0&&r.t-l[t].t=0&&e.t-l[s].t=0&&Math.abs(o)>=2*Math.abs(n)&&Math.abs(o)-Math.abs(n)>=.8}function nt(l){const r=l[l.length-1];if(r===void 0)return!1;let t=l.length-2;for(;t>=0&&r.t-l[t].t=.05}let I;const L=()=>{if(I!==void 0)return I;if(typeof navigator>"u")return I=!1;if(/iP(hone|od|ad)/.test(navigator.userAgent))return I=!0;const l=navigator.maxTouchPoints;return I=navigator.platform==="MacIntel"&&l!==void 0&&l>0};let k;const lt=()=>k!==void 0?k:k=L()&&typeof CSS<"u"&&typeof CSS.supports=="function"&&CSS.supports("overflow-anchor: auto"),ot=()=>!L()||lt(),rt=()=>{I=void 0,k=void 0},A={capture:!0,passive:!0};class ht{constructor(r){this.host=r,this._touch=null,this.inMotion=()=>this._touch!==null&&this._touch.phase!=="lateFling",this.dragging=()=>{var t;return((t=this._touch)==null?void 0:t.phase)==="down"&&this._touch.moved},this.tail=()=>{var t;return((t=this._touch)==null?void 0:t.phase)==="tail"?this._touch.moved?"fling":"tap":null},this.onScroll=({isScrolling:t,ownReadBack:e,moved:s,external:i})=>{var o,n,h;return t&&((o=this._touch)==null?void 0:o.phase)==="tail"?this._openTail(this._touch.moved||!e,!1):((n=this._touch)==null?void 0:n.phase)==="lateFling"&&(this.host.now()>this._touch.until?this._touch=null:i&&t&&s&&this._openTail(!0,!1)),!e&&t&&s&&((h=this._touch)==null?void 0:h.phase)==="down"?(this._touch.moved=!0,!0):!1},this.touchStart=()=>{this.end(),this._touch={phase:"down",moved:!1}},this.touchEnd=()=>{var t;const e=((t=this._touch)==null?void 0:t.phase)==="down"&&this._touch.moved;this.end(),this._openTail(e,e)},this.end=()=>{var t;const e=this.host.getWindow();((t=this._touch)==null?void 0:t.phase)==="tail"&&e!=null&&e.clearTimeout(this._touch.timerId),this._touch=null},this.listen=t=>{const e=()=>this.touchStart(),s=()=>this.touchEnd();return t.addEventListener("touchstart",e,A),t.addEventListener("touchend",s,A),t.addEventListener("touchcancel",s,A),()=>{t.removeEventListener("touchstart",e,A),t.removeEventListener("touchend",s,A),t.removeEventListener("touchcancel",s,A),this.end()}},this._openTail=(t,e)=>{const s=this.host.getWindow();if(s==null)return;this.end();const i={phase:"tail",moved:t,awaitingFling:e,timerId:s.setTimeout(()=>{if(this.host.tailStillMoving()){this._openTail(i.moved,i.awaitingFling);return}this._touch=i.awaitingFling?{phase:"lateFling",until:this.host.now()+1e3}:null,this.host.onTailClosed()},150)};this._touch=i}}get touch(){return this._touch}}const H=l=>{const{clientWidth:r,clientHeight:t}=l;return{width:r,height:t}},q=l=>{const r=new Set;return{schedule:t=>{const e=l.requestAnimationFrame(()=>{r.delete(e),t()});r.add(e)},cancel:()=>{r.forEach(t=>l.cancelAnimationFrame(t)),r.clear()}}},N=l=>l,G=l=>{const r=l.overscan*Math.max(l.lanes??1,1),t=Math.max(l.startIndex-r,0),s=Math.min(l.endIndex+r,l.count-1)-t+1,i=new Array(s);for(let o=0;o{const t=l.scrollElement;if(!t)return;const e=l.targetWindow;if(!e)return;const s=n=>{const{width:h,height:a}=n;r({width:Math.round(h),height:Math.round(a)})};if(s(H(t)),!e.ResizeObserver)return()=>{};const i=q(e),o=new e.ResizeObserver(()=>{const n=()=>{s(H(t))};l.options.useAnimationFrameWithResizeObserver?i.schedule(n):n()});return o.observe(t,{box:"content-box"}),()=>{o.unobserve(t),i.cancel()}},P={passive:!0},ct=(l,r)=>{const t=l.scrollElement;if(!t)return;const e=()=>{r({width:t.innerWidth,height:t.innerHeight})};return e(),t.addEventListener("resize",e,P),()=>{t.removeEventListener("resize",e)}},ut=typeof window>"u"?!0:"onscrollend"in window,U=(l,r,t)=>{const e=l.scrollElement;if(!e)return;const s=l.targetWindow;if(!s)return;const i=l.options.useScrollendEvent&&ut;let o=0;const n=i?null:K(s,()=>r(t(e),!1),l.options.isScrollingResetDelay),h=u=>()=>{o=t(e),n?.(),r(o,u)},a=h(!0),c=h(!1);return e.addEventListener("scroll",a,P),i&&e.addEventListener("scrollend",c,P),()=>{e.removeEventListener("scroll",a),i&&e.removeEventListener("scrollend",c),n?.cancel()}},ft=(l,r)=>U(l,r,t=>{const{horizontal:e,isRtl:s}=l.options;return e?t.scrollLeft*(s&&-1||1):t.scrollTop}),dt=(l,r)=>U(l,r,t=>l.options.horizontal?t.scrollX:t.scrollY),X=(l,r,t)=>{if(t.options.useCachedMeasurements){const e=t.indexFromElement(l),s=t.options.getItemKey(e);return t.itemSizeCache.get(s)??t.options.estimateSize(e)}if(r?.borderBoxSize){const e=r.borderBoxSize[0];if(e)return Math.round(e[t.options.horizontal?"inlineSize":"blockSize"])}if(!r){const e=t.indexFromElement(l),s=t.options.getItemKey(e),i=t.itemSizeCache.get(s);if(i!==void 0)return i}return l[t.options.horizontal?"offsetWidth":"offsetHeight"]},Y=(l,{adjustments:r=0,behavior:t},e)=>{var s;const{scrollElement:i,options:o}=e,n=l+r,h=o.horizontal&&o.isRtl&&i!==null&&"ownerDocument"in i;(s=i?.scrollTo)==null||s.call(i,{[o.horizontal?"left":"top"]:h?-n:n,behavior:t})},mt=Y,gt=Y,$=(l,r)=>l==="smooth"?r?"smoothLanded":"smooth":"instant",W=l=>l?.phase==="smooth"||l?.phase==="smoothLanded";function pt(l,r,t,e){if(r===0)return!1;const s=e(0),i=new Set;let o=0;for(;o=r)return!1;for(let h=0;hthis.targetWindow,now:()=>this.now(),tailStillMoving:()=>{const t=this.scrollState;if(t?.phase!=="queued"||this.pendingWrite!==null)return!1;const e=this._readElementOffset();return e===null||this._lastElementOffset===null||Math.abs(e-this._lastElementOffset)<1?!1:(this._lastElementOffset=e,t.samples=[...t.samples??[],{t:this.now(),offset:e}],!0)},onTailClosed:()=>{const t=this.scrollState;t?.phase==="queued"&&nt(t.samples??[])&&(this.scrollState=null),this._flushHoldIfReady(!0),this._writeAwaitingCommand(),this._retryClampedAdjustment()}}),this._lastElementOffset=null,this.pendingWrite=null,this._clampedAdjustment=null,this.elementsCache=new Map,this._resizeBatch=null,this.now=()=>{var t,e,s;return((s=(e=(t=this.targetWindow)==null?void 0:t.performance)==null?void 0:e.now)==null?void 0:s.call(e))??Date.now()},this.observer=(()=>{let t=null,e=null,s=!1,i=!1;const o=new Set,n=()=>{if(t)return t;const a=this.targetWindow;if(!a||!a.ResizeObserver)return null;const c=e=q(a);return t=new a.ResizeObserver(u=>{const g=()=>{const d={sync:!1};this._resizeBatch=d;try{u.forEach(h)}finally{this._resizeBatch=null,d.notify&&(this.notify(d.sync),this._noteClampMax(),this._flushHoldIfReady(!0),this._retryClampedAdjustment(),this.measureElement(null))}};if(this.options.useAnimationFrameWithResizeObserver)c.schedule(g);else{s=!0;try{g()}finally{s=!1}}})},h=a=>{const c=a.target,u=this.indexFromElement(c);if(!c.isConnected){this.observer.unobserve(c);for(const[g,d]of this.elementsCache)if(d===c){this.elementsCache.delete(g);break}return}this.isIndexInRange(u)&&this.shouldMeasureDuringScroll(u)&&this.resizeItem(u,this.options.measureElement(c,a,this))};return{disconnect:()=>{var a;(a=n())==null||a.disconnect(),e?.cancel(),o.clear(),i=!1,t=null,e=null},observe:a=>{const c=n();if(c){if(!s){c.observe(a,{box:"border-box"});return}o.add(a),i||(i=!0,e?.schedule(()=>{i=!1,o.forEach(u=>{u.isConnected&&t?.observe(u,{box:"border-box"})}),o.clear()}))}},unobserve:a=>{var c;o.delete(a),(c=n())==null||c.unobserve(a)}}})(),this.range=null,this.setOptions=t=>{var e;const s={debug:!1,initialOffset:0,overscan:1,paddingStart:0,paddingEnd:0,scrollPaddingStart:0,scrollPaddingEnd:0,horizontal:!1,getItemKey:N,rangeExtractor:G,onChange:()=>{},measureElement:X,initialRect:{width:0,height:0},scrollMargin:0,gap:0,indexAttribute:"data-index",initialMeasurementsCache:[],lanes:1,anchorTo:"start",followOnAppend:!1,scrollEndThreshold:1,isScrollingResetDelay:150,enabled:!0,isRtl:!1,useScrollendEvent:!1,useAnimationFrameWithResizeObserver:!1,laneAssignmentMode:"estimate",useCachedMeasurements:!1};for(const c in t){const u=t[c];u!==void 0&&(s[c]=u)}const i=this.options;let o=null,n=null,h=!1;if(i!==void 0&&i.enabled&&s.enabled&&s.anchorTo==="end"&&this.scrollElement!==null){const c=i.count,u=s.count,g=this.getMeasurements(),d=((e=this._singleLaneMeasurements)==null?void 0:e.items)??g,S=f=>J(d[f]),p=c>0?S(0):null,y=c>0?S(c-1):null;if(u!==c||c>0&&u>0&&(s.getItemKey(0)!==p||s.getItemKey(u-1)!==y)){h=!0;const f=c>0?this.getVirtualItemForOffset(this.getScrollOffset())??g[0]:null;f&&(o=[f.key,this.getScrollOffset()-f.start]);const M=s.followOnAppend===!0?"auto":s.followOnAppend||null;M&&u>0&&this.isAtEnd(i.scrollEndThreshold)&&(c===0||s.getItemKey(u-1)!==y)&&(u>c||pt(c,u,S,s.getItemKey))&&(n=M)}}this.options=s,h&&(this.pendingMin=0,this.itemSizeCacheVersion++);let a=null;if(o&&this.scrollOffset!==null&&!this._awaitingCommandReadBack()){const[c,u]=o,g=this.getMeasurements(),{count:d,getItemKey:S}=this.options;let p=0;for(;p{var e,s;(s=(e=this.options).onChange)==null||s.call(e,this,t)},this.maybeNotify=E(()=>(this.calculateRange(),[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]),t=>{this.notify(t)},{key:!1,debug:()=>this.options.debug,initialDeps:[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]}),this.cleanup=()=>{var t;this.unsubs.filter(Boolean).forEach(e=>e()),this.unsubs=[],this.observer.disconnect(),this.rafId!=null&&this.targetWindow&&(this.targetWindow.cancelAnimationFrame(this.rafId),this.rafId=null),this.scrollState=null,(t=this._momentumStop)==null||t.restore(),this._momentumStop=null,this.isScrolling=!1,this.scrollDirection=null,this._holdOffset=0,this._reportedOverscroll=!1,this._clampedAdjustment=null,this.pendingWrite=null,this.scrollElement=null,this.targetWindow=null},this._didMount=()=>()=>{this.cleanup()},this._willUpdate=()=>{var t;const e=this.options.enabled?this.options.getScrollElement():null;if(this.scrollElement!==e){if(this.cleanup(),!e){this.maybeNotify();return}this.scrollElement=e,this.scrollElement&&"ownerDocument"in this.scrollElement?this.targetWindow=this.scrollElement.ownerDocument.defaultView:this.targetWindow=((t=this.scrollElement)==null?void 0:t.window)??null,this.elementsCache.forEach(i=>{this.observer.observe(i)}),this.unsubs.push(this.options.observeElementRect(this,i=>{this.scrollRect=i,this.maybeNotify()})),this.unsubs.push(this.options.observeElementOffset(this,(i,o)=>{var n,h,a,c;const u=o?this._elementMaxScrollOffset():null,g=this._reportedOverscroll;this._reportedOverscroll=i<0||u!==null&&i>u+1;const d=i;this._lastElementOffset=d,i=this._toLayoutOffset(i);const S=this._classifyScrollEvent(i,o,u);if(S==="spurious")return;S==="readBack"?i=this.pendingWrite.target:S==="clampedReadBack"&&((n=this.scrollState)==null?void 0:n.index)!=null&&ithis.getScrollStateTarget(this.scrollState)})&&(this.scrollState=null),this.pendingWrite=null,this._clampedAdjustment!==null&&Math.abs(i-this._clampedAdjustment.maxAtWrite)>=1.5&&(this._clampedAdjustment=null);const p=this.getScrollOffset(),y=this.scrollDirection;this.scrollDirection=o?p===i?this.scrollDirection:pm-f.t<=200),{t:m,offset:d}],(y!==null&&this.scrollDirection!==y||it(_.samples))&&(this.scrollState=null)}this._iosGesture.onScroll({isScrolling:o,ownReadBack:S==="readBack",moved:i!==p,external:S==="external"&&((h=this.scrollState)==null?void 0:h.phase)!=="smooth"&&((a=this.scrollState)==null?void 0:a.phase)!=="smoothFinishing"})&&((c=this.scrollState)==null?void 0:c.phase)==="queued"&&(this.scrollState=null),this._flushHoldIfReady(!0),this.scrollState&&this.scheduleScrollReconcile(),this.maybeNotify()})),"addEventListener"in this.scrollElement&&this.unsubs.push(this._iosGesture.listen(this.scrollElement)),this._scrollToOffset(this.getScrollOffset())}this._noteClampMax();const s=this.pendingScrollAnchor;this.pendingScrollAnchor=null,s&&this.scrollElement&&this.options.enabled&&(s.kind==="anchor"?this._canWriteScroll()?W(this.scrollState)&&!w(this.getScrollOffset()-s.delta,this.scrollState.lastTargetOffset)||(this._followOffsetTarget(s.delta),this._scrollToOffset(this.getScrollOffset())):(this._holdOffset+=s.delta,this._followOffsetTarget(s.delta),this.notify(!1)):this.scrollToEnd({behavior:s.behavior})),this._flushHoldIfReady(!1),this._retryClampedAdjustment()},this._classifyScrollEvent=(t,e,s)=>{var i;return tt({offset:t,isScrolling:e,elementMax:s,holdOffset:this._holdOffset,scrollOffset:this.scrollOffset,pendingWrite:this.pendingWrite,clampedMaxAtWrite:((i=this._clampedAdjustment)==null?void 0:i.maxAtWrite)??null})},this._retryClampedAdjustment=()=>{if(this._clampedAdjustment===null||!this.scrollElement||!this.options.enabled||!this._canWriteScroll())return;const{target:t,maxAtWrite:e}=this._clampedAdjustment,s=this.getMaxScrollOffset();s>e+.5&&(this._clampedAdjustment=t>s+.5?{target:t,maxAtWrite:s}:null,this._scrollToOffset(t))},this._writeAwaitingCommand=()=>{const t=this.scrollState;if(t===null||t.phase!=="queued")return;const{behavior:e}=t;this.getMeasurements();const s=this.getScrollStateTarget(t);t.lastTargetOffset=s,this._clampedAdjustment=null,t.phase=$(e,t.index!=null&&w(s,this.getScrollOffset())),this._scrollToOffset(s,{behavior:e}),this.scheduleScrollReconcile()},this._readElementOffset=()=>{const t=this.scrollElement;if(t==null)return null;const{horizontal:e,isRtl:s}=this.options;return typeof t.scrollTop=="number"?e?t.scrollLeft*(s&&-1||1):t.scrollTop:typeof t.scrollY=="number"?e?t.scrollX:t.scrollY:null},this._canWriteScroll=()=>!(L()&&this._iosGesture.inMotion())&&!this._reportedOverscroll,this._flushHoldIfReady=t=>{var e;if(this._holdOffset===0||!this.scrollElement||!this.options.enabled||!this._canWriteScroll()||((e=this.scrollState)==null?void 0:e.phase)==="smooth")return;this._holdOffset=0,this.scrollOffset!==null&&this.scrollOffset<0&&(this.scrollOffset=0),this.notify(t);const s=this.getCommandWrittenOffset()??this.getScrollOffset();this._clampedAdjustment=this._clampedBy(s),this._scrollToOffset(s)},this.rafId=null,this.getSize=()=>this.options.enabled?(this.scrollRect=this.scrollRect??this.options.initialRect,this.scrollRect[this.options.horizontal?"width":"height"]):(this.scrollRect=null,0),this.getScrollOffset=()=>this.options.enabled?(this.scrollOffset=this.scrollOffset??(typeof this.options.initialOffset=="function"?this.options.initialOffset():this.options.initialOffset),this.scrollOffset):(this.scrollOffset=null,0),this.getMeasurementOptions=E(()=>[this.options.count,this.options.paddingStart,this.options.scrollMargin,this.options.getItemKey,this.options.enabled,this.options.lanes,this.options.laneAssignmentMode,this.options.gap],(t,e,s,i,o,n,h,a)=>(this.prevLanes!==void 0&&this.prevLanes!==n&&(this.lanesChangedFlag=!0),this.prevLanes=n,this.pendingMin=null,{count:t,paddingStart:e,scrollMargin:s,getItemKey:i,enabled:o,lanes:n,laneAssignmentMode:h,gap:a}),{key:!1}),this.isIndexInRange=t=>t>=0&&t[this.getMeasurementOptions(),this.itemSizeCacheVersion],({count:t,paddingStart:e,scrollMargin:s,getItemKey:i,enabled:o,lanes:n,laneAssignmentMode:h,gap:a},c)=>{var u;const g=this.itemSizeCache;if(!o)return this.measurementsCache=[],this._singleLaneMeasurements=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),[];if(this.laneAssignments.size>t)for(const m of this.laneAssignments.keys())m>=t&&this.laneAssignments.delete(m);this.lanesChangedFlag&&(this.lanesChangedFlag=!1,this.lanesSettling=!0,this.measurementsCache=[],this._singleLaneMeasurements=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),this.pendingMin=null),this.measurementsCache.length===0&&!this.lanesSettling&&(this.measurementsCache=this.options.initialMeasurementsCache,this.measurementsCache.forEach(m=>{this.itemSizeCache.set(m.key,m.size)}));const d=this.lanesSettling?0:this.pendingMin??0;if(this.pendingMin=null,this.lanesSettling&&this.measurementsCache.length===t&&(this.lanesSettling=!1),n===1){const m=t*2;let f=(u=this._singleLaneMeasurements)==null?void 0:u.flat;if(!f||f.length0&&v.set(f.subarray(0,d*2)),f=v}const M=d===0?new Array(t):this._singleLaneMeasurements.items.slice();let O;if(d===0)O=e+s;else{const v=d-1;O=f[v*2]+f[v*2+1]+a}for(let v=d;v1){O=M;const F=p[O],x=F!==void 0?S[F]:void 0;b=x?x.end+a:e+s}else if(_===n){let F=0,x=y[0],V=p[0];for(let C=1;Cthis.options.debug}),this.calculateRange=E(()=>[this.getMeasurements(),this.getSize(),this.getScrollOffset(),this.options.lanes,this._getLayoutTotalSize()+this.options.scrollMargin],(t,e,s,i,o)=>{if(t.length===0||e===0)return this.range=null,null;const n=Math.max(o-e,0);return this.range=vt(t,e,Math.min(s,n),i,i===1&&this._singleLaneMeasurements!==null?this._singleLaneMeasurements.flat:null),this.range},{key:!1,debug:()=>this.options.debug}),this.getVirtualIndexes=E(()=>{let t=null,e=null;const s=this.calculateRange();return s&&(t=s.startIndex,e=s.endIndex),this.maybeNotify.updateDeps([this.isScrolling,t,e]),[this.options.rangeExtractor,this.options.overscan,this.options.count,t,e,this.options.lanes]},(t,e,s,i,o,n)=>i===null||o===null?[]:t({startIndex:i,endIndex:o,overscan:e,count:s,lanes:n}),{key:!1,debug:()=>this.options.debug}),this.indexFromElement=t=>{const e=this.options.indexAttribute,s=t.getAttribute(e);return s?parseInt(s,10):(console.warn(`Missing attribute name '${e}={index}' on measured element.`),-1)},this.shouldMeasureDuringScroll=t=>{var e;if(!W(this.scrollState))return!0;const s=this.scrollState.index??((e=this.getVirtualItemForOffset(this.scrollState.lastTargetOffset))==null?void 0:e.index);if(s!==void 0&&this.range){const i=Math.max(this.options.overscan,Math.ceil((this.range.endIndex-this.range.startIndex)/2)),o=Math.max(0,s-i),n=Math.min(this.options.count-1,s+i);return t>=o&&t<=n}return!0},this.measureElement=t=>{if(!t){this.elementsCache.forEach((o,n)=>{o.isConnected||(this.observer.unobserve(o),this.elementsCache.delete(n))});return}const e=this.indexFromElement(t);if(!this.isIndexInRange(e))return;const s=this.options.getItemKey(e),i=this.elementsCache.get(s);i!==t&&(i&&this.observer.unobserve(i),this.observer.observe(t),this.elementsCache.set(s,t)),t.isConnected&&(!this.isScrolling||this.scrollState)&&this.shouldMeasureDuringScroll(e)&&this.resizeItem(e,this.options.measureElement(t,void 0,this))},this.resizeItem=(t,e)=>{var s,i,o;if(!this.isIndexInRange(t))return;(s=this._resizeBatch)!=null&&s.sync&&this.getMeasurements();let n,h,a;const c=(i=this._singleLaneMeasurements)==null?void 0:i.flat;if(this.options.lanes===1&&c!=null)a=this.options.getItemKey(t),h=c[t*2],n=c[t*2+1];else{const d=this.measurementsCache[t];if(!d)return;a=d.key,h=d.start,n=d.size}const u=this.itemSizeCache.get(a)??n,g=e-u;if(g!==0){const d=this.options.anchorTo==="end"&&!W(this.scrollState)&&this.getVirtualDistanceFromEnd()<=this.options.scrollEndThreshold,S=d?this._getLayoutTotalSize():0,p=this.getScrollOffset(),_=!this.itemSizeCache.has(a)?h[this.getVirtualIndexes(),this.getMeasurements(),this._holdOffset],(t,e,s)=>{const i=[];for(let o=0,n=t.length;othis.options.debug}),this.getVirtualItemForOffset=t=>{var e;const s=this.getMeasurements();if(s.length===0)return;const i=(e=this._singleLaneMeasurements)==null?void 0:e.flat,o=this.options.lanes===1&&i!=null,n=Q(0,s.length-1,o?h=>i[h*2]:h=>B(s[h]).start,t);return B(s[n])},this._noteClampMax=()=>{if(!this._awaitingCommandReadBack())return;const t=this._elementMaxScrollOffset();if(t===null)return;const e=this.pendingWrite,s=Math.max(t,0)+this._holdOffset;if(s>=e.target)return;const i=e.lowMaxes??(e.lowMaxes=[]);i[i.length-1]!==s&&i.push(s)},this.getMaxScrollOffset=()=>(this._elementMaxScrollOffset()??0)+this._holdOffset,this._toLayoutOffset=t=>{var e;let s=t+this._holdOffset;return((e=this.pendingScrollAnchor)==null?void 0:e.kind)==="anchor"&&(s+=this.pendingScrollAnchor.delta),s},this._elementMaxScrollOffset=()=>{const t=this.scrollElement;if(t===null)return null;if("scrollHeight"in t)return this.options.horizontal?t.scrollWidth-t.clientWidth:t.scrollHeight-t.clientHeight;if("document"in t){const e=t.document.documentElement;return this.options.horizontal?e.scrollWidth-t.innerWidth:e.scrollHeight-t.innerHeight}return null},this._momentumStop=null,this._stopMomentum=t=>{const e=this.scrollElement,s=this.targetWindow;if(e===null||s===null)return;const i="document"in e?e.document.scrollingElement:e;if(!i?.style)return;if(this._momentumStop){this._momentumStop.done=t;return}const{style:o}=i,n=this.options.horizontal?"overflow-x":"overflow-y",h=o.getPropertyValue(n),a=o.getPropertyPriority(n),c=()=>this.options.horizontal?i.clientHeight:i.clientWidth,u=c();if(o.setProperty(n,"hidden","important"),c()!==u){o.setProperty(n,h,a);return}const g={restore:()=>o.getPropertyValue(n)!=="hidden"||o.getPropertyPriority(n)!=="important"?!1:(o.setProperty(n,h,a),!0),done:t};this._momentumStop=g,s.requestAnimationFrame(()=>s.requestAnimationFrame(()=>{this._momentumStop===g&&(this._momentumStop=null,g.restore()&&g.done())}))},this._clampedBy=t=>{const e=this._elementMaxScrollOffset();if(e===null)return null;const s=e+this._holdOffset;return t>s+.5?{target:t,maxAtWrite:s}:null},this.getVirtualDistanceFromEnd=()=>Math.max(this._getLayoutTotalSize()+this.options.scrollMargin-this.getSize()-this.getScrollOffset(),0),this.getDistanceFromEnd=()=>Math.max(this.getMaxScrollOffset()-this.getScrollOffset(),0),this.isAtEnd=(t=this.options.scrollEndThreshold)=>this.getDistanceFromEnd()<=t,this.getOffsetForAlignment=(t,e,s=0)=>{if(!this.scrollElement)return 0;const i=this.getSize(),o=this.getScrollOffset();e==="auto"&&(e=t>=o+i?"end":"start"),e==="center"?t+=(s-i)/2:e==="end"&&(t-=i);const n=this.getMaxScrollOffset();return Math.max(Math.min(n,t),0)},this.getOffsetForIndex=(t,e="auto")=>{t=Math.max(0,Math.min(t,this.options.count-1));const s=this.getSize(),i=this.getScrollOffset(),o=this.getMeasurements()[t];if(!o)return;if(e==="auto")if(o.end>=i+s-this.options.scrollPaddingEnd)e="end";else if(o.start<=i+this.options.scrollPaddingStart)e="start";else return[i,e];if(e==="end"&&t===this.options.count-1){const h=this.getMaxScrollOffset(),a=h-this.options.paddingEnd+this.options.scrollPaddingEnd;return[Math.max(Math.min(a,h),0),e]}const n=e==="end"?o.end+this.options.scrollPaddingEnd:o.start-this.options.scrollPaddingStart;return[this.getOffsetForAlignment(n,e,o.size),e]},this.scrollToOffset=(t,{align:e="start",behavior:s="auto"}={})=>{this._startScroll({index:null,align:e},this.getOffsetForAlignment(t,e),s,"absolute")},this.scrollToIndex=(t,{align:e="auto",behavior:s="auto"}={})=>{t=Math.max(0,Math.min(t,this.options.count-1));const i=this.getOffsetForIndex(t,e);if(!i)return;const[o,n]=i;this._startScroll({index:t,align:n},o,s,"absolute")},this.scrollBy=(t,{behavior:e="auto"}={})=>{const s=this.getOffsetForAlignment((this.getCommandWrittenOffset()??this.getScrollOffset())+t,"start");this._startScroll({index:null,align:"start"},s,e,"relative")},this.scrollToEnd=({behavior:t="auto"}={})=>{if(this.options.count>0){this._startScroll({index:this.options.count-1,align:"end",toEnd:!0},Math.max(this.getMaxScrollOffset(),0),t,"absolute");return}this.scrollToOffset(Math.max(this.getTotalSize()-this.getSize(),0),{behavior:t})},this.cancelScroll=()=>{this.rafId!=null&&this.targetWindow&&(this.targetWindow.cancelAnimationFrame(this.rafId),this.rafId=null),this.scrollState=null},this.getTotalSize=()=>Math.max(this._getLayoutTotalSize()-this._holdOffset,0),this._getLayoutTotalSize=()=>{var t,e;const s=this.getMeasurements();let i;if(s.length===0)i=this.options.paddingStart;else if(this.options.lanes===1){const o=s.length-1,n=(t=this._singleLaneMeasurements)==null?void 0:t.flat;n!=null?i=n[o*2]+n[o*2+1]:i=((e=s[o])==null?void 0:e.end)??0}else{const o=Array(this.options.lanes).fill(null);let n=s.length-1;for(;n>=0&&o.some(h=>h===null);){const h=s[n];o[h.lane]===null&&(o[h.lane]=h.end),n--}i=Math.max(...o.filter(h=>h!==null))}return Math.max(i-this.options.scrollMargin+this.options.paddingEnd,0)},this.takeSnapshot=()=>{const t=[];if(this.itemSizeCache.size===0)return t;const e=this.getMeasurements();for(const s of e)s&&this.itemSizeCache.has(s.key)&&t.push({index:s.index,key:s.key,start:s.start,size:s.size,end:s.end,lane:s.lane});return t},this._scrollToOffset=(t,{adjustments:e,behavior:s}={})=>{const i={target:t+(e??0),smooth:s==="smooth"};return this.pendingWrite=i,this.options.scrollToFn(t-this._holdOffset,{behavior:s,adjustments:e},this),i},this.getCommandWrittenOffset=()=>this.scrollState!==null&&this.pendingWrite!==null&&!this.pendingWrite.smooth?this.pendingWrite.target:null,this._awaitingCommandReadBack=()=>{const t=this.getCommandWrittenOffset();return!(L()&&this._iosGesture.dragging())&&t!==null&&!w(t,this.getScrollOffset())},this._followOffsetTarget=t=>{this.scrollState!==null&&this.scrollState.index==null&&(this.scrollState.lastTargetOffset+=t)},this.measure=()=>{this.pendingMin=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),this.itemSizeCacheVersion++,this.notify(!1)},this.setOptions(r)}get scrollAdjustments(){return 0}applyScrollAdjustment(r,t){if(r===0)return!1;if(this._followOffsetTarget(r),this._canWriteScroll()){const e=this.getScrollOffset();this._clampedAdjustment=this._clampedBy(e+r);const s=this._scrollToOffset(e,{adjustments:r,behavior:t});return this.scrollOffset!==null&&(this.pendingWrite===s&&(this.scrollOffset+=r),this.scrollOffset<0&&(this.scrollOffset=0)),!0}else return this._holdOffset+=r,this.scrollOffset!==null&&(this.scrollOffset+=r),!0}scheduleScrollReconcile(){if(!this.targetWindow){this.scrollState=null;return}this.rafId==null&&(this.rafId=this.targetWindow.requestAnimationFrame(()=>{this.rafId=null,this.reconcileScroll()}))}getScrollStateTarget(r){const t=r.toEnd?[Math.max(this.getMaxScrollOffset(),0)]:r.index!=null?this.getOffsetForIndex(r.index,r.align):void 0;return t?t[0]:r.lastTargetOffset}reconcileScroll(){if(!this.scrollState||!this.scrollElement)return;if(this.now()-this.scrollState.startedAt>5e3){this.scrollState=null;return}if(this.scrollState.phase==="queued"){this.scheduleScrollReconcile();return}this.getMeasurements();const e=this.getScrollStateTarget(this.scrollState),s=this.scrollState.index==null?1:this.options.useAnimationFrameWithResizeObserver?3:2,i=e!==this.scrollState.lastTargetOffset;if(!i&&w(e,this.getScrollOffset())){if(this.scrollState.phase==="smooth"?this.scrollState.phase="smoothLanded":this.scrollState.phase==="smoothFinishing"&&(this.scrollState.phase="instant"),this.scrollState.stableFrames++,this.scrollState.stableFrames>=s){this.getScrollOffset()!==e&&this._scrollToOffset(e,{behavior:"auto"}),this.scrollState=null;return}}else if(this.scrollState.stableFrames=0,i){const o=this.getSize()||600,n=Math.abs(e-this.getScrollOffset()),h=W(this.scrollState)&&n>o;this.scrollState.lastTargetOffset=e,h?this.scrollState.phase="smooth":this.scrollState.phase==="smooth"?this.scrollState.phase="smoothFinishing":this.scrollState.phase==="smoothLanded"&&(this.scrollState.phase="instant"),this._scrollToOffset(e,{behavior:h?"smooth":"auto"})}this.scheduleScrollReconcile()}_startScroll(r,t,e,s){this.pendingScrollAnchor=null,this._clampedAdjustment=null;const i=this._iosGesture.tail();if(s==="absolute"&&i!==null){if(i==="fling"&&ot()){this._stopMomentum(()=>{const o=this.scrollState;o?.phase!=="queued"||this._iosGesture.tail()!=="fling"||this._reportedOverscroll||(this._iosGesture.end(),this._flushHoldIfReady(!0),this._writeAwaitingCommand())}),this.scrollState={...r,startedAt:this.now(),lastTargetOffset:t,stableFrames:0,phase:"queued",behavior:e,samples:[]},this.scheduleScrollReconcile();return}this._iosGesture.end()}this.scrollState={...r,startedAt:this.now(),lastTargetOffset:t,stableFrames:0,behavior:e,phase:$(e,r.index!=null&&w(t,this.getScrollOffset()))},this._scrollToOffset(t,{behavior:e}),this.scheduleScrollReconcile()}}const Q=(l,r,t,e)=>{for(;l<=r;){const s=(l+r)/2|0,i=t(s);if(ie)r=s-1;else return s}return l>0?l-1:0};function _t(l,r,t){let e=0;for(;e<=r;){const s=(e+r)/2|0,i=l[s*2];if(it)r=s-1;else return s}return e>0?e-1:0}function vt(l,r,t,e,s){const i=l.length-1;if(l.length<=e)return{startIndex:0,endIndex:i};if(e===1&&s!==null){const a=_t(s,i,t);let c=a;const u=t+r;for(;cl[a].start,t),h=n;if(e===1)for(;h1){const a=Array(e).fill(0);for(;hu=0&&c.some(u=>u>=t);){const u=l[n];c[u.lane]=u.start,n--}n=Math.max(0,n-n%e),h=Math.min(i,h+(e-1-h%e))}return{startIndex:n,endIndex:h}}export{St as Virtualizer,rt as _resetIOSDetectionForTests,w as approxEqual,K as debounce,N as defaultKeyExtractor,G as defaultRangeExtractor,gt as elementScroll,X as measureElement,E as memo,B as notUndefined,ft as observeElementOffset,at as observeElementRect,dt as observeWindowOffset,ct as observeWindowRect,mt as windowScroll}; +//# sourceMappingURL=/sm/cf5dc331d520c9ee44c63cde1674cf87be1c76ee49e877760ef0d17968d2d80e.map \ No newline at end of file diff --git a/tests/Blazor.TanStack.VirtualCore.E2E/InteropModuleTests.cs b/tests/Blazor.TanStack.VirtualCore.E2E/InteropModuleTests.cs index 4f98a8d..474cc97 100644 --- a/tests/Blazor.TanStack.VirtualCore.E2E/InteropModuleTests.cs +++ b/tests/Blazor.TanStack.VirtualCore.E2E/InteropModuleTests.cs @@ -675,13 +675,14 @@ public async Task ScrollToIndex_WhileATouchFlingIsMovingTheList_StopsTheMomentum list.viewport.scrollTop = 600; await nextFrame(); - const command = list.instance.scrollToIndex(100, 'start', 'auto'); + list.instance.scrollToIndex(100, 'start', 'auto'); const sample = { overflowWhenIssued: list.viewport.style.getPropertyValue('overflow-y'), scrollTopWhenIssued: list.viewport.scrollTop, }; - await command; - await delay(100); + // Past the ~150 ms after the last fling frame at which virtual-core writes a + // command it could not stop the fling for. + await delay(300); sample.overflowAfterwards = list.viewport.style.getPropertyValue('overflow-y'); sample.scrollTopAfterwards = list.viewport.scrollTop; list.instance.dispose(); @@ -707,11 +708,13 @@ public async Task ScrollToIndex_WhileATouchFlingIsMovingTheList_StopsTheMomentum Assert.Equal(5000, wheel.ScrollTopWhenIssued); // A scrollbar that takes layout space would vanish for two frames and reflow the list (a - // touchscreen laptop), so the overflow is put back before anything paints and the write - // goes out at once. + // touchscreen laptop), so the overflow is put back before anything paints. The fling is not + // stopped, so virtual-core holds the write until the fling has ended. var classicScrollbar = runs[2]; Assert.Equal("auto", classicScrollbar.OverflowWhenIssued); - Assert.Equal(5000, classicScrollbar.ScrollTopWhenIssued); + Assert.Equal(600, classicScrollbar.ScrollTopWhenIssued); + Assert.Equal("auto", classicScrollbar.OverflowAfterwards); + Assert.Equal(5000, classicScrollbar.ScrollTopAfterwards); _fixture.AssertNoJsErrors(); } diff --git a/tests/Blazor.TanStack.VirtualCore.E2E/OptionCoverageTests.cs b/tests/Blazor.TanStack.VirtualCore.E2E/OptionCoverageTests.cs index d717406..2aae845 100644 --- a/tests/Blazor.TanStack.VirtualCore.E2E/OptionCoverageTests.cs +++ b/tests/Blazor.TanStack.VirtualCore.E2E/OptionCoverageTests.cs @@ -351,8 +351,9 @@ public async Task IsRtl_ScrollToIndex_BringsTheItemOnScreen() var rail = await ReadRtlRailAsync(targetIndex: 500); // Assert - // virtual-core reads the offset negated under isRtl but writes it as is, and a positive - // scrollLeft in a right-to-left container clamps to the start - the jump would go nowhere. + // A positive scrollLeft in a right-to-left container clamps to the start, so the write has to + // be negated exactly once - virtual-core does it since 3.19.11, and negating it here as well + // would send the jump back to the start. Assert.True(rail.TargetVisible, $"Expected item 500 on screen; the rendered window is {rail.FirstIndex}-{rail.LastIndex}."); _fixture.AssertNoJsErrors(); } From db4ca33f4024e835be9cd3e059b166e393e3e092 Mon Sep 17 00:00:00 2001 From: Kebechet Date: Tue, 3 Nov 2026 20:40:39 +0100 Subject: [PATCH 2/2] Note the mid-fling command virtual-core drops on touchscreen laptops With a classic scrollbar virtual-core cannot stop a fling, so it holds a command issued during it until the fling ends, then reads the fling's last speed as a finger having caught it and drops the command. Reproduced in the real-browser harness: a fling that moves on after the command and stops at the end of the list (16 px/ms over its last 40 ms) or fading out (0.12 px/ms) both lose it. The check is meant for iOS, where a catching finger fires no touch event; the fix belongs upstream. Phones are unaffected, since their overlay scrollbars let virtual-core stop the fling. --- README.md | 2 ++ .../Blazor.TanStack.VirtualCore.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0e54c0..67fa936 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,8 @@ This is a real coupling: a virtual-core release could change them without it bei Behaviour that belongs in virtual-core is carried here only until the upstream fix ships, and listed here with the upstream PR and the test that pins it. There are none in this release: the three that 3.19.8 carried - the Android touch-fling stop (https://github.com/TanStack/virtual/pull/1347), the negated `IsRtl` scroll write (https://github.com/TanStack/virtual/pull/1291) and the measurement refresh before `getOffsetForIndex` (https://github.com/TanStack/virtual/pull/1363) - shipped in virtual-core 3.19.9 and 3.19.11. Their tests stay, and now pin virtual-core's own behaviour. +⚠️ **Known virtual-core issue, carried without a stopgap: on a touchscreen laptop, a scroll command issued mid-fling can be lost.** With a classic scrollbar, one that takes layout space, a `ScrollToIndex`, `ScrollToOffset` or `ScrollToEnd` issued while a touch fling is still moving the list is dropped when the fling moves on after it and then stops - at the end of the list, or fading out. virtual-core cannot stop that fling, because hiding the scrollbar would reflow the list, so it holds the command until the fling ends. It then reads the fling's last speed as a finger having caught the fling and drops the command - a check written for iOS, where a finger that catches a fling fires no touch event; elsewhere it does. Phones are not affected: their overlay scrollbars let virtual-core stop the fling and write the command. + ⚠️ **Remove a stopgap together with the bump to the virtual-core release that contains the fix.** Kept past it, a stopgap applies the fix a second time - the `IsRtl` negation would have sent every write back to the start. ## Vendored library provenance diff --git a/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj b/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj index cc49b5c..0aa9851 100644 --- a/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj +++ b/src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj @@ -29,7 +29,7 @@ 3.19.11 3.19.11: @tanstack/virtual-core 3.19.8 -> 3.19.11, and the three stopgaps it makes redundant are gone. -virtual-core now does itself what this package did in its place: it stops a touch fling before a ScrollToIndex, ScrollToOffset or ScrollToEnd issued during it on Android too, negates the scroll write of a horizontal IsRtl list, and computes GetOffsetForIndex - and the first write of ScrollToIndex - from the current measurements. Behaviour that changes with it: with a classic scrollbar that takes layout space (a touchscreen laptop), a command issued mid-fling now waits for the fling to end, about 150 ms after its last frame, instead of being written at once; a ScrollBy issued mid-fling is written at once without stopping the fling, because its target is relative to where the fling is; and under IsRtl in window-scroller mode the write is no longer negated, which matches how virtual-core reads the window's offset. Also from virtual-core: rows that a render inside a ResizeObserver callback mounts are observed from the next frame, and a scroll offset left past the end of a list that just shrank keeps a full rendered range instead of only the last row. +virtual-core now does itself what this package did in its place: it stops a touch fling before a ScrollToIndex, ScrollToOffset or ScrollToEnd issued during it on Android too, negates the scroll write of a horizontal IsRtl list, and computes GetOffsetForIndex - and the first write of ScrollToIndex - from the current measurements. Behaviour that changes with it: with a classic scrollbar that takes layout space (a touchscreen laptop), a command issued mid-fling now waits for the fling to end, about 150 ms after its last frame, instead of being written at once - and is dropped when the fling moves on after the command, a virtual-core issue described in the README; a ScrollBy issued mid-fling is written at once without stopping the fling, because its target is relative to where the fling is; and under IsRtl in window-scroller mode the write is no longer negated, which matches how virtual-core reads the window's offset. Also from virtual-core: rows that a render inside a ResizeObserver callback mounts are observed from the next frame, and a scroll offset left past the end of a list that just shrank keeps a full rendered range instead of only the last row. 3.19.8.4: the rows an Items change moves are no longer painted hidden, and InvalidateItemSize keeps the size a row is rendered at.