igniteui-angular 22.1.4 → 22.2.0-rc.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 (142) hide show
  1. package/README.md +2 -2
  2. package/combo/README.md +19 -0
  3. package/drop-down/README.md +40 -4
  4. package/fesm2022/igniteui-angular-accordion.mjs +7 -7
  5. package/fesm2022/igniteui-angular-action-strip.mjs +10 -10
  6. package/fesm2022/igniteui-angular-avatar.mjs +7 -7
  7. package/fesm2022/igniteui-angular-badge.mjs +7 -7
  8. package/fesm2022/igniteui-angular-banner.mjs +10 -10
  9. package/fesm2022/igniteui-angular-bottom-nav.mjs +22 -22
  10. package/fesm2022/igniteui-angular-button-group.mjs +12 -13
  11. package/fesm2022/igniteui-angular-button-group.mjs.map +1 -1
  12. package/fesm2022/igniteui-angular-calendar.mjs +61 -61
  13. package/fesm2022/igniteui-angular-card.mjs +31 -31
  14. package/fesm2022/igniteui-angular-carousel.mjs +22 -22
  15. package/fesm2022/igniteui-angular-chat-extras.mjs +6 -6
  16. package/fesm2022/igniteui-angular-chat.mjs +12 -12
  17. package/fesm2022/igniteui-angular-checkbox.mjs +7 -7
  18. package/fesm2022/igniteui-angular-chips.mjs +10 -10
  19. package/fesm2022/igniteui-angular-combo.mjs +268 -120
  20. package/fesm2022/igniteui-angular-combo.mjs.map +1 -1
  21. package/fesm2022/igniteui-angular-core.mjs +188 -59
  22. package/fesm2022/igniteui-angular-core.mjs.map +1 -1
  23. package/fesm2022/igniteui-angular-date-picker.mjs +71 -80
  24. package/fesm2022/igniteui-angular-date-picker.mjs.map +1 -1
  25. package/fesm2022/igniteui-angular-dialog.mjs +13 -13
  26. package/fesm2022/igniteui-angular-directives.mjs +321 -457
  27. package/fesm2022/igniteui-angular-directives.mjs.map +1 -1
  28. package/fesm2022/igniteui-angular-drop-down.mjs +331 -104
  29. package/fesm2022/igniteui-angular-drop-down.mjs.map +1 -1
  30. package/fesm2022/igniteui-angular-expansion-panel.mjs +28 -28
  31. package/fesm2022/igniteui-angular-grids-core.mjs +552 -516
  32. package/fesm2022/igniteui-angular-grids-core.mjs.map +1 -1
  33. package/fesm2022/igniteui-angular-grids-grid.mjs +79 -80
  34. package/fesm2022/igniteui-angular-grids-grid.mjs.map +1 -1
  35. package/fesm2022/igniteui-angular-grids-hierarchical-grid.mjs +59 -46
  36. package/fesm2022/igniteui-angular-grids-hierarchical-grid.mjs.map +1 -1
  37. package/fesm2022/igniteui-angular-grids-lite.mjs +12 -12
  38. package/fesm2022/igniteui-angular-grids-pivot-grid.mjs +113 -86
  39. package/fesm2022/igniteui-angular-grids-pivot-grid.mjs.map +1 -1
  40. package/fesm2022/igniteui-angular-grids-tree-grid.mjs +57 -57
  41. package/fesm2022/igniteui-angular-grids-tree-grid.mjs.map +1 -1
  42. package/fesm2022/igniteui-angular-icon.mjs +10 -10
  43. package/fesm2022/igniteui-angular-input-group.mjs +47 -61
  44. package/fesm2022/igniteui-angular-input-group.mjs.map +1 -1
  45. package/fesm2022/igniteui-angular-list.mjs +40 -40
  46. package/fesm2022/igniteui-angular-navbar.mjs +13 -13
  47. package/fesm2022/igniteui-angular-navigation-drawer.mjs +16 -16
  48. package/fesm2022/igniteui-angular-paginator.mjs +19 -19
  49. package/fesm2022/igniteui-angular-progressbar.mjs +19 -19
  50. package/fesm2022/igniteui-angular-query-builder.mjs +23 -23
  51. package/fesm2022/igniteui-angular-query-builder.mjs.map +1 -1
  52. package/fesm2022/igniteui-angular-radio.mjs +59 -35
  53. package/fesm2022/igniteui-angular-radio.mjs.map +1 -1
  54. package/fesm2022/igniteui-angular-select.mjs +35 -43
  55. package/fesm2022/igniteui-angular-select.mjs.map +1 -1
  56. package/fesm2022/igniteui-angular-simple-combo.mjs +44 -33
  57. package/fesm2022/igniteui-angular-simple-combo.mjs.map +1 -1
  58. package/fesm2022/igniteui-angular-slider.mjs +28 -28
  59. package/fesm2022/igniteui-angular-snackbar.mjs +7 -7
  60. package/fesm2022/igniteui-angular-splitter.mjs +13 -13
  61. package/fesm2022/igniteui-angular-stepper.mjs +34 -34
  62. package/fesm2022/igniteui-angular-switch.mjs +7 -7
  63. package/fesm2022/igniteui-angular-tabs.mjs +34 -34
  64. package/fesm2022/igniteui-angular-time-picker.mjs +29 -38
  65. package/fesm2022/igniteui-angular-time-picker.mjs.map +1 -1
  66. package/fesm2022/igniteui-angular-toast.mjs +7 -7
  67. package/fesm2022/igniteui-angular-tree.mjs +28 -28
  68. package/fesm2022/igniteui-angular-virtual-scroll.mjs +1257 -0
  69. package/fesm2022/igniteui-angular-virtual-scroll.mjs.map +1 -0
  70. package/fesm2022/igniteui-angular.mjs +1 -0
  71. package/fesm2022/igniteui-angular.mjs.map +1 -1
  72. package/input-group/README.md +2 -0
  73. package/lib/core/styles/components/scrollbar/scrollbar-component.scss +0 -2
  74. package/lib/core/styles/components/scrollbar/scrollbar-theme.scss +23 -51
  75. package/lib/grids/core/src/column-actions/themes/_derived.scss +3 -3
  76. package/lib/grids/core/src/filtering/excel-style/themes/_derived.scss +3 -3
  77. package/lib/grids/themes/_derived.scss +3 -3
  78. package/lib/query-builder/src/query-builder/themes/_derived.scss +3 -3
  79. package/migrations/migration-collection.json +10 -0
  80. package/migrations/update-22_2_0/changes/theme-changes.json +77 -0
  81. package/migrations/update-22_2_0/index.d.ts +3 -0
  82. package/migrations/update-22_2_0/index.js +18 -0
  83. package/migrations/update-22_2_0/index.spec.d.ts +1 -0
  84. package/migrations/update-22_2_0/index.spec.js +88 -0
  85. package/migrations/update-22_2_0_grid-summary-borders/changes/theme-changes.json +17 -0
  86. package/migrations/update-22_2_0_grid-summary-borders/index.d.ts +3 -0
  87. package/migrations/update-22_2_0_grid-summary-borders/index.js +64 -0
  88. package/migrations/update-22_2_0_grid-summary-borders/index.spec.d.ts +1 -0
  89. package/migrations/update-22_2_0_grid-summary-borders/index.spec.js +100 -0
  90. package/package.json +6 -2
  91. package/schematics/tsconfig.tsbuildinfo +1 -1
  92. package/simple-combo/README.md +3 -0
  93. package/skills/igniteui-angular-components/references/form-controls.md +58 -0
  94. package/styles/igniteui-angular-dark.css +1 -1
  95. package/styles/igniteui-angular.css +1 -1
  96. package/styles/igniteui-bootstrap-dark.css +1 -1
  97. package/styles/igniteui-bootstrap-light.css +1 -1
  98. package/styles/igniteui-dark-green.css +1 -1
  99. package/styles/igniteui-fluent-dark-excel.css +1 -1
  100. package/styles/igniteui-fluent-dark-word.css +1 -1
  101. package/styles/igniteui-fluent-dark.css +1 -1
  102. package/styles/igniteui-fluent-light-excel.css +1 -1
  103. package/styles/igniteui-fluent-light-word.css +1 -1
  104. package/styles/igniteui-fluent-light.css +1 -1
  105. package/styles/igniteui-indigo-dark.css +1 -1
  106. package/styles/igniteui-indigo-light.css +1 -1
  107. package/styles/maps/igniteui-angular-dark.css.map +1 -1
  108. package/styles/maps/igniteui-angular.css.map +1 -1
  109. package/styles/maps/igniteui-bootstrap-dark.css.map +1 -1
  110. package/styles/maps/igniteui-bootstrap-light.css.map +1 -1
  111. package/styles/maps/igniteui-dark-green.css.map +1 -1
  112. package/styles/maps/igniteui-fluent-dark-excel.css.map +1 -1
  113. package/styles/maps/igniteui-fluent-dark-word.css.map +1 -1
  114. package/styles/maps/igniteui-fluent-dark.css.map +1 -1
  115. package/styles/maps/igniteui-fluent-light-excel.css.map +1 -1
  116. package/styles/maps/igniteui-fluent-light-word.css.map +1 -1
  117. package/styles/maps/igniteui-fluent-light.css.map +1 -1
  118. package/styles/maps/igniteui-indigo-dark.css.map +1 -1
  119. package/styles/maps/igniteui-indigo-light.css.map +1 -1
  120. package/types/igniteui-angular-calendar.d.ts +1 -1
  121. package/types/igniteui-angular-combo.d.ts +55 -12
  122. package/types/igniteui-angular-core.d.ts +64 -11
  123. package/types/igniteui-angular-date-picker.d.ts +3 -4
  124. package/types/igniteui-angular-directives.d.ts +50 -164
  125. package/types/igniteui-angular-drop-down.d.ts +76 -4
  126. package/types/igniteui-angular-grids-core.d.ts +63 -28
  127. package/types/igniteui-angular-grids-grid.d.ts +2 -7
  128. package/types/igniteui-angular-grids-hierarchical-grid.d.ts +5 -4
  129. package/types/igniteui-angular-grids-pivot-grid.d.ts +11 -5
  130. package/types/igniteui-angular-grids-tree-grid.d.ts +1 -1
  131. package/types/igniteui-angular-icon.d.ts +1 -1
  132. package/types/igniteui-angular-input-group.d.ts +1 -2
  133. package/types/igniteui-angular-query-builder.d.ts +1 -1
  134. package/types/igniteui-angular-radio.d.ts +16 -1
  135. package/types/igniteui-angular-select.d.ts +1 -2
  136. package/types/igniteui-angular-simple-combo.d.ts +7 -0
  137. package/types/igniteui-angular-stepper.d.ts +1 -1
  138. package/types/igniteui-angular-time-picker.d.ts +1 -2
  139. package/types/igniteui-angular-tree.d.ts +3 -3
  140. package/types/igniteui-angular-virtual-scroll.d.ts +408 -0
  141. package/types/igniteui-angular.d.ts +1 -0
  142. package/virtual-scroll/README.md +332 -0
@@ -0,0 +1,1257 @@
1
+ import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { signal, computed, inject, TemplateRef, Directive, ElementRef, NgZone, DOCUMENT, PLATFORM_ID, contentChild, viewChild, input, output, effect, untracked, afterNextRender, afterRenderEffect, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
4
+ import { clamp, isLeftToRight } from 'igniteui-angular/core';
5
+
6
+ /** The maximum scroll coordinate is a per-document constant, so probe once. */
7
+ const _maxBrowserSizeCache = new WeakMap();
8
+ /** Measures the largest scroll coordinate the browser can represent. */
9
+ function probeMaxBrowserSize(doc) {
10
+ const cached = _maxBrowserSizeCache.get(doc);
11
+ if (cached !== undefined) {
12
+ return cached;
13
+ }
14
+ const container = doc.body ?? doc.documentElement;
15
+ if (!container) {
16
+ return Number.POSITIVE_INFINITY;
17
+ }
18
+ const probe = doc.createElement("div");
19
+ probe.style.position = "absolute";
20
+ probe.style.top = `${Number.MAX_SAFE_INTEGER}px`;
21
+ probe.style.width = "0";
22
+ probe.style.height = "0";
23
+ probe.style.visibility = "hidden";
24
+ container.appendChild(probe);
25
+ // The rect is viewport relative, so add back how far the document is scrolled.
26
+ const scrollOffset = doc.documentElement?.scrollTop ?? 0;
27
+ const size = Math.abs(probe.getBoundingClientRect().top) + scrollOffset;
28
+ container.removeChild(probe);
29
+ _maxBrowserSizeCache.set(doc, size);
30
+ return size;
31
+ }
32
+ /**
33
+ * Fills `tree` with the partial range sums of `sizes` in one O(N) pass.
34
+ * `tree` is a 1-indexed Fenwick array of `sizes.length + 1` zeroed entries.
35
+ * Returns the total sum.
36
+ */
37
+ function buildTree(tree, sizes) {
38
+ const length = sizes.length;
39
+ let total = 0;
40
+ for (let i = 1; i <= length; i++) {
41
+ tree[i] += sizes[i - 1];
42
+ total += sizes[i - 1];
43
+ const parent = i + (i & -i);
44
+ if (parent <= length) {
45
+ tree[parent] += tree[i];
46
+ }
47
+ }
48
+ return total;
49
+ }
50
+ /**
51
+ * Binary Indexed Tree (Fenwick tree) over item sizes. Each hot-path operation
52
+ * is O(log N): point update (item measured), prefix sum (scroll offset), and
53
+ * index at offset (scroll to item, through binary lifting).
54
+ */
55
+ class SizeTree {
56
+ constructor(sizes, tree, total, measured) {
57
+ this.length = sizes.length;
58
+ this._sizes = sizes;
59
+ this._tree = tree;
60
+ this._total = total;
61
+ this._measured = measured;
62
+ this._topBit = this.length > 0 ? 1 << (31 - Math.clz32(this.length)) : 0;
63
+ }
64
+ /** Creates a tree of `length` unmeasured items, each set to `fillSize`. O(N). */
65
+ static filled(length, fillSize) {
66
+ return SizeTree._build(new Float64Array(length).fill(fillSize), new Uint8Array(length));
67
+ }
68
+ /** Builds a tree from a sizes array and its matching measured flags. O(N). */
69
+ static _build(sizes, measured) {
70
+ const tree = new Float64Array(sizes.length + 1);
71
+ const total = buildTree(tree, sizes);
72
+ return new SizeTree(sizes, tree, total, measured);
73
+ }
74
+ /** Total size of all items. O(1). */
75
+ get totalSize() {
76
+ return this._total;
77
+ }
78
+ /**
79
+ * Prefix sum of items [0, i): the virtual scroll offset at the leading
80
+ * edge of item i. O(log N).
81
+ */
82
+ prefixSum(i) {
83
+ let sum = 0;
84
+ for (let j = i; j > 0; j -= j & -j) {
85
+ sum += this._tree[j];
86
+ }
87
+ return sum;
88
+ }
89
+ /**
90
+ * Sets the size of the item at a 0-based index and marks it measured, so
91
+ * later `applyEstimate` calls leave it alone. Returns true when the size
92
+ * changed. O(log N).
93
+ */
94
+ update(index, newSize) {
95
+ if (index < 0 || index >= this.length) {
96
+ return false;
97
+ }
98
+ const old = this._sizes[index];
99
+ this._measured[index] = 1;
100
+ if (old === newSize) {
101
+ return false;
102
+ }
103
+ const delta = newSize - old;
104
+ this._sizes[index] = newSize;
105
+ this._total += delta;
106
+ for (let i = index + 1; i <= this.length; i += i & -i) {
107
+ this._tree[i] += delta;
108
+ }
109
+ return true;
110
+ }
111
+ /**
112
+ * Returns a new tree of `newLength` items in one O(N) pass. Sizes and
113
+ * measured flags are kept up to `min(this.length, newLength, retainCount)`.
114
+ * The remainder is filled with `fillSize` and marked unmeasured. Pass a
115
+ * `retainCount` below the item count when the data behind those indices
116
+ * changed identity.
117
+ */
118
+ cloneResized(newLength, fillSize, retainCount = newLength) {
119
+ const sizes = new Float64Array(newLength).fill(fillSize);
120
+ const measured = new Uint8Array(newLength);
121
+ const retained = Math.max(0, Math.min(this.length, newLength, retainCount));
122
+ sizes.set(this._sizes.subarray(0, retained));
123
+ measured.set(this._measured.subarray(0, retained));
124
+ return SizeTree._build(sizes, measured);
125
+ }
126
+ /**
127
+ * Sets `estimatedSize` on each unmeasured item. Returns true when at least
128
+ * one size changed.
129
+ *
130
+ * One estimate change can touch most of the list, so this rebuilds in one
131
+ * O(N) pass instead of one O(log N) `update` per item.
132
+ */
133
+ applyEstimate(estimatedSize) {
134
+ let changed = false;
135
+ for (let i = 0; i < this.length; i++) {
136
+ if (!this._measured[i] && this._sizes[i] !== estimatedSize) {
137
+ this._sizes[i] = estimatedSize;
138
+ changed = true;
139
+ }
140
+ }
141
+ if (!changed) {
142
+ return false;
143
+ }
144
+ this._tree.fill(0);
145
+ this._total = buildTree(this._tree, this._sizes);
146
+ return true;
147
+ }
148
+ /**
149
+ * Returns the 0-based index of the item containing the scroll `offset`:
150
+ * the largest i where `prefixSum(i) <= offset < prefixSum(i + 1)`. O(log N).
151
+ */
152
+ findIndexAtOffset(offset) {
153
+ if (offset <= 0 || this.length === 0) {
154
+ return 0;
155
+ }
156
+ let index = 0;
157
+ let remaining = offset;
158
+ for (let bit = this._topBit; bit > 0; bit >>= 1) {
159
+ const next = index + bit;
160
+ if (next <= this.length && this._tree[next] <= remaining) {
161
+ index = next;
162
+ remaining -= this._tree[index];
163
+ }
164
+ }
165
+ return Math.min(this.length - 1, index);
166
+ }
167
+ }
168
+ /**
169
+ * Pure scroll-math engine for one axis of virtual scrolling. A Fenwick tree
170
+ * holds all size state, exposed through signals so downstream `computed()`
171
+ * values (visible range, spacer size, translate offset) react to any change
172
+ * of item sizes or item count.
173
+ *
174
+ * ### Virtual and DOM coordinates
175
+ *
176
+ * Browsers limit how far an element can scroll. When the total item size is
177
+ * larger than that limit, the engine maps the *virtual* scroll range
178
+ * (`0…totalSize - viewport`) onto the *DOM* scroll range the browser can
179
+ * represent (`0…domSize - viewport`), see `_scrollRanges`. Incoming scroll
180
+ * positions are converted to virtual offsets, outgoing offsets to DOM ones.
181
+ * Items render at their real pixel size, so item sizes are always virtual.
182
+ */
183
+ class VirtualScrollEngine {
184
+ constructor() {
185
+ this._maxBrowserSize = Number.POSITIVE_INFINITY;
186
+ this._tree = null;
187
+ /** Bumped on every structural change: resize, measurement or estimate. */
188
+ this._version = signal(0, /* @ts-ignore */
189
+ ...(ngDevMode ? [{ debugName: "_version" }] : /* istanbul ignore next */ []));
190
+ /**
191
+ * Read this from a `computed()` to make it recompute on any size change.
192
+ * `totalSize` and `domSize` already do.
193
+ */
194
+ this.version = this._version.asReadonly();
195
+ /** Total virtual size of all items in px. */
196
+ this.totalSize = computed(() => {
197
+ this._version();
198
+ return this._tree?.totalSize ?? 0;
199
+ }, /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "totalSize" }] : /* istanbul ignore next */ []));
201
+ /**
202
+ * Total size in DOM space, clamped to the maximum browser size.
203
+ *
204
+ * Depends on `_version` directly rather than on `totalSize()`. The maximum
205
+ * can change while the total does not (it is probed after the first
206
+ * render), and an unchanged `totalSize` would not propagate.
207
+ */
208
+ this.domSize = computed(() => {
209
+ this._version();
210
+ return Math.min(this._tree?.totalSize ?? 0, this._maxBrowserSize);
211
+ }, /* @ts-ignore */
212
+ ...(ngDevMode ? [{ debugName: "domSize" }] : /* istanbul ignore next */ []));
213
+ }
214
+ /**
215
+ * Probes the document's maximum scroll coordinate and rescales. Notifies,
216
+ * because the probe can only run after the first render, by which point
217
+ * `domSize` has already been read at the uncompressed total.
218
+ */
219
+ initMaxBrowserSize(doc) {
220
+ this._maxBrowserSize = probeMaxBrowserSize(doc);
221
+ this._invalidate();
222
+ }
223
+ /**
224
+ * Resizes the internal sizes array to `length`. Measured sizes below
225
+ * `retainCount` are kept, the remainder falls back to `estimatedSize`.
226
+ * Callers that only append can keep the default `retainCount`. Callers
227
+ * whose data changed identity at some index must pass that index, so the
228
+ * stale measurements after it are discarded.
229
+ */
230
+ resize(length, estimatedSize, retainCount = length) {
231
+ if (this._tree?.length === length && retainCount >= length) {
232
+ return;
233
+ }
234
+ this._tree = this._tree
235
+ ? this._tree.cloneResized(length, estimatedSize, retainCount)
236
+ : SizeTree.filled(length, estimatedSize);
237
+ this._invalidate();
238
+ }
239
+ /** Records the measured DOM size for a single item. */
240
+ measureItem(index, size) {
241
+ if (this._tree?.update(index, size)) {
242
+ this._invalidate();
243
+ }
244
+ }
245
+ /**
246
+ * Applies a new estimate to every item not yet measured in the DOM. Use
247
+ * this when `estimatedItemSize` changes but the item count does not,
248
+ * because `resize` is then a no-op.
249
+ */
250
+ updateEstimatedSize(estimatedSize) {
251
+ if (this._tree?.applyEstimate(estimatedSize)) {
252
+ this._invalidate();
253
+ }
254
+ }
255
+ /**
256
+ * Returns the DOM scroll offset in px that puts the item at `index` at the
257
+ * leading edge of a `viewportSize` px viewport.
258
+ */
259
+ getScrollOffsetForIndex(index, viewportSize) {
260
+ if (!this._tree || index <= 0) {
261
+ return 0;
262
+ }
263
+ return this._toDom(this._tree.prefixSum(Math.min(index, this._tree.length)), viewportSize);
264
+ }
265
+ /**
266
+ * Returns the DOM scroll offset that positions the item at `index` in a
267
+ * `viewportSize` px viewport, aligned by `align` and clamped to the
268
+ * reachable scroll range.
269
+ *
270
+ * The slack is computed in virtual space against the item's real size and
271
+ * converted to DOM space once, at the end. Under compression one DOM pixel
272
+ * is several virtual pixels, so mixed coordinates would scale the slack.
273
+ */
274
+ getAlignedScrollOffset(index, viewportSize, align) {
275
+ const bounds = this._itemBounds(index);
276
+ if (!bounds) {
277
+ return 0;
278
+ }
279
+ const [start, end] = bounds;
280
+ const slack = viewportSize - Math.max(0, end - start);
281
+ let offset = start;
282
+ if (align === "center") {
283
+ offset -= slack / 2;
284
+ }
285
+ else if (align === "end") {
286
+ offset -= slack;
287
+ }
288
+ return clamp(this._toDom(offset, viewportSize), 0, Math.max(0, this.domSize() - viewportSize));
289
+ }
290
+ /**
291
+ * The alignment `scrollIntoView({ block: 'nearest' })` resolves to for the
292
+ * item at `index` at the given DOM scroll position: `start` for an item
293
+ * before the viewport, `end` for one past it, and `null` for one that needs
294
+ * no scrolling because it is fully inside the viewport, or larger than the
295
+ * viewport and covering it.
296
+ */
297
+ getNearestAlignment(index, scrollPosition, viewportSize) {
298
+ const bounds = this._itemBounds(index);
299
+ if (!bounds) {
300
+ return null;
301
+ }
302
+ const [start, end] = bounds;
303
+ const viewStart = this._toVirtual(scrollPosition, viewportSize);
304
+ const viewEnd = viewStart + viewportSize;
305
+ const contained = start >= viewStart && end <= viewEnd;
306
+ const spanning = start <= viewStart && end >= viewEnd;
307
+ if (contained || spanning) {
308
+ return null;
309
+ }
310
+ return start < viewStart ? "start" : "end";
311
+ }
312
+ /** Returns the visible and over-scanned item range for the given scroll state. */
313
+ getVisibleRange(scrollPosition, viewportSize, overScan) {
314
+ if (!this._tree || this._tree.length === 0 || viewportSize <= 0) {
315
+ return { startIndex: 0, endIndex: -1 };
316
+ }
317
+ // The viewport is not compressed. Items render at their real pixel size,
318
+ // so a `viewportSize` px viewport always shows that many virtual pixels of
319
+ // items, at any compression of the scroll range.
320
+ const startOffset = this._toVirtual(scrollPosition, viewportSize);
321
+ const first = this._tree.findIndexAtOffset(startOffset);
322
+ const last = this._tree.findIndexAtOffset(startOffset + viewportSize);
323
+ return {
324
+ startIndex: Math.max(0, first - overScan),
325
+ endIndex: Math.min(this._tree.length - 1, last + overScan),
326
+ };
327
+ }
328
+ /**
329
+ * Sum of the real sizes of the items in [startIndex, endIndex]. The render
330
+ * pass uses it to clamp the content translate offset, so rendered items do
331
+ * not overflow past `domSize` under coordinate compression.
332
+ */
333
+ getPhysicalRangeSize(startIndex, endIndex) {
334
+ if (!this._tree) {
335
+ return 0;
336
+ }
337
+ const start = Math.max(0, startIndex);
338
+ const end = Math.min(Math.max(endIndex + 1, start), this._tree.length);
339
+ return this._tree.prefixSum(end) - this._tree.prefixSum(start);
340
+ }
341
+ /**
342
+ * The virtual [start, end] offsets of the item at `index`, clamped into the
343
+ * item range. Null while there are no items.
344
+ */
345
+ _itemBounds(index) {
346
+ if (!this._tree || this._tree.length === 0) {
347
+ return null;
348
+ }
349
+ const clamped = clamp(index, 0, this._tree.length - 1);
350
+ return [this._tree.prefixSum(clamped), this._tree.prefixSum(clamped + 1)];
351
+ }
352
+ _invalidate() {
353
+ this._version.update((v) => v + 1);
354
+ }
355
+ /**
356
+ * A virtual offset as the DOM scroll position that shows it, for a
357
+ * `viewportSize` px viewport.
358
+ */
359
+ _toDom(virtualOffset, viewportSize) {
360
+ const [virtualRange, domRange] = this._scrollRanges(viewportSize);
361
+ return (virtualOffset * domRange) / virtualRange;
362
+ }
363
+ /**
364
+ * A DOM scroll position as the virtual offset it shows, for a
365
+ * `viewportSize` px viewport.
366
+ */
367
+ _toVirtual(domOffset, viewportSize) {
368
+ const [virtualRange, domRange] = this._scrollRanges(viewportSize);
369
+ return (Math.max(0, domOffset) * virtualRange) / domRange;
370
+ }
371
+ /**
372
+ * The virtual and DOM scroll ranges to map between; the identity while the
373
+ * content fits the browser's limit. One viewport of items renders at real
374
+ * size at any scroll position, so both ranges exclude it: mapping the totals
375
+ * instead would leave the last `viewport * (ratio - 1)` virtual pixels past
376
+ * the largest DOM offset. Callers multiply before dividing, so exact
377
+ * positions come back exact.
378
+ */
379
+ _scrollRanges(viewportSize) {
380
+ const totalSize = this._tree?.totalSize ?? 0;
381
+ const domRange = this._maxBrowserSize - viewportSize;
382
+ if (totalSize <= this._maxBrowserSize || domRange <= 0) {
383
+ return [1, 1];
384
+ }
385
+ return [totalSize - viewportSize, domRange];
386
+ }
387
+ }
388
+
389
+ /** Template context for a single item of the virtual scroll. */
390
+ class IgxVsItemContext {
391
+ constructor(
392
+ /** The current item in the virtual scroll. */
393
+ $implicit,
394
+ /** The index of the current item. */
395
+ index,
396
+ /** The total number of items in the virtual scroll. */
397
+ count) {
398
+ this.$implicit = $implicit;
399
+ this.index = index;
400
+ this.count = count;
401
+ }
402
+ /** Whether the current item is the first in the list. */
403
+ get first() {
404
+ return this.index === 0;
405
+ }
406
+ /** Whether the current item is the last in the list. */
407
+ get last() {
408
+ return this.index === this.count - 1;
409
+ }
410
+ /** Whether the current item is at an even index. */
411
+ get even() {
412
+ return this.index % 2 === 0;
413
+ }
414
+ /** Whether the current item is at an odd index. */
415
+ get odd() {
416
+ return !this.even;
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Directive to mark an `ng-template` as the item template for the virtual scroll component.
422
+ * The template provided by this directive will be used to render each item in the virtual scroll.
423
+ * The context for the template will include the item data and its index.
424
+ *
425
+ * @example
426
+ * ```html
427
+ * <igx-virtual-scroll [data]="items">
428
+ * <ng-template igxVirtualItem let-item let-i="index">
429
+ * <div>{{ i }}: {{ item }}</div>
430
+ * </ng-template>
431
+ * </igx-virtual-scroll>
432
+ * ```
433
+ */
434
+ class IgxVirtualItemDirective {
435
+ constructor() {
436
+ this.template = inject(TemplateRef);
437
+ }
438
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: IgxVirtualItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
439
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: IgxVirtualItemDirective, isStandalone: true, selector: "ng-template[igxVirtualItem]", ngImport: i0 }); }
440
+ }
441
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: IgxVirtualItemDirective, decorators: [{
442
+ type: Directive,
443
+ args: [{ selector: "ng-template[igxVirtualItem]" }]
444
+ }] });
445
+
446
+ /** Defaults for the inputs, also used as the fallback for invalid values. */
447
+ const DEFAULT_OVER_SCAN = 2;
448
+ const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
449
+ /** How close to the end of `data` the window must get to emit `dataRequest`. */
450
+ const DATA_REQUEST_THRESHOLD = 5;
451
+ const DATA_REQUEST_MIN_COUNT = 20;
452
+ const DATA_REQUEST_OVER_SCAN_FACTOR = 4;
453
+ /** Give-up bounds for the two loops that wait for the layout to stabilize. */
454
+ const MAX_LAYOUT_SETTLE_PASSES = 20;
455
+ const MAX_SCROLL_CORRECTION_PASSES = 5;
456
+ const SCROLL_END_TIMEOUT_MS = 2000;
457
+ const SCROLL_OFFSET_EPSILON_PX = 1;
458
+ /** How long the scroll position must stay unchanged to count as settled. */
459
+ const SCROLL_IDLE_MS = 100;
460
+ /**
461
+ * Upper limit on one `requestAnimationFrame` wait. A hidden tab or a detached
462
+ * element gets no frames, and `layoutComplete` must still resolve there.
463
+ */
464
+ const LAYOUT_FRAME_TIMEOUT_MS = 100;
465
+ const EMPTY_RANGE = Object.freeze({ startIndex: 0, endIndex: -1 });
466
+ /** A consumer-supplied index, count or size, reduced to a whole non-negative number. */
467
+ function toCount(value) {
468
+ const count = Math.trunc(value);
469
+ return Number.isFinite(count) ? Math.max(0, count) : 0;
470
+ }
471
+ function loadedEqual(a, b) {
472
+ return (a.items === b.items &&
473
+ a.startIndex === b.startIndex &&
474
+ a.totalCount === b.totalCount &&
475
+ a.windowed === b.windowed);
476
+ }
477
+ function rangesEqual(a, b) {
478
+ return a.startIndex === b.startIndex && a.endIndex === b.endIndex;
479
+ }
480
+ function statesEqual(a, b) {
481
+ return (a !== null &&
482
+ rangesEqual(a, b) &&
483
+ a.viewportSize === b.viewportSize &&
484
+ a.totalSize === b.totalSize);
485
+ }
486
+ /** The data index an item wrapper carries, or -1 when it has none. */
487
+ function itemIndex(element) {
488
+ const index = Number.parseInt(element.dataset["index"] ?? "", 10);
489
+ return Number.isInteger(index) && index >= 0 ? index : -1;
490
+ }
491
+ function onAbort(abort, cancel) {
492
+ abort.addEventListener("abort", cancel, { once: true });
493
+ }
494
+ /**
495
+ * A virtual scroll component for large lists. Only the items visible in the
496
+ * viewport (plus a configurable over-scan) are rendered.
497
+ *
498
+ * @igxKeywords virtual, scroll, virtualization, list
499
+ * @igxGroup Grids & Lists
500
+ *
501
+ * @example
502
+ * ```html
503
+ * <igx-virtual-scroll [data]="items" style="height: 400px">
504
+ * <ng-template igxVirtualItem let-item let-i="index">
505
+ * <div>{{ i }}: {{ item }}</div>
506
+ * </ng-template>
507
+ * </igx-virtual-scroll>
508
+ * ```
509
+ */
510
+ class IgxVirtualScrollComponent {
511
+ //#endregion
512
+ constructor() {
513
+ //#region Dependency injection
514
+ this._hostRef = inject(ElementRef);
515
+ this._zone = inject(NgZone);
516
+ this._document = inject(DOCUMENT);
517
+ this._isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
518
+ //#endregion
519
+ //#region Internal state
520
+ this._engine = new VirtualScrollEngine();
521
+ this._viewportResizeObserver = null;
522
+ this._itemResizeObserver = null;
523
+ this._onScroll = null;
524
+ /** Elements currently registered with the item resize observer. */
525
+ this._observedItems = new Set();
526
+ /** The data index each observed wrapper element last hosted. */
527
+ this._observedItemIndexes = new WeakMap();
528
+ /**
529
+ * The live scroll offset on the active axis. A plain field, not a signal:
530
+ * `_visibleRange` reads it but is invalidated by `_scrollTick`, so a scroll
531
+ * that does not move the rendered window schedules no work.
532
+ */
533
+ this._scrollPosition = 0;
534
+ /** Bumped only when a scroll actually moves the rendered window. */
535
+ this._scrollTick = signal(0, /* @ts-ignore */
536
+ ...(ngDevMode ? [{ debugName: "_scrollTick" }] : /* istanbul ignore next */ []));
537
+ /** The measured viewport, or `null` while the host has never been laid out. */
538
+ this._viewportSize = signal(null, /* @ts-ignore */
539
+ ...(ngDevMode ? [{ debugName: "_viewportSize" }] : /* istanbul ignore next */ []));
540
+ /**
541
+ * Whether the host is laid out left-to-right, as of the last read of the
542
+ * horizontal axis. `_contentTransform` depends on it, so a `dir` change
543
+ * flips the wrapper even while the rendered window stays put.
544
+ */
545
+ this._ltr = signal(true, /* @ts-ignore */
546
+ ...(ngDevMode ? [{ debugName: "_ltr" }] : /* istanbul ignore next */ []));
547
+ this._lastEmittedState = null;
548
+ this._hasPendingDataRequest = false;
549
+ /**
550
+ * The `startIndex` of the last emitted `dataRequest`, which is also the
551
+ * item count at that emit. See `_checkDataRequest`.
552
+ */
553
+ this._lastDataRequestIndex = -1;
554
+ this._layoutCompletePromise = null;
555
+ this._scrollRequestId = 0;
556
+ //#endregion
557
+ //#region View and content children
558
+ this._itemDirective = contentChild(IgxVirtualItemDirective, /* @ts-ignore */
559
+ ...(ngDevMode ? [{ debugName: "_itemDirective" }] : /* istanbul ignore next */ []));
560
+ this._contentDivRef = viewChild("contentDiv", /* @ts-ignore */
561
+ ...(ngDevMode ? [{ debugName: "_contentDivRef" }] : /* istanbul ignore next */ []));
562
+ //#endregion
563
+ //#region Public inputs
564
+ /**
565
+ * The array of items to virtualize.
566
+ *
567
+ * Compared by reference: mutating the array in place (`data.push(...)`)
568
+ * causes no update. Assign a new array instead. The `dataRequest` flow
569
+ * also expects a new array.
570
+ */
571
+ this.data = input([], /* @ts-ignore */
572
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
573
+ /**
574
+ * Scroll orientation of the virtual scroll.
575
+ * Can be either "vertical" or "horizontal".
576
+ * Default is "vertical".
577
+ */
578
+ this.orientation = input("vertical", /* @ts-ignore */
579
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
580
+ /**
581
+ * Number of extra items to render beyond the visible area of the viewport.
582
+ * Higher values reduce blank flashes during fast scrolling but may impact performance.
583
+ * Default is 2.
584
+ */
585
+ this.overScan = input(DEFAULT_OVER_SCAN, /* @ts-ignore */
586
+ ...(ngDevMode ? [{ debugName: "overScan" }] : /* istanbul ignore next */ []));
587
+ /**
588
+ * Estimated item size in pixels used before an item is measured in the DOM.
589
+ * The engine replaces this with the actual measured size after the first render of each item.
590
+ * Default is 50 pixels.
591
+ * Setting this to a value close to the actual average item size can improve initial rendering performance.
592
+ */
593
+ this.estimatedItemSize = input(DEFAULT_ESTIMATED_ITEM_SIZE, /* @ts-ignore */
594
+ ...(ngDevMode ? [{ debugName: "estimatedItemSize" }] : /* istanbul ignore next */ []));
595
+ /**
596
+ * A loaded page of a larger collection, for data that arrives a page at a time.
597
+ *
598
+ * Takes the place of `data` while it is set. The list is as long as `totalCount`, so the
599
+ * scrollbar spans the whole collection while only the page is in memory. Indices the page
600
+ * does not cover render nothing; use `stateChange` to see which range is wanted and supply
601
+ * the page that covers it.
602
+ *
603
+ * Measured sizes are kept per index across pages while `totalCount` holds: the indices still
604
+ * mean the same records. A page with another `totalCount` is a different collection, and
605
+ * every size is measured again. A page that keeps the count but puts different records at
606
+ * the same indices keeps the sizes measured for the previous ones until those rows render.
607
+ *
608
+ * @example
609
+ * ```html
610
+ * <igx-virtual-scroll [dataWindow]="page" (stateChange)="load($event)" style="height: 400px">
611
+ * <ng-template igxVirtualItem let-item>{{ item?.name }}</ng-template>
612
+ * </igx-virtual-scroll>
613
+ * ```
614
+ */
615
+ this.dataWindow = input(null, /* @ts-ignore */
616
+ ...(ngDevMode ? [{ debugName: "dataWindow" }] : /* istanbul ignore next */ []));
617
+ /**
618
+ * @hidden @internal
619
+ *
620
+ * Viewport size in pixels to render the first window against, for a list that is hidden
621
+ * until the change detection pass that reveals it and so has no size to measure in it.
622
+ *
623
+ * A hint for that first render: once the host has been laid out its own size takes over,
624
+ * zero included. Changing `orientation` begins an axis with no measurement of its own, so
625
+ * the hint applies again there. Negative, `NaN` and infinite values count as no hint.
626
+ *
627
+ * A workaround for the drop-down family, which reads and focuses items in the pass that
628
+ * opens the list. To be removed once those components render their list on reveal.
629
+ */
630
+ this.initialViewportSize = input(0, /* @ts-ignore */
631
+ ...(ngDevMode ? [{ debugName: "initialViewportSize" }] : /* istanbul ignore next */ []));
632
+ /**
633
+ * Item template provided programmatically. Takes precedence over a content
634
+ * `ng-template[igxVirtualItem]` when both are provided.
635
+ *
636
+ * Items are measured by their border box, so margins accumulate as drift
637
+ * down the list. Use padding on the item, or a gap on a wrapper, instead.
638
+ *
639
+ * Only the current window is in the DOM, so assistive technology cannot
640
+ * infer an item's position from the markup. Templates that render a role
641
+ * with set semantics (`listitem`, `option`, `row`, ...) should map the
642
+ * context's `index` and `count` onto `aria-posinset` and `aria-setsize`.
643
+ */
644
+ this.itemTemplate = input(null, /* @ts-ignore */
645
+ ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
646
+ //#endregion
647
+ //#region Public outputs
648
+ /** Emitted when the rendered virtual window changes. */
649
+ this.stateChange = output();
650
+ /**
651
+ * Emitted when the rendered window comes within a few items of the end of
652
+ * `data`. Also emitted on the first render, when the loaded items do not
653
+ * fill the viewport. Listen to this event to append more items
654
+ * (infinite / remote scrolling).
655
+ */
656
+ this.dataRequest = output();
657
+ //#endregion
658
+ //#region Derived state
659
+ this._isVertical = computed(() => this.orientation() === "vertical", /* @ts-ignore */
660
+ ...(ngDevMode ? [{ debugName: "_isVertical" }] : /* istanbul ignore next */ []));
661
+ this._resolvedTemplate = computed(() => this.itemTemplate() ?? this._itemDirective()?.template ?? null, /* @ts-ignore */
662
+ ...(ngDevMode ? [{ debugName: "_resolvedTemplate" }] : /* istanbul ignore next */ []));
663
+ /**
664
+ * What the component has to work with, from whichever data input is in use. A page is
665
+ * trusted to be no longer than the collection it says it belongs to.
666
+ */
667
+ this._loaded = computed(() => {
668
+ const window = this.dataWindow();
669
+ if (!window) {
670
+ const items = this.data() ?? [];
671
+ return { items, startIndex: 0, totalCount: items.length, windowed: false };
672
+ }
673
+ const items = window.items ?? [];
674
+ const startIndex = toCount(window.startIndex);
675
+ return {
676
+ items,
677
+ startIndex,
678
+ totalCount: Math.max(toCount(window.totalCount), startIndex + items.length),
679
+ windowed: true,
680
+ };
681
+ }, { ...(ngDevMode ? { debugName: "_loaded" } : /* istanbul ignore next */ {}), equal: loadedEqual });
682
+ /** The measured size once the host has been laid out, the hint until then. */
683
+ this._effectiveViewportSize = computed(() => this._viewportSize() ?? toCount(this.initialViewportSize()), /* @ts-ignore */
684
+ ...(ngDevMode ? [{ debugName: "_effectiveViewportSize" }] : /* istanbul ignore next */ []));
685
+ /** The configured `overScan`, normalized to a non-negative integer. */
686
+ this._normalizedOverScan = computed(() => {
687
+ const value = Number(this.overScan());
688
+ return Number.isFinite(value)
689
+ ? Math.max(0, Math.floor(value))
690
+ : DEFAULT_OVER_SCAN;
691
+ }, /* @ts-ignore */
692
+ ...(ngDevMode ? [{ debugName: "_normalizedOverScan" }] : /* istanbul ignore next */ []));
693
+ /** The configured `estimatedItemSize`, normalized to a positive number. */
694
+ this._normalizedItemSize = computed(() => {
695
+ const value = Number(this.estimatedItemSize());
696
+ return Number.isFinite(value) && value > 0
697
+ ? value
698
+ : DEFAULT_ESTIMATED_ITEM_SIZE;
699
+ }, /* @ts-ignore */
700
+ ...(ngDevMode ? [{ debugName: "_normalizedItemSize" }] : /* istanbul ignore next */ []));
701
+ /**
702
+ * The window to render for the current scroll position and viewport. Empty
703
+ * until an item template is resolved, because nothing renders without one.
704
+ *
705
+ * The scroll position is read from the plain `_scrollPosition` field so
706
+ * that a recompute triggered by a measurement uses the live offset, while
707
+ * `_scrollTick` limits recomputes to scrolls that actually move the window.
708
+ */
709
+ this._visibleRange = computed(() => {
710
+ // Depend on the engine so the range recomputes whenever item sizes or
711
+ // the item count change.
712
+ this._engine.version();
713
+ this._scrollTick();
714
+ return this._resolvedTemplate()
715
+ ? this._engine.getVisibleRange(this._scrollPosition, this._effectiveViewportSize(), this._normalizedOverScan())
716
+ : EMPTY_RANGE;
717
+ }, { ...(ngDevMode ? { debugName: "_visibleRange" } : /* istanbul ignore next */ {}), equal: rangesEqual });
718
+ /** The track size, in DOM space. */
719
+ this._spaceSize = this._engine.domSize;
720
+ /** The part of the rendered range a page actually covers. */
721
+ this._loadedRange = computed(() => {
722
+ const { startIndex, endIndex } = this._visibleRange();
723
+ const { items, startIndex: from } = this._loaded();
724
+ return {
725
+ startIndex: Math.max(startIndex, from),
726
+ endIndex: Math.min(endIndex, from + items.length - 1),
727
+ };
728
+ }, { ...(ngDevMode ? { debugName: "_loadedRange" } : /* istanbul ignore next */ {}), equal: rangesEqual });
729
+ /** The item contexts for the currently rendered window, in render order. */
730
+ this._renderedItems = computed(() => {
731
+ const { startIndex, endIndex } = this._loadedRange();
732
+ const { items, startIndex: from, totalCount } = this._loaded();
733
+ const rendered = [];
734
+ for (let i = startIndex; i <= endIndex; i++) {
735
+ rendered.push(new IgxVsItemContext(items[i - from], i, totalCount));
736
+ }
737
+ return rendered;
738
+ }, /* @ts-ignore */
739
+ ...(ngDevMode ? [{ debugName: "_renderedItems" }] : /* istanbul ignore next */ []));
740
+ /**
741
+ * The `translateY` / `translateX` for the content wrapper. It is absolutely
742
+ * positioned at the origin of a `domSize` px track, so translating it to
743
+ * the first rendered item's offset puts that item at its virtual position.
744
+ */
745
+ this._contentTransform = computed(() => {
746
+ // The offsets below are plain reads of the engine's size state, so depend
747
+ // on its version explicitly.
748
+ this._engine.version();
749
+ const range = this._loadedRange();
750
+ // Under coordinate compression item positions are scaled down but item
751
+ // sizes are not. Without this cap the rendered range would overflow past
752
+ // domSize at the end of the list, pushing the last items beyond the
753
+ // maximum browser scroll coordinate.
754
+ const position = clamp(this._engine.getScrollOffsetForIndex(range.startIndex, this._effectiveViewportSize()), 0, this._engine.domSize() -
755
+ this._engine.getPhysicalRangeSize(range.startIndex, range.endIndex));
756
+ if (this._isVertical()) {
757
+ return `translateY(${position}px)`;
758
+ }
759
+ // In RTL the wrapper is anchored to the right edge of the track, so it
760
+ // translates towards the negative (leading) direction.
761
+ return `translateX(${this._ltr() ? position : -position}px)`;
762
+ }, /* @ts-ignore */
763
+ ...(ngDevMode ? [{ debugName: "_contentTransform" }] : /* istanbul ignore next */ []));
764
+ // Sync the engine's item count with `data`, discarding the measurements
765
+ // of items whose identity changed.
766
+ effect(() => {
767
+ const loaded = this._loaded();
768
+ untracked(() => {
769
+ const previous = this._previousItems;
770
+ const retained = this._retainCount(previous, loaded);
771
+ this._previousItems = loaded;
772
+ this._engine.resize(loaded.totalCount, this._normalizedItemSize(), retained);
773
+ this._remeasureFrom(retained);
774
+ // New data (or a reset) clears any in-flight data request so the next
775
+ // approach to the end of the list can emit again.
776
+ this._hasPendingDataRequest = false;
777
+ });
778
+ });
779
+ // Re-apply the estimate when it changes but the item count does not,
780
+ // because `resize` is then a no-op.
781
+ effect(() => {
782
+ const size = this._normalizedItemSize();
783
+ untracked(() => this._engine.updateEstimatedSize(size));
784
+ });
785
+ // The scroll offset of the previous axis does not carry over.
786
+ effect(() => {
787
+ this.orientation();
788
+ untracked(() => {
789
+ if (!this._isBrowser) {
790
+ return;
791
+ }
792
+ // The size of the previous axis says nothing about the new one.
793
+ this._viewportSize.set(null);
794
+ this._measureViewport();
795
+ this._scrollPosition = this._currentAxisScroll();
796
+ this._scrollTick.update((v) => v + 1);
797
+ });
798
+ });
799
+ afterNextRender(() => {
800
+ this._engine.initMaxBrowserSize(this._document);
801
+ this._measureViewport();
802
+ this._setupScrollListener();
803
+ this._setupViewportResizeObserver();
804
+ });
805
+ // Runs after the DOM reflects the current window, and re-runs whenever
806
+ // the window or the engine's sizes change.
807
+ afterRenderEffect({
808
+ read: () => {
809
+ // What is wanted and what is rendered change independently: the viewport can move
810
+ // into indices no page covers, and a page can arrive without the viewport moving.
811
+ this._visibleRange();
812
+ this._renderedItems();
813
+ this._engine.version();
814
+ untracked(() => {
815
+ this._scheduleItemMeasurement();
816
+ this._checkDataRequest();
817
+ this._emitStateChange();
818
+ });
819
+ },
820
+ });
821
+ }
822
+ ngOnDestroy() {
823
+ this._teardown();
824
+ }
825
+ //#region Public API
826
+ /**
827
+ * Resolves when the virtual scroll has settled: the current render pass is
828
+ * complete, the item-size measurements it triggers are complete, and so
829
+ * are the renders those measurements schedule.
830
+ */
831
+ get layoutComplete() {
832
+ if (!this._layoutCompletePromise) {
833
+ this._layoutCompletePromise = this._resolveLayoutComplete();
834
+ }
835
+ return this._layoutCompletePromise;
836
+ }
837
+ /**
838
+ * Scrolls to the specified item index.
839
+ *
840
+ * Items outside the rendered window have only an estimated size, so the
841
+ * first jump can miss the target. The items at the landing point are then
842
+ * measured and the scroll position is corrected, until the offset is
843
+ * stable. The returned promise resolves on that final offset; callers that
844
+ * need only the first, approximate scroll can ignore it.
845
+ *
846
+ * @param index The index of the item to scroll to.
847
+ * @param options `block` / `inline` select the alignment (`start`,
848
+ * `center`, `end` or `nearest`); `behavior` selects `auto` or `smooth`.
849
+ */
850
+ async scrollToIndex(index, options) {
851
+ const clampedIndex = clamp(index, 0, Math.max(0, this._loaded().totalCount - 1));
852
+ // A newer call supersedes a correction loop that still runs for a
853
+ // previous call, for example under rapid, repeated calls.
854
+ const requestId = ++this._scrollRequestId;
855
+ let offset = this._getAlignedScrollOffset(clampedIndex, options);
856
+ await this._scrollAndWaitForEnd(offset, options?.behavior ?? "auto");
857
+ for (let i = 0; i < MAX_SCROLL_CORRECTION_PASSES; i++) {
858
+ await this.layoutComplete;
859
+ if (requestId !== this._scrollRequestId) {
860
+ return;
861
+ }
862
+ const corrected = this._getAlignedScrollOffset(clampedIndex, options);
863
+ if (Math.abs(corrected - offset) < SCROLL_OFFSET_EPSILON_PX) {
864
+ break;
865
+ }
866
+ offset = corrected;
867
+ await this._scrollAndWaitForEnd(offset, "auto");
868
+ if (requestId !== this._scrollRequestId) {
869
+ return;
870
+ }
871
+ }
872
+ }
873
+ //#endregion
874
+ //#region Scrolling
875
+ /**
876
+ * Re-reads the host's direction, which `dir` can change at runtime. `:dir()`
877
+ * is a selector match, so this costs no layout.
878
+ */
879
+ _syncDirection() {
880
+ const ltr = isLeftToRight(this._hostRef.nativeElement);
881
+ this._ltr.set(ltr);
882
+ return ltr;
883
+ }
884
+ /** The current real scroll position on the active axis, normalized for RTL. */
885
+ _currentAxisScroll() {
886
+ const host = this._hostRef.nativeElement;
887
+ if (this._isVertical()) {
888
+ return host.scrollTop;
889
+ }
890
+ // Standards-compliant browsers expose a negative scrollLeft in RTL.
891
+ return this._syncDirection() ? host.scrollLeft : -host.scrollLeft;
892
+ }
893
+ /** Applies a scroll offset to the active axis, accounting for RTL. */
894
+ _applyScroll(offset, behavior) {
895
+ const host = this._hostRef.nativeElement;
896
+ if (this._isVertical()) {
897
+ host.scrollTo({ top: offset, behavior });
898
+ return;
899
+ }
900
+ host.scrollTo({ left: this._syncDirection() ? offset : -offset, behavior });
901
+ }
902
+ /**
903
+ * The scroll offset that aligns `index` in the viewport according to
904
+ * `options`, from the engine's current size data. As more items are
905
+ * measured, the same input can give a different, more accurate result.
906
+ *
907
+ * `nearest` keeps the current offset for an item already in view and
908
+ * otherwise brings the item to its nearer edge, as native `scrollIntoView`
909
+ * does.
910
+ */
911
+ _getAlignedScrollOffset(index, options) {
912
+ const requested = this._isVertical()
913
+ ? (options?.block ?? "start")
914
+ : (options?.inline ?? options?.block ?? "start");
915
+ const current = this._currentAxisScroll();
916
+ const viewport = this._effectiveViewportSize();
917
+ const align = requested === "nearest"
918
+ ? this._engine.getNearestAlignment(index, current, viewport)
919
+ : requested;
920
+ if (align === null) {
921
+ return current;
922
+ }
923
+ return this._engine.getAlignedScrollOffset(index, viewport, align);
924
+ }
925
+ /**
926
+ * Applies a scroll offset to the active axis and waits for the scroll,
927
+ * instant or smooth, to settle.
928
+ *
929
+ * `scrollend` does not fire when the requested offset does not move the
930
+ * scroll position, so that case resolves immediately. The deadline covers
931
+ * an event that never arrives, for example when the element is detached
932
+ * mid-scroll.
933
+ */
934
+ _scrollAndWaitForEnd(offset, behavior) {
935
+ if (!this._isBrowser ||
936
+ Math.abs(this._currentAxisScroll() - offset) < SCROLL_OFFSET_EPSILON_PX) {
937
+ return Promise.resolve();
938
+ }
939
+ return this._withDeadline(SCROLL_END_TIMEOUT_MS, (abort) => {
940
+ // `scrollend` reports exactly when a scroll has settled. Safari before
941
+ // 18.2 does not have it, and the scroll-idle timer stands in there.
942
+ const settled = "onscrollend" in this._hostRef.nativeElement
943
+ ? this._waitForScrollEnd(abort)
944
+ : this._waitForScrollIdle(abort);
945
+ // Applied only after the listener is attached, so an instant scroll
946
+ // cannot settle before something watches for it.
947
+ this._applyScroll(offset, behavior);
948
+ return settled;
949
+ });
950
+ }
951
+ _waitForScrollEnd(abort) {
952
+ return this._promiseOutsideZone((resolve) => {
953
+ this._hostRef.nativeElement.addEventListener("scrollend", resolve, {
954
+ once: true,
955
+ signal: abort,
956
+ });
957
+ });
958
+ }
959
+ /**
960
+ * Resolves when no `scroll` event arrives for `SCROLL_IDLE_MS`: the closest
961
+ * replacement for `scrollend`. The first timer starts immediately, so a
962
+ * scroll that does not move still settles.
963
+ */
964
+ _waitForScrollIdle(abort) {
965
+ return this._promiseOutsideZone((resolve) => {
966
+ let id = setTimeout(resolve, SCROLL_IDLE_MS);
967
+ this._hostRef.nativeElement.addEventListener("scroll", () => {
968
+ clearTimeout(id);
969
+ id = setTimeout(resolve, SCROLL_IDLE_MS);
970
+ }, { passive: true, signal: abort });
971
+ onAbort(abort, () => clearTimeout(id));
972
+ });
973
+ }
974
+ //#endregion
975
+ //#region Async helpers
976
+ /** A promise whose subscription work stays out of the Angular zone. */
977
+ _promiseOutsideZone(subscribe) {
978
+ return this._zone.runOutsideAngular(() => new Promise((resolve) => subscribe(() => resolve())));
979
+ }
980
+ /**
981
+ * Resolves with `task` or with a deadline of `ms`, whichever comes first.
982
+ * The signal then tears down the other, so no live timer or dangling
983
+ * listener remains.
984
+ */
985
+ _withDeadline(ms, task) {
986
+ const controller = new AbortController();
987
+ return Promise.race([
988
+ task(controller.signal),
989
+ this._promiseOutsideZone((resolve) => {
990
+ const id = setTimeout(resolve, ms);
991
+ onAbort(controller.signal, () => clearTimeout(id));
992
+ }),
993
+ ]).finally(() => controller.abort());
994
+ }
995
+ /**
996
+ * Resolves on the next animation frame, or after `LAYOUT_FRAME_TIMEOUT_MS`
997
+ * when no frame arrives. A hidden tab or a detached element gets no frames
998
+ * and has no layout to wait for, so resolving early there is safe.
999
+ */
1000
+ _nextFrame() {
1001
+ if (!this._isBrowser) {
1002
+ return Promise.resolve();
1003
+ }
1004
+ return this._withDeadline(LAYOUT_FRAME_TIMEOUT_MS, (abort) => this._promiseOutsideZone((resolve) => {
1005
+ const id = requestAnimationFrame(resolve);
1006
+ onAbort(abort, () => cancelAnimationFrame(id));
1007
+ }));
1008
+ }
1009
+ /**
1010
+ * Waits out the frames in which the item measurements land. Each
1011
+ * measurement that changes a size bumps the engine's version and schedules
1012
+ * another render, so the layout has settled once the version holds still
1013
+ * across two consecutive frames.
1014
+ */
1015
+ async _resolveLayoutComplete() {
1016
+ try {
1017
+ let lastVersion = -1;
1018
+ for (let i = 0; i < MAX_LAYOUT_SETTLE_PASSES; i++) {
1019
+ await this._nextFrame();
1020
+ const version = untracked(this._engine.version);
1021
+ if (version === lastVersion) {
1022
+ break;
1023
+ }
1024
+ lastVersion = version;
1025
+ }
1026
+ }
1027
+ finally {
1028
+ // Cleared here, not after the loop, so a run that throws cannot leave
1029
+ // the getter with a permanently rejected promise.
1030
+ this._layoutCompletePromise = null;
1031
+ }
1032
+ }
1033
+ //#endregion
1034
+ //#region Measurement
1035
+ _measureViewport() {
1036
+ const host = this._hostRef.nativeElement;
1037
+ // A host with no box is hidden or detached, not sized: its last measurement is kept so
1038
+ // it renders in the pass that reveals it. A laid-out zero is a size like any other.
1039
+ if (!host.isConnected || host.getClientRects().length === 0) {
1040
+ return;
1041
+ }
1042
+ const size = this._isVertical() ? host.clientHeight : host.clientWidth;
1043
+ if (size !== untracked(this._viewportSize)) {
1044
+ this._viewportSize.set(size);
1045
+ }
1046
+ }
1047
+ _setupViewportResizeObserver() {
1048
+ this._viewportResizeObserver?.disconnect();
1049
+ this._zone.runOutsideAngular(() => {
1050
+ this._viewportResizeObserver = new ResizeObserver(() => this._measureViewport());
1051
+ this._viewportResizeObserver.observe(this._hostRef.nativeElement);
1052
+ });
1053
+ }
1054
+ _setupScrollListener() {
1055
+ const host = this._hostRef.nativeElement;
1056
+ if (this._onScroll) {
1057
+ host.removeEventListener("scroll", this._onScroll);
1058
+ }
1059
+ this._zone.runOutsideAngular(() => {
1060
+ this._onScroll = () => this._handleScroll();
1061
+ host.addEventListener("scroll", this._onScroll, { passive: true });
1062
+ });
1063
+ }
1064
+ /**
1065
+ * Records the new scroll offset and invalidates the rendered window only
1066
+ * when it actually moves. Without the guard, a scroll inside a single item
1067
+ * would schedule a change detection pass for an identical result.
1068
+ */
1069
+ _handleScroll() {
1070
+ this._scrollPosition = this._currentAxisScroll();
1071
+ const next = this._engine.getVisibleRange(this._scrollPosition, untracked(this._effectiveViewportSize), untracked(this._normalizedOverScan));
1072
+ if (!rangesEqual(next, untracked(this._visibleRange))) {
1073
+ this._scrollTick.update((v) => v + 1);
1074
+ }
1075
+ }
1076
+ _handleItemResize(entries) {
1077
+ for (const entry of entries) {
1078
+ const index = itemIndex(entry.target);
1079
+ if (index < 0) {
1080
+ continue;
1081
+ }
1082
+ const measured = this._isVertical()
1083
+ ? (entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height)
1084
+ : (entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width);
1085
+ if (measured > 0) {
1086
+ this._engine.measureItem(index, measured);
1087
+ }
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Synchronizes the item observer with the rendered window, applying only
1092
+ * the difference. A newly observed element gets one initial measurement.
1093
+ *
1094
+ * An element whose `data-index` changed is re-registered, because
1095
+ * `observe` on an already observed element is a no-op. `@for` tracks by
1096
+ * slot and reuses the wrapper elements, so after a scroll the same element
1097
+ * can host a different item at an identical size. The observer stays quiet
1098
+ * about that and the new index would keep its estimated size.
1099
+ */
1100
+ _scheduleItemMeasurement() {
1101
+ const content = this._contentDivRef()?.nativeElement;
1102
+ if (!content) {
1103
+ return;
1104
+ }
1105
+ const observer = this._getItemResizeObserver();
1106
+ for (const element of [...this._observedItems]) {
1107
+ if (element.parentNode !== content) {
1108
+ observer.unobserve(element);
1109
+ this._observedItems.delete(element);
1110
+ }
1111
+ }
1112
+ for (const element of Array.from(content.children)) {
1113
+ const index = itemIndex(element);
1114
+ if (this._observedItems.has(element)) {
1115
+ if (this._observedItemIndexes.get(element) === index) {
1116
+ continue;
1117
+ }
1118
+ observer.unobserve(element);
1119
+ }
1120
+ observer.observe(element);
1121
+ this._observedItems.add(element);
1122
+ this._observedItemIndexes.set(element, index);
1123
+ }
1124
+ }
1125
+ /**
1126
+ * Forgets the wrappers from `index` on, so the next pass registers them
1127
+ * again: the resize dropped their sizes, and a wrapper whose size did not
1128
+ * change reports nothing on its own.
1129
+ */
1130
+ _remeasureFrom(index) {
1131
+ for (const element of this._observedItems) {
1132
+ if ((this._observedItemIndexes.get(element) ?? -1) >= index) {
1133
+ this._observedItemIndexes.delete(element);
1134
+ }
1135
+ }
1136
+ }
1137
+ _getItemResizeObserver() {
1138
+ if (!this._itemResizeObserver) {
1139
+ this._itemResizeObserver = this._zone.runOutsideAngular(() => new ResizeObserver((entries) => this._handleItemResize(entries)));
1140
+ }
1141
+ return this._itemResizeObserver;
1142
+ }
1143
+ //#endregion
1144
+ //#region Events
1145
+ /**
1146
+ * The number of leading items that kept their identity across a `data`
1147
+ * change: the index of the first item whose measured size no longer
1148
+ * matches its rendered content. An append (the `dataRequest` flow) retains
1149
+ * all items. A filter or a replacement retains only the unchanged prefix.
1150
+ */
1151
+ _firstChangedIndex(previous, current) {
1152
+ const shared = Math.min(previous.length, current.length);
1153
+ for (let i = 0; i < shared; i++) {
1154
+ if (previous[i] !== current[i]) {
1155
+ return i;
1156
+ }
1157
+ }
1158
+ return shared;
1159
+ }
1160
+ /**
1161
+ * How many leading items keep their measured size across a change. A page of the same
1162
+ * collection keeps all of them - its indices still mean the same records. A page with
1163
+ * another `totalCount` comes from a filtered or otherwise different collection, and
1164
+ * switching inputs starts over, so neither keeps any.
1165
+ */
1166
+ _retainCount(previous, current) {
1167
+ if (previous?.windowed !== current.windowed) {
1168
+ return 0;
1169
+ }
1170
+ if (!current.windowed) {
1171
+ return this._firstChangedIndex(previous.items, current.items);
1172
+ }
1173
+ return previous.totalCount === current.totalCount ? current.totalCount : 0;
1174
+ }
1175
+ /**
1176
+ * Emits `stateChange`. Skipped when the window is empty or equal to the
1177
+ * last reported one, because measurement passes re-render without a window
1178
+ * change.
1179
+ */
1180
+ _emitStateChange() {
1181
+ const { startIndex, endIndex } = untracked(this._visibleRange);
1182
+ if (endIndex < startIndex) {
1183
+ return;
1184
+ }
1185
+ const state = {
1186
+ startIndex,
1187
+ endIndex,
1188
+ viewportSize: untracked(this._effectiveViewportSize),
1189
+ totalSize: untracked(this._engine.totalSize),
1190
+ };
1191
+ if (statesEqual(this._lastEmittedState, state)) {
1192
+ return;
1193
+ }
1194
+ this._lastEmittedState = { ...state };
1195
+ this.stateChange.emit(state);
1196
+ }
1197
+ _checkDataRequest() {
1198
+ const loaded = untracked(this._loaded);
1199
+ // `dataRequest` asks for items to append, which a sized collection does not need. The
1200
+ // count `data` had reached before says nothing once a window is bound, so forget it here.
1201
+ if (loaded.windowed) {
1202
+ this._lastDataRequestIndex = -1;
1203
+ return;
1204
+ }
1205
+ if (this._hasPendingDataRequest) {
1206
+ return;
1207
+ }
1208
+ const { endIndex } = untracked(this._visibleRange);
1209
+ const total = loaded.items.length;
1210
+ if (total === 0 || endIndex < total - DATA_REQUEST_THRESHOLD) {
1211
+ return;
1212
+ }
1213
+ // Each `data` change clears `_hasPendingDataRequest`, including one that
1214
+ // appends nothing. Without this second guard, a consumer whose source is
1215
+ // exhausted, and that reassigns `data` in response to a request, would
1216
+ // receive the same request on each reassignment.
1217
+ if (this._lastDataRequestIndex === total) {
1218
+ return;
1219
+ }
1220
+ this._hasPendingDataRequest = true;
1221
+ this._lastDataRequestIndex = total;
1222
+ this.dataRequest.emit({
1223
+ startIndex: total,
1224
+ count: Math.max(untracked(this._normalizedOverScan) * DATA_REQUEST_OVER_SCAN_FACTOR, DATA_REQUEST_MIN_COUNT),
1225
+ });
1226
+ }
1227
+ //#endregion
1228
+ _teardown() {
1229
+ const host = this._hostRef.nativeElement;
1230
+ if (this._onScroll) {
1231
+ host.removeEventListener("scroll", this._onScroll);
1232
+ this._onScroll = null;
1233
+ }
1234
+ this._viewportResizeObserver?.disconnect();
1235
+ this._viewportResizeObserver = null;
1236
+ this._itemResizeObserver?.disconnect();
1237
+ this._itemResizeObserver = null;
1238
+ this._observedItems.clear();
1239
+ }
1240
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: IgxVirtualScrollComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1241
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: IgxVirtualScrollComponent, isStandalone: true, selector: "igx-virtual-scroll", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, overScan: { classPropertyName: "overScan", publicName: "overScan", isSignal: true, isRequired: false, transformFunction: null }, estimatedItemSize: { classPropertyName: "estimatedItemSize", publicName: "estimatedItemSize", isSignal: true, isRequired: false, transformFunction: null }, dataWindow: { classPropertyName: "dataWindow", publicName: "dataWindow", isSignal: true, isRequired: false, transformFunction: null }, initialViewportSize: { classPropertyName: "initialViewportSize", publicName: "initialViewportSize", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stateChange: "stateChange", dataRequest: "dataRequest" }, host: { attributes: { "role": "list" }, properties: { "attr.data-orientation": "orientation()" }, classAttribute: "igx-virtual-scroll" }, queries: [{ propertyName: "_itemDirective", first: true, predicate: IgxVirtualItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "_contentDivRef", first: true, predicate: ["contentDiv"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"igx-virtual-scroll__track\"\n role=\"presentation\"\n [style.height.px]=\"_isVertical() ? _spaceSize() : null\"\n [style.width.px]=\"!_isVertical() ? _spaceSize() : null\"\n>\n <div\n class=\"igx-virtual-scroll__content\"\n #contentDiv\n role=\"presentation\"\n [style.transform]=\"_contentTransform()\"\n >\n @if (_resolvedTemplate(); as template) {\n @for (context of _renderedItems(); track $index) {\n <div\n class=\"igx-virtual-item\"\n role=\"presentation\"\n [attr.data-index]=\"context.index\"\n >\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"context\"\n />\n </div>\n }\n }\n </div>\n</div>\n", styles: ["@layer ig.base{.igx-virtual-scroll{display:block;position:relative;overflow:auto}.igx-virtual-scroll[data-orientation=vertical]{overflow-y:auto;overflow-x:hidden}.igx-virtual-scroll[data-orientation=horizontal]{overflow-x:auto;overflow-y:hidden}.igx-virtual-scroll__track{position:relative;width:100%;min-height:100%}.igx-virtual-scroll[data-orientation=horizontal] .igx-virtual-scroll__track{height:100%;width:auto;min-height:unset}.igx-virtual-scroll__content{position:absolute;top:0;left:0;width:100%;will-change:transform;contain:layout style paint}.igx-virtual-scroll[data-orientation=horizontal] .igx-virtual-scroll__content{display:flex;flex-direction:row;height:100%;width:auto}.igx-virtual-scroll[data-orientation=horizontal]:dir(rtl) .igx-virtual-scroll__content{left:auto;right:0}.igx-virtual-scroll[data-orientation=horizontal] .igx-virtual-item{flex-shrink:0;height:100%}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
1242
+ }
1243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: IgxVirtualScrollComponent, decorators: [{
1244
+ type: Component,
1245
+ args: [{ selector: "igx-virtual-scroll", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [NgTemplateOutlet], host: {
1246
+ class: "igx-virtual-scroll",
1247
+ role: "list",
1248
+ "[attr.data-orientation]": "orientation()",
1249
+ }, template: "<div\n class=\"igx-virtual-scroll__track\"\n role=\"presentation\"\n [style.height.px]=\"_isVertical() ? _spaceSize() : null\"\n [style.width.px]=\"!_isVertical() ? _spaceSize() : null\"\n>\n <div\n class=\"igx-virtual-scroll__content\"\n #contentDiv\n role=\"presentation\"\n [style.transform]=\"_contentTransform()\"\n >\n @if (_resolvedTemplate(); as template) {\n @for (context of _renderedItems(); track $index) {\n <div\n class=\"igx-virtual-item\"\n role=\"presentation\"\n [attr.data-index]=\"context.index\"\n >\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"context\"\n />\n </div>\n }\n }\n </div>\n</div>\n", styles: ["@layer ig.base{.igx-virtual-scroll{display:block;position:relative;overflow:auto}.igx-virtual-scroll[data-orientation=vertical]{overflow-y:auto;overflow-x:hidden}.igx-virtual-scroll[data-orientation=horizontal]{overflow-x:auto;overflow-y:hidden}.igx-virtual-scroll__track{position:relative;width:100%;min-height:100%}.igx-virtual-scroll[data-orientation=horizontal] .igx-virtual-scroll__track{height:100%;width:auto;min-height:unset}.igx-virtual-scroll__content{position:absolute;top:0;left:0;width:100%;will-change:transform;contain:layout style paint}.igx-virtual-scroll[data-orientation=horizontal] .igx-virtual-scroll__content{display:flex;flex-direction:row;height:100%;width:auto}.igx-virtual-scroll[data-orientation=horizontal]:dir(rtl) .igx-virtual-scroll__content{left:auto;right:0}.igx-virtual-scroll[data-orientation=horizontal] .igx-virtual-item{flex-shrink:0;height:100%}}\n"] }]
1250
+ }], ctorParameters: () => [], propDecorators: { _itemDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => IgxVirtualItemDirective), { isSignal: true }] }], _contentDivRef: [{ type: i0.ViewChild, args: ["contentDiv", { isSignal: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], overScan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overScan", required: false }] }], estimatedItemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "estimatedItemSize", required: false }] }], dataWindow: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataWindow", required: false }] }], initialViewportSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialViewportSize", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }], dataRequest: [{ type: i0.Output, args: ["dataRequest"] }] } });
1251
+
1252
+ /**
1253
+ * Generated bundle index. Do not edit.
1254
+ */
1255
+
1256
+ export { IgxVirtualItemDirective, IgxVirtualScrollComponent, IgxVsItemContext };
1257
+ //# sourceMappingURL=igniteui-angular-virtual-scroll.mjs.map