@ng-native/components 0.2.0 → 0.3.0

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.
Files changed (48) hide show
  1. package/dist/content-container.d.ts +25 -0
  2. package/dist/content-container.js +35 -0
  3. package/dist/content-container.js.map +1 -0
  4. package/dist/gestures-web.d.ts +12 -0
  5. package/dist/gestures-web.js +32 -0
  6. package/dist/gestures-web.js.map +1 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/keyboard-avoiding-view.d.ts +6 -1
  10. package/dist/keyboard-avoiding-view.js +23 -6
  11. package/dist/keyboard-avoiding-view.js.map +1 -1
  12. package/dist/pressable.d.ts +33 -4
  13. package/dist/pressable.js +55 -3
  14. package/dist/pressable.js.map +1 -1
  15. package/dist/reanimated-web.d.ts +26 -0
  16. package/dist/reanimated-web.js +59 -0
  17. package/dist/reanimated-web.js.map +1 -0
  18. package/dist/refresh-control.d.ts +4 -1
  19. package/dist/refresh-control.js +4 -1
  20. package/dist/refresh-control.js.map +1 -1
  21. package/dist/scroll-view.d.ts +17 -1
  22. package/dist/scroll-view.js +59 -6
  23. package/dist/scroll-view.js.map +1 -1
  24. package/dist/section-list.d.ts +14 -2
  25. package/dist/section-list.js +20 -4
  26. package/dist/section-list.js.map +1 -1
  27. package/dist/sticky-headers.d.ts +6 -0
  28. package/dist/sticky-headers.js +7 -2
  29. package/dist/sticky-headers.js.map +1 -1
  30. package/dist/switch.d.ts +11 -1
  31. package/dist/switch.js +16 -3
  32. package/dist/switch.js.map +1 -1
  33. package/dist/text.d.ts +10 -1
  34. package/dist/text.js +15 -1
  35. package/dist/text.js.map +1 -1
  36. package/dist/touchable-opacity.d.ts +2 -1
  37. package/dist/touchable-opacity.js +3 -0
  38. package/dist/touchable-opacity.js.map +1 -1
  39. package/dist/view-base.d.ts +28 -18
  40. package/dist/view-base.js +37 -22
  41. package/dist/view-base.js.map +1 -1
  42. package/dist/virtual-list.d.ts +49 -5
  43. package/dist/virtual-list.js +129 -42
  44. package/dist/virtual-list.js.map +1 -1
  45. package/dist/zero-size-warning.d.ts +30 -0
  46. package/dist/zero-size-warning.js +58 -0
  47. package/dist/zero-size-warning.js.map +1 -0
  48. package/package.json +7 -4
@@ -28,6 +28,16 @@ export interface VirtualRow<T> {
28
28
  */
29
29
  readonly parked?: true;
30
30
  }
31
+ /**
32
+ * Space around the rows, inside the scrolling content: one number for every side, or a side each,
33
+ * as FlatList's `contentContainerStyle` padding.
34
+ */
35
+ export type VirtualListPadding = number | {
36
+ readonly top?: number;
37
+ readonly right?: number;
38
+ readonly bottom?: number;
39
+ readonly left?: number;
40
+ };
31
41
  /** A row height: one for every row, or one per item. */
32
42
  export type VirtualItemHeight<T> = number | ((item: T, index: number) => number);
33
43
  /**
@@ -143,6 +153,14 @@ export declare class VirtualList<T> extends ScrollViewProps {
143
153
  * honest the scroll bar is and the less a jump to an unmeasured row has to correct.
144
154
  */
145
155
  readonly estimatedItemHeight: import("@angular/core").InputSignal<VirtualItemHeight<T>>;
156
+ /**
157
+ * Space around the rows, inside the scrolling content. The leading side comes before the first
158
+ * row and the trailing side after the last; the sides across the axis inset every row. The
159
+ * header and footer sit outside it, as FlatList's do outside `contentContainerStyle`.
160
+ */
161
+ readonly contentPadding: import("@angular/core").InputSignal<VirtualListPadding>;
162
+ /** Space between one row and the next, along the axis: not before the first or after the last. */
163
+ readonly rowGap: import("@angular/core").InputSignalWithTransform<number, unknown>;
146
164
  /**
147
165
  * An item's identity, as FlatList's `keyExtractor`: what a measured height, a slot and a held
148
166
  * position follow across an insert. Without it an item is its own key, which is enough for items
@@ -225,6 +243,12 @@ export declare class VirtualList<T> extends ScrollViewProps {
225
243
  private readonly pinning;
226
244
  /** Rows size themselves and are laid out in flow, rather than placed at a known offset. */
227
245
  readonly measuring: import("@angular/core").Signal<boolean>;
246
+ /** The padding on each side. */
247
+ private readonly edges;
248
+ /** The padding before the first row, along the axis. */
249
+ private readonly lead;
250
+ /** The rows' insets across the axis, as the props that place an absolutely positioned row. */
251
+ private readonly across;
228
252
  /** Heights native has reported, by key. */
229
253
  private readonly measured;
230
254
  /** Bumped when a measurement changes, which is what re-sizes the list. */
@@ -243,8 +267,13 @@ export declare class VirtualList<T> extends ScrollViewProps {
243
267
  private readonly indexOfKey;
244
268
  /** The index, with a dependency on its measurements for whoever is reading it. */
245
269
  private layout;
246
- /** The list's own total extent along its axis. */
270
+ /**
271
+ * The list's own total extent along its axis: the padding at both ends, the rows, and a gap
272
+ * between each two of them.
273
+ */
247
274
  private readonly extent;
275
+ /** A fixed-height row and the gap after it. */
276
+ private stride;
248
277
  protected readonly canvas: import("@angular/core").Signal<{
249
278
  width: number;
250
279
  flexDirection: string;
@@ -267,12 +296,18 @@ export declare class VirtualList<T> extends ScrollViewProps {
267
296
  * A style binding validates its values as the DOM's, and warns on a list in development.
268
297
  */
269
298
  private readonly flipText;
299
+ /**
300
+ * What places a row in flow: the gap after it and the padding across the axis, as margins, since
301
+ * native lays the row out. Empty when there are none, so a plain list commits no margins.
302
+ */
303
+ private readonly flowSpacing;
270
304
  /** A row in flow, flipped back when the list is inverted. One object, so rows never re-clone. */
271
305
  private readonly flowStyle;
272
- /** Top of a row. */
306
+ /** Top of a row: the leading padding, and every row and gap before it. */
273
307
  private topOf;
308
+ /** A row's own size, without the gap after it. */
274
309
  private heightOf;
275
- /** First row whose bottom is past `y`. */
310
+ /** First row whose bottom, or the gap after it, is past `y`. */
276
311
  private indexAt;
277
312
  /** The row the viewport's leading edge is in, taken at each scroll. */
278
313
  private anchor;
@@ -327,7 +362,16 @@ export declare class VirtualList<T> extends ScrollViewProps {
327
362
  private successorOf;
328
363
  /** Make a queued correction, after the commit that moved the rows has gone to native. */
329
364
  private flushScroll;
365
+ /** In development, a list with rows that stays at zero size; see `ZeroSizeWarning`. */
366
+ private readonly zeroSize;
330
367
  constructor();
368
+ /** Whether the viewport has been laid out yet, so a zero in it is a measurement. */
369
+ private viewportMeasured;
370
+ /**
371
+ * In development, rows that arrive in a list already measured at zero, or go from one, with no
372
+ * new layout to say so: an update to `items` alone does not lay the viewport out again.
373
+ */
374
+ private checkZeroSize;
331
375
  /** The rows to render, and the edges of the run they sit in. */
332
376
  private readonly span;
333
377
  readonly window: import("@angular/core").Signal<VirtualRow<T>[]>;
@@ -479,7 +523,7 @@ export declare class VirtualList<T> extends ScrollViewProps {
479
523
  y?: number;
480
524
  };
481
525
  }>): void;
482
- /** RN's sticky header commits its translate after 64 ms without a scroll event, as this does. */
526
+ /** A pinned row's translate, settled once scrolling pauses: see `STICKY_SETTLE_MS`. */
483
527
  private settleAfterPause;
484
528
  protected onLayout(event: NativeSyntheticEvent<{
485
529
  layout?: {
@@ -508,7 +552,7 @@ export declare class VirtualList<T> extends ScrollViewProps {
508
552
  }>): void;
509
553
  private checkEnd;
510
554
  static ɵfac: i0.ɵɵFactoryDeclaration<VirtualList<any>, never>;
511
- static ɵcmp: i0.ɵɵComponentDeclaration<VirtualList<any>, "virtual-list", ["virtualList"], { "items": { "alias": "items"; "required": true; "isSignal": true; }; "itemHeight": { "alias": "itemHeight"; "required": false; "isSignal": true; }; "estimatedItemHeight": { "alias": "estimatedItemHeight"; "required": false; "isSignal": true; }; "keyExtractor": { "alias": "keyExtractor"; "required": false; "isSignal": true; }; "itemType": { "alias": "itemType"; "required": false; "isSignal": true; }; "maintainVisibleContentPosition": { "alias": "maintainVisibleContentPosition"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "endReachedThreshold": { "alias": "endReachedThreshold"; "required": false; "isSignal": true; }; "horizontal": { "alias": "horizontal"; "required": false; "isSignal": true; }; "inverted": { "alias": "inverted"; "required": false; "isSignal": true; }; "stickyIndices": { "alias": "stickyIndices"; "required": false; "isSignal": true; }; "stickyHeader": { "alias": "stickyHeader"; "required": false; "isSignal": true; }; "keyboardShouldPersistTaps": { "alias": "keyboardShouldPersistTaps"; "required": false; "isSignal": true; }; "itemVisiblePercentThreshold": { "alias": "itemVisiblePercentThreshold"; "required": false; "isSignal": true; }; }, { "endReached": "endReached"; "viewableItemsChanged": "viewableItemsChanged"; }, ["separator", "refreshControl"], ["refresh-control", "[listHeader]", "*", "[listFooter]"], true, never>;
555
+ static ɵcmp: i0.ɵɵComponentDeclaration<VirtualList<any>, "virtual-list", ["virtualList"], { "items": { "alias": "items"; "required": true; "isSignal": true; }; "itemHeight": { "alias": "itemHeight"; "required": false; "isSignal": true; }; "estimatedItemHeight": { "alias": "estimatedItemHeight"; "required": false; "isSignal": true; }; "contentPadding": { "alias": "contentPadding"; "required": false; "isSignal": true; }; "rowGap": { "alias": "rowGap"; "required": false; "isSignal": true; }; "keyExtractor": { "alias": "keyExtractor"; "required": false; "isSignal": true; }; "itemType": { "alias": "itemType"; "required": false; "isSignal": true; }; "maintainVisibleContentPosition": { "alias": "maintainVisibleContentPosition"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "endReachedThreshold": { "alias": "endReachedThreshold"; "required": false; "isSignal": true; }; "horizontal": { "alias": "horizontal"; "required": false; "isSignal": true; }; "inverted": { "alias": "inverted"; "required": false; "isSignal": true; }; "stickyIndices": { "alias": "stickyIndices"; "required": false; "isSignal": true; }; "stickyHeader": { "alias": "stickyHeader"; "required": false; "isSignal": true; }; "keyboardShouldPersistTaps": { "alias": "keyboardShouldPersistTaps"; "required": false; "isSignal": true; }; "itemVisiblePercentThreshold": { "alias": "itemVisiblePercentThreshold"; "required": false; "isSignal": true; }; }, { "endReached": "endReached"; "viewableItemsChanged": "viewableItemsChanged"; }, ["separator", "refreshControl"], ["refresh-control", "[listHeader]", "*", "[listFooter]"], true, never>;
512
556
  }
513
557
  /**
514
558
  * Put on each row's view: `<view [virtualListRow]="row">`. Applies the row's style, and reports
@@ -1,9 +1,11 @@
1
- import { Component, DestroyRef, Directive, ElementRef, TemplateRef, afterEveryRender, booleanAttribute, computed, contentChild, inject, input, linkedSignal, numberAttribute, output, signal, viewChild, } from '@angular/core';
1
+ import { Component, DestroyRef, Directive, ElementRef, TemplateRef, afterEveryRender, booleanAttribute, computed, contentChild, effect, inject, input, linkedSignal, numberAttribute, output, signal, untracked, viewChild, } from '@angular/core';
2
2
  import { nativePlatform, pinnedRange, } from '@ng-native/fabric';
3
3
  import { HeightIndex } from "./height-index.js";
4
4
  import { dismissKeyboardOnTap } from "./keyboard-taps.js";
5
5
  import { RefreshControl } from "./refresh-control.js";
6
+ import { STICKY_SETTLE_MS } from "./sticky-headers.js";
6
7
  import { TemplateSlot } from "./template-slot.js";
8
+ import { ZeroSizeWarning, checksZeroSize, written } from "./zero-size-warning.js";
7
9
  import { View } from "./view.js";
8
10
  import { ScrollViewProps } from "./scroll-view-props.js";
9
11
  import * as i0 from "@angular/core";
@@ -111,6 +113,15 @@ export class VirtualList extends ScrollViewProps {
111
113
  */
112
114
  estimatedItemHeight = input(50, /* @ts-ignore */
113
115
  ...(ngDevMode ? [{ debugName: "estimatedItemHeight" }] : /* istanbul ignore next */ []));
116
+ /**
117
+ * Space around the rows, inside the scrolling content. The leading side comes before the first
118
+ * row and the trailing side after the last; the sides across the axis inset every row. The
119
+ * header and footer sit outside it, as FlatList's do outside `contentContainerStyle`.
120
+ */
121
+ contentPadding = input(0, /* @ts-ignore */
122
+ ...(ngDevMode ? [{ debugName: "contentPadding" }] : /* istanbul ignore next */ []));
123
+ /** Space between one row and the next, along the axis: not before the first or after the last. */
124
+ rowGap = input(0, { ...(ngDevMode ? { debugName: "rowGap" } : /* istanbul ignore next */ {}), transform: numberAttribute });
114
125
  /**
115
126
  * An item's identity, as FlatList's `keyExtractor`: what a measured height, a slot and a held
116
127
  * position follow across an insert. Without it an item is its own key, which is enough for items
@@ -201,6 +212,25 @@ export class VirtualList extends ScrollViewProps {
201
212
  /** Rows size themselves and are laid out in flow, rather than placed at a known offset. */
202
213
  measuring = computed(() => this.itemHeight() === undefined, /* @ts-ignore */
203
214
  ...(ngDevMode ? [{ debugName: "measuring" }] : /* istanbul ignore next */ []));
215
+ /** The padding on each side. */
216
+ edges = computed(() => {
217
+ const padding = this.contentPadding();
218
+ if (typeof padding === 'number') {
219
+ return { top: padding, right: padding, bottom: padding, left: padding };
220
+ }
221
+ const { top = 0, right = 0, bottom = 0, left = 0 } = padding ?? {};
222
+ return { top, right, bottom, left };
223
+ }, /* @ts-ignore */
224
+ ...(ngDevMode ? [{ debugName: "edges" }] : /* istanbul ignore next */ []));
225
+ /** The padding before the first row, along the axis. */
226
+ lead = computed(() => this.horizontal() ? this.edges().left : this.edges().top, /* @ts-ignore */
227
+ ...(ngDevMode ? [{ debugName: "lead" }] : /* istanbul ignore next */ []));
228
+ /** The rows' insets across the axis, as the props that place an absolutely positioned row. */
229
+ across = computed(() => {
230
+ const { top, right, bottom, left } = this.edges();
231
+ return this.horizontal() ? { top, bottom } : { left, right };
232
+ }, /* @ts-ignore */
233
+ ...(ngDevMode ? [{ debugName: "across" }] : /* istanbul ignore next */ []));
204
234
  /** Heights native has reported, by key. */
205
235
  measured = new Map();
206
236
  /** Bumped when a measurement changes, which is what re-sizes the list. */
@@ -225,19 +255,22 @@ export class VirtualList extends ScrollViewProps {
225
255
  const height = this.itemHeight();
226
256
  if (typeof height === 'number')
227
257
  return null;
258
+ // Each row's entry is its size and the gap after it, so where a row starts counts the gaps
259
+ // before it without a second table.
260
+ const gap = this.rowGap();
228
261
  const items = this.items();
229
262
  const sizes = new Float64Array(items.length);
230
263
  if (height) {
231
264
  for (let i = 0; i < items.length; i++)
232
- sizes[i] = height(items[i], i);
265
+ sizes[i] = height(items[i], i) + gap;
233
266
  return new HeightIndex(sizes);
234
267
  }
235
268
  const estimate = this.estimatedItemHeight();
236
269
  const keys = this.keys();
237
270
  for (let i = 0; i < items.length; i++) {
238
- sizes[i] =
239
- this.measured.get(keys ? keys[i] : items[i]) ??
240
- (typeof estimate === 'number' ? estimate : estimate(items[i], i));
271
+ const size = this.measured.get(keys ? keys[i] : items[i]) ??
272
+ (typeof estimate === 'number' ? estimate : estimate(items[i], i));
273
+ sizes[i] = size + gap;
241
274
  }
242
275
  return new HeightIndex(sizes);
243
276
  }, /* @ts-ignore */
@@ -258,14 +291,23 @@ export class VirtualList extends ScrollViewProps {
258
291
  this.measuredVersion();
259
292
  return table;
260
293
  }
261
- /** The list's own total extent along its axis. */
294
+ /**
295
+ * The list's own total extent along its axis: the padding at both ends, the rows, and a gap
296
+ * between each two of them.
297
+ */
262
298
  extent = computed(() => {
263
299
  const table = this.layout();
264
- if (table)
265
- return table.total;
266
- return this.items().length * this.itemHeight();
300
+ const count = this.items().length;
301
+ const rows = table ? table.total : count * this.stride();
302
+ const { top, right, bottom, left } = this.edges();
303
+ const padding = this.horizontal() ? left + right : top + bottom;
304
+ return padding + (count ? rows - this.rowGap() : 0);
267
305
  }, /* @ts-ignore */
268
306
  ...(ngDevMode ? [{ debugName: "extent" }] : /* istanbul ignore next */ []));
307
+ /** A fixed-height row and the gap after it. */
308
+ stride() {
309
+ return this.itemHeight() + this.rowGap();
310
+ }
269
311
  canvas = computed(() => this.horizontal() ? { width: this.extent(), flexDirection: 'row' } : { height: this.extent() }, /* @ts-ignore */
270
312
  ...(ngDevMode ? [{ debugName: "canvas" }] : /* istanbul ignore next */ []));
271
313
  /**
@@ -292,27 +334,45 @@ export class VirtualList extends ScrollViewProps {
292
334
  return this.horizontal() ? 'scaleX(-1)' : 'scaleY(-1)';
293
335
  }, /* @ts-ignore */
294
336
  ...(ngDevMode ? [{ debugName: "flipText" }] : /* istanbul ignore next */ []));
337
+ /**
338
+ * What places a row in flow: the gap after it and the padding across the axis, as margins, since
339
+ * native lays the row out. Empty when there are none, so a plain list commits no margins.
340
+ */
341
+ flowSpacing = computed(() => {
342
+ const gap = this.rowGap();
343
+ const { top, right, bottom, left } = this.edges();
344
+ const spacing = this.horizontal()
345
+ ? { marginRight: gap, marginTop: top, marginBottom: bottom }
346
+ : { marginBottom: gap, marginLeft: left, marginRight: right };
347
+ return Object.fromEntries(Object.entries(spacing).filter(([, value]) => value !== 0));
348
+ }, /* @ts-ignore */
349
+ ...(ngDevMode ? [{ debugName: "flowSpacing" }] : /* istanbul ignore next */ []));
295
350
  /** A row in flow, flipped back when the list is inverted. One object, so rows never re-clone. */
296
351
  flowStyle = computed(() => {
297
352
  const flip = this.flipText();
298
- return flip ? { transform: flip } : IN_FLOW;
353
+ const spacing = this.flowSpacing();
354
+ if (!flip && Object.keys(spacing).length === 0)
355
+ return IN_FLOW;
356
+ return flip ? { ...spacing, transform: flip } : spacing;
299
357
  }, /* @ts-ignore */
300
358
  ...(ngDevMode ? [{ debugName: "flowStyle" }] : /* istanbul ignore next */ []));
301
- /** Top of a row. */
359
+ /** Top of a row: the leading padding, and every row and gap before it. */
302
360
  topOf(index) {
303
361
  const table = this.layout();
304
- return table ? table.topOf(index) : index * this.itemHeight();
362
+ return this.lead() + (table ? table.topOf(index) : index * this.stride());
305
363
  }
364
+ /** A row's own size, without the gap after it. */
306
365
  heightOf(index) {
307
366
  const table = this.layout();
308
- return table ? table.heightOf(index) : this.itemHeight();
367
+ return table ? table.heightOf(index) - this.rowGap() : this.itemHeight();
309
368
  }
310
- /** First row whose bottom is past `y`. */
369
+ /** First row whose bottom, or the gap after it, is past `y`. */
311
370
  indexAt(y) {
312
371
  const table = this.layout();
372
+ const along = y - this.lead();
313
373
  if (!table)
314
- return Math.max(0, Math.floor(y / this.itemHeight()));
315
- return table.indexAt(y);
374
+ return Math.max(0, Math.floor(along / this.stride()));
375
+ return table.indexAt(along);
316
376
  }
317
377
  // --- holding position -------------------------------------------------------------------
318
378
  /** The row the viewport's leading edge is in, taken at each scroll. */
@@ -450,6 +510,10 @@ export class VirtualList extends ScrollViewProps {
450
510
  else
451
511
  this.scrollToOffset({ offset: this.headerHeight() + pending.offset, animated: false });
452
512
  }
513
+ /** In development, a list with rows that stays at zero size; see `ZeroSizeWarning`. */
514
+ zeroSize = checksZeroSize()
515
+ ? new ZeroSizeWarning(() => written(this.node), () => !!this.horizontal())
516
+ : null;
453
517
  constructor() {
454
518
  super();
455
519
  afterEveryRender(() => {
@@ -461,9 +525,25 @@ export class VirtualList extends ScrollViewProps {
461
525
  inject(DestroyRef).onDestroy(() => {
462
526
  stop();
463
527
  clearTimeout(this.settleTimer);
528
+ this.zeroSize?.stop();
464
529
  for (const held of this.drives.values())
465
530
  held.drive.stop();
466
531
  });
532
+ if (this.zeroSize)
533
+ this.checkZeroSize(this.zeroSize);
534
+ }
535
+ /** Whether the viewport has been laid out yet, so a zero in it is a measurement. */
536
+ viewportMeasured = false;
537
+ /**
538
+ * In development, rows that arrive in a list already measured at zero, or go from one, with no
539
+ * new layout to say so: an update to `items` alone does not lay the viewport out again.
540
+ */
541
+ checkZeroSize(warning) {
542
+ effect(() => {
543
+ const hasRows = this.items().length > 0;
544
+ if (this.viewportMeasured)
545
+ warning.laidOut(untracked(this.viewport), hasRows);
546
+ });
467
547
  }
468
548
  // --- the window ---------------------------------------------------------------------------
469
549
  /** The rows to render, and the edges of the run they sit in. */
@@ -520,7 +600,7 @@ export class VirtualList extends ScrollViewProps {
520
600
  let lead = span ? this.topOf(span.first) : 0;
521
601
  const pinned = this.pinnedIndex();
522
602
  if (span && pinned !== null && pinned < span.first) {
523
- lead = this.nativeSticky ? this.topOf(pinned) : lead - this.heightOf(pinned);
603
+ lead = this.nativeSticky ? this.topOf(pinned) : lead - this.heightOf(pinned) - this.rowGap();
524
604
  }
525
605
  return this.horizontal() ? { width: Math.max(0, lead) } : { height: Math.max(0, lead) };
526
606
  }, /* @ts-ignore */
@@ -543,7 +623,7 @@ export class VirtualList extends ScrollViewProps {
543
623
  style: this.nativeSticky
544
624
  ? this.stickyStyle(pinned)
545
625
  : this.measuring()
546
- ? this.pinnedFlowStyle(this.topOf(first) - this.heightOf(pinned))
626
+ ? this.pinnedFlowStyle(this.topOf(first) - this.heightOf(pinned) - this.rowGap())
547
627
  : this.slotStyle(pinned, this.offset()),
548
628
  }));
549
629
  }
@@ -568,11 +648,12 @@ export class VirtualList extends ScrollViewProps {
568
648
  */
569
649
  leadWindow(rows, pinned, first) {
570
650
  const lead = rows[1];
571
- const gap = this.topOf(first) - this.topOf(pinned) - this.heightOf(pinned);
572
- if (!lead || gap <= 0)
651
+ // The pinned row's own gap already follows it in flow.
652
+ const between = this.topOf(first) - this.topOf(pinned) - this.heightOf(pinned) - this.rowGap();
653
+ if (!lead || between <= 0)
573
654
  return;
574
655
  const margin = this.horizontal() ? 'marginLeft' : 'marginTop';
575
- rows[1] = this.hold({ ...lead, style: { ...lead.style, [margin]: gap } });
656
+ rows[1] = this.hold({ ...lead, style: { ...lead.style, [margin]: between } });
576
657
  }
577
658
  styleFor(index, pinned) {
578
659
  if (this.nativeSticky && this.nextSticky().has(index))
@@ -587,7 +668,7 @@ export class VirtualList extends ScrollViewProps {
587
668
  const shift = Math.max(0, this.offset() - at);
588
669
  const move = `${this.horizontal() ? 'translateX' : 'translateY'}(${shift}px)`;
589
670
  const flip = this.flipText();
590
- return { zIndex: 1, transform: flip ? `${move} ${flip}` : move };
671
+ return { ...this.flowSpacing(), zIndex: 1, transform: flip ? `${move} ${flip}` : move };
591
672
  }
592
673
  /**
593
674
  * A sticky row native moves: at its own place, with the translate native had reached when
@@ -595,7 +676,9 @@ export class VirtualList extends ScrollViewProps {
595
676
  */
596
677
  stickyStyle(index) {
597
678
  const horizontal = this.horizontal();
598
- const style = this.measuring() ? {} : { ...this.slotStyle(index) };
679
+ const style = this.measuring()
680
+ ? { ...this.flowSpacing() }
681
+ : { ...this.slotStyle(index) };
599
682
  style['zIndex'] = 1;
600
683
  const top = this.topOf(index);
601
684
  const shift = Math.min(Math.max(0, this.settled() - top), this.stopOf(index) - top);
@@ -740,7 +823,7 @@ export class VirtualList extends ScrollViewProps {
740
823
  const index = this.indexOfKey()?.get(row.key);
741
824
  if (index === undefined)
742
825
  return;
743
- this.table()?.set(index, size);
826
+ this.table()?.set(index, size + this.rowGap());
744
827
  this.measuredVersion.update((version) => version + 1);
745
828
  }
746
829
  /** Drop the measurements of items long gone, so churn cannot grow the map without bound. */
@@ -805,16 +888,16 @@ export class VirtualList extends ScrollViewProps {
805
888
  const size = this.heightOf(row.index);
806
889
  const horizontal = this.horizontal();
807
890
  const flip = this.flipText();
891
+ const across = this.across();
808
892
  const cached = this.measuredGaps.get(row.key);
809
- if (cached &&
810
- cached[horizontal ? 'left' : 'top'] === start &&
811
- cached[horizontal ? 'width' : 'height'] === size &&
812
- cached['transform'] === flip) {
893
+ const placed = horizontal
894
+ ? { ...across, left: start, width: size, transform: flip }
895
+ : { ...across, top: start, height: size, transform: flip };
896
+ if (cached && holds(cached, placed))
813
897
  return cached;
814
- }
815
898
  const style = horizontal
816
- ? { position: 'absolute', top: 0, bottom: 0, left: start, width: size, flexDirection: 'row' }
817
- : { position: 'absolute', left: 0, right: 0, top: start, height: size };
899
+ ? { position: 'absolute', ...across, left: start, width: size, flexDirection: 'row' }
900
+ : { position: 'absolute', ...across, top: start, height: size };
818
901
  style['justifyContent'] = 'flex-end';
819
902
  style['pointerEvents'] = 'box-none';
820
903
  if (flip)
@@ -953,18 +1036,16 @@ export class VirtualList extends ScrollViewProps {
953
1036
  const key = horizontal ? 'left' : 'top';
954
1037
  const extent = horizontal ? 'width' : 'height';
955
1038
  const flip = this.flipText();
1039
+ const across = this.across();
956
1040
  let style = this.slots.get(index);
957
1041
  // The flip is part of the comparison because turning the list over changes every row, and a
958
1042
  // cached slot would otherwise keep the orientation it was built with.
959
- const stale = !style ||
960
- style[extent] !== size ||
961
- style[key] !== start ||
962
- style['transform'] !== flip ||
963
- pinnedAt !== undefined;
1043
+ const placed = { ...across, [extent]: size, [key]: start, transform: flip };
1044
+ const stale = !style || pinnedAt !== undefined || !holds(style, placed);
964
1045
  if (stale) {
965
1046
  style = horizontal
966
- ? { position: 'absolute', top: 0, bottom: 0, left: start, width: size }
967
- : { position: 'absolute', left: 0, right: 0, top: start, height: size };
1047
+ ? { position: 'absolute', ...across, left: start, width: size }
1048
+ : { position: 'absolute', ...across, top: start, height: size };
968
1049
  // A pinned row draws over the ones sliding under it; the transform is the row's half of
969
1050
  // the inversion.
970
1051
  if (pinnedAt !== undefined)
@@ -999,14 +1080,16 @@ export class VirtualList extends ScrollViewProps {
999
1080
  this.checkEnd(along);
1000
1081
  this.announceViewable(content);
1001
1082
  }
1002
- /** RN's sticky header commits its translate after 64 ms without a scroll event, as this does. */
1083
+ /** A pinned row's translate, settled once scrolling pauses: see `STICKY_SETTLE_MS`. */
1003
1084
  settleAfterPause(content) {
1004
1085
  clearTimeout(this.settleTimer);
1005
- this.settleTimer = setTimeout(() => this.settled.set(content), 64);
1086
+ this.settleTimer = setTimeout(() => this.settled.set(content), STICKY_SETTLE_MS);
1006
1087
  }
1007
1088
  onLayout(event) {
1008
1089
  const layout = event.nativeEvent?.layout;
1009
1090
  this.viewport.set((this.horizontal() ? layout?.width : layout?.height) ?? 0);
1091
+ this.viewportMeasured = true;
1092
+ this.zeroSize?.laidOut(this.viewport(), this.items().length > 0);
1010
1093
  this.announceViewable(this.lastOffset);
1011
1094
  }
1012
1095
  /** The header's extent along the axis, which every row sits after. */
@@ -1054,7 +1137,7 @@ export class VirtualList extends ScrollViewProps {
1054
1137
  this.endReached.emit({ distanceFromEnd });
1055
1138
  }
1056
1139
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1057
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: VirtualList, isStandalone: true, selector: "virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, itemHeight: { classPropertyName: "itemHeight", publicName: "itemHeight", isSignal: true, isRequired: false, transformFunction: null }, estimatedItemHeight: { classPropertyName: "estimatedItemHeight", publicName: "estimatedItemHeight", isSignal: true, isRequired: false, transformFunction: null }, keyExtractor: { classPropertyName: "keyExtractor", publicName: "keyExtractor", isSignal: true, isRequired: false, transformFunction: null }, itemType: { classPropertyName: "itemType", publicName: "itemType", isSignal: true, isRequired: false, transformFunction: null }, maintainVisibleContentPosition: { classPropertyName: "maintainVisibleContentPosition", publicName: "maintainVisibleContentPosition", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, endReachedThreshold: { classPropertyName: "endReachedThreshold", publicName: "endReachedThreshold", isSignal: true, isRequired: false, transformFunction: null }, horizontal: { classPropertyName: "horizontal", publicName: "horizontal", isSignal: true, isRequired: false, transformFunction: null }, inverted: { classPropertyName: "inverted", publicName: "inverted", isSignal: true, isRequired: false, transformFunction: null }, stickyIndices: { classPropertyName: "stickyIndices", publicName: "stickyIndices", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, keyboardShouldPersistTaps: { classPropertyName: "keyboardShouldPersistTaps", publicName: "keyboardShouldPersistTaps", isSignal: true, isRequired: false, transformFunction: null }, itemVisiblePercentThreshold: { classPropertyName: "itemVisiblePercentThreshold", publicName: "itemVisiblePercentThreshold", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { endReached: "endReached", viewableItemsChanged: "viewableItemsChanged" }, host: { listeners: { "scroll": "onScroll($event)", "layout": "onLayout($event)", "contentSizeChange": "onContentSize($event)" }, properties: { "flexDirection": "horizontal() ? 'row' : 'column'", "horizontal": "horizontal()", "nestedScrollEnabled": "nestedScroll()", "transform": "flip()", "scrollEventThrottle": "scrollEventThrottle()" } }, queries: [{ propertyName: "separator", first: true, predicate: VirtualListSeparator, descendants: true, isSignal: true }, { propertyName: "refreshControl", first: true, predicate: RefreshControl, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "canvasView", first: true, predicate: ["rows"], descendants: true, read: ElementRef, isSignal: true }], exportAs: ["virtualList"], usesInheritance: true, ngImport: i0, template: `
1140
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: VirtualList, isStandalone: true, selector: "virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, itemHeight: { classPropertyName: "itemHeight", publicName: "itemHeight", isSignal: true, isRequired: false, transformFunction: null }, estimatedItemHeight: { classPropertyName: "estimatedItemHeight", publicName: "estimatedItemHeight", isSignal: true, isRequired: false, transformFunction: null }, contentPadding: { classPropertyName: "contentPadding", publicName: "contentPadding", isSignal: true, isRequired: false, transformFunction: null }, rowGap: { classPropertyName: "rowGap", publicName: "rowGap", isSignal: true, isRequired: false, transformFunction: null }, keyExtractor: { classPropertyName: "keyExtractor", publicName: "keyExtractor", isSignal: true, isRequired: false, transformFunction: null }, itemType: { classPropertyName: "itemType", publicName: "itemType", isSignal: true, isRequired: false, transformFunction: null }, maintainVisibleContentPosition: { classPropertyName: "maintainVisibleContentPosition", publicName: "maintainVisibleContentPosition", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, endReachedThreshold: { classPropertyName: "endReachedThreshold", publicName: "endReachedThreshold", isSignal: true, isRequired: false, transformFunction: null }, horizontal: { classPropertyName: "horizontal", publicName: "horizontal", isSignal: true, isRequired: false, transformFunction: null }, inverted: { classPropertyName: "inverted", publicName: "inverted", isSignal: true, isRequired: false, transformFunction: null }, stickyIndices: { classPropertyName: "stickyIndices", publicName: "stickyIndices", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, keyboardShouldPersistTaps: { classPropertyName: "keyboardShouldPersistTaps", publicName: "keyboardShouldPersistTaps", isSignal: true, isRequired: false, transformFunction: null }, itemVisiblePercentThreshold: { classPropertyName: "itemVisiblePercentThreshold", publicName: "itemVisiblePercentThreshold", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { endReached: "endReached", viewableItemsChanged: "viewableItemsChanged" }, host: { listeners: { "scroll": "onScroll($event)", "layout": "onLayout($event)", "contentSizeChange": "onContentSize($event)" }, properties: { "flexDirection": "horizontal() ? 'row' : 'column'", "horizontal": "horizontal()", "nestedScrollEnabled": "nestedScroll()", "transform": "flip()", "scrollEventThrottle": "scrollEventThrottle()" } }, queries: [{ propertyName: "separator", first: true, predicate: VirtualListSeparator, descendants: true, isSignal: true }, { propertyName: "refreshControl", first: true, predicate: RefreshControl, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "canvasView", first: true, predicate: ["rows"], descendants: true, read: ElementRef, isSignal: true }], exportAs: ["virtualList"], usesInheritance: true, ngImport: i0, template: `
1058
1141
  <ng-content select="refresh-control" />
1059
1142
  <view [style]="headerStyle()" (layout)="onHeaderLayout($event)"
1060
1143
  ><ng-content select="[listHeader]"
@@ -1122,7 +1205,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
1122
1205
  '(contentSizeChange)': 'onContentSize($event)',
1123
1206
  },
1124
1207
  }]
1125
- }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], estimatedItemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "estimatedItemHeight", required: false }] }], keyExtractor: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyExtractor", required: false }] }], itemType: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemType", required: false }] }], maintainVisibleContentPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "maintainVisibleContentPosition", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], endReachedThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "endReachedThreshold", required: false }] }], horizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "horizontal", required: false }] }], inverted: [{ type: i0.Input, args: [{ isSignal: true, alias: "inverted", required: false }] }], stickyIndices: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyIndices", required: false }] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], keyboardShouldPersistTaps: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardShouldPersistTaps", required: false }] }], itemVisiblePercentThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemVisiblePercentThreshold", required: false }] }], endReached: [{ type: i0.Output, args: ["endReached"] }], viewableItemsChanged: [{ type: i0.Output, args: ["viewableItemsChanged"] }], separator: [{ type: i0.ContentChild, args: [i0.forwardRef(() => VirtualListSeparator), { isSignal: true }] }], refreshControl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RefreshControl), { isSignal: true }] }], canvasView: [{ type: i0.ViewChild, args: ['rows', { ...{ read: ElementRef }, isSignal: true }] }] } });
1208
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], estimatedItemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "estimatedItemHeight", required: false }] }], contentPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentPadding", required: false }] }], rowGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowGap", required: false }] }], keyExtractor: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyExtractor", required: false }] }], itemType: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemType", required: false }] }], maintainVisibleContentPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "maintainVisibleContentPosition", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], endReachedThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "endReachedThreshold", required: false }] }], horizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "horizontal", required: false }] }], inverted: [{ type: i0.Input, args: [{ isSignal: true, alias: "inverted", required: false }] }], stickyIndices: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyIndices", required: false }] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], keyboardShouldPersistTaps: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardShouldPersistTaps", required: false }] }], itemVisiblePercentThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemVisiblePercentThreshold", required: false }] }], endReached: [{ type: i0.Output, args: ["endReached"] }], viewableItemsChanged: [{ type: i0.Output, args: ["viewableItemsChanged"] }], separator: [{ type: i0.ContentChild, args: [i0.forwardRef(() => VirtualListSeparator), { isSignal: true }] }], refreshControl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RefreshControl), { isSignal: true }] }], canvasView: [{ type: i0.ViewChild, args: ['rows', { ...{ read: ElementRef }, isSignal: true }] }] } });
1209
+ /** Whether a cached slot style already has every one of these props. */
1210
+ function holds(style, props) {
1211
+ return Object.entries(props).every(([prop, value]) => style[prop] === value);
1212
+ }
1126
1213
  /**
1127
1214
  * Put on each row's view: `<view [virtualListRow]="row">`. Applies the row's style, and reports
1128
1215
  * the row's layout to the list, which is how a list of rows that size themselves learns how tall