| 3 | import type { CachedBounds, SizeFunction } from "./types"; |
| 4 | |
| 5 | export function getOffsetForIndex<Props extends object>({ |
| 6 | align, |
| 7 | cachedBounds, |
| 8 | index, |
| 9 | itemCount, |
| 10 | itemSize, |
| 11 | containerScrollOffset, |
| 12 | containerSize |
| 13 | }: { |
| 14 | align: Align; |
| 15 | cachedBounds: CachedBounds; |
| 16 | index: number; |
| 17 | itemCount: number; |
| 18 | itemSize: number | SizeFunction<Props>; |
| 19 | containerScrollOffset: number; |
| 20 | containerSize: number; |
| 21 | }) { |
| 22 | if (index < 0 || index >= itemCount) { |
| 23 | throw RangeError(`Invalid index specified: ${index}`, { |
| 24 | cause: `Index ${index} is not within the range of 0 - ${itemCount - 1}` |
| 25 | }); |
| 26 | } |
| 27 | |
| 28 | // Populate the target bounds before estimating the total from cached sizes. |
| 29 | const bounds = cachedBounds.get(index); |
| 30 | const estimatedTotalSize = getEstimatedSize({ |
| 31 | cachedBounds, |
| 32 | itemCount, |
| 33 | itemSize |
| 34 | }); |
| 35 | |
| 36 | const maxOffset = Math.max( |
| 37 | 0, |
| 38 | Math.min(estimatedTotalSize - containerSize, bounds.scrollOffset) |
| 39 | ); |
| 40 | const minOffset = Math.max( |
| 41 | 0, |
| 42 | bounds.scrollOffset - containerSize + bounds.size |
| 43 | ); |
| 44 | |
| 45 | // Visibility depends on the row itself, not the estimated scroll extent. |
| 46 | // For oversized rows, leave the offset alone when the row fills the viewport. |
| 47 | const isVisible = |
| 48 | bounds.size > containerSize |
| 49 | ? containerScrollOffset >= bounds.scrollOffset && |
| 50 | containerScrollOffset <= minOffset |
| 51 | : containerScrollOffset >= minOffset && |
| 52 | containerScrollOffset <= bounds.scrollOffset; |
| 53 | |
| 54 | if (align === "smart") { |
| 55 | align = isVisible ? "auto" : "center"; |
| 56 | } |
| 57 | |
| 58 | switch (align) { |
| 59 | case "start": { |
| 60 | return maxOffset; |
| 61 | } |
| 62 | case "end": { |