@kanso-protocol/virtual-list 2.0.2 → 2.0.3

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.
@@ -139,23 +139,32 @@ class KpVirtualListComponent {
139
139
  queueMicrotask(() => this.rangeChange.emit({ start, end }));
140
140
  }
141
141
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
142
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: KpVirtualListComponent, isStandalone: true, selector: "kp-virtual-list", inputs: { items: "items", itemHeight: "itemHeight", viewportHeight: "viewportHeight", overscan: "overscan", trackBy: "trackBy" }, outputs: { rangeChange: "rangeChange" }, host: { attributes: { "role": "list" }, properties: { "attr.aria-rowcount": "items.length" } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: KpVirtualRowDirective, descendants: true }], viewQueries: [{ propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
142
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: KpVirtualListComponent, isStandalone: true, selector: "kp-virtual-list", inputs: { items: "items", itemHeight: "itemHeight", viewportHeight: "viewportHeight", overscan: "overscan", trackBy: "trackBy" }, outputs: { rangeChange: "rangeChange" }, queries: [{ propertyName: "rowTemplate", first: true, predicate: KpVirtualRowDirective, descendants: true }], viewQueries: [{ propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
143
143
  <div
144
144
  #viewport
145
145
  class="kp-virtual-list__viewport"
146
146
  [style.height.px]="viewportHeight"
147
+ tabindex="0"
147
148
  (scroll)="onScroll()"
148
149
  >
149
150
  <div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
151
+ <!-- role="list" sits on the immediate parent of the listitems so
152
+ aria-required-children resolves against direct children (the
153
+ viewport/spacer wrappers between the host and the items
154
+ confused axe's parent-child walk). aria-setsize/posinset are
155
+ per-item, which is what the ARIA spec actually permits — they
156
+ aren't valid on role="list" itself. -->
150
157
  <div
151
158
  class="kp-virtual-list__window"
159
+ role="list"
152
160
  [style.transform]="windowTransform"
153
161
  >
154
162
  @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
155
163
  <div
156
164
  class="kp-virtual-list__row"
157
165
  role="listitem"
158
- [attr.aria-rowindex]="visibleStart + i + 1"
166
+ [attr.aria-setsize]="items.length"
167
+ [attr.aria-posinset]="visibleStart + i + 1"
159
168
  [style.height.px]="itemHeight"
160
169
  >
161
170
  @if (rowTemplate) {
@@ -168,30 +177,36 @@ class KpVirtualListComponent {
168
177
  </div>
169
178
  </div>
170
179
  </div>
171
- `, isInline: true, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
180
+ `, isInline: true, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict;background:var(--kp-color-surface-base)}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
172
181
  }
173
182
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualListComponent, decorators: [{
174
183
  type: Component,
175
- args: [{ selector: 'kp-virtual-list', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
176
- role: 'list',
177
- '[attr.aria-rowcount]': 'items.length',
178
- }, template: `
184
+ args: [{ selector: 'kp-virtual-list', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {}, template: `
179
185
  <div
180
186
  #viewport
181
187
  class="kp-virtual-list__viewport"
182
188
  [style.height.px]="viewportHeight"
189
+ tabindex="0"
183
190
  (scroll)="onScroll()"
184
191
  >
185
192
  <div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
193
+ <!-- role="list" sits on the immediate parent of the listitems so
194
+ aria-required-children resolves against direct children (the
195
+ viewport/spacer wrappers between the host and the items
196
+ confused axe's parent-child walk). aria-setsize/posinset are
197
+ per-item, which is what the ARIA spec actually permits — they
198
+ aren't valid on role="list" itself. -->
186
199
  <div
187
200
  class="kp-virtual-list__window"
201
+ role="list"
188
202
  [style.transform]="windowTransform"
189
203
  >
190
204
  @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
191
205
  <div
192
206
  class="kp-virtual-list__row"
193
207
  role="listitem"
194
- [attr.aria-rowindex]="visibleStart + i + 1"
208
+ [attr.aria-setsize]="items.length"
209
+ [attr.aria-posinset]="visibleStart + i + 1"
195
210
  [style.height.px]="itemHeight"
196
211
  >
197
212
  @if (rowTemplate) {
@@ -204,7 +219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
204
219
  </div>
205
220
  </div>
206
221
  </div>
207
- `, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"] }]
222
+ `, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict;background:var(--kp-color-surface-base)}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"] }]
208
223
  }], propDecorators: { items: [{
209
224
  type: Input
210
225
  }], itemHeight: [{
@@ -1 +1 @@
1
- {"version":3,"file":"kanso-protocol-virtual-list.mjs","sources":["../../../../../packages/components/virtual-list/src/virtual-list.component.ts","../../../../../packages/components/virtual-list/src/kanso-protocol-virtual-list.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChild,\n Directive,\n ElementRef,\n EventEmitter,\n HostListener,\n Input,\n NgZone,\n OnChanges,\n OnDestroy,\n Output,\n SimpleChanges,\n TemplateRef,\n ViewChild,\n inject,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\n\nexport interface KpVirtualRange {\n start: number;\n end: number;\n}\n\n/**\n * Marker directive — pairs the row template with `<kp-virtual-list>`.\n *\n * @example\n * <kp-virtual-list ...>\n * <ng-template kpVirtualRow let-item let-i=\"index\">\n * <div>row {{ i }}: {{ item.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Directive({\n selector: '[kpVirtualRow]',\n standalone: true,\n})\nexport class KpVirtualRowDirective<T = unknown> {\n readonly template = inject(TemplateRef<{ $implicit: T; index: number }>);\n}\n\n/**\n * Kanso Protocol — VirtualList\n *\n * Window-mode virtual scroller for **fixed-height rows**. Renders only the\n * rows currently visible in the viewport (plus a configurable `[overscan]`\n * buffer). Lets the consumer hand any item shape via a projected\n * `<ng-template kpVirtualRow let-item let-i=\"index\">`.\n *\n * Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /\n * itemHeight`), no measurement pass, no layout thrash. Variable-height\n * support is on the roadmap (`KpVariableVirtualListComponent`); for now,\n * size each row to a uniform height.\n *\n * Use this for tables / message lists / log views with thousands of rows.\n * Below ~100 rows just render them — virtualization adds complexity for\n * negligible gain.\n *\n * @example\n * <kp-virtual-list\n * [items]=\"rows\"\n * [itemHeight]=\"40\"\n * [viewportHeight]=\"480\"\n * [overscan]=\"6\"\n * (rangeChange)=\"onRange($event)\"\n * >\n * <ng-template kpVirtualRow let-row let-i=\"index\">\n * <div class=\"row\" [class.row--alt]=\"i % 2\">{{ row.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Component({\n selector: 'kp-virtual-list',\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n role: 'list',\n '[attr.aria-rowcount]': 'items.length',\n },\n template: `\n <div\n #viewport\n class=\"kp-virtual-list__viewport\"\n [style.height.px]=\"viewportHeight\"\n (scroll)=\"onScroll()\"\n >\n <div class=\"kp-virtual-list__spacer\" [style.height.px]=\"totalHeight\">\n <div\n class=\"kp-virtual-list__window\"\n [style.transform]=\"windowTransform\"\n >\n @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {\n <div\n class=\"kp-virtual-list__row\"\n role=\"listitem\"\n [attr.aria-rowindex]=\"visibleStart + i + 1\"\n [style.height.px]=\"itemHeight\"\n >\n @if (rowTemplate) {\n <ng-container\n *ngTemplateOutlet=\"rowTemplate.template; context: { $implicit: item, index: visibleStart + i }\"\n />\n }\n </div>\n }\n </div>\n </div>\n </div>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n }\n .kp-virtual-list__viewport {\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n contain: strict;\n }\n .kp-virtual-list__spacer {\n position: relative;\n width: 100%;\n }\n .kp-virtual-list__window {\n position: absolute;\n inset-block-start: 0;\n inset-inline: 0;\n will-change: transform;\n }\n .kp-virtual-list__row {\n box-sizing: border-box;\n width: 100%;\n }\n `],\n})\nexport class KpVirtualListComponent<T = unknown> implements OnChanges, OnDestroy {\n /** The full list. The component never iterates the whole thing — only the visible window. */\n @Input() items: readonly T[] = [];\n\n /** Pixel height of one row. Required; must be uniform across rows. */\n @Input() itemHeight = 40;\n\n /** Pixel height of the scroll viewport. */\n @Input() viewportHeight = 400;\n\n /** Extra rows rendered above + below the visible window to soften scroll-flicker. */\n @Input() overscan = 4;\n\n /** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */\n @Input() trackBy: ((index: number, item: T) => unknown) | null = null;\n\n @Output() readonly rangeChange = new EventEmitter<KpVirtualRange>();\n\n @ViewChild('viewport', { static: true }) private viewportRef!: ElementRef<HTMLDivElement>;\n @ContentChild(KpVirtualRowDirective) rowTemplate?: KpVirtualRowDirective<T>;\n\n visibleStart = 0;\n visibleEnd = 0;\n\n private readonly cdr = inject(ChangeDetectorRef);\n private readonly zone = inject(NgZone);\n\n ngOnChanges(changes: SimpleChanges): void {\n if ('items' in changes || 'itemHeight' in changes || 'viewportHeight' in changes || 'overscan' in changes) {\n // Synchronous recompute: ngOnChanges fires BEFORE template eval in the\n // same CD pass, so visibleStart/End read by the template are already\n // up-to-date — no ExpressionChangedAfterItHasBeenChecked.\n this.recompute(/* fromInputChange */ true);\n }\n }\n\n ngOnDestroy(): void {\n /* HostListener auto-unbinds */\n }\n\n get totalHeight(): number {\n return this.items.length * this.itemHeight;\n }\n\n get visibleItems(): readonly T[] {\n return this.items.slice(this.visibleStart, this.visibleEnd);\n }\n\n get windowTransform(): string {\n return `translate3d(0, ${this.visibleStart * this.itemHeight}px, 0)`;\n }\n\n trackByOrIndex(index: number, item: T): unknown {\n if (this.trackBy) return this.trackBy(this.visibleStart + index, item);\n return this.visibleStart + index;\n }\n\n /** Imperatively scroll to a specific row index. */\n scrollToIndex(index: number, position: 'start' | 'center' | 'end' = 'start'): void {\n const el = this.viewportRef?.nativeElement;\n if (!el) return;\n const top =\n position === 'start'\n ? index * this.itemHeight\n : position === 'end'\n ? index * this.itemHeight - this.viewportHeight + this.itemHeight\n : index * this.itemHeight - this.viewportHeight / 2 + this.itemHeight / 2;\n el.scrollTop = Math.max(0, Math.min(top, this.totalHeight - this.viewportHeight));\n }\n\n onScroll(): void {\n this.recompute(/* fromInputChange */ false);\n }\n\n private recompute(fromInputChange: boolean): void {\n const el = this.viewportRef?.nativeElement;\n const scrollTop = el?.scrollTop ?? 0;\n const total = this.items.length;\n\n let start = 0;\n let end = 0;\n if (total > 0 && this.itemHeight > 0) {\n const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);\n const firstVisible = Math.floor(scrollTop / this.itemHeight);\n start = Math.max(0, firstVisible - this.overscan);\n end = Math.min(total, firstVisible + visibleCount + this.overscan);\n }\n\n if (this.visibleStart === start && this.visibleEnd === end) return;\n\n this.visibleStart = start;\n this.visibleEnd = end;\n\n if (!fromInputChange) {\n // Scroll-driven recompute happens outside CD; mark so the visible\n // window re-renders.\n this.cdr.markForCheck();\n }\n // Always defer the emit. From input changes: avoids reentrancy during CD.\n // From scroll: lets consumers mutate state without re-entering the\n // current event loop synchronously.\n queueMicrotask(() => this.rangeChange.emit({ start, end }));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"kanso-protocol-virtual-list.mjs","sources":["../../../../../packages/components/virtual-list/src/virtual-list.component.ts","../../../../../packages/components/virtual-list/src/kanso-protocol-virtual-list.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChild,\n Directive,\n ElementRef,\n EventEmitter,\n HostListener,\n Input,\n NgZone,\n OnChanges,\n OnDestroy,\n Output,\n SimpleChanges,\n TemplateRef,\n ViewChild,\n inject,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\n\nexport interface KpVirtualRange {\n start: number;\n end: number;\n}\n\n/**\n * Marker directive — pairs the row template with `<kp-virtual-list>`.\n *\n * @example\n * <kp-virtual-list ...>\n * <ng-template kpVirtualRow let-item let-i=\"index\">\n * <div>row {{ i }}: {{ item.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Directive({\n selector: '[kpVirtualRow]',\n standalone: true,\n})\nexport class KpVirtualRowDirective<T = unknown> {\n readonly template = inject(TemplateRef<{ $implicit: T; index: number }>);\n}\n\n/**\n * Kanso Protocol — VirtualList\n *\n * Window-mode virtual scroller for **fixed-height rows**. Renders only the\n * rows currently visible in the viewport (plus a configurable `[overscan]`\n * buffer). Lets the consumer hand any item shape via a projected\n * `<ng-template kpVirtualRow let-item let-i=\"index\">`.\n *\n * Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /\n * itemHeight`), no measurement pass, no layout thrash. Variable-height\n * support is on the roadmap (`KpVariableVirtualListComponent`); for now,\n * size each row to a uniform height.\n *\n * Use this for tables / message lists / log views with thousands of rows.\n * Below ~100 rows just render them — virtualization adds complexity for\n * negligible gain.\n *\n * @example\n * <kp-virtual-list\n * [items]=\"rows\"\n * [itemHeight]=\"40\"\n * [viewportHeight]=\"480\"\n * [overscan]=\"6\"\n * (rangeChange)=\"onRange($event)\"\n * >\n * <ng-template kpVirtualRow let-row let-i=\"index\">\n * <div class=\"row\" [class.row--alt]=\"i % 2\">{{ row.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Component({\n selector: 'kp-virtual-list',\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {},\n template: `\n <div\n #viewport\n class=\"kp-virtual-list__viewport\"\n [style.height.px]=\"viewportHeight\"\n tabindex=\"0\"\n (scroll)=\"onScroll()\"\n >\n <div class=\"kp-virtual-list__spacer\" [style.height.px]=\"totalHeight\">\n <!-- role=\"list\" sits on the immediate parent of the listitems so\n aria-required-children resolves against direct children (the\n viewport/spacer wrappers between the host and the items\n confused axe's parent-child walk). aria-setsize/posinset are\n per-item, which is what the ARIA spec actually permits — they\n aren't valid on role=\"list\" itself. -->\n <div\n class=\"kp-virtual-list__window\"\n role=\"list\"\n [style.transform]=\"windowTransform\"\n >\n @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {\n <div\n class=\"kp-virtual-list__row\"\n role=\"listitem\"\n [attr.aria-setsize]=\"items.length\"\n [attr.aria-posinset]=\"visibleStart + i + 1\"\n [style.height.px]=\"itemHeight\"\n >\n @if (rowTemplate) {\n <ng-container\n *ngTemplateOutlet=\"rowTemplate.template; context: { $implicit: item, index: visibleStart + i }\"\n />\n }\n </div>\n }\n </div>\n </div>\n </div>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n }\n .kp-virtual-list__viewport {\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n contain: strict;\n /* Explicit bg so axe-core's color-contrast walk-up resolves to a\n known surface for the row content. With contain:strict + an\n implicit transparent bg, the walk-up gets confused and reports\n the row text against an indeterminate parent bg. The bg here\n is invisible in practice because every row template paints its\n own surface on top. */\n background: var(--kp-color-surface-base);\n }\n .kp-virtual-list__spacer {\n position: relative;\n width: 100%;\n }\n .kp-virtual-list__window {\n position: absolute;\n inset-block-start: 0;\n inset-inline: 0;\n will-change: transform;\n }\n .kp-virtual-list__row {\n box-sizing: border-box;\n width: 100%;\n }\n `],\n})\nexport class KpVirtualListComponent<T = unknown> implements OnChanges, OnDestroy {\n /** The full list. The component never iterates the whole thing — only the visible window. */\n @Input() items: readonly T[] = [];\n\n /** Pixel height of one row. Required; must be uniform across rows. */\n @Input() itemHeight = 40;\n\n /** Pixel height of the scroll viewport. */\n @Input() viewportHeight = 400;\n\n /** Extra rows rendered above + below the visible window to soften scroll-flicker. */\n @Input() overscan = 4;\n\n /** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */\n @Input() trackBy: ((index: number, item: T) => unknown) | null = null;\n\n @Output() readonly rangeChange = new EventEmitter<KpVirtualRange>();\n\n @ViewChild('viewport', { static: true }) private viewportRef!: ElementRef<HTMLDivElement>;\n @ContentChild(KpVirtualRowDirective) rowTemplate?: KpVirtualRowDirective<T>;\n\n visibleStart = 0;\n visibleEnd = 0;\n\n private readonly cdr = inject(ChangeDetectorRef);\n private readonly zone = inject(NgZone);\n\n ngOnChanges(changes: SimpleChanges): void {\n if ('items' in changes || 'itemHeight' in changes || 'viewportHeight' in changes || 'overscan' in changes) {\n // Synchronous recompute: ngOnChanges fires BEFORE template eval in the\n // same CD pass, so visibleStart/End read by the template are already\n // up-to-date — no ExpressionChangedAfterItHasBeenChecked.\n this.recompute(/* fromInputChange */ true);\n }\n }\n\n ngOnDestroy(): void {\n /* HostListener auto-unbinds */\n }\n\n get totalHeight(): number {\n return this.items.length * this.itemHeight;\n }\n\n get visibleItems(): readonly T[] {\n return this.items.slice(this.visibleStart, this.visibleEnd);\n }\n\n get windowTransform(): string {\n return `translate3d(0, ${this.visibleStart * this.itemHeight}px, 0)`;\n }\n\n trackByOrIndex(index: number, item: T): unknown {\n if (this.trackBy) return this.trackBy(this.visibleStart + index, item);\n return this.visibleStart + index;\n }\n\n /** Imperatively scroll to a specific row index. */\n scrollToIndex(index: number, position: 'start' | 'center' | 'end' = 'start'): void {\n const el = this.viewportRef?.nativeElement;\n if (!el) return;\n const top =\n position === 'start'\n ? index * this.itemHeight\n : position === 'end'\n ? index * this.itemHeight - this.viewportHeight + this.itemHeight\n : index * this.itemHeight - this.viewportHeight / 2 + this.itemHeight / 2;\n el.scrollTop = Math.max(0, Math.min(top, this.totalHeight - this.viewportHeight));\n }\n\n onScroll(): void {\n this.recompute(/* fromInputChange */ false);\n }\n\n private recompute(fromInputChange: boolean): void {\n const el = this.viewportRef?.nativeElement;\n const scrollTop = el?.scrollTop ?? 0;\n const total = this.items.length;\n\n let start = 0;\n let end = 0;\n if (total > 0 && this.itemHeight > 0) {\n const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);\n const firstVisible = Math.floor(scrollTop / this.itemHeight);\n start = Math.max(0, firstVisible - this.overscan);\n end = Math.min(total, firstVisible + visibleCount + this.overscan);\n }\n\n if (this.visibleStart === start && this.visibleEnd === end) return;\n\n this.visibleStart = start;\n this.visibleEnd = end;\n\n if (!fromInputChange) {\n // Scroll-driven recompute happens outside CD; mark so the visible\n // window re-renders.\n this.cdr.markForCheck();\n }\n // Always defer the emit. From input changes: avoids reentrancy during CD.\n // From scroll: lets consumers mutate state without re-entering the\n // current event loop synchronously.\n queueMicrotask(() => this.rangeChange.emit({ start, end }));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@kanso-protocol/virtual-list",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "license": "MIT",
5
5
  "peerDependencies": {
6
6
  "@angular/core": ">=21.0.0",
7
7
  "@angular/common": ">=21.0.0",
8
- "@kanso-protocol/core": ">=2.0.2"
8
+ "@kanso-protocol/core": ">=2.0.3"
9
9
  },
10
10
  "description": "Kanso Protocol — virtual-list (component).",
11
11
  "author": "GregNBlack",