@pdanpdan/virtual-scroll 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -87,6 +87,25 @@ export interface ScrollToIndexOptions {
87
87
  * @internal
88
88
  */
89
89
  isCorrection?: boolean;
90
+
91
+ /**
92
+ * If true, only calculates the target position without performing the actual scroll.
93
+ * Useful for extensions that need to validate if a snap is necessary.
94
+ * @default false
95
+ */
96
+ dryRun?: boolean;
97
+ }
98
+
99
+ /** Result of the `scrollToIndex` method. */
100
+ export interface ScrollToIndexResult {
101
+ /** Target relative horizontal position in virtual units (VU). */
102
+ targetX: number;
103
+ /** Target relative vertical position in virtual units (VU). */
104
+ targetY: number;
105
+ /** Target display horizontal position (DU). */
106
+ displayTargetX: number;
107
+ /** Target display vertical position (DU). */
108
+ displayTargetY: number;
90
109
  }
91
110
 
92
111
  /** Represents an item currently rendered in the virtual scroll area. */
@@ -192,9 +211,10 @@ export type PaddingValue = number | { x?: number; y?: number; };
192
211
  * - 'start': Aligns the first visible item to the viewport start if at least 50% visible, otherwise aligns the next item.
193
212
  * - 'center': Aligns the item that intersects the viewport center to the center.
194
213
  * - 'end': Aligns the last visible item to the viewport end if at least 50% visible, otherwise aligns the previous item.
214
+ * - 'next': Snaps to the next (closest) snap position in the direction of the scroll.
195
215
  * - 'auto': Intelligent snapping based on scroll direction. Acts as 'end' when scrolling towards start, and 'start' when scrolling towards end.
196
216
  */
197
- export type SnapMode = boolean | 'start' | 'center' | 'end' | 'auto';
217
+ export type SnapMode = boolean | 'start' | 'center' | 'end' | 'next' | 'auto';
198
218
 
199
219
  /** Base configuration properties shared between the component and the composable. */
200
220
  export interface VirtualScrollBaseProps<T = unknown> {
@@ -205,7 +225,7 @@ export interface VirtualScrollBaseProps<T = unknown> {
205
225
  * Fixed size of each item in virtual units (VU) or a function that returns the size of an item.
206
226
  * Pass `0`, `null` or `undefined` for automatic dynamic size detection via `ResizeObserver`.
207
227
  */
208
- itemSize?: number | ((item: T, index: number) => number) | null | undefined;
228
+ itemSize?: number | (number | null | undefined)[] | ((item: T, index: number) => number) | null | undefined;
209
229
 
210
230
  /**
211
231
  * Direction of the virtual scroll.
@@ -246,7 +266,7 @@ export interface VirtualScrollBaseProps<T = unknown> {
246
266
  * Fixed width of columns in VU, an array of widths, or a function returning widths.
247
267
  * Pass `0`, `null` or `undefined` for dynamic column detection.
248
268
  */
249
- columnWidth?: number | number[] | ((index: number) => number) | null | undefined;
269
+ columnWidth?: number | (number | null | undefined)[] | ((index: number) => number) | null | undefined;
250
270
 
251
271
  /**
252
272
  * Pixel padding at the start of the scroll container in display pixels (DU).
@@ -283,11 +303,13 @@ export interface VirtualScrollBaseProps<T = unknown> {
283
303
 
284
304
  /**
285
305
  * Whether data is currently loading.
306
+ * While true, the loading slot is shown and `load` events are suppressed.
286
307
  */
287
308
  loading?: boolean | undefined;
288
309
 
289
310
  /**
290
- * Whether to automatically restore and maintain scroll position when items are prepended to the array.
311
+ * Whether to automatically maintain scroll position when items are prepended to the array.
312
+ * Useful for "load more" chat interfaces.
291
313
  */
292
314
  restoreScrollOnPrepend?: boolean | undefined;
293
315
 
@@ -342,6 +364,7 @@ export interface VirtualScrollBaseProps<T = unknown> {
342
364
 
343
365
  /**
344
366
  * Whether to snap to items after scrolling stops.
367
+ * Options: false, true, 'auto', 'next', 'start', 'center', 'end'.
345
368
  * @default false
346
369
  */
347
370
  snap?: SnapMode | undefined;
@@ -514,10 +537,20 @@ export interface VirtualScrollComponentProps<T = unknown> extends VirtualScrollB
514
537
  /** The HTML tag to use for each item. */
515
538
  itemTag?: string;
516
539
  /** Whether the content in the 'header' slot is sticky. */
540
+ /**
541
+ * If true, measures the header slot size and adds it to the scroll padding.
542
+ * Can be combined with CSS for sticky headers.
543
+ */
517
544
  stickyHeader?: boolean;
518
- /** Whether the content in the 'footer' slot is sticky. */
545
+ /**
546
+ * If true, measures the footer slot size and adds it to the scroll padding.
547
+ * Can be combined with CSS for sticky footers.
548
+ */
519
549
  stickyFooter?: boolean;
520
- /** Whether to use virtual scrollbars for styling purposes. */
550
+ /**
551
+ * Whether to use virtual scrollbars.
552
+ * Automatically enabled when content size exceeds browser limits.
553
+ */
521
554
  virtualScrollbar?: boolean;
522
555
  }
523
556
 
@@ -547,8 +580,12 @@ export interface VirtualScrollInstance<T = unknown> extends VirtualScrollCompone
547
580
  getItemOffset: (index: number) => number;
548
581
  /** Helper to get the size of a specific item along the scroll axis. */
549
582
  getItemSize: (index: number) => number;
583
+ /** Whether the component is in table mode. */
584
+ isTable: boolean;
585
+ /** The tag used for rendering items. */
586
+ itemTag: string;
550
587
  /** Programmatically scroll to a specific row and/or column. */
551
- scrollToIndex: (rowIndex?: number | null, colIndex?: number | null, options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions) => void;
588
+ scrollToIndex: (rowIndex?: number | null, colIndex?: number | null, options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions) => ScrollToIndexResult;
552
589
  /** Programmatically scroll to a specific pixel offset. */
553
590
  scrollToOffset: (x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => void;
554
591
  /** Resets all dynamic measurements and re-initializes from props. */
@@ -681,6 +718,8 @@ export interface RangeParams {
681
718
  usableHeight: number;
682
719
  /** Total item count. */
683
720
  itemsLength: number;
721
+ /** Column count (for grid mode). */
722
+ columnCount?: number;
684
723
  /** Buffer items before. */
685
724
  bufferBefore: number;
686
725
  /** Buffer items after. */
@@ -796,7 +835,7 @@ export interface ItemStyleParams<T = unknown> {
796
835
  /** Scroll direction. */
797
836
  direction: ScrollDirection;
798
837
  /** Configured item size logic. */
799
- itemSize: number | ((item: T, index: number) => number) | null | undefined;
838
+ itemSize: number | (number | null | undefined)[] | ((item: T, index: number) => number) | null | undefined;
800
839
  /** Parent container tag. */
801
840
  containerTag: string;
802
841
  /** Padding start on X axis. */
@@ -22,7 +22,7 @@ export const BROWSER_MAX_SIZE = 10000000;
22
22
  * @returns `true` if the container is the global window object.
23
23
  */
24
24
  export function isWindow(container?: HTMLElement | Window | null): container is Window {
25
- return container === null || container === document.documentElement || (typeof window !== 'undefined' && container === window);
25
+ return container === null || (typeof document !== 'undefined' && container === document.documentElement) || (typeof window !== 'undefined' && container === window);
26
26
  }
27
27
 
28
28
  /**
@@ -95,7 +95,7 @@ export function scrollTo(container: HTMLElement | Window | null | undefined, opt
95
95
  * @returns `true` if the options object contains scroll-to-index specific properties.
96
96
  */
97
97
  export function isScrollToIndexOptions(options: unknown): options is ScrollToIndexOptions {
98
- return typeof options === 'object' && options != null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
98
+ return typeof options === 'object' && options != null && ('align' in options || 'behavior' in options || 'isCorrection' in options || 'dryRun' in options);
99
99
  }
100
100
 
101
101
  /**
@@ -1189,6 +1189,48 @@ export function resolveSnap(
1189
1189
  }
1190
1190
  }
1191
1191
 
1192
+ if (mode === 'next') {
1193
+ if (dir === 'end') {
1194
+ // Scrolling items UP (towards end of list) -> snap to NEXT item start
1195
+ const size = getSize(currentIdx);
1196
+ if (size > viewSize) {
1197
+ return null;
1198
+ }
1199
+ const scrolledOut = relScroll - getQuery(currentIdx);
1200
+ const threshold = Math.min(5, size * 0.1);
1201
+ if (scrolledOut <= threshold) {
1202
+ return {
1203
+ index: currentIdx,
1204
+ align: 'start' as const,
1205
+ };
1206
+ }
1207
+ return {
1208
+ index: Math.min(count - 1, currentIdx + 1),
1209
+ align: 'start' as const,
1210
+ };
1211
+ } else if (dir === 'start') {
1212
+ // Scrolling items DOWN (towards start of list) -> snap to PREVIOUS item end
1213
+ const size = getSize(currentEndIdx);
1214
+ if (size > viewSize) {
1215
+ return null;
1216
+ }
1217
+ const scrolledOutBottom = (getQuery(currentEndIdx) + size) - (relScroll + viewSize);
1218
+ const threshold = Math.min(5, size * 0.1);
1219
+ if (scrolledOutBottom <= threshold) {
1220
+ return {
1221
+ index: currentEndIdx,
1222
+ align: 'end' as const,
1223
+ };
1224
+ }
1225
+ return {
1226
+ index: Math.max(0, currentEndIdx - 1),
1227
+ align: 'end' as const,
1228
+ };
1229
+ } else {
1230
+ return null;
1231
+ }
1232
+ }
1233
+
1192
1234
  if (effectiveMode === 'start') {
1193
1235
  const size = getSize(currentIdx);
1194
1236
  // Ignore items larger than viewport to prevent jarring jumps