@symbiote-native/angular 3.1.2 → 3.2.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 (121) hide show
  1. package/build/angular/accessibility-events.d.ts +12 -0
  2. package/build/angular/accessibility-events.js +28 -0
  3. package/build/angular/accessibility-events.js.map +1 -0
  4. package/build/angular/accessibility-inputs.d.ts +40 -0
  5. package/build/angular/accessibility-inputs.js +130 -0
  6. package/build/angular/accessibility-inputs.js.map +1 -0
  7. package/build/angular/bootstrap.d.ts +3 -1
  8. package/build/angular/bootstrap.js +4 -1
  9. package/build/angular/bootstrap.js.map +1 -1
  10. package/build/angular/components/flat-list/flat-list-template.d.ts +1 -0
  11. package/build/angular/components/flat-list/flat-list-template.js +158 -0
  12. package/build/angular/components/flat-list/flat-list-template.js.map +1 -0
  13. package/build/angular/components/flat-list/index.d.ts +6 -61
  14. package/build/angular/components/flat-list/index.js +26 -463
  15. package/build/angular/components/flat-list/index.js.map +1 -1
  16. package/build/angular/components/keyboard-avoiding-view/index.d.ts +4 -40
  17. package/build/angular/components/keyboard-avoiding-view/index.js +7 -131
  18. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  19. package/build/angular/components/modal/index.d.ts +5 -41
  20. package/build/angular/components/modal/index.js +8 -133
  21. package/build/angular/components/modal/index.js.map +1 -1
  22. package/build/angular/components/section-list/index.d.ts +4 -51
  23. package/build/angular/components/section-list/index.js +18 -335
  24. package/build/angular/components/section-list/index.js.map +1 -1
  25. package/build/angular/components/virtualized-list/cell-registry.d.ts +35 -0
  26. package/build/angular/components/virtualized-list/cell-registry.js +133 -0
  27. package/build/angular/components/virtualized-list/cell-registry.js.map +1 -0
  28. package/build/angular/components/virtualized-list/index.d.ts +18 -253
  29. package/build/angular/components/virtualized-list/index.js +112 -1178
  30. package/build/angular/components/virtualized-list/index.js.map +1 -1
  31. package/build/angular/components/virtualized-list/list-a11y-forward.d.ts +1 -0
  32. package/build/angular/components/virtualized-list/list-a11y-forward.js +42 -0
  33. package/build/angular/components/virtualized-list/list-a11y-forward.js.map +1 -0
  34. package/build/angular/components/virtualized-list/list-bags.d.ts +32 -0
  35. package/build/angular/components/virtualized-list/list-bags.js +120 -0
  36. package/build/angular/components/virtualized-list/list-bags.js.map +1 -0
  37. package/build/angular/components/virtualized-list/list-effects.d.ts +31 -0
  38. package/build/angular/components/virtualized-list/list-effects.js +83 -0
  39. package/build/angular/components/virtualized-list/list-effects.js.map +1 -0
  40. package/build/angular/components/virtualized-list/list-events.d.ts +17 -0
  41. package/build/angular/components/virtualized-list/list-events.js +34 -0
  42. package/build/angular/components/virtualized-list/list-events.js.map +1 -0
  43. package/build/angular/components/virtualized-list/list-handle.d.ts +35 -0
  44. package/build/angular/components/virtualized-list/list-handle.js +61 -0
  45. package/build/angular/components/virtualized-list/list-handle.js.map +1 -0
  46. package/build/angular/components/virtualized-list/list-inputs.d.ts +78 -0
  47. package/build/angular/components/virtualized-list/list-inputs.js +150 -0
  48. package/build/angular/components/virtualized-list/list-inputs.js.map +1 -0
  49. package/build/angular/components/virtualized-list/list-props.d.ts +58 -0
  50. package/build/angular/components/virtualized-list/list-props.js +3 -0
  51. package/build/angular/components/virtualized-list/list-props.js.map +1 -0
  52. package/build/angular/components/virtualized-list/list-styles.d.ts +23 -0
  53. package/build/angular/components/virtualized-list/list-styles.js +26 -0
  54. package/build/angular/components/virtualized-list/list-styles.js.map +1 -0
  55. package/build/angular/components/virtualized-list/list-template.d.ts +1 -0
  56. package/build/angular/components/virtualized-list/list-template.js +176 -0
  57. package/build/angular/components/virtualized-list/list-template.js.map +1 -0
  58. package/build/angular/components/virtualized-section-list/index.d.ts +13 -70
  59. package/build/angular/components/virtualized-section-list/index.js +22 -455
  60. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  61. package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -0
  62. package/build/angular/components/virtualized-section-list/section-list-template.js +107 -0
  63. package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -0
  64. package/build/angular/finalization-registry.d.ts +4 -0
  65. package/build/angular/finalization-registry.js +17 -0
  66. package/build/angular/finalization-registry.js.map +1 -0
  67. package/build/angular/index.d.ts +5 -1
  68. package/build/angular/index.js +5 -1
  69. package/build/angular/index.js.map +1 -1
  70. package/build/angular/native-view-base.d.ts +19 -0
  71. package/build/angular/native-view-base.js +48 -0
  72. package/build/angular/native-view-base.js.map +1 -0
  73. package/build/angular/render/index.d.ts +2 -1
  74. package/build/angular/render/index.js +46 -73
  75. package/build/angular/render/index.js.map +1 -1
  76. package/build/angular/services/connect-watched-signal.d.ts +2 -0
  77. package/build/angular/services/connect-watched-signal.js +9 -0
  78. package/build/angular/services/connect-watched-signal.js.map +1 -0
  79. package/build/angular/services/create-event-value-hook.d.ts +3 -0
  80. package/build/angular/services/create-event-value-hook.js +17 -0
  81. package/build/angular/services/create-event-value-hook.js.map +1 -0
  82. package/build/angular/services/create-resource-hook.d.ts +7 -0
  83. package/build/angular/services/create-resource-hook.js +20 -0
  84. package/build/angular/services/create-resource-hook.js.map +1 -0
  85. package/build/angular/services/index.d.ts +4 -0
  86. package/build/angular/services/index.js +4 -0
  87. package/build/angular/services/index.js.map +1 -1
  88. package/build/angular/services/permissions-service-base.d.ts +14 -0
  89. package/build/angular/services/permissions-service-base.js +38 -0
  90. package/build/angular/services/permissions-service-base.js.map +1 -0
  91. package/package.json +6 -6
  92. package/src/accessibility-events.ts +13 -0
  93. package/src/accessibility-inputs.ts +89 -0
  94. package/src/bootstrap.ts +6 -2
  95. package/src/components/flat-list/flat-list-template.ts +159 -0
  96. package/src/components/flat-list/index.ts +30 -307
  97. package/src/components/keyboard-avoiding-view/index.ts +4 -78
  98. package/src/components/modal/index.ts +7 -80
  99. package/src/components/section-list/index.ts +21 -172
  100. package/src/components/virtualized-list/cell-registry.ts +199 -0
  101. package/src/components/virtualized-list/index.ts +145 -1181
  102. package/src/components/virtualized-list/list-a11y-forward.ts +42 -0
  103. package/src/components/virtualized-list/list-bags.ts +142 -0
  104. package/src/components/virtualized-list/list-effects.ts +116 -0
  105. package/src/components/virtualized-list/list-events.ts +31 -0
  106. package/src/components/virtualized-list/list-handle.ts +92 -0
  107. package/src/components/virtualized-list/list-inputs.ts +149 -0
  108. package/src/components/virtualized-list/list-props.ts +81 -0
  109. package/src/components/virtualized-list/list-styles.ts +62 -0
  110. package/src/components/virtualized-list/list-template.ts +175 -0
  111. package/src/components/virtualized-section-list/index.ts +40 -351
  112. package/src/components/virtualized-section-list/section-list-template.ts +108 -0
  113. package/src/finalization-registry.ts +18 -0
  114. package/src/index.ts +12 -1
  115. package/src/native-view-base.ts +51 -0
  116. package/src/render/index.ts +76 -77
  117. package/src/services/connect-watched-signal.ts +15 -0
  118. package/src/services/create-event-value-hook.ts +24 -0
  119. package/src/services/create-resource-hook.ts +32 -0
  120. package/src/services/index.ts +4 -0
  121. package/src/services/permissions-service-base.ts +46 -0
@@ -1,339 +1,66 @@
1
- // VirtualizedList, the Angular lifecycle half. The windowing engine (offset table, window
2
- // compute, batch throttle, viewability, edge-reached, the child PLAN, the imperative-handle
3
- // geometry) lives in @symbiote-native/components/state, shared verbatim with the React and Vue adapters
4
- // so every adapter keeps the same feature surface. Here Angular supplies only the lifecycle: plain fields
5
- // for scroll offset / viewport / measurement bumps, a once-per-CD metrics + view recompute in
6
- // ngDoCheck (the Angular twin of React render / Vue `computed` — it owns the controlled
7
- // committedWindow throttle and assembles the windowed cells before the template bindings are read),
8
- // and the after-commit work (batch fill, onEndReached/onStartReached, viewability, initialScroll, MVCP)
9
- // in ngAfterViewChecked. OnPush + ChangeDetectorRef.markForCheck() drives re-render off the native
10
- // scroll/layout/measure callbacks (they fire outside Angular's own event bindings, so they must
11
- // mark the view dirty). This is the Angular twin of the React useReducer/useEffect and Vue
12
- // ref/computed/watch over the same shared functions. It writes the `scroll-view`/
13
- // `horizontal-scroll-view` TAG directly, exactly as the React/Vue lists drive their scroll tag.
14
- //
15
- // Per-item rendering is TEMPLATES, not a callback: React/Vue `renderItem: (info) => element` does
16
- // not translate to Angular. The app supplies the cell via `<ng-template vListItem>` (and the
17
- // header/footer/empty/separator slots via vListHeader/vListFooter/vListEmpty/vListSeparator); the
18
- // list captures them with @ContentChild and stamps the WINDOWED slice through VListOutletDirective
19
- // (a core-only NgTemplateOutlet twin — @angular/common is not a dependency). See ./directives.ts.
20
- // Every React/Vue behavior is present (windowing, onEndReached, viewability, headers/footers/empty/
21
- // separators, refresh, imperative scroll, horizontal, inverted, MVCP); only the cell AUTHORING
22
- // shape differs, since a callback prop returning an element has no Angular equivalent.
23
- //
24
- // Lists have no Descriptor render fn — the cell content is the framework's own children.
25
- // Cells/spacers are plain view host elements.
26
- import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, Output, TemplateRef, ViewChild, computed, inject, signal, } from '@angular/core';
27
- import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildViewabilityPairs, computeWindow, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveAccessibilityProps, resolveItemKey, buildScrollViewHandle, } from '@symbiote-native/components';
28
- import { dlog, flattenStyle, isSymbioteNode, } from '@symbiote-native/engine';
1
+ // Angular lifecycle half of VirtualizedList, the windowing engine is shared with React and Vue
2
+ // The bound surface is in ./list-inputs.ts, cells in ./cell-registry.ts, effects in
3
+ // ./list-effects.ts, styles in ./list-styles.ts and the authoring templates in ./directives.ts
4
+ import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, ViewChild, inject, } from '@angular/core';
5
+ import { EMPTY_OFFSET, FIRST_INDEX, LIST_ACTION_KIND, buildListPlan, buildScrollViewHandle, buildViewabilityPairs, computeWindow, createInitialListState, listEffectSignature, reduceList, resolveItemKey, } from '@symbiote-native/components';
6
+ import { dlog, isSymbioteNode, } from '@symbiote-native/engine';
29
7
  import { countAngular } from '../../diagnostics';
30
8
  import { anchorHostStyle, SymbioteHostPropsDirective, SymbioteStyleInputDirective, ViewHost, } from '../../primitives';
31
- import { VListEmptyDirective, VListFooterDirective, VListHeaderDirective, VListItemDirective, VListOutletDirective, VListSeparatorDirective, } from './directives';
32
- import { gateWanted, injectGateDemandAbove, } from '../../gate-demand';
9
+ import { CellRegistry } from './cell-registry';
10
+ import { VListOutletDirective } from './directives';
11
+ import { ListEffectRunner } from './list-effects';
12
+ import { VirtualizedListBagsBase } from './list-bags';
13
+ import { resolveListStyles } from './list-styles';
14
+ import { VIRTUALIZED_LIST_TEMPLATE } from './list-template';
33
15
  import * as i0 from "@angular/core";
34
16
  import * as i1 from "../../primitives/shared";
35
17
  export { VListEmptyDirective, VListFooterDirective, VListHeaderDirective, VListItemDirective, VListSeparatorDirective, } from './directives';
36
- // The scroll body is identical on both axes; only the outer tag differs (the axis comes from the
37
- // TAG, never a prop — `<horizontal-scroll-view>` vs `<scroll-view>`, matching every other adapter,
38
- // `.claude/rules/host-primitive-tier.md`). No shared-outlet primitive exists to de-duplicate this
39
- // (this package's own NgTemplateOutlet twin is keyed on a per-cell context, not a static block, and
40
- // ngc's partial evaluator needs the decorator's `template` to be one literal), so the body is
41
- // duplicated across the two branches — the same shape Svelte's own `{#if horizontal}` takes.
42
- export class VirtualizedList {
18
+ export class VirtualizedList extends VirtualizedListBagsBase {
43
19
  constructor() {
44
- // The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
45
- // `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
46
- // shaped scroll-view/refresh-control bag keys are fed from these.
47
- this.endReached = new EventEmitter();
48
- this.startReached = new EventEmitter();
49
- this.refresh = new EventEmitter();
50
- this.viewableItemsChanged = new EventEmitter();
51
- this.scrollToIndexFailed = new EventEmitter();
52
- this.accessibilityAction = new EventEmitter();
53
- this.accessibilityTap = new EventEmitter();
54
- this.magicTap = new EventEmitter();
55
- this.accessibilityEscape = new EventEmitter();
56
- // This wrapper binds the four gated accessibility events on the component it renders, which
57
- // Angular forces to be unconditional and which would light that component's gates on every
58
- // instance. It answers for them instead — see `gate-demand.ts`.
59
- this.gateDemandAbove = injectGateDemandAbove();
60
- // `buildScrollViewHandle` reads the node lazily through the getter above and no-ops before
61
- // commit — the same function every other adapter feeds from its own host-instance accessor.
20
+ super(...arguments);
62
21
  this.scrollHandle = buildScrollViewHandle(() => this.scrollNode);
63
- // --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
22
+ // Template-bound view state, assembled by `recomputeView` in `ngDoCheck`
64
23
  this.itemCount = EMPTY_OFFSET;
65
24
  this.windowCells = [];
66
- // The nearest sticky-header cell force-mounted outside [first,last] (plan.forcedStickyCell), the
67
- // Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
68
- // leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
69
- // buildListPlan's doc comment for the exact child order this assumes.
25
+ // The nearest sticky cell mounted outside the window, `gapSpacerStyle` fills up to the window
70
26
  this.forcedStickyCell = null;
71
27
  this.leadingSpacerStyle = null;
72
28
  this.gapSpacerStyle = null;
73
29
  this.trailingSpacerStyle = null;
74
30
  this.cellStyle = undefined;
75
- // Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
76
- // ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
77
- // `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
78
- // is always pre-flattened via the engine's own flattenStyle, never left as an array.
79
- this.resolvedStyle = undefined;
80
- this.resolvedContentContainerStyle = undefined;
81
- this.resolvedMaintainVisibleContentPosition = undefined;
82
- // The offset we are imperatively driving native to before the scroll handle attaches (rides down
83
- // as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
84
- this.commandedOffset = undefined;
85
- // The one folded state cell (the Angular twin of React's stateRef): scroll offset, viewport,
86
- // measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
87
- // driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
88
- // state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
31
+ // `renderVersion` bumps on every transition that changes render state, for the recompute dedup
89
32
  this.listState = createInitialListState();
90
33
  this.renderVersion = EMPTY_OFFSET;
91
- this.cellMeasures = new Map();
92
- // A cell's context object is the ONLY thing its stamped view reads, and VListOutletDirective
93
- // refreshes that view whenever the object's IDENTITY changes. recomputeView runs on every frame
94
- // the window moves, so minting a fresh context per cell made a four-row slide cost a refresh of
95
- // EVERY cell in the window. MEASURED on PATH B geometry (544 entries, 320px viewport, RN
96
- // defaults), per fling frame: 233-cell window, 4 cells genuinely entering, 228 outlet updates —
97
- // each one a copyContextFields plus a markForCheck that walks to the root. Solid, same geometry,
98
- // same shared reducer, same engine output: 4. Handing back the SAME object for a cell whose item
99
- // and index did not move keeps `[vListOutletContext]` reference-equal, so ngOnChanges never fires
100
- // for it. The fast adapters get this by construction — Solid's <For> is keyed on the cell KEY
101
- // strings precisely so "the plan's freshly-built cell objects" cannot rebuild every row
102
- // (adapters/solid/src/components/virtualized-list/shared.tsx, the cellKeys memo).
103
- this.cellContexts = new Map();
104
- this.nextCellContexts = new Map();
105
- // Everything a cell's CONTENT can depend on that is not its own item identity. RN's contract for
106
- // "re-render the cells, the data object itself did not change" is extraData; without it here an
107
- // extraData flip would reuse every context and repaint nothing.
108
- this.cellContextEpoch = undefined;
109
- this.separatorOverrides = new Map();
110
- // The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
111
- // hands back a delay.
112
- this.viewableTimer = null;
113
- this.batchTimer = null;
114
- // Dedupes the after-commit effects: they run only when the windowing signature changed.
34
+ this.cells = new CellRegistry({
35
+ itemAt: index => this.getItem(this.data, index),
36
+ itemCount: () => this.listState.metrics.count,
37
+ isHorizontal: () => this.isHorizontal,
38
+ markForCheck: () => this.cdr.markForCheck(),
39
+ measured: (index, length, offset) => this.dispatch({ kind: LIST_ACTION_KIND.measure, index, length, offset }),
40
+ });
41
+ this.effects = new ListEffectRunner({
42
+ scrollToPixel: (offset, animated) => this.scrollToPixel(offset, animated),
43
+ dispatch: action => this.dispatch(action),
44
+ endReached: this.endReached,
45
+ startReached: this.startReached,
46
+ scrollToIndexFailed: this.scrollToIndexFailed,
47
+ });
115
48
  this.lastEffectSignature = '';
116
- // Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
117
- // windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
118
- // including ones triggered by something else entirely in the app. A fresh context object flows
119
- // into VListOutletDirective, which (correctly) sees its `context` @Input() change and calls
120
- // `viewRef.markForCheck()` — which reschedules ANOTHER change-detection tick via the zoneless
121
- // scheduler (adapters/angular/src/render.ts). Recomputing unconditionally therefore free-runs
122
- // forever the moment a list actually has cells to stamp (a real, previously-dormant bug only
123
- // exposed once flat-list.test.ts's projection fix let cells render for the first time — see that
124
- // test's comment). Guarding recompute behind "did anything relevant actually change" breaks the
125
- // cycle: unrelated CD passes reuse the same windowCells/context identities, so
126
- // VListOutletDirective's ngOnChanges sees no change and never reschedules.
49
+ // Without this dedup a rebuilt cell context reschedules another tick and the list free-runs
127
50
  this.lastRecompute = undefined;
128
51
  this.cdr = inject(ChangeDetectorRef);
129
- // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
130
- // onto (see anchorHostStyle's doc comment) — NOT `scrollHostRef` above, which targets the real
131
- // inner `<scroll-view>`/`<horizontal-scroll-view>` tag one level down.
52
+ // This component's own host, the anchor that `class="..."` at the use site resolves onto
132
53
  this.elementRef = inject(ElementRef);
133
- // Bridges the non-reactive @Input fields into the reactive graph so `foldedAccessibility` below
134
- // can memoize a bag derived from them (read its own comment for why signal inputs are not an
135
- // option while this package's unit suite runs on JIT).
136
- //
137
- // ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
138
- // (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
139
- // callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
140
- // would memoize a window that stops re-computing mid-scroll. Make that state its own signal
141
- // instead of widening this one.
142
- this.inputsRevision = signal(0, /* @ts-ignore */
143
- ...(ngDevMode ? [{ debugName: "inputsRevision" }] : /* istanbul ignore next */ []));
144
- // MEASURED (while ScrollView was still a wrapper): the template fed 20 separate inputs off this
145
- // one bag, and Angular does not cache a getter across binding expressions - as a getter it
146
- // rebuilt the object 20 times per refresh, and every consumer saw a fresh reference, so all 20
147
- // reported "changed" and wrote through. A computed evaluates once and hands back the SAME object
148
- // until an input actually changes. Every dependency below is an @Input, which is what makes
149
- // inputsRevision a complete dependency set here — `scrollViewBag()` reuses this result rather
150
- // than re-running the fold.
151
- this.foldedAccessibility = computed(() => {
152
- this.inputsRevision();
153
- return resolveAccessibilityProps({
154
- testID: this.testID,
155
- nativeID: this.nativeID,
156
- accessible: this.accessible,
157
- accessibilityLabel: this.accessibilityLabel,
158
- accessibilityHint: this.accessibilityHint,
159
- accessibilityRole: this.accessibilityRole,
160
- accessibilityState: this.accessibilityState,
161
- accessibilityValue: this.accessibilityValue,
162
- accessibilityActions: this.accessibilityActions,
163
- accessibilityLabelledBy: this.accessibilityLabelledBy,
164
- importantForAccessibility: this.importantForAccessibility,
165
- accessibilityLiveRegion: this.accessibilityLiveRegion,
166
- screenReaderFocusable: this.screenReaderFocusable,
167
- accessibilityViewIsModal: this.accessibilityViewIsModal,
168
- accessibilityElementsHidden: this.accessibilityElementsHidden,
169
- accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
170
- accessibilityLanguage: this.accessibilityLanguage,
171
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
172
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
173
- accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
174
- role: this.role,
175
- 'aria-label': this.ariaLabel,
176
- 'aria-labelledby': this.ariaLabelledBy,
177
- 'aria-live': this.ariaLive,
178
- 'aria-hidden': this.ariaHidden,
179
- 'aria-busy': this.ariaBusy,
180
- 'aria-checked': this.ariaChecked,
181
- 'aria-disabled': this.ariaDisabled,
182
- 'aria-expanded': this.ariaExpanded,
183
- 'aria-selected': this.ariaSelected,
184
- 'aria-modal': this.ariaModal,
185
- 'aria-valuemax': this.ariaValueMax,
186
- 'aria-valuemin': this.ariaValueMin,
187
- 'aria-valuenow': this.ariaValueNow,
188
- 'aria-valuetext': this.ariaValueText,
189
- });
190
- }, /* @ts-ignore */
191
- ...(ngDevMode ? [{ debugName: "foldedAccessibility" }] : /* istanbul ignore next */ []));
192
- // onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
193
- // through it, which an @Output() can't carry), so this is a stable arrow field read straight into
194
- // scrollViewBag()'s `onScroll`. onLayout is unconditional too — this list always needs its own
195
- // layout for windowing, regardless of whether an app subscribes to anything.
196
- this.onScrollTick = (event) => {
197
- countAngular('scrollTicks');
198
- const offset = readScrollOffset(event, this.isHorizontal);
199
- if (offset === undefined)
200
- return;
201
- dlog(`Angular VirtualizedList onScroll offset=${offset}`);
202
- // A real native scroll supersedes any pending commanded offset.
203
- this.commandedOffset = undefined;
204
- this.dispatch({ kind: 'scroll', offset });
205
- // Compose, don't clobber: internal windowing ran first, now the user's onScroll.
206
- this.onScroll?.(event);
207
- };
208
- this.onLayoutTick = (event) => {
209
- const length = readLayoutLength(event, this.isHorizontal);
210
- if (length === undefined)
211
- return;
212
- dlog(`Angular VirtualizedList onLayout viewport=${length}`);
213
- this.dispatch({ kind: 'layout', length });
214
- };
215
- // RefreshControl's refresh and the scroll tag's accessibility events reach here as plain
216
- // listener-prop values in scrollViewBag()/refreshControlBag() — these fields just adapt
217
- // VirtualizedList's own @Output() into a plain re-emit callback.
218
- this.handleRefresh = () => {
219
- this.refresh.emit();
220
- };
221
- this.accessibilityActionTick = (event) => {
222
- this.accessibilityAction.emit(event);
223
- };
224
- this.accessibilityTapTick = (event) => {
225
- this.accessibilityTap.emit(event);
226
- };
227
- this.magicTapTick = (event) => {
228
- this.magicTap.emit(event);
229
- };
230
- this.accessibilityEscapeTick = (event) => {
231
- this.accessibilityEscape.emit(event);
232
- };
233
54
  this.keyFor = (index) => {
234
55
  const item = this.getItem(this.data, index);
235
56
  return resolveItemKey(item, index, this.keyExtractor);
236
57
  };
237
58
  }
238
- wantsGate(name) {
239
- return gateWanted(this.gateDemandAbove, name, this[name]);
240
- }
241
- get cellTemplate() {
242
- return this.itemTemplate ?? this.itemDir?.templateRef;
243
- }
244
- get separatorTemplate() {
245
- return this.itemSeparatorTemplate ?? this.separatorDir?.templateRef;
246
- }
247
59
  get scrollNode() {
248
60
  const node = this.scrollHostRef?.nativeElement;
249
61
  return isSymbioteNode(node) ? node : null;
250
62
  }
251
- get shouldRenderRefreshControl() {
252
- return this.refreshRequested ?? this.refresh.observed;
253
- }
254
- // The flat bag fed to the scroll tag via `[symbioteHostProps]`. A plain method, not `computed()`:
255
- // most of these fields (resolvedStyle, renderedStickyIndices, commandedOffset, …) are recomputed
256
- // in ngDoCheck/dispatch as ordinary fields, not signals, so a computed() memoized only against
257
- // `inputsRevision` would go stale the moment one of them changed outside an @Input() write. The
258
- // directive itself diffs per key before writing to the engine (`primitives/shared.ts`), so a
259
- // fresh object every CD pass costs nothing beyond the literal allocation.
260
- scrollViewBag() {
261
- return {
262
- ...this.foldedAccessibility(),
263
- style: this.resolvedStyle,
264
- contentContainerStyle: this.resolvedContentContainerStyle,
265
- onScroll: this.onScrollTick,
266
- onLayout: this.onLayoutTick,
267
- onScrollBeginDrag: this.onScrollBeginDrag,
268
- onScrollEndDrag: this.onScrollEndDrag,
269
- onMomentumScrollBegin: this.onMomentumScrollBegin,
270
- onMomentumScrollEnd: this.onMomentumScrollEnd,
271
- scrollEventThrottle: this.scrollEventThrottle,
272
- keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
273
- keyboardDismissMode: this.keyboardDismissMode,
274
- // RN's VirtualizedList spreads its props onto the ScrollView, this one included.
275
- removeClippedSubviews: this.removeClippedSubviews,
276
- nestedScrollEnabled: this.nestedScrollEnabled,
277
- // VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
278
- isInvertedVirtualizedList: this.isInverted ? true : undefined,
279
- contentOffset: this.commandedOffset,
280
- // `stickyHeaderIndices` is deliberately NOT forwarded — it numbers the scroll view's PAINT
281
- // children, and a windowed list paints a header, a spacer and a slice, so the positions move
282
- // every time the window slides and the behavior re-wraps a different child each pass. The
283
- // cells carry the `sticky-header` TAG instead, which pins by DOCUMENT order.
284
- maintainVisibleContentPosition: this.resolvedMaintainVisibleContentPosition,
285
- // The four boolean-gated Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): the
286
- // TAG has no `.observed` of its own, so `wantsGate` (own subscriber, or the demand from
287
- // whatever wraps this list) is what decides whether the flag reaches Fabric at all.
288
- onAccessibilityAction: this.wantsGate('accessibilityAction')
289
- ? this.accessibilityActionTick
290
- : undefined,
291
- onAccessibilityTap: this.wantsGate('accessibilityTap')
292
- ? this.accessibilityTapTick
293
- : undefined,
294
- onMagicTap: this.wantsGate('magicTap') ? this.magicTapTick : undefined,
295
- onAccessibilityEscape: this.wantsGate('accessibilityEscape')
296
- ? this.accessibilityEscapeTick
297
- : undefined,
298
- };
299
- }
300
- // The `refresh-control` tag's own bag — an ordinary first child, re-parented per platform by the
301
- // scroll-view behavior (`../refresh-control-props.ts`). `onRefresh` is a plain listener prop, not
302
- // a gated one: RefreshControl's `refresh` is unaffected by `fabric-boolean-event-gates.md`.
303
- refreshControlBag() {
304
- return {
305
- refreshing: this.refreshing ?? false,
306
- progressViewOffset: this.progressViewOffset,
307
- onRefresh: this.handleRefresh,
308
- };
309
- }
310
- get isHorizontal() {
311
- return this.horizontal === true;
312
- }
313
- get isInverted() {
314
- return this.inverted === true;
315
- }
316
- get windowSizeValue() {
317
- return this.windowSize ?? DEFAULT_WINDOW_SIZE;
318
- }
319
- get initialNumToRenderValue() {
320
- return this.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER;
321
- }
322
- get maxToRenderPerBatchValue() {
323
- return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
324
- }
325
- get updateCellsBatchingPeriodValue() {
326
- return (this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD);
327
- }
328
- get onEndReachedThresholdValue() {
329
- return this.onEndReachedThreshold;
330
- }
331
- get onStartReachedThresholdValue() {
332
- return this.onStartReachedThreshold;
333
- }
334
- // The reducer inputs, folded off the @Input()s each call. The edge/viewability listeners map to
335
- // `.observed` (Angular's "is anyone bound to this @Output()"), so the reducer emits only when the
336
- // consumer listens — the same on-demand gating the prop-callback era had.
63
+ // Edge and viewability listeners map to `.observed`, so the reducer emits only if listened to
337
64
  buildInputs() {
338
65
  return {
339
66
  data: this.data,
@@ -346,8 +73,8 @@ export class VirtualizedList {
346
73
  initialNumToRender: this.initialNumToRenderValue,
347
74
  maxToRenderPerBatch: this.maxToRenderPerBatchValue,
348
75
  updateCellsBatchingPeriod: this.updateCellsBatchingPeriodValue,
349
- onEndReachedThreshold: this.onEndReachedThresholdValue,
350
- onStartReachedThreshold: this.onStartReachedThresholdValue,
76
+ onEndReachedThreshold: this.onEndReachedThreshold,
77
+ onStartReachedThreshold: this.onStartReachedThreshold,
351
78
  onEndReachedActive: this.endReached.observed,
352
79
  onStartReachedActive: this.startReached.observed,
353
80
  viewabilityPairs: buildViewabilityPairs(this.viewableItemsChanged.observed
@@ -357,12 +84,11 @@ export class VirtualizedList {
357
84
  initialScrollIndex: this.initialScrollIndex,
358
85
  };
359
86
  }
360
- // Map a native event / imperative call to an action, run the returned effects, and mark the view
361
- // dirty when render state changed (the native callbacks fire outside Angular's own bindings).
87
+ // Native callbacks fire outside Angular's bindings, so a changed state must mark the view dirty
362
88
  dispatch(action) {
363
89
  const inputs = this.buildInputs();
364
90
  const result = reduceList(this.listState, action, inputs);
365
- this.runEffects(result.effects, inputs);
91
+ this.effects.run(result.effects, inputs);
366
92
  if (!result.changed)
367
93
  return;
368
94
  this.renderVersion += 1;
@@ -371,30 +97,14 @@ export class VirtualizedList {
371
97
  countAngular('listMarks');
372
98
  this.cdr.markForCheck();
373
99
  }
374
- // The reducer reports `changed` for EVERY scroll offset - it has to, since the offset feeds
375
- // end-reached distance, viewability and the batch-fill timer, all handled by runEffects. The
376
- // TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the whole
377
- // ancestor chain 60 times a second on a sticky screen for a window that had not moved.
378
- //
379
- // So the question here is "would a render leave the window exactly where it is", and it is asked
380
- // of the state as it is NOW. It must NOT be answered by comparing the last-rendered window with
381
- // the one before it: that window was derived from the PREVIOUS scroll offset, so once two
382
- // consecutive derives agree the comparison reads "settled" for every offset that ever follows -
383
- // no mark, no CD, no ngDoCheck, no refresh-metrics, so the window can never move again and the
384
- // list goes permanently deaf mid-scroll (device: examples/angular Benchmark sticky path B froze
385
- // at exactly the window the first viewport asked for, blank below it).
386
- //
387
- // computeWindow re-run over the CACHED offset table answers it directly. Two scans, no
388
- // allocation - buildOffsets, the O(count) allocating half, still runs once per render only. It is
389
- // the same function the render will use, so this cannot drift away from the window it predicts.
100
+ // Asks whether a render would leave the window where it is, of the state as it is now
101
+ // Comparing the last two rendered windows would read settled forever and the list goes deaf
390
102
  isWindowSettled(action) {
391
- // A measurement rewrites the very offset table the prediction reads, so it has no cached
392
- // geometry to answer from.
393
- if (action.kind === 'measure')
103
+ // A measurement rewrites the offset table the prediction reads
104
+ if (action.kind === LIST_ACTION_KIND.measure)
394
105
  return false;
395
106
  const m = this.listState.metrics;
396
- // Mid-fill: the committed window is still climbing toward target one batch step per render, and
397
- // only a render advances it. Skipping the mark here is what stalls the incremental fill.
107
+ // Mid-fill the window climbs one batch step per render, only a render advances it
398
108
  if (m.first !== m.target.first || m.last !== m.target.last)
399
109
  return false;
400
110
  const next = computeWindow(m.count, m.offsets, m.lengths, this.listState.scrollOffset, this.listState.viewportLength, this.windowSizeValue, this.initialNumToRenderValue);
@@ -404,80 +114,7 @@ export class VirtualizedList {
404
114
  `[${next.first}, ${next.last}] at offset ${this.listState.scrollOffset}`);
405
115
  return isSettled;
406
116
  }
407
- runEffects(effects, inputs) {
408
- for (const effect of effects) {
409
- switch (effect.kind) {
410
- case 'scroll-to':
411
- this.scrollToPixel(effect.offset, effect.animated);
412
- break;
413
- case 'fire-end-reached':
414
- this.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
415
- break;
416
- case 'fire-start-reached':
417
- this.startReached.emit({
418
- distanceFromStart: effect.distanceFromStart,
419
- });
420
- break;
421
- case 'fire-scroll-to-index-failed':
422
- this.scrollToIndexFailed.emit({
423
- index: effect.index,
424
- highestMeasuredFrameIndex: effect.highestMeasuredFrameIndex,
425
- averageItemLength: effect.averageItemLength,
426
- });
427
- break;
428
- case 'schedule-refill': {
429
- if (this.batchTimer !== null)
430
- clearTimeout(this.batchTimer);
431
- this.batchTimer = setTimeout(() => {
432
- this.batchTimer = null;
433
- this.dispatch({ kind: 'batch-tick' });
434
- }, effect.delay);
435
- break;
436
- }
437
- case 'fire-viewable': {
438
- const pairs = inputs.viewabilityPairs;
439
- const info = effect.info;
440
- const map = effect.map;
441
- const fire = () => {
442
- for (const pair of pairs) {
443
- pair.onViewableItemsChanged({
444
- ...info,
445
- viewabilityConfig: pair.viewabilityConfig,
446
- });
447
- }
448
- this.dispatch({ kind: 'viewable-fired', map });
449
- };
450
- if (this.viewableTimer !== null) {
451
- clearTimeout(this.viewableTimer);
452
- this.viewableTimer = null;
453
- }
454
- if (effect.delay > EMPTY_OFFSET) {
455
- this.viewableTimer = setTimeout(() => {
456
- this.viewableTimer = null;
457
- fire();
458
- }, effect.delay);
459
- }
460
- else {
461
- fire();
462
- }
463
- break;
464
- }
465
- }
466
- }
467
- }
468
- // foldedAccessibility reads plain @Input fields, which are NOT reactive on their own - this bump
469
- // is what tells that computed an input changed. It must stay in ngOnChanges: that is the single
470
- // moment Angular has finished writing every changed input for this pass, and it runs before
471
- // ngDoCheck below, so the recompute already sees the current values.
472
- ngOnChanges() {
473
- this.inputsRevision.update(revision => revision + 1);
474
- }
475
- // Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
476
- // template-bound fields render this pass — computing them in ngAfterContentChecked instead would
477
- // trip ExpressionChangedAfterItHasBeenChecked). The dedup guard runs refresh-metrics (which owns
478
- // the controlled committedWindow throttle) only when something relevant changed, so the window
479
- // grows exactly one step per meaningful CD. The projected templates (ContentChild) are resolved
480
- // from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
117
+ // Runs before the bindings are read, a later hook would trip the changed-after-checked guard
481
118
  ngDoCheck() {
482
119
  countAngular('listChecks');
483
120
  const recomputeInputs = [
@@ -493,12 +130,9 @@ export class VirtualizedList {
493
130
  this.stickyHeaderIndices,
494
131
  this.maintainVisibleContentPosition,
495
132
  this.style,
496
- // `recomputeView` folds this into the committed style, but it arrives through
497
- // addClass/removeClass, never as an @Input - so without it here no entry moves on a class
498
- // toggle and the gate skips the recompute forever. Reference-stable, so the dedup holds.
133
+ // Arrives through `addClass` and `removeClass`, never as an input
499
134
  anchorHostStyle(this.elementRef),
500
135
  this.contentContainerStyle,
501
- // Folds scroll / layout / measure / batch-tick: dispatch bumps renderVersion on any change.
502
136
  this.renderVersion,
503
137
  this.headerDir !== undefined,
504
138
  this.footerDir !== undefined,
@@ -512,9 +146,8 @@ export class VirtualizedList {
512
146
  previous.every((value, index) => value === recomputeInputs[index])) {
513
147
  return;
514
148
  }
515
- // The single derive-per-CD: recompute the window off the current state before the view reads it.
516
149
  countAngular('listRecomputes');
517
- reduceList(this.listState, { kind: 'refresh-metrics' }, this.buildInputs());
150
+ reduceList(this.listState, { kind: LIST_ACTION_KIND.refreshMetrics }, this.buildInputs());
518
151
  this.recomputeView();
519
152
  }
520
153
  ngAfterViewChecked() {
@@ -523,72 +156,29 @@ export class VirtualizedList {
523
156
  return;
524
157
  this.lastEffectSignature = signature;
525
158
  const inputs = this.buildInputs();
526
- const result = reduceList(this.listState, { kind: 'commit' }, inputs);
527
- this.runEffects(result.effects, inputs);
159
+ const result = reduceList(this.listState, { kind: LIST_ACTION_KIND.commit }, inputs);
160
+ this.effects.run(result.effects, inputs);
528
161
  }
529
162
  ngOnDestroy() {
530
- if (this.viewableTimer !== null)
531
- clearTimeout(this.viewableTimer);
532
- if (this.batchTimer !== null)
533
- clearTimeout(this.batchTimer);
163
+ this.effects.dispose();
534
164
  }
535
165
  recomputeView() {
536
166
  const m = this.listState.metrics;
537
167
  this.itemCount = m.count;
538
- const hasHeader = this.headerDir !== undefined;
539
- const hasSeparators = this.separatorTemplate !== undefined;
540
- const stickySet = this.stickyHeaderIndices !== undefined
541
- ? new Set(this.stickyHeaderIndices)
542
- : undefined;
543
- this.resolvedContentContainerStyle = this.isHorizontal
544
- ? [this.contentContainerStyle, { width: m.total }]
545
- : this.contentContainerStyle;
546
- this.resolvedStyle = flattenStyle([
547
- anchorHostStyle(this.elementRef),
548
- // VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
549
- this.isInverted
550
- ? [this.isHorizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE, this.style]
551
- : this.style,
552
- ]);
553
- this.cellStyle = this.isInverted
554
- ? this.isHorizontal
555
- ? INVERTED_X_STYLE
556
- : INVERTED_Y_STYLE
557
- : undefined;
558
- this.resolvedMaintainVisibleContentPosition =
559
- this.maintainVisibleContentPosition === undefined
560
- ? undefined
561
- : {
562
- ...this.maintainVisibleContentPosition,
563
- minIndexForVisible: this.maintainVisibleContentPosition.minIndexForVisible +
564
- (hasHeader ? 1 : 0),
565
- };
566
- // A change in anything a cell's content reads other than its own item invalidates every cached
567
- // context: identity reuse is only sound while the reused object still describes the cell.
568
- const epoch = [
168
+ this.applyStyles(m.total);
169
+ this.cells.beginPass([
569
170
  this.data,
570
171
  this.extraData,
571
172
  this.getItem,
572
173
  this.keyExtractor,
573
- ];
574
- const previousEpoch = this.cellContextEpoch;
575
- this.cellContextEpoch = epoch;
576
- if (previousEpoch === undefined ||
577
- previousEpoch.length !== epoch.length ||
578
- !previousEpoch.every((value, index) => value === epoch[index])) {
579
- this.cellContexts.clear();
580
- }
174
+ ]);
581
175
  if (m.count === FIRST_INDEX) {
582
- this.cellContexts.clear();
583
- this.nextCellContexts.clear();
584
- this.windowCells = [];
585
- this.forcedStickyCell = null;
586
- this.leadingSpacerStyle = null;
587
- this.gapSpacerStyle = null;
588
- this.trailingSpacerStyle = null;
589
- dlog(`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`);
176
+ this.showEmpty();
590
177
  return;
591
178
  }
179
+ const stickySet = this.stickyHeaderIndices !== undefined
180
+ ? new Set(this.stickyHeaderIndices)
181
+ : undefined;
592
182
  const plan = buildListPlan({
593
183
  count: m.count,
594
184
  first: m.first,
@@ -598,121 +188,67 @@ export class VirtualizedList {
598
188
  total: m.total,
599
189
  keyFor: this.keyFor,
600
190
  stickyIndices: stickySet,
601
- hasHeader,
191
+ hasHeader: this.headerDir !== undefined,
602
192
  });
603
- this.leadingSpacerStyle =
604
- plan.leadingExtent > EMPTY_OFFSET
605
- ? this.spacerStyle(plan.leadingExtent)
606
- : null;
607
- this.gapSpacerStyle =
608
- plan.gapExtent > EMPTY_OFFSET ? this.spacerStyle(plan.gapExtent) : null;
609
- this.trailingSpacerStyle =
610
- plan.trailingExtent > EMPTY_OFFSET
611
- ? this.spacerStyle(plan.trailingExtent)
612
- : null;
613
- // `plan.stickyChildPositions` is deliberately NOT read — it was the input to the index form.
614
- dlog(`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
615
- `childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
616
- `forcedStickyCell=${plan.forcedStickyCell === undefined ? 'none' : plan.forcedStickyCell.index} ` +
617
- `window=[${m.first},${m.last}] hasHeader=${this.headerDir !== undefined}`);
618
- this.forcedStickyCell =
619
- plan.forcedStickyCell !== undefined
620
- ? this.buildWindowCell(plan.forcedStickyCell.index, plan.forcedStickyCell.key, false,
621
- // Sticky by construction — `forcedStickyCell` exists only for an index in `stickySet`.
622
- true)
623
- : null;
624
- const cells = [];
625
- for (const planned of plan.cells) {
626
- // RN gates the separator on the last index of the DATA, not the WINDOW
627
- // (VirtualizedList.js:793 `getItemCount(data) - 1`). The separator lives INSIDE the
628
- // measuring wrapper, so gating on the window would shift a cell's height as it slides past.
629
- const includeSeparator = hasSeparators && planned.index < m.count - 1;
630
- cells.push(this.buildWindowCell(planned.index, planned.key, includeSeparator, stickySet?.has(planned.index) === true));
631
- }
632
- this.windowCells = cells;
633
- // Swap, don't prune: a cell that left the window is simply absent from the pass that just ran,
634
- // so the old map is the garbage and the new one is exactly the live set.
635
- this.cellContexts = this.nextCellContexts;
636
- this.nextCellContexts = new Map();
193
+ this.applySpacers(plan);
194
+ this.buildCells(plan, stickySet);
637
195
  dlog(`Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
638
- `(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rendered=${cells.length})`);
639
- }
640
- // Assembles one cell (window or forced-sticky) — the context, measure closure, and optional
641
- // separator context — so both plan.cells and plan.forcedStickyCell build identically.
642
- buildWindowCell(index, key, includeSeparator, isSticky) {
643
- const item = this.getItem(this.data, index);
644
- return {
645
- key,
646
- index,
647
- context: this.contextFor(key, index, item),
648
- measure: this.cellMeasure(index),
649
- separatorContext: includeSeparator
650
- ? this.buildSeparatorContext(index, item)
651
- : undefined,
652
- isSticky,
653
- };
196
+ `(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rendered=${this.windowCells.length})`);
197
+ }
198
+ applyStyles(total) {
199
+ const styles = resolveListStyles({
200
+ isHorizontal: this.isHorizontal,
201
+ isInverted: this.isInverted,
202
+ total,
203
+ hasHeader: this.headerDir !== undefined,
204
+ style: this.style,
205
+ contentContainerStyle: this.contentContainerStyle,
206
+ anchorStyle: anchorHostStyle(this.elementRef),
207
+ maintainVisibleContentPosition: this.maintainVisibleContentPosition,
208
+ });
209
+ this.resolvedStyle = styles.style;
210
+ this.resolvedContentContainerStyle = styles.contentContainerStyle;
211
+ this.cellStyle = styles.cellStyle;
212
+ this.resolvedMaintainVisibleContentPosition =
213
+ styles.maintainVisibleContentPosition;
654
214
  }
655
- // The context for one cell, reused by identity when the cell did not move. See cellContexts.
656
- contextFor(key, index, item) {
657
- const cached = this.cellContexts.get(key);
658
- if (cached !== undefined &&
659
- cached.$implicit === item &&
660
- cached.index === index) {
661
- this.nextCellContexts.set(key, cached);
662
- return cached;
663
- }
664
- const context = {
665
- $implicit: item,
666
- index,
667
- separators: this.makeSeparators(index),
668
- };
669
- this.nextCellContexts.set(key, context);
670
- return context;
215
+ showEmpty() {
216
+ this.cells.clear();
217
+ this.windowCells = [];
218
+ this.forcedStickyCell = null;
219
+ this.leadingSpacerStyle = null;
220
+ this.gapSpacerStyle = null;
221
+ this.trailingSpacerStyle = null;
222
+ dlog(`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`);
671
223
  }
672
- buildSeparatorContext(index, item) {
673
- const overrides = this.separatorOverrides.get(index);
674
- const highlighted = overrides?.highlighted ?? false;
675
- const context = {
676
- $implicit: highlighted,
677
- highlighted,
678
- leadingItem: item,
679
- trailingItem: this.getItem(this.data, index + 1),
680
- };
681
- // Fold any custom updateProps keys through the context's index signature (RN lets a row drive
682
- // arbitrary separator props); the typed fields above stay authoritative.
683
- if (overrides !== undefined) {
684
- for (const key of Object.keys(overrides)) {
685
- if (key === 'highlighted' ||
686
- key === 'leadingItem' ||
687
- key === 'trailingItem')
688
- continue;
689
- context[key] = overrides[key];
690
- }
691
- }
692
- return context;
224
+ applySpacers(plan) {
225
+ this.leadingSpacerStyle = this.spacerStyle(plan.leadingExtent);
226
+ this.gapSpacerStyle = this.spacerStyle(plan.gapExtent);
227
+ this.trailingSpacerStyle = this.spacerStyle(plan.trailingExtent);
693
228
  }
694
229
  spacerStyle(extent) {
230
+ if (extent <= EMPTY_OFFSET)
231
+ return null;
695
232
  return this.isHorizontal ? { width: extent } : { height: extent };
696
233
  }
697
- cellMeasure(index) {
698
- const existing = this.cellMeasures.get(index);
699
- if (existing !== undefined)
700
- return existing;
701
- const measure = (event) => {
702
- const length = readLayoutLength(event, this.isHorizontal);
703
- if (length === undefined)
704
- return;
705
- const offset = readLayoutOffset(event, this.isHorizontal);
706
- dlog(`Angular VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`);
707
- // The reducer guards a fixed getItemLayout and a repeated length; a no-op leaves state and the
708
- // view untouched (dispatch marks for check only when something changed).
709
- this.dispatch({ kind: 'measure', index, length, offset });
710
- };
711
- this.cellMeasures.set(index, measure);
712
- return measure;
234
+ buildCells(plan, stickySet) {
235
+ const count = this.listState.metrics.count;
236
+ const forced = plan.forcedStickyCell;
237
+ dlog(`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
238
+ `childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
239
+ `forcedStickyCell=${forced === undefined ? 'none' : forced.index}`);
240
+ // Sticky by construction: a forced cell exists only for an index in `stickySet`
241
+ this.forcedStickyCell =
242
+ forced !== undefined
243
+ ? this.cells.windowCell(forced.index, forced.key, false, true)
244
+ : null;
245
+ const hasSeparators = this.separatorTemplate !== undefined;
246
+ // The separator gates on the last index of the data, not of the window, or a cell's height
247
+ // would shift as it slides past
248
+ this.windowCells = plan.cells.map(planned => this.cells.windowCell(planned.index, planned.key, hasSeparators && planned.index < count - 1, stickySet?.has(planned.index) === true));
249
+ this.cells.endPass();
713
250
  }
714
- // Per-cell onLayout rides the engine's structural (layout) event channel — Angular forbids an
715
- // [onLayout] property binding (NG8002). $event arrives untyped, narrowed before reaching measure.
251
+ // Angular forbids an `[onLayout]` binding, so the event arrives untyped and is narrowed here
716
252
  handleCellLayout(measure, event) {
717
253
  if (this.isSymbioteEvent(event))
718
254
  measure(event);
@@ -720,84 +256,6 @@ export class VirtualizedList {
720
256
  isSymbioteEvent(value) {
721
257
  return (typeof value === 'object' && value !== null && 'nativeEvent' in value);
722
258
  }
723
- mergeSeparator(gapIndex, patch) {
724
- const count = this.listState.metrics.count;
725
- if (!isSeparatorGapInRange(gapIndex, count))
726
- return;
727
- this.separatorOverrides.set(gapIndex, {
728
- ...this.separatorOverrides.get(gapIndex),
729
- ...patch,
730
- });
731
- this.cdr.markForCheck();
732
- }
733
- makeSeparators(index) {
734
- return {
735
- highlight: () => {
736
- dlog(`Angular VirtualizedList separator highlight cell=${index}`);
737
- this.mergeSeparator(index - 1, { highlighted: true });
738
- this.mergeSeparator(index, { highlighted: true });
739
- },
740
- unhighlight: () => {
741
- dlog(`Angular VirtualizedList separator unhighlight cell=${index}`);
742
- this.mergeSeparator(index - 1, { highlighted: false });
743
- this.mergeSeparator(index, { highlighted: false });
744
- },
745
- updateProps: (select, newProps) => {
746
- this.mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
747
- },
748
- };
749
- }
750
- // ---- imperative handle (the shared IVirtualizedListHandle surface) ----
751
- // Each scroll resolves to an offset (or a scroll-to-index failure) inside the reducer, then rides
752
- // the scroll-to effect through scrollToPixel.
753
- scrollToOffset(params) {
754
- this.dispatch({
755
- kind: 'scroll-to-offset',
756
- offset: params.offset,
757
- animated: params.animated ?? true,
758
- });
759
- }
760
- scrollToIndex(params) {
761
- this.dispatch({
762
- kind: 'scroll-to-index',
763
- index: params.index,
764
- animated: params.animated ?? true,
765
- viewPosition: params.viewPosition ?? FIRST_INDEX,
766
- viewOffset: params.viewOffset ?? EMPTY_OFFSET,
767
- });
768
- }
769
- scrollToItem(params) {
770
- this.dispatch({
771
- kind: 'scroll-to-item',
772
- item: params.item,
773
- animated: params.animated ?? true,
774
- viewPosition: params.viewPosition ?? FIRST_INDEX,
775
- });
776
- }
777
- scrollToEnd(params) {
778
- this.dispatch({
779
- kind: 'scroll-to-end',
780
- animated: params?.animated ?? true,
781
- });
782
- }
783
- flashScrollIndicators() {
784
- this.scrollHandle.flashScrollIndicators();
785
- }
786
- getNativeScrollRef() {
787
- return this.scrollNode !== null ? this.scrollHandle : null;
788
- }
789
- getScrollableNode() {
790
- return this.scrollNode !== null ? this.scrollHandle : null;
791
- }
792
- getScrollResponder() {
793
- return this.scrollNode !== null ? this.scrollHandle : null;
794
- }
795
- getScrollNode() {
796
- return this.scrollHandle.getScrollNode();
797
- }
798
- recordInteraction() {
799
- this.dispatch({ kind: 'record-interaction' });
800
- }
801
259
  scrollToPixel(offset, animated) {
802
260
  const clamped = Math.max(EMPTY_OFFSET, offset);
803
261
  const target = this.isHorizontal
@@ -812,181 +270,8 @@ export class VirtualizedList {
812
270
  this.commandedOffset = target;
813
271
  this.cdr.markForCheck();
814
272
  }
815
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
816
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VirtualizedList, isStandalone: true, selector: "VirtualizedList", inputs: { data: "data", getItem: "getItem", getItemCount: "getItemCount", keyExtractor: "keyExtractor", getItemLayout: "getItemLayout", horizontal: "horizontal", inverted: "inverted", extraData: "extraData", onEndReachedThreshold: "onEndReachedThreshold", onStartReachedThreshold: "onStartReachedThreshold", refreshing: "refreshing", progressViewOffset: "progressViewOffset", refreshRequested: "refreshRequested", viewabilityConfig: "viewabilityConfig", viewabilityConfigCallbackPairs: "viewabilityConfigCallbackPairs", initialNumToRender: "initialNumToRender", initialScrollIndex: "initialScrollIndex", maxToRenderPerBatch: "maxToRenderPerBatch", updateCellsBatchingPeriod: "updateCellsBatchingPeriod", windowSize: "windowSize", stickyHeaderIndices: "stickyHeaderIndices", maintainVisibleContentPosition: "maintainVisibleContentPosition", onScroll: "onScroll", onScrollBeginDrag: "onScrollBeginDrag", onScrollEndDrag: "onScrollEndDrag", onMomentumScrollBegin: "onMomentumScrollBegin", onMomentumScrollEnd: "onMomentumScrollEnd", scrollEventThrottle: "scrollEventThrottle", keyboardShouldPersistTaps: "keyboardShouldPersistTaps", keyboardDismissMode: "keyboardDismissMode", removeClippedSubviews: "removeClippedSubviews", nestedScrollEnabled: "nestedScrollEnabled", style: "style", contentContainerStyle: "contentContainerStyle", testID: "testID", nativeID: "nativeID", accessible: "accessible", accessibilityLabel: "accessibilityLabel", accessibilityHint: "accessibilityHint", accessibilityRole: "accessibilityRole", accessibilityState: "accessibilityState", accessibilityValue: "accessibilityValue", accessibilityActions: "accessibilityActions", accessibilityLabelledBy: "accessibilityLabelledBy", importantForAccessibility: "importantForAccessibility", accessibilityLiveRegion: "accessibilityLiveRegion", screenReaderFocusable: "screenReaderFocusable", accessibilityViewIsModal: "accessibilityViewIsModal", accessibilityElementsHidden: "accessibilityElementsHidden", accessibilityIgnoresInvertColors: "accessibilityIgnoresInvertColors", accessibilityLanguage: "accessibilityLanguage", accessibilityRespondsToUserInteraction: "accessibilityRespondsToUserInteraction", accessibilityShowsLargeContentViewer: "accessibilityShowsLargeContentViewer", accessibilityLargeContentTitle: "accessibilityLargeContentTitle", role: "role", ariaLabel: "ariaLabel", ariaLabelledBy: "ariaLabelledBy", ariaLive: "ariaLive", ariaHidden: "ariaHidden", ariaBusy: "ariaBusy", ariaChecked: "ariaChecked", ariaDisabled: "ariaDisabled", ariaExpanded: "ariaExpanded", ariaSelected: "ariaSelected", ariaModal: "ariaModal", ariaValueMax: "ariaValueMax", ariaValueMin: "ariaValueMin", ariaValueNow: "ariaValueNow", ariaValueText: "ariaValueText", itemTemplate: "itemTemplate", itemSeparatorTemplate: "itemSeparatorTemplate" }, outputs: { endReached: "endReached", startReached: "startReached", refresh: "refresh", viewableItemsChanged: "viewableItemsChanged", scrollToIndexFailed: "scrollToIndexFailed", accessibilityAction: "accessibilityAction", accessibilityTap: "accessibilityTap", magicTap: "magicTap", accessibilityEscape: "accessibilityEscape" }, queries: [{ propertyName: "itemDir", first: true, predicate: VListItemDirective, descendants: true }, { propertyName: "headerDir", first: true, predicate: VListHeaderDirective, descendants: true }, { propertyName: "footerDir", first: true, predicate: VListFooterDirective, descendants: true }, { propertyName: "emptyDir", first: true, predicate: VListEmptyDirective, descendants: true }, { propertyName: "separatorDir", first: true, predicate: VListSeparatorDirective, descendants: true }], viewQueries: [{ propertyName: "scrollHostRef", first: true, predicate: ["scrollHost"], descendants: true, read: ElementRef }], usesOnChanges: true, hostDirectives: [{ directive: i1.SymbioteStyleInputDirective, inputs: ["style", "style"] }], ngImport: i0, template: `
817
- @if (isHorizontal) {
818
- <horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
819
- @if (shouldRenderRefreshControl) {
820
- <refresh-control
821
- [symbioteHostProps]="refreshControlBag()"
822
- ></refresh-control>
823
- }
824
-
825
- @if (headerDir !== undefined) {
826
- <view>
827
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
828
- </view>
829
- }
830
-
831
- @if (itemCount === 0) {
832
- @if (emptyDir !== undefined) {
833
- <view>
834
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
835
- </view>
836
- }
837
- } @else {
838
- @if (leadingSpacerStyle !== null) {
839
- <view [style]="leadingSpacerStyle"></view>
840
- }
841
- @if (forcedStickyCell !== null) {
842
- <sticky-header
843
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
844
- [style]="cellStyle"
845
- >
846
- <ng-container
847
- [vListOutlet]="cellTemplate"
848
- [vListOutletContext]="forcedStickyCell.context"
849
- ></ng-container>
850
- </sticky-header>
851
- }
852
- @if (gapSpacerStyle !== null) {
853
- <view [style]="gapSpacerStyle"></view>
854
- }
855
- @for (cell of windowCells; track cell.key) {
856
- @if (cell.isSticky) {
857
- <sticky-header
858
- (layout)="handleCellLayout(cell.measure, $event)"
859
- [style]="cellStyle"
860
- >
861
- <ng-container
862
- [vListOutlet]="cellTemplate"
863
- [vListOutletContext]="cell.context"
864
- ></ng-container>
865
- @if (cell.separatorContext !== undefined) {
866
- <ng-container
867
- [vListOutlet]="separatorTemplate"
868
- [vListOutletContext]="cell.separatorContext"
869
- ></ng-container>
870
- }
871
- </sticky-header>
872
- } @else {
873
- <view
874
- (layout)="handleCellLayout(cell.measure, $event)"
875
- [style]="cellStyle"
876
- >
877
- <ng-container
878
- [vListOutlet]="cellTemplate"
879
- [vListOutletContext]="cell.context"
880
- ></ng-container>
881
- @if (cell.separatorContext !== undefined) {
882
- <ng-container
883
- [vListOutlet]="separatorTemplate"
884
- [vListOutletContext]="cell.separatorContext"
885
- ></ng-container>
886
- }
887
- </view>
888
- }
889
- }
890
- @if (trailingSpacerStyle !== null) {
891
- <view [style]="trailingSpacerStyle"></view>
892
- }
893
- }
894
-
895
- @if (footerDir !== undefined) {
896
- <view>
897
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
898
- </view>
899
- }
900
- </horizontal-scroll-view>
901
- } @else {
902
- <scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
903
- @if (shouldRenderRefreshControl) {
904
- <refresh-control
905
- [symbioteHostProps]="refreshControlBag()"
906
- ></refresh-control>
907
- }
908
-
909
- @if (headerDir !== undefined) {
910
- <view>
911
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
912
- </view>
913
- }
914
-
915
- @if (itemCount === 0) {
916
- @if (emptyDir !== undefined) {
917
- <view>
918
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
919
- </view>
920
- }
921
- } @else {
922
- @if (leadingSpacerStyle !== null) {
923
- <view [style]="leadingSpacerStyle"></view>
924
- }
925
- @if (forcedStickyCell !== null) {
926
- <sticky-header
927
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
928
- [style]="cellStyle"
929
- >
930
- <ng-container
931
- [vListOutlet]="cellTemplate"
932
- [vListOutletContext]="forcedStickyCell.context"
933
- ></ng-container>
934
- </sticky-header>
935
- }
936
- @if (gapSpacerStyle !== null) {
937
- <view [style]="gapSpacerStyle"></view>
938
- }
939
- <!-- Separator sits INSIDE the measuring view, matching RN's cell renderer
940
- (VirtualizedListCellRenderer.js:218-221) — as a sibling it'd add an extra flex gap.
941
- See list-geometry-feedback-loop.md. -->
942
- @for (cell of windowCells; track cell.key) {
943
- @if (cell.isSticky) {
944
- <sticky-header
945
- (layout)="handleCellLayout(cell.measure, $event)"
946
- [style]="cellStyle"
947
- >
948
- <ng-container
949
- [vListOutlet]="cellTemplate"
950
- [vListOutletContext]="cell.context"
951
- ></ng-container>
952
- @if (cell.separatorContext !== undefined) {
953
- <ng-container
954
- [vListOutlet]="separatorTemplate"
955
- [vListOutletContext]="cell.separatorContext"
956
- ></ng-container>
957
- }
958
- </sticky-header>
959
- } @else {
960
- <view
961
- (layout)="handleCellLayout(cell.measure, $event)"
962
- [style]="cellStyle"
963
- >
964
- <ng-container
965
- [vListOutlet]="cellTemplate"
966
- [vListOutletContext]="cell.context"
967
- ></ng-container>
968
- @if (cell.separatorContext !== undefined) {
969
- <ng-container
970
- [vListOutlet]="separatorTemplate"
971
- [vListOutletContext]="cell.separatorContext"
972
- ></ng-container>
973
- }
974
- </view>
975
- }
976
- }
977
- @if (trailingSpacerStyle !== null) {
978
- <view [style]="trailingSpacerStyle"></view>
979
- }
980
- }
981
-
982
- @if (footerDir !== undefined) {
983
- <view>
984
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
985
- </view>
986
- }
987
- </scroll-view>
988
- }
989
- `, isInline: true, dependencies: [{ kind: "directive", type: SymbioteHostPropsDirective, selector: "[symbioteHostProps]", inputs: ["symbioteHostProps"], exportAs: ["symbioteHost"] }, { kind: "directive", type: VListOutletDirective, selector: "[vListOutlet]", inputs: ["vListOutlet", "vListOutletContext"] }, { kind: "component", type: ViewHost, selector: "view" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
273
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
274
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VirtualizedList, isStandalone: true, selector: "VirtualizedList", viewQueries: [{ propertyName: "scrollHostRef", first: true, predicate: ["scrollHost"], descendants: true, read: ElementRef }], usesInheritance: true, hostDirectives: [{ directive: i1.SymbioteStyleInputDirective, inputs: ["style", "style"] }], ngImport: i0, template: "\n @if (isHorizontal) {\n <horizontal-scroll-view #scrollHost [symbioteHostProps]=\"scrollViewBag()\">\n @if (shouldRenderRefreshControl) {\n <refresh-control\n [symbioteHostProps]=\"refreshControlBag()\"\n ></refresh-control>\n }\n\n @if (headerDir !== undefined) {\n <view>\n <ng-container [vListOutlet]=\"headerDir.templateRef\"></ng-container>\n </view>\n }\n\n @if (itemCount === 0) {\n @if (emptyDir !== undefined) {\n <view>\n <ng-container [vListOutlet]=\"emptyDir.templateRef\"></ng-container>\n </view>\n }\n } @else {\n @if (leadingSpacerStyle !== null) {\n <view [style]=\"leadingSpacerStyle\"></view>\n }\n @if (forcedStickyCell !== null) {\n <sticky-header\n (layout)=\"handleCellLayout(forcedStickyCell.measure, $event)\"\n [style]=\"cellStyle\"\n >\n <ng-container\n [vListOutlet]=\"cellTemplate\"\n [vListOutletContext]=\"forcedStickyCell.context\"\n ></ng-container>\n </sticky-header>\n }\n @if (gapSpacerStyle !== null) {\n <view [style]=\"gapSpacerStyle\"></view>\n }\n @for (cell of windowCells; track cell.key) {\n @if (cell.isSticky) {\n <sticky-header\n (layout)=\"handleCellLayout(cell.measure, $event)\"\n [style]=\"cellStyle\"\n >\n <ng-container\n [vListOutlet]=\"cellTemplate\"\n [vListOutletContext]=\"cell.context\"\n ></ng-container>\n @if (cell.separatorContext !== undefined) {\n <ng-container\n [vListOutlet]=\"separatorTemplate\"\n [vListOutletContext]=\"cell.separatorContext\"\n ></ng-container>\n }\n </sticky-header>\n } @else {\n <view\n (layout)=\"handleCellLayout(cell.measure, $event)\"\n [style]=\"cellStyle\"\n >\n <ng-container\n [vListOutlet]=\"cellTemplate\"\n [vListOutletContext]=\"cell.context\"\n ></ng-container>\n @if (cell.separatorContext !== undefined) {\n <ng-container\n [vListOutlet]=\"separatorTemplate\"\n [vListOutletContext]=\"cell.separatorContext\"\n ></ng-container>\n }\n </view>\n }\n }\n @if (trailingSpacerStyle !== null) {\n <view [style]=\"trailingSpacerStyle\"></view>\n }\n }\n\n @if (footerDir !== undefined) {\n <view>\n <ng-container [vListOutlet]=\"footerDir.templateRef\"></ng-container>\n </view>\n }\n </horizontal-scroll-view>\n } @else {\n <scroll-view #scrollHost [symbioteHostProps]=\"scrollViewBag()\">\n @if (shouldRenderRefreshControl) {\n <refresh-control\n [symbioteHostProps]=\"refreshControlBag()\"\n ></refresh-control>\n }\n\n @if (headerDir !== undefined) {\n <view>\n <ng-container [vListOutlet]=\"headerDir.templateRef\"></ng-container>\n </view>\n }\n\n @if (itemCount === 0) {\n @if (emptyDir !== undefined) {\n <view>\n <ng-container [vListOutlet]=\"emptyDir.templateRef\"></ng-container>\n </view>\n }\n } @else {\n @if (leadingSpacerStyle !== null) {\n <view [style]=\"leadingSpacerStyle\"></view>\n }\n @if (forcedStickyCell !== null) {\n <sticky-header\n (layout)=\"handleCellLayout(forcedStickyCell.measure, $event)\"\n [style]=\"cellStyle\"\n >\n <ng-container\n [vListOutlet]=\"cellTemplate\"\n [vListOutletContext]=\"forcedStickyCell.context\"\n ></ng-container>\n </sticky-header>\n }\n @if (gapSpacerStyle !== null) {\n <view [style]=\"gapSpacerStyle\"></view>\n }\n <!-- The separator sits INSIDE the measuring view, matching RN's cell renderer, as a\n sibling it would add an extra flex gap -->\n @for (cell of windowCells; track cell.key) {\n @if (cell.isSticky) {\n <sticky-header\n (layout)=\"handleCellLayout(cell.measure, $event)\"\n [style]=\"cellStyle\"\n >\n <ng-container\n [vListOutlet]=\"cellTemplate\"\n [vListOutletContext]=\"cell.context\"\n ></ng-container>\n @if (cell.separatorContext !== undefined) {\n <ng-container\n [vListOutlet]=\"separatorTemplate\"\n [vListOutletContext]=\"cell.separatorContext\"\n ></ng-container>\n }\n </sticky-header>\n } @else {\n <view\n (layout)=\"handleCellLayout(cell.measure, $event)\"\n [style]=\"cellStyle\"\n >\n <ng-container\n [vListOutlet]=\"cellTemplate\"\n [vListOutletContext]=\"cell.context\"\n ></ng-container>\n @if (cell.separatorContext !== undefined) {\n <ng-container\n [vListOutlet]=\"separatorTemplate\"\n [vListOutletContext]=\"cell.separatorContext\"\n ></ng-container>\n }\n </view>\n }\n }\n @if (trailingSpacerStyle !== null) {\n <view [style]=\"trailingSpacerStyle\"></view>\n }\n }\n\n @if (footerDir !== undefined) {\n <view>\n <ng-container [vListOutlet]=\"footerDir.templateRef\"></ng-container>\n </view>\n }\n </scroll-view>\n }\n ", isInline: true, dependencies: [{ kind: "directive", type: SymbioteHostPropsDirective, selector: "[symbioteHostProps]", inputs: ["symbioteHostProps"], exportAs: ["symbioteHost"] }, { kind: "directive", type: VListOutletDirective, selector: "[vListOutlet]", inputs: ["vListOutlet", "vListOutletContext"] }, { kind: "component", type: ViewHost, selector: "view" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
990
275
  }
991
276
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, decorators: [{
992
277
  type: Component,
@@ -999,360 +284,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
999
284
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
1000
285
  imports: [SymbioteHostPropsDirective, VListOutletDirective, ViewHost],
1001
286
  changeDetection: ChangeDetectionStrategy.OnPush,
1002
- template: `
1003
- @if (isHorizontal) {
1004
- <horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
1005
- @if (shouldRenderRefreshControl) {
1006
- <refresh-control
1007
- [symbioteHostProps]="refreshControlBag()"
1008
- ></refresh-control>
1009
- }
1010
-
1011
- @if (headerDir !== undefined) {
1012
- <view>
1013
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1014
- </view>
1015
- }
1016
-
1017
- @if (itemCount === 0) {
1018
- @if (emptyDir !== undefined) {
1019
- <view>
1020
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
1021
- </view>
1022
- }
1023
- } @else {
1024
- @if (leadingSpacerStyle !== null) {
1025
- <view [style]="leadingSpacerStyle"></view>
1026
- }
1027
- @if (forcedStickyCell !== null) {
1028
- <sticky-header
1029
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
1030
- [style]="cellStyle"
1031
- >
1032
- <ng-container
1033
- [vListOutlet]="cellTemplate"
1034
- [vListOutletContext]="forcedStickyCell.context"
1035
- ></ng-container>
1036
- </sticky-header>
1037
- }
1038
- @if (gapSpacerStyle !== null) {
1039
- <view [style]="gapSpacerStyle"></view>
1040
- }
1041
- @for (cell of windowCells; track cell.key) {
1042
- @if (cell.isSticky) {
1043
- <sticky-header
1044
- (layout)="handleCellLayout(cell.measure, $event)"
1045
- [style]="cellStyle"
1046
- >
1047
- <ng-container
1048
- [vListOutlet]="cellTemplate"
1049
- [vListOutletContext]="cell.context"
1050
- ></ng-container>
1051
- @if (cell.separatorContext !== undefined) {
1052
- <ng-container
1053
- [vListOutlet]="separatorTemplate"
1054
- [vListOutletContext]="cell.separatorContext"
1055
- ></ng-container>
1056
- }
1057
- </sticky-header>
1058
- } @else {
1059
- <view
1060
- (layout)="handleCellLayout(cell.measure, $event)"
1061
- [style]="cellStyle"
1062
- >
1063
- <ng-container
1064
- [vListOutlet]="cellTemplate"
1065
- [vListOutletContext]="cell.context"
1066
- ></ng-container>
1067
- @if (cell.separatorContext !== undefined) {
1068
- <ng-container
1069
- [vListOutlet]="separatorTemplate"
1070
- [vListOutletContext]="cell.separatorContext"
1071
- ></ng-container>
1072
- }
1073
- </view>
1074
- }
1075
- }
1076
- @if (trailingSpacerStyle !== null) {
1077
- <view [style]="trailingSpacerStyle"></view>
1078
- }
1079
- }
1080
-
1081
- @if (footerDir !== undefined) {
1082
- <view>
1083
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1084
- </view>
1085
- }
1086
- </horizontal-scroll-view>
1087
- } @else {
1088
- <scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
1089
- @if (shouldRenderRefreshControl) {
1090
- <refresh-control
1091
- [symbioteHostProps]="refreshControlBag()"
1092
- ></refresh-control>
1093
- }
1094
-
1095
- @if (headerDir !== undefined) {
1096
- <view>
1097
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1098
- </view>
1099
- }
1100
-
1101
- @if (itemCount === 0) {
1102
- @if (emptyDir !== undefined) {
1103
- <view>
1104
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
1105
- </view>
1106
- }
1107
- } @else {
1108
- @if (leadingSpacerStyle !== null) {
1109
- <view [style]="leadingSpacerStyle"></view>
1110
- }
1111
- @if (forcedStickyCell !== null) {
1112
- <sticky-header
1113
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
1114
- [style]="cellStyle"
1115
- >
1116
- <ng-container
1117
- [vListOutlet]="cellTemplate"
1118
- [vListOutletContext]="forcedStickyCell.context"
1119
- ></ng-container>
1120
- </sticky-header>
1121
- }
1122
- @if (gapSpacerStyle !== null) {
1123
- <view [style]="gapSpacerStyle"></view>
1124
- }
1125
- <!-- Separator sits INSIDE the measuring view, matching RN's cell renderer
1126
- (VirtualizedListCellRenderer.js:218-221) — as a sibling it'd add an extra flex gap.
1127
- See list-geometry-feedback-loop.md. -->
1128
- @for (cell of windowCells; track cell.key) {
1129
- @if (cell.isSticky) {
1130
- <sticky-header
1131
- (layout)="handleCellLayout(cell.measure, $event)"
1132
- [style]="cellStyle"
1133
- >
1134
- <ng-container
1135
- [vListOutlet]="cellTemplate"
1136
- [vListOutletContext]="cell.context"
1137
- ></ng-container>
1138
- @if (cell.separatorContext !== undefined) {
1139
- <ng-container
1140
- [vListOutlet]="separatorTemplate"
1141
- [vListOutletContext]="cell.separatorContext"
1142
- ></ng-container>
1143
- }
1144
- </sticky-header>
1145
- } @else {
1146
- <view
1147
- (layout)="handleCellLayout(cell.measure, $event)"
1148
- [style]="cellStyle"
1149
- >
1150
- <ng-container
1151
- [vListOutlet]="cellTemplate"
1152
- [vListOutletContext]="cell.context"
1153
- ></ng-container>
1154
- @if (cell.separatorContext !== undefined) {
1155
- <ng-container
1156
- [vListOutlet]="separatorTemplate"
1157
- [vListOutletContext]="cell.separatorContext"
1158
- ></ng-container>
1159
- }
1160
- </view>
1161
- }
1162
- }
1163
- @if (trailingSpacerStyle !== null) {
1164
- <view [style]="trailingSpacerStyle"></view>
1165
- }
1166
- }
1167
-
1168
- @if (footerDir !== undefined) {
1169
- <view>
1170
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1171
- </view>
1172
- }
1173
- </scroll-view>
1174
- }
1175
- `,
287
+ template: VIRTUALIZED_LIST_TEMPLATE,
1176
288
  }]
1177
- }], propDecorators: { endReached: [{
1178
- type: Output
1179
- }], startReached: [{
1180
- type: Output
1181
- }], refresh: [{
1182
- type: Output
1183
- }], viewableItemsChanged: [{
1184
- type: Output
1185
- }], scrollToIndexFailed: [{
1186
- type: Output
1187
- }], accessibilityAction: [{
1188
- type: Output
1189
- }], accessibilityTap: [{
1190
- type: Output
1191
- }], magicTap: [{
1192
- type: Output
1193
- }], accessibilityEscape: [{
1194
- type: Output
1195
- }], data: [{
1196
- type: Input,
1197
- args: [{ required: true }]
1198
- }], getItem: [{
1199
- type: Input,
1200
- args: [{ required: true }]
1201
- }], getItemCount: [{
1202
- type: Input,
1203
- args: [{ required: true }]
1204
- }], keyExtractor: [{
1205
- type: Input
1206
- }], getItemLayout: [{
1207
- type: Input
1208
- }], horizontal: [{
1209
- type: Input
1210
- }], inverted: [{
1211
- type: Input
1212
- }], extraData: [{
1213
- type: Input
1214
- }], onEndReachedThreshold: [{
1215
- type: Input
1216
- }], onStartReachedThreshold: [{
1217
- type: Input
1218
- }], refreshing: [{
1219
- type: Input
1220
- }], progressViewOffset: [{
1221
- type: Input
1222
- }], refreshRequested: [{
1223
- type: Input
1224
- }], viewabilityConfig: [{
1225
- type: Input
1226
- }], viewabilityConfigCallbackPairs: [{
1227
- type: Input
1228
- }], initialNumToRender: [{
1229
- type: Input
1230
- }], initialScrollIndex: [{
1231
- type: Input
1232
- }], maxToRenderPerBatch: [{
1233
- type: Input
1234
- }], updateCellsBatchingPeriod: [{
1235
- type: Input
1236
- }], windowSize: [{
1237
- type: Input
1238
- }], stickyHeaderIndices: [{
1239
- type: Input
1240
- }], maintainVisibleContentPosition: [{
1241
- type: Input
1242
- }], onScroll: [{
1243
- type: Input
1244
- }], onScrollBeginDrag: [{
1245
- type: Input
1246
- }], onScrollEndDrag: [{
1247
- type: Input
1248
- }], onMomentumScrollBegin: [{
1249
- type: Input
1250
- }], onMomentumScrollEnd: [{
1251
- type: Input
1252
- }], scrollEventThrottle: [{
1253
- type: Input
1254
- }], keyboardShouldPersistTaps: [{
1255
- type: Input
1256
- }], keyboardDismissMode: [{
1257
- type: Input
1258
- }], removeClippedSubviews: [{
1259
- type: Input
1260
- }], nestedScrollEnabled: [{
1261
- type: Input
1262
- }], style: [{
1263
- type: Input
1264
- }], contentContainerStyle: [{
1265
- type: Input
1266
- }], testID: [{
1267
- type: Input
1268
- }], nativeID: [{
1269
- type: Input
1270
- }], accessible: [{
1271
- type: Input
1272
- }], accessibilityLabel: [{
1273
- type: Input
1274
- }], accessibilityHint: [{
1275
- type: Input
1276
- }], accessibilityRole: [{
1277
- type: Input
1278
- }], accessibilityState: [{
1279
- type: Input
1280
- }], accessibilityValue: [{
1281
- type: Input
1282
- }], accessibilityActions: [{
1283
- type: Input
1284
- }], accessibilityLabelledBy: [{
1285
- type: Input
1286
- }], importantForAccessibility: [{
1287
- type: Input
1288
- }], accessibilityLiveRegion: [{
1289
- type: Input
1290
- }], screenReaderFocusable: [{
1291
- type: Input
1292
- }], accessibilityViewIsModal: [{
1293
- type: Input
1294
- }], accessibilityElementsHidden: [{
1295
- type: Input
1296
- }], accessibilityIgnoresInvertColors: [{
1297
- type: Input
1298
- }], accessibilityLanguage: [{
1299
- type: Input
1300
- }], accessibilityRespondsToUserInteraction: [{
1301
- type: Input
1302
- }], accessibilityShowsLargeContentViewer: [{
1303
- type: Input
1304
- }], accessibilityLargeContentTitle: [{
1305
- type: Input
1306
- }], role: [{
1307
- type: Input
1308
- }], ariaLabel: [{
1309
- type: Input
1310
- }], ariaLabelledBy: [{
1311
- type: Input
1312
- }], ariaLive: [{
1313
- type: Input
1314
- }], ariaHidden: [{
1315
- type: Input
1316
- }], ariaBusy: [{
1317
- type: Input
1318
- }], ariaChecked: [{
1319
- type: Input
1320
- }], ariaDisabled: [{
1321
- type: Input
1322
- }], ariaExpanded: [{
1323
- type: Input
1324
- }], ariaSelected: [{
1325
- type: Input
1326
- }], ariaModal: [{
1327
- type: Input
1328
- }], ariaValueMax: [{
1329
- type: Input
1330
- }], ariaValueMin: [{
1331
- type: Input
1332
- }], ariaValueNow: [{
1333
- type: Input
1334
- }], ariaValueText: [{
1335
- type: Input
1336
- }], itemDir: [{
1337
- type: ContentChild,
1338
- args: [VListItemDirective]
1339
- }], headerDir: [{
1340
- type: ContentChild,
1341
- args: [VListHeaderDirective]
1342
- }], footerDir: [{
1343
- type: ContentChild,
1344
- args: [VListFooterDirective]
1345
- }], emptyDir: [{
1346
- type: ContentChild,
1347
- args: [VListEmptyDirective]
1348
- }], separatorDir: [{
1349
- type: ContentChild,
1350
- args: [VListSeparatorDirective]
1351
- }], itemTemplate: [{
1352
- type: Input
1353
- }], itemSeparatorTemplate: [{
1354
- type: Input
1355
- }], scrollHostRef: [{
289
+ }], propDecorators: { scrollHostRef: [{
1356
290
  type: ViewChild,
1357
291
  args: ['scrollHost', { read: ElementRef }]
1358
292
  }] } });