@testgorilla/tgo-ui 11.2.2 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testgorilla/tgo-ui",
3
- "version": "11.2.2",
3
+ "version": "11.3.0",
4
4
  "license": "proprietary-license",
5
5
  "lint-staged": {
6
6
  "{projects,components}/**/*.ts": [
@@ -5,62 +5,63 @@ import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
5
5
  import * as i1 from '@angular/common';
6
6
 
7
7
  /**
8
- * Column definition for the infinite-scroll table.
9
- *
10
- * The shape intentionally mirrors the existing `ui-table` `TableColumn` (the fields that carry
11
- * over to a virtualized body) so consumers can migrate their column configs and cell templates
12
- * with a selector swap. Types have no runtime footprint, so this is a deliberate duplication
13
- * rather than a shared import — it keeps the new component fully independent of `ui-table`.
8
+ * The structural contract `selectable` puts on a row: the checkbox reflects `selected`, and `key` /
9
+ * `rowIdKey` lookups read arbitrary fields. Declared once here rather than asserted at each read site, so
10
+ * the requirement is discoverable from the types instead of only from the docs.
14
11
  */
15
- interface InfiniteScrollTableColumn<T = any> {
16
- /** Stable identifier for the column; also used to read the default cell value from a row. */
12
+ type InfiniteScrollTableRowLike = Record<string, unknown> & {
13
+ selected?: boolean;
14
+ };
15
+ interface InfiniteScrollTableColumn<T> {
16
+ /** Also reads the default cell value. A plain string, so a synthetic column can have no matching field. */
17
17
  key: string;
18
18
  /** Header label rendered when no `headerCellTemplate` is provided. */
19
19
  title?: string;
20
+ headerCellTemplate?: TemplateRef<InfiniteScrollTableHeaderCellContext<T>>;
21
+ rowCellTemplate?: TemplateRef<InfiniteScrollTableRowCellContext<T>>;
20
22
  /**
21
- * Consumer-supplied header cell template.
22
- * Context: `{ $implicit: column.title, column, colIndex }` matches `ui-table`.
23
- */
24
- headerCellTemplate?: TemplateRef<unknown>;
25
- /**
26
- * Consumer-supplied row cell template.
27
- * Context: `{ $implicit: element, rowIndex }` — matches `ui-table`.
23
+ * Pins the column. Only honoured on the **first** column (pins left) and the **last** column (pins
24
+ * right); anywhere else it is ignored, with a dev-mode warning.
28
25
  */
29
- rowCellTemplate?: TemplateRef<unknown>;
30
- /** When set, the first sticky column pins to the left and the last sticky column pins to the right. */
31
26
  isSticky?: boolean;
32
27
  styles?: InfiniteScrollTableColumnStyles;
33
28
  }
34
29
  interface InfiniteScrollTableColumnStyles {
35
- 'min-width'?: string;
30
+ /**
31
+ * Authoritative column width (default `160px`), applied as `flex-basis`, `min-width` AND `max-width`:
32
+ * the column never grows or shrinks and overflowing content is clipped, which is what keeps the header
33
+ * and body scroll containers pixel-aligned. Leftover row width goes to a trailing spacer instead.
34
+ */
36
35
  width?: string;
37
36
  alignment?: InfiniteScrollTableColumnAlignment;
38
37
  }
39
38
  type InfiniteScrollTableColumnAlignment = 'left' | 'right' | 'center';
40
- /** Context object passed to the error template so it can trigger a retry. */
39
+ interface InfiniteScrollTableRowCellContext<T> {
40
+ $implicit: T;
41
+ rowIndex: number;
42
+ }
43
+ interface InfiniteScrollTableHeaderCellContext<T> {
44
+ $implicit: string | undefined;
45
+ column: InfiniteScrollTableColumn<T>;
46
+ colIndex: number;
47
+ }
41
48
  interface InfiniteScrollTableErrorContext {
42
49
  $implicit: () => void;
43
50
  retry: () => void;
44
51
  }
45
52
  /**
46
- * Payload emitted on `rowClick`: the row plus its data index.
47
- *
48
- * Note: this is a **shallow copy** (`{ ...row, index }`, matching `ui-table`), so a class instance's
49
- * prototype is not preserved and any pre-existing `index` field on the row is overwritten.
53
+ * `row` is the consumer's own object by reference not a copy — so class prototypes survive and a row's
54
+ * own `index` field is never shadowed. Same for `InfiniteScrollTableRowSelection`.
50
55
  */
51
- type InfiniteScrollTableRowClick<T> = T & {
56
+ interface InfiniteScrollTableRowClick<T> {
57
+ row: T;
52
58
  index: number;
53
- };
54
- /**
55
- * Payload emitted on `selectionChange`: the row, its data index, and the new selected state.
56
- *
57
- * As with `rowClick`, this is a **shallow copy** (`{ ...row, index, selected }`) — no prototype, and
58
- * any pre-existing `index` / `selected` field on the row is overwritten.
59
- */
60
- type InfiniteScrollTableRowSelection<T> = T & {
59
+ }
60
+ interface InfiniteScrollTableRowSelection<T> {
61
+ row: T;
61
62
  index: number;
62
63
  selected: boolean;
63
- };
64
+ }
64
65
 
65
66
  /** Internal row wrapper so real rows and skeleton placeholders flow through one virtual list. */
66
67
  type RenderRow<T> = {
@@ -72,177 +73,248 @@ type RenderRow<T> = {
72
73
  id: string;
73
74
  };
74
75
  /**
75
- * A generic, virtualized table with scroll-driven infinite loading.
76
- *
77
- * Owns rendering, fixed-height vertical virtualization (CDK), horizontal scroll, a pinned header,
78
- * sticky first/last columns, first-load and load-more skeletons, and a guarded `loadMore` event.
79
- * All content and layout come from consumer inputs — nothing domain-specific lives here.
80
- *
81
- * Independent of `ui-table`: it reuses only stable leaf primitives (`ui-skeleton`) and duplicates
82
- * the column/context type shapes so consumers can migrate with a selector swap.
76
+ * A generic, virtualized table with scroll-driven infinite loading. Independent of `ui-table`: it reuses
77
+ * only stable leaf primitives (`ui-skeleton`) and duplicates the column/context type shapes so consumers
78
+ * can migrate with a selector swap.
83
79
  */
84
80
  declare class InfiniteScrollTableComponent<T> {
85
- /** Rows loaded so far. The consumer appends pages to this array. */
86
- readonly data: _angular_core.InputSignal<T[]>;
87
- /** Column definitions. The first `isSticky` column pins left; the last `isSticky` column pins right. */
88
- readonly columns: _angular_core.InputSignal<InfiniteScrollTableColumn<T>[]>;
89
81
  /**
90
- * Fixed row height in px used as the virtualization `itemSize`.
91
- * Variable / multi-line row heights are unsupported in this variant.
82
+ * Rows loaded so far. **Must change by reference** (`[...rows, ...page]`): a signal input cannot observe
83
+ * `rows.push(...)`, so an in-place append renders nothing and leaves `loadMore` disarmed.
92
84
  */
85
+ readonly data: _angular_core.InputSignal<readonly T[]>;
86
+ readonly columns: _angular_core.InputSignal<InfiniteScrollTableColumn<T>[]>;
87
+ /** Fixed row height in px (the virtualization `itemSize`). Variable row heights are unsupported. */
93
88
  readonly rowHeight: _angular_core.InputSignal<number>;
94
- /** Optional stable row identity so recycled rows re-render correctly. Takes precedence over `rowIdKey`. */
89
+ /** Stable row identity so recycled rows re-render correctly. Takes precedence over `rowIdKey`. */
95
90
  readonly trackBy: _angular_core.InputSignal<TrackByFunction<T> | undefined>;
96
- /** Optional key on each row that holds a stable id. Used when `trackBy` is not provided. */
91
+ /** Key on each row holding a stable id. Used when `trackBy` is not provided. */
97
92
  readonly rowIdKey: _angular_core.InputSignal<string | undefined>;
98
93
  /**
99
- * Accepted for API parity with `ui-table`. In this bounded layout the header is always rendered
100
- * above the internal scroll region and stays visible regardless `.ui-ist` has `overflow: hidden`,
101
- * so the header cannot stick to an outer scrolling page. Kept so `ui-table` templates migrate
102
- * unchanged. Default `true`.
94
+ * CDK's rendered buffers. Both default to a multiple of `rowHeight` (~5 and ~10 rows) rather than a
95
+ * fixed px value, which would mean a different row count at every `rowHeight`.
103
96
  */
104
- readonly stickyHeader: _angular_core.InputSignalWithTransform<boolean, unknown>;
97
+ readonly minBufferPx: _angular_core.InputSignal<number | undefined>;
98
+ readonly maxBufferPx: _angular_core.InputSignal<number | undefined>;
105
99
  /** Max height of the internal scroll region (e.g. `'70vh'`). The body shrinks to fit fewer rows. */
106
100
  readonly maxHeight: _angular_core.InputSignal<string>;
107
- /** Condensed spacing. */
101
+ /** Stretch to the parent's height instead of the content's; `maxHeight` becomes a cap, not the target. */
102
+ readonly fillHeight: _angular_core.InputSignalWithTransform<boolean, unknown>;
108
103
  readonly isCondensed: _angular_core.InputSignalWithTransform<boolean, unknown>;
109
104
  /**
110
- * Enables row selection. When `true`, a dedicated checkbox column is prepended (pinned to the left)
111
- * and each row's checkbox reflects the row's `selected` property; toggling it emits `selectionChange`.
112
- * Selection stays data-driven and consumer-managed, exactly like `ui-table`. Default `false`.
105
+ * Prepends a left-pinned checkbox column driven by each row's `selected` property. Consumer-managed
106
+ * (like `ui-table`): toggling only emits `selectionChange`.
113
107
  */
114
108
  readonly selectable: _angular_core.InputSignalWithTransform<boolean, unknown>;
109
+ /**
110
+ * Makes rows focusable and activatable by click or Enter/Space. Off by default: a read-only table
111
+ * must not create a tab stop per rendered row or imply interactivity it does not have.
112
+ */
113
+ readonly rowsClickable: _angular_core.InputSignalWithTransform<boolean, unknown>;
115
114
  /** First-load state: renders a full body of skeleton rows. */
116
115
  readonly loading: _angular_core.InputSignalWithTransform<boolean, unknown>;
117
116
  /** Appends skeleton rows below the existing rows while the next page is fetched. */
118
117
  readonly loadingMore: _angular_core.InputSignalWithTransform<boolean, unknown>;
119
- /** Whether more pages exist. When `false`, `loadMore` never fires and the load-more skeleton is hidden. */
118
+ /** Required, not defaulted: a silent "never load" default would disable the headline behaviour. */
120
119
  readonly hasMore: _angular_core.InputSignalWithTransform<boolean, unknown>;
121
- /** Number of skeleton rows for the first-load state. */
122
120
  readonly skeletonRowCount: _angular_core.InputSignal<number>;
123
- /** How many rows from the end trigger `loadMore`. */
121
+ /** Number of skeleton rows appended below the loaded rows while `loadingMore` is true. */
122
+ readonly loadMoreSkeletonCount: _angular_core.InputSignal<number>;
123
+ /**
124
+ * Skeleton placeholder columns for a lazily-loaded column set. They deliberately sit OUTSIDE `columns`,
125
+ * so they cannot change which column is last and unpin a sticky last column.
126
+ */
127
+ readonly loadingColumnCount: _angular_core.InputSignalWithTransform<number, unknown>;
124
128
  readonly loadMoreThreshold: _angular_core.InputSignal<number>;
125
- /** Empty-state template, shown when `data` is empty and not loading. */
129
+ /** Shown when `data` is empty and not loading. */
126
130
  readonly noDataTemplate: _angular_core.InputSignal<TemplateRef<unknown> | undefined>;
127
- /** Error template, shown when `error` is `true`. Receives `{ $implicit: retry, retry }` as context. */
131
+ /** Shown when `error` is `true`. Receives `{ $implicit: retry, retry }` as context. */
128
132
  readonly errorTemplate: _angular_core.InputSignal<TemplateRef<InfiniteScrollTableErrorContext> | undefined>;
129
- /** Whether a page fetch failed. */
130
133
  readonly error: _angular_core.InputSignalWithTransform<boolean, unknown>;
131
134
  /**
132
- * Fires when the user scrolls within `loadMoreThreshold` rows of the end.
133
- * Guarded: emits only when `hasMore && !loading && !loadingMore`, and only once per approach.
135
+ * Shown once `hasMore` goes `false` with rows loaded. Deliberately has NO built-in fallback (unlike
136
+ * `noDataTemplate` / `errorTemplate`): a single-page list would otherwise flash "no more results".
134
137
  */
138
+ readonly endOfListTemplate: _angular_core.InputSignal<TemplateRef<unknown> | undefined>;
139
+ /** Guarded: only when `hasMore && !loading && !loadingMore`, and only once per approach to the end. */
135
140
  readonly loadMore: _angular_core.OutputEmitterRef<void>;
136
- /** Fires from the error template's retry affordance. */
137
141
  readonly retry: _angular_core.OutputEmitterRef<void>;
138
- /** Fires when a data row is clicked; payload is the row plus its data index. */
139
142
  readonly rowClick: _angular_core.OutputEmitterRef<InfiniteScrollTableRowClick<T>>;
140
- /**
141
- * Fires when a row's selection checkbox is toggled (only when `selectable`). Payload is the row,
142
- * its data index, and the new selected state. The consumer updates its data's `selected` property.
143
- */
143
+ /** The consumer is expected to apply `selected` back onto its own data. */
144
144
  readonly selectionChange: _angular_core.OutputEmitterRef<InfiniteScrollTableRowSelection<T>>;
145
- /** Fixed width of the checkbox column; exposed so the template can offset a sticky first column. */
146
- protected readonly checkboxColumnWidth = 56;
147
145
  protected readonly viewport: _angular_core.Signal<CdkVirtualScrollViewport | undefined>;
148
146
  protected readonly headerScroll: _angular_core.Signal<ElementRef<HTMLElement> | undefined>;
149
- /** Horizontal-scroll state, mirroring the existing table's `scrollSettings`. */
150
147
  protected readonly scrollStart: _angular_core.WritableSignal<boolean>;
151
148
  protected readonly scrollEnd: _angular_core.WritableSignal<boolean>;
152
149
  protected readonly horizontalScroll: _angular_core.WritableSignal<boolean>;
153
- /**
154
- * Measured width (px) of the body viewport's vertical scrollbar. Reserved as right padding on the
155
- * header so the header's content width exactly equals the body's, keeping every column — including
156
- * the last — aligned between header and body regardless of browser/scrollbar behaviour.
157
- */
150
+ /** Reserved as right padding on the header, so its content width exactly equals the body's. */
158
151
  protected readonly scrollbarWidth: _angular_core.WritableSignal<number>;
152
+ /** Added to the viewport height so on a short list the scrollbar doesn't eat into the rows. */
153
+ protected readonly hScrollbarHeight: _angular_core.WritableSignal<number>;
159
154
  /**
160
- * Measured height (px) of the body viewport's horizontal scrollbar. When a short list also has a
161
- * horizontal scrollbar, this is added to the viewport height so the scrollbar doesn't eat into the
162
- * rows' vertical space and force a spurious vertical scrollbar. Mirrors `scrollbarWidth`.
155
+ * Paging latch. Three fields, one rule: `loadMore` emits at most once per page that lands while the
156
+ * user is inside the trigger zone. `isLoadMoreArmed` is the latch itself, written here and by
157
+ * `checkLoadMore`; `dataLengthAtEmit` records how long `data` was when the last emission went out, so
158
+ * "the requested page arrived" is a length comparison rather than a guess about scroll positions;
159
+ * `firstVisibleIndex` is where the user actually is.
160
+ *
161
+ * The invariant that matters: arming and firing must read the SAME trigger zone. They used to derive it
162
+ * from two different thresholds, which opened bands the latch could never leave.
163
163
  */
164
- protected readonly hScrollbarHeight: _angular_core.WritableSignal<number>;
165
- /** Guards `loadMore` to a single emission per approach to the end. */
166
- private loadMoreArmed;
164
+ private isLoadMoreArmed;
165
+ private dataLengthAtEmit;
166
+ /** In-place-mutation detector; see the effect that reads these for the discriminator. */
167
+ private wasLoadingMore;
168
+ private lastDataReference;
169
+ private lastDataLength;
167
170
  /** Pending frame for a deferred CDK viewport re-measure, so repeated triggers coalesce to one. */
168
171
  private remeasureFrame;
172
+ /** Pending frame for the deferred scroll reset. Cancelled on destroy, like every other frame here. */
173
+ private resetScrollFrame;
174
+ private previousHeadIdentity;
175
+ /** Latches once any non-empty `data` is seen, so only the very first render is exempt from resetting. */
176
+ private hasSeenHead;
177
+ /** Previous `loading` value, for spotting the false -> true edge that starts a reset. */
178
+ private wasLoading;
169
179
  /**
170
- * True when the browser can drive the header via a CSS scroll timeline (see the SCSS): the header
171
- * then tracks the body's horizontal scroll on the compositor, with no per-frame main-thread work.
172
- * In that case the JS `scrollLeft` sync is skipped so the offset isn't applied twice; it stays as
173
- * the fallback for browsers without scroll-driven animations.
180
+ * First visible row index, kept current from `viewport.scrolledIndexChange` (no layout read) rather
181
+ * than `viewport.getRenderedRange().start`, which includes CDK's read-ahead buffer.
174
182
  */
175
- private readonly usesScrollTimelineHeaderSync;
183
+ private firstVisibleIndex;
176
184
  protected readonly skeletonTheme: {
177
185
  background: string;
178
186
  'border-radius': string;
179
187
  height: string;
180
188
  'margin-bottom': string;
181
189
  };
182
- protected readonly lastStickyKey: _angular_core.Signal<string | undefined>;
183
- protected readonly firstStickyKey: _angular_core.Signal<string | undefined>;
184
- /** Real rows, with skeleton placeholders in first-load / load-more states, as one virtual list. */
190
+ protected readonly stickyStartKey: _angular_core.Signal<string | undefined>;
191
+ protected readonly stickyEndKey: _angular_core.Signal<string | undefined>;
192
+ protected readonly defaultColumnWidth = "160px";
193
+ protected readonly effectiveMinBufferPx: _angular_core.Signal<number>;
194
+ /**
195
+ * The `Math.max` is load-bearing: CDK throws in dev mode when `maxBufferPx < minBufferPx`, which a
196
+ * consumer triggers just by raising `minBufferPx` above the derived default.
197
+ */
198
+ protected readonly effectiveMaxBufferPx: _angular_core.Signal<number>;
199
+ /** Stable keys for the placeholder columns, so `@for` can track them across renders. */
200
+ protected readonly loadingColumns: _angular_core.Signal<string[]>;
185
201
  protected readonly renderRows: _angular_core.Signal<RenderRow<T>[]>;
186
202
  protected readonly hasData: _angular_core.Signal<boolean>;
187
- /** The virtualized body is shown for the first-load skeletons and whenever there are rows. */
188
203
  protected readonly showViewport: _angular_core.Signal<boolean>;
204
+ /**
205
+ * `loadingMore` counts as loading here too: a consumer that retries a failed first page through the same
206
+ * flag it uses for every page would otherwise render "no rows" with `aria-busy` set and the live region
207
+ * announcing a fetch.
208
+ */
189
209
  protected readonly showEmptyState: _angular_core.Signal<boolean>;
190
210
  protected readonly showErrorState: _angular_core.Signal<boolean>;
191
211
  protected readonly showErrorFooter: _angular_core.Signal<boolean>;
192
212
  /**
193
- * Viewport height shrinks to the content and is capped at `maxHeight` (CSS `min()` mixes units).
194
- * When a horizontal scrollbar is present it reserves that scrollbar's measured height on top of the
195
- * content, so a short list doesn't overflow and grow a spurious vertical scrollbar. Still capped at
196
- * `maxHeight`.
213
+ * The negative clauses keep this out of slots other states own, and `!loadingMore()` closes a one-tick
214
+ * flash: a fetch completing and the consumer flipping `hasMore` need not land in the same cycle.
215
+ */
216
+ protected readonly showEndOfList: _angular_core.Signal<boolean>;
217
+ /**
218
+ * CSS `min()` because the cap mixes units. The scrollbar reserve stops a short list from growing a
219
+ * spurious vertical scrollbar; fill mode returns `null` so flex owns the sizing and the reserve is moot.
197
220
  */
198
- protected readonly viewportHeight: _angular_core.Signal<string>;
199
- /** Screen-reader announcement for the busy states. */
200
- protected readonly statusMessage: _angular_core.Signal<"Loading" | "Loading more rows" | "">;
221
+ protected readonly viewportHeight: _angular_core.Signal<string | null>;
222
+ protected readonly translationContext = "INFINITE_SCROLL_TABLE.";
223
+ /** A translation key, resolved in the template: screen-reader users are not all English speakers. */
224
+ protected readonly statusMessageKey: _angular_core.Signal<string>;
201
225
  protected readonly errorContext: InfiniteScrollTableErrorContext;
202
226
  protected readonly renderRowTrackBy: TrackByFunction<RenderRow<T>>;
203
227
  constructor();
204
- protected getCellValue(item: T, key: string): unknown;
228
+ private cancelResetScrollFrame;
229
+ /** No-ops only when no rows are rendered: the empty state, or an error with no rows. */
230
+ scrollToIndex(index: number, behavior?: ScrollBehavior): void;
231
+ scrollToTop(behavior?: ScrollBehavior): void;
205
232
  /**
206
- * The single authoritative width for a column: its declared width, else its min-width, else the
207
- * default. The template applies this identically as `flex-basis`, `min-width` AND `max-width` (with
208
- * `flex-grow:0; flex-shrink:0; overflow:hidden`) to the header cell and every body cell, so a column
209
- * is exactly this width in both containers — content is clipped and can never widen it. This is what
210
- * keeps header and body columns aligned end-to-end regardless of cell content or horizontal scroll.
233
+ * The single place `T` is widened to its structural contract. Every field read on a row goes through
234
+ * here, so the assertion exists once instead of at each call site.
211
235
  */
236
+ private asRowLike;
237
+ protected getCellValue(item: T, key: string): unknown;
238
+ /** Applied as `flex-basis`, `min-width` AND `max-width` on header and body cells alike — see the SCSS. */
212
239
  protected columnWidth(column: InfiniteScrollTableColumn<T>): string;
213
- protected onRowClick(row: RenderRow<T>): void;
214
- /**
215
- * Whether a row should show the selected highlight. Mirrors `ui-table`: only when `selectable` is
216
- * enabled and the row's data carries a truthy `selected` property. Skeleton rows are never selected.
217
- */
240
+ protected emitRowClick(event: Event, row: RenderRow<T>): void;
218
241
  protected isRowSelected(row: RenderRow<T>): boolean;
219
- /** Emits the new selection state when a row's checkbox is toggled. Consumer-managed, like `ui-table`. */
220
- protected onSelectionToggle(row: RenderRow<T>, selected: boolean): void;
242
+ protected emitSelectionChange(row: RenderRow<T>, selected: boolean): void;
243
+ /** Reads `--ui-ist-checkbox-width` so the offset cannot drift from the checkbox column's own width. */
244
+ protected stickyStartLeft(column: InfiniteScrollTableColumn<T>): string | null;
221
245
  /**
222
- * Left offset (px) for a sticky-start consumer column: when the checkbox column is present it must
223
- * sit to the right of it, so the first sticky column is pushed over by the checkbox column width.
246
+ * Only the outermost start-pinned cell may cast the sticky shadow: the checkbox cell is pinned too, and
247
+ * shadowing both would drop a shadow across the column beside it.
224
248
  */
225
- protected stickyStartOffset(column: InfiniteScrollTableColumn<T>): number | null;
226
- /** Keyboard equivalent of a row click (Enter/Space) for the currently focused data row. */
227
- protected onRowActivate(event: Event, row: RenderRow<T>): void;
249
+ protected isStickyStartEdge(column: InfiniteScrollTableColumn<T>): boolean;
250
+ protected activateRow(event: Event, row: RenderRow<T>): void;
251
+ /** A clickable row matches `[tabindex]` itself, so `currentTarget` must be excluded explicitly. */
252
+ private isInteractiveTarget;
253
+ /** Whether the user is selecting text, in which case a click is a selection gesture, not activation. */
254
+ private hasTextSelection;
228
255
  /**
229
- * Re-measure the CDK virtual viewport after its box may have changed. Deferred to the next frame so
230
- * it reads the settled layout and never fires a "ResizeObserver loop limit exceeded" warning when
231
- * driven from the ResizeObserver; repeated triggers coalesce to a single measure. The re-measure is
232
- * what makes CDK recompute the rendered range to fill a viewport whose height grew at runtime.
256
+ * Deferred a frame to read the settled layout and never trip "ResizeObserver loop limit exceeded";
257
+ * repeated triggers coalesce to one measure.
233
258
  */
234
259
  private scheduleViewportRemeasure;
235
- private updateHeaderScrollMetric;
236
- private onRetry;
237
- /** Keep the header horizontally aligned with the body and update the sticky-shadow state. */
260
+ /**
261
+ * The third `checkLoadMore` trigger, and the only one that survives a page landing without a rendered-range
262
+ * change. CDK emits a range only when the virtual-for LENGTH changes, so a page whose row count exactly
263
+ * replaces the load-more skeletons (`data + skeletons` before, `data` after) moves nothing CDK watches —
264
+ * and when the new rows add no scrollable height, no scroll event can ever arrive either. Runs after the
265
+ * re-measure so the zone is tested against the size CDK just settled on.
266
+ */
267
+ private settleLoadMore;
268
+ private emitRetry;
269
+ /**
270
+ * The index fallback is identity-in-name-only — it cannot tell rows apart after a reorder — which is why
271
+ * the automatic scroll reset cannot work without `trackBy` or `rowIdKey`.
272
+ */
273
+ private rowIdentity;
274
+ /**
275
+ * Assigning an absolute `scrollLeft` is drift-free by construction: it self-corrects every scroll event
276
+ * and the browser clamps when the header's maximum is momentarily smaller. A progress-normalized CSS
277
+ * scroll-timeline cannot guarantee that, which is why it was removed — do not reintroduce one.
278
+ */
238
279
  private syncHorizontalScroll;
239
280
  /**
240
- * Emits `loadMore` once when the rendered range reaches the trigger zone near the end,
241
- * re-arming when the user scrolls back out. Mirrors the app's `ngx-infinite-scroll` semantics.
281
+ * How many rows the viewport shows. Zero until CDK has measured, which callers must treat as
282
+ * "unknown" rather than "none".
283
+ */
284
+ private visibleRowCount;
285
+ /**
286
+ * Rows from the end that count as "approaching it". Capped at a screenful: a threshold wider than the
287
+ * viewport cannot mean approaching, and uncapped it made the zone unleavable at every scroll position,
288
+ * which is how an oversized `loadMoreThreshold` used to walk a whole dataset unattended. Clamped at 0 so
289
+ * a negative value can't push the trigger point past the end and disable paging outright.
290
+ */
291
+ private triggerZoneRows;
292
+ /**
293
+ * The rendered range includes CDK's read-ahead buffer, so measuring it conflates the user reaching the
294
+ * end with CDK merely pre-rendering it. The visible end has no such slack, so it tracks the user's
295
+ * actual scroll position instead.
296
+ */
297
+ private isInTriggerZone;
298
+ /**
299
+ * Re-arms the latch when something other than scrolling has to do it. Two cases, both of which used to
300
+ * be terminal stalls:
301
+ *
302
+ * - **The requested page landed.** `data` grew past its length at the last emission, so that emission is
303
+ * spent. Without this, re-arming needed the user to scroll *out* of the zone and back — impossible when
304
+ * the page was smaller than the threshold, and impossible to even attempt when the new rows added no
305
+ * scrollable height.
306
+ * - **The zone can't be left.** `visibleEnd` bottoms out at `visibleRows`, so while that floor sits
307
+ * inside the zone no amount of scrolling re-arms anything. Also covers a not-yet-measured viewport,
308
+ * which must count as unleavable or the first fill never starts.
309
+ *
310
+ * Deliberately not driven by `loadingMore` or `error`: a failed fetch clears `loadingMore` without
311
+ * growing `data`, and must stay disarmed rather than re-fire in a loop.
242
312
  */
313
+ private rearmLoadMoreIfUnreachable;
314
+ /** Emits at most once per page that lands while the user is inside the trigger zone. */
243
315
  private checkLoadMore;
244
316
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<InfiniteScrollTableComponent<any>, never>;
245
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<InfiniteScrollTableComponent<any>, "ui-infinite-scroll-table", never, { "data": { "alias": "data"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": true; "isSignal": true; }; "rowHeight": { "alias": "rowHeight"; "required": true; "isSignal": true; }; "trackBy": { "alias": "trackBy"; "required": false; "isSignal": true; }; "rowIdKey": { "alias": "rowIdKey"; "required": false; "isSignal": true; }; "stickyHeader": { "alias": "stickyHeader"; "required": false; "isSignal": true; }; "maxHeight": { "alias": "maxHeight"; "required": false; "isSignal": true; }; "isCondensed": { "alias": "isCondensed"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingMore": { "alias": "loadingMore"; "required": false; "isSignal": true; }; "hasMore": { "alias": "hasMore"; "required": false; "isSignal": true; }; "skeletonRowCount": { "alias": "skeletonRowCount"; "required": false; "isSignal": true; }; "loadMoreThreshold": { "alias": "loadMoreThreshold"; "required": false; "isSignal": true; }; "noDataTemplate": { "alias": "noDataTemplate"; "required": false; "isSignal": true; }; "errorTemplate": { "alias": "errorTemplate"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; }, { "loadMore": "loadMore"; "retry": "retry"; "rowClick": "rowClick"; "selectionChange": "selectionChange"; }, never, never, true, never>;
317
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<InfiniteScrollTableComponent<any>, "ui-infinite-scroll-table", never, { "data": { "alias": "data"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": true; "isSignal": true; }; "rowHeight": { "alias": "rowHeight"; "required": true; "isSignal": true; }; "trackBy": { "alias": "trackBy"; "required": false; "isSignal": true; }; "rowIdKey": { "alias": "rowIdKey"; "required": false; "isSignal": true; }; "minBufferPx": { "alias": "minBufferPx"; "required": false; "isSignal": true; }; "maxBufferPx": { "alias": "maxBufferPx"; "required": false; "isSignal": true; }; "maxHeight": { "alias": "maxHeight"; "required": false; "isSignal": true; }; "fillHeight": { "alias": "fillHeight"; "required": false; "isSignal": true; }; "isCondensed": { "alias": "isCondensed"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "rowsClickable": { "alias": "rowsClickable"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingMore": { "alias": "loadingMore"; "required": false; "isSignal": true; }; "hasMore": { "alias": "hasMore"; "required": true; "isSignal": true; }; "skeletonRowCount": { "alias": "skeletonRowCount"; "required": false; "isSignal": true; }; "loadMoreSkeletonCount": { "alias": "loadMoreSkeletonCount"; "required": false; "isSignal": true; }; "loadingColumnCount": { "alias": "loadingColumnCount"; "required": false; "isSignal": true; }; "loadMoreThreshold": { "alias": "loadMoreThreshold"; "required": false; "isSignal": true; }; "noDataTemplate": { "alias": "noDataTemplate"; "required": false; "isSignal": true; }; "errorTemplate": { "alias": "errorTemplate"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "endOfListTemplate": { "alias": "endOfListTemplate"; "required": false; "isSignal": true; }; }, { "loadMore": "loadMore"; "retry": "retry"; "rowClick": "rowClick"; "selectionChange": "selectionChange"; }, never, never, true, never>;
246
318
  }
247
319
 
248
320
  declare class InfiniteScrollTableComponentModule {
@@ -252,4 +324,4 @@ declare class InfiniteScrollTableComponentModule {
252
324
  }
253
325
 
254
326
  export { InfiniteScrollTableComponent, InfiniteScrollTableComponentModule };
255
- export type { InfiniteScrollTableColumn, InfiniteScrollTableColumnAlignment, InfiniteScrollTableColumnStyles, InfiniteScrollTableErrorContext, InfiniteScrollTableRowClick, InfiniteScrollTableRowSelection };
327
+ export type { InfiniteScrollTableColumn, InfiniteScrollTableColumnAlignment, InfiniteScrollTableColumnStyles, InfiniteScrollTableErrorContext, InfiniteScrollTableHeaderCellContext, InfiniteScrollTableRowCellContext, InfiniteScrollTableRowClick, InfiniteScrollTableRowLike, InfiniteScrollTableRowSelection };