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