@testgorilla/tgo-ui 11.2.3 → 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.
@@ -1,143 +1,155 @@
1
1
  import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf, ScrollingModule } from '@angular/cdk/scrolling';
2
- import { NgTemplateOutlet, CommonModule } from '@angular/common';
2
+ import { NgTemplateOutlet, AsyncPipe, CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { input, booleanAttribute, output, viewChild, signal, computed, effect, untracked, isDevMode, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
5
- import { Subscription } from 'rxjs';
4
+ import { input, booleanAttribute, numberAttribute, output, viewChild, signal, computed, effect, untracked, isDevMode, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
5
+ import { Subscription, animationFrameScheduler } from 'rxjs';
6
+ import { auditTime } from 'rxjs/operators';
6
7
  import { CheckboxComponent } from '@testgorilla/tgo-ui/components/checkbox';
8
+ import { UiTranslatePipe } from '@testgorilla/tgo-ui/components/core';
7
9
  import { SkeletonComponent } from '@testgorilla/tgo-ui/components/skeleton';
8
10
 
9
- /** Default number of skeleton rows rendered for the initial (full-body) load. */
10
11
  const DEFAULT_FIRST_LOAD_SKELETON_COUNT = 10;
11
- /** Number of skeleton rows appended below existing rows while the next page is fetched. */
12
- const LOAD_MORE_SKELETON_COUNT = 3;
13
- /** Default distance (in rows) from the end at which `loadMore` fires. */
12
+ const DEFAULT_LOAD_MORE_SKELETON_COUNT = 3;
14
13
  const DEFAULT_LOAD_MORE_THRESHOLD = 5;
15
- /** Fixed width (px) of the checkbox selection column. Must match `.ui-ist__cell--checkbox` in the SCSS. */
16
- const CHECKBOX_COLUMN_WIDTH = 56;
17
14
  /**
18
- * A generic, virtualized table with scroll-driven infinite loading.
19
- *
20
- * Owns rendering, fixed-height vertical virtualization (CDK), horizontal scroll, a pinned header,
21
- * sticky first/last columns, first-load and load-more skeletons, and a guarded `loadMore` event.
22
- * All content and layout come from consumer inputs nothing domain-specific lives here.
23
- *
24
- * Independent of `ui-table`: it reuses only stable leaf primitives (`ui-skeleton`) and duplicates
25
- * the column/context type shapes so consumers can migrate with a selector swap.
15
+ * Fallback width for a column that declares none, shared with placeholder columns so both resolve to one
16
+ * identical width — header/body alignment depends on that.
17
+ */
18
+ const DEFAULT_COLUMN_WIDTH = '160px';
19
+ /** Elements owning their own click semantics: a click inside one must not also activate the row. */
20
+ const INTERACTIVE_TARGET_SELECTOR = 'a, button, input, select, textarea, [role="button"], [tabindex]';
21
+ /**
22
+ * A generic, virtualized table with scroll-driven infinite loading. Independent of `ui-table`: it reuses
23
+ * only stable leaf primitives (`ui-skeleton`) and duplicates the column/context type shapes so consumers
24
+ * can migrate with a selector swap.
26
25
  */
27
26
  class InfiniteScrollTableComponent {
28
27
  constructor() {
29
- /** Rows loaded so far. The consumer appends pages to this array. */
30
- this.data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
31
- /** Column definitions. The first `isSticky` column pins left; the last `isSticky` column pins right. */
32
- this.columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
33
28
  /**
34
- * Fixed row height in px used as the virtualization `itemSize`.
35
- * Variable / multi-line row heights are unsupported in this variant.
29
+ * Rows loaded so far. **Must change by reference** (`[...rows, ...page]`): a signal input cannot observe
30
+ * `rows.push(...)`, so an in-place append renders nothing and leaves `loadMore` disarmed.
36
31
  */
32
+ this.data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
33
+ this.columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
34
+ /** Fixed row height in px (the virtualization `itemSize`). Variable row heights are unsupported. */
37
35
  this.rowHeight = input.required(...(ngDevMode ? [{ debugName: "rowHeight" }] : /* istanbul ignore next */ []));
38
- /** Optional stable row identity so recycled rows re-render correctly. Takes precedence over `rowIdKey`. */
36
+ /** Stable row identity so recycled rows re-render correctly. Takes precedence over `rowIdKey`. */
39
37
  this.trackBy = input(...(ngDevMode ? [undefined, { debugName: "trackBy" }] : /* istanbul ignore next */ []));
40
- /** Optional key on each row that holds a stable id. Used when `trackBy` is not provided. */
38
+ /** Key on each row holding a stable id. Used when `trackBy` is not provided. */
41
39
  this.rowIdKey = input(...(ngDevMode ? [undefined, { debugName: "rowIdKey" }] : /* istanbul ignore next */ []));
42
40
  /**
43
- * Accepted for API parity with `ui-table`. In this bounded layout the header is always rendered
44
- * above the internal scroll region and stays visible regardless `.ui-ist` has `overflow: hidden`,
45
- * so the header cannot stick to an outer scrolling page. Kept so `ui-table` templates migrate
46
- * unchanged. Default `true`.
41
+ * CDK's rendered buffers. Both default to a multiple of `rowHeight` (~5 and ~10 rows) rather than a
42
+ * fixed px value, which would mean a different row count at every `rowHeight`.
47
43
  */
48
- this.stickyHeader = input(true, { ...(ngDevMode ? { debugName: "stickyHeader" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
44
+ this.minBufferPx = input(undefined, ...(ngDevMode ? [{ debugName: "minBufferPx" }] : /* istanbul ignore next */ []));
45
+ this.maxBufferPx = input(undefined, ...(ngDevMode ? [{ debugName: "maxBufferPx" }] : /* istanbul ignore next */ []));
49
46
  /** Max height of the internal scroll region (e.g. `'70vh'`). The body shrinks to fit fewer rows. */
50
47
  this.maxHeight = input('70vh', ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
51
- /** Condensed spacing. */
48
+ /** Stretch to the parent's height instead of the content's; `maxHeight` becomes a cap, not the target. */
49
+ this.fillHeight = input(false, { ...(ngDevMode ? { debugName: "fillHeight" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
52
50
  this.isCondensed = input(false, { ...(ngDevMode ? { debugName: "isCondensed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
53
51
  /**
54
- * Enables row selection. When `true`, a dedicated checkbox column is prepended (pinned to the left)
55
- * and each row's checkbox reflects the row's `selected` property; toggling it emits `selectionChange`.
56
- * Selection stays data-driven and consumer-managed, exactly like `ui-table`. Default `false`.
52
+ * Prepends a left-pinned checkbox column driven by each row's `selected` property. Consumer-managed
53
+ * (like `ui-table`): toggling only emits `selectionChange`.
57
54
  */
58
55
  this.selectable = input(false, { ...(ngDevMode ? { debugName: "selectable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
56
+ /**
57
+ * Makes rows focusable and activatable by click or Enter/Space. Off by default: a read-only table
58
+ * must not create a tab stop per rendered row or imply interactivity it does not have.
59
+ */
60
+ this.rowsClickable = input(false, { ...(ngDevMode ? { debugName: "rowsClickable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
59
61
  /** First-load state: renders a full body of skeleton rows. */
60
62
  this.loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
61
63
  /** Appends skeleton rows below the existing rows while the next page is fetched. */
62
64
  this.loadingMore = input(false, { ...(ngDevMode ? { debugName: "loadingMore" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
63
- /** Whether more pages exist. When `false`, `loadMore` never fires and the load-more skeleton is hidden. */
64
- this.hasMore = input(false, { ...(ngDevMode ? { debugName: "hasMore" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
65
- /** Number of skeleton rows for the first-load state. */
65
+ /** Required, not defaulted: a silent "never load" default would disable the headline behaviour. */
66
+ this.hasMore = input.required({ ...(ngDevMode ? { debugName: "hasMore" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
66
67
  this.skeletonRowCount = input(DEFAULT_FIRST_LOAD_SKELETON_COUNT, ...(ngDevMode ? [{ debugName: "skeletonRowCount" }] : /* istanbul ignore next */ []));
67
- /** How many rows from the end trigger `loadMore`. */
68
+ /** Number of skeleton rows appended below the loaded rows while `loadingMore` is true. */
69
+ this.loadMoreSkeletonCount = input(DEFAULT_LOAD_MORE_SKELETON_COUNT, ...(ngDevMode ? [{ debugName: "loadMoreSkeletonCount" }] : /* istanbul ignore next */ []));
70
+ /**
71
+ * Skeleton placeholder columns for a lazily-loaded column set. They deliberately sit OUTSIDE `columns`,
72
+ * so they cannot change which column is last and unpin a sticky last column.
73
+ */
74
+ this.loadingColumnCount = input(0, { ...(ngDevMode ? { debugName: "loadingColumnCount" } : /* istanbul ignore next */ {}), transform: numberAttribute });
68
75
  this.loadMoreThreshold = input(DEFAULT_LOAD_MORE_THRESHOLD, ...(ngDevMode ? [{ debugName: "loadMoreThreshold" }] : /* istanbul ignore next */ []));
69
- /** Empty-state template, shown when `data` is empty and not loading. */
76
+ /** Shown when `data` is empty and not loading. */
70
77
  this.noDataTemplate = input(...(ngDevMode ? [undefined, { debugName: "noDataTemplate" }] : /* istanbul ignore next */ []));
71
- /** Error template, shown when `error` is `true`. Receives `{ $implicit: retry, retry }` as context. */
78
+ /** Shown when `error` is `true`. Receives `{ $implicit: retry, retry }` as context. */
72
79
  this.errorTemplate = input(...(ngDevMode ? [undefined, { debugName: "errorTemplate" }] : /* istanbul ignore next */ []));
73
- /** Whether a page fetch failed. */
74
80
  this.error = input(false, { ...(ngDevMode ? { debugName: "error" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
75
81
  /**
76
- * Fires when the user scrolls within `loadMoreThreshold` rows of the end.
77
- * Guarded: emits only when `hasMore && !loading && !loadingMore`, and only once per approach.
82
+ * Shown once `hasMore` goes `false` with rows loaded. Deliberately has NO built-in fallback (unlike
83
+ * `noDataTemplate` / `errorTemplate`): a single-page list would otherwise flash "no more results".
78
84
  */
85
+ this.endOfListTemplate = input(...(ngDevMode ? [undefined, { debugName: "endOfListTemplate" }] : /* istanbul ignore next */ []));
86
+ /** Guarded: only when `hasMore && !loading && !loadingMore`, and only once per approach to the end. */
79
87
  this.loadMore = output();
80
- /** Fires from the error template's retry affordance. */
81
88
  this.retry = output();
82
- /** Fires when a data row is clicked; payload is the row plus its data index. */
83
89
  this.rowClick = output();
84
- /**
85
- * Fires when a row's selection checkbox is toggled (only when `selectable`). Payload is the row,
86
- * its data index, and the new selected state. The consumer updates its data's `selected` property.
87
- */
90
+ /** The consumer is expected to apply `selected` back onto its own data. */
88
91
  this.selectionChange = output();
89
- /** Fixed width of the checkbox column; exposed so the template can offset a sticky first column. */
90
- this.checkboxColumnWidth = CHECKBOX_COLUMN_WIDTH;
91
92
  this.viewport = viewChild(CdkVirtualScrollViewport, ...(ngDevMode ? [{ debugName: "viewport" }] : /* istanbul ignore next */ []));
92
93
  this.headerScroll = viewChild('headerScroll', ...(ngDevMode ? [{ debugName: "headerScroll" }] : /* istanbul ignore next */ []));
93
- /** Horizontal-scroll state, mirroring the existing table's `scrollSettings`. */
94
94
  this.scrollStart = signal(true, ...(ngDevMode ? [{ debugName: "scrollStart" }] : /* istanbul ignore next */ []));
95
95
  this.scrollEnd = signal(false, ...(ngDevMode ? [{ debugName: "scrollEnd" }] : /* istanbul ignore next */ []));
96
96
  this.horizontalScroll = signal(false, ...(ngDevMode ? [{ debugName: "horizontalScroll" }] : /* istanbul ignore next */ []));
97
- /**
98
- * Measured width (px) of the body viewport's vertical scrollbar. Reserved as right padding on the
99
- * header so the header's content width exactly equals the body's, keeping every column — including
100
- * the last — aligned between header and body regardless of browser/scrollbar behaviour.
101
- */
97
+ /** Reserved as right padding on the header, so its content width exactly equals the body's. */
102
98
  this.scrollbarWidth = signal(0, ...(ngDevMode ? [{ debugName: "scrollbarWidth" }] : /* istanbul ignore next */ []));
99
+ /** Added to the viewport height so on a short list the scrollbar doesn't eat into the rows. */
100
+ this.hScrollbarHeight = signal(0, ...(ngDevMode ? [{ debugName: "hScrollbarHeight" }] : /* istanbul ignore next */ []));
103
101
  /**
104
- * Measured height (px) of the body viewport's horizontal scrollbar. When a short list also has a
105
- * horizontal scrollbar, this is added to the viewport height so the scrollbar doesn't eat into the
106
- * rows' vertical space and force a spurious vertical scrollbar. Mirrors `scrollbarWidth`.
102
+ * Paging latch. Three fields, one rule: `loadMore` emits at most once per page that lands while the
103
+ * user is inside the trigger zone. `isLoadMoreArmed` is the latch itself, written here and by
104
+ * `checkLoadMore`; `dataLengthAtEmit` records how long `data` was when the last emission went out, so
105
+ * "the requested page arrived" is a length comparison rather than a guess about scroll positions;
106
+ * `firstVisibleIndex` is where the user actually is.
107
+ *
108
+ * The invariant that matters: arming and firing must read the SAME trigger zone. They used to derive it
109
+ * from two different thresholds, which opened bands the latch could never leave.
107
110
  */
108
- this.hScrollbarHeight = signal(0, ...(ngDevMode ? [{ debugName: "hScrollbarHeight" }] : /* istanbul ignore next */ []));
109
- /** Guards `loadMore` to a single emission per approach to the end. */
110
- this.loadMoreArmed = true;
111
+ this.isLoadMoreArmed = true;
112
+ this.dataLengthAtEmit = 0;
113
+ /** In-place-mutation detector; see the effect that reads these for the discriminator. */
114
+ this.wasLoadingMore = false;
115
+ this.lastDataLength = 0;
116
+ /** Latches once any non-empty `data` is seen, so only the very first render is exempt from resetting. */
117
+ this.hasSeenHead = false;
118
+ /** Previous `loading` value, for spotting the false -> true edge that starts a reset. */
119
+ this.wasLoading = false;
111
120
  /**
112
- * True when the browser can drive the header via a CSS scroll timeline (see the SCSS): the header
113
- * then tracks the body's horizontal scroll on the compositor, with no per-frame main-thread work.
114
- * In that case the JS `scrollLeft` sync is skipped so the offset isn't applied twice; it stays as
115
- * the fallback for browsers without scroll-driven animations.
121
+ * First visible row index, kept current from `viewport.scrolledIndexChange` (no layout read) rather
122
+ * than `viewport.getRenderedRange().start`, which includes CDK's read-ahead buffer.
116
123
  */
117
- this.usesScrollTimelineHeaderSync = typeof CSS !== 'undefined' &&
118
- CSS !== null &&
119
- typeof CSS.supports === 'function' &&
120
- CSS.supports('animation-timeline: scroll()');
124
+ this.firstVisibleIndex = 0;
121
125
  this.skeletonTheme = {
122
- background: '#F4F4F4',
126
+ background: 'var(--ui-ist-skeleton-bg)',
123
127
  'border-radius': '4px',
124
128
  height: '24px',
125
129
  'margin-bottom': '0',
126
130
  };
127
- // Sticky rule mirrors ui-table (positional): the LAST column pins right when it's sticky; any other
128
- // sticky column pins left. This way an actions column (always the last column) is always pinned to
129
- // the right, even when it's the only sticky column.
130
- this.lastStickyKey = computed(() => {
131
+ // Pinning is positional by contract: only the FIRST column pins left, only the LAST pins right.
132
+ // `isSticky` elsewhere is ignored (with a dev warning), not applied as `left: 0` that would overlay
133
+ // the columns before it.
134
+ this.stickyStartKey = computed(() => {
131
135
  const columns = this.columns();
132
- const lastColumn = columns[columns.length - 1];
133
- return lastColumn?.isSticky ? lastColumn.key : undefined;
134
- }, ...(ngDevMode ? [{ debugName: "lastStickyKey" }] : /* istanbul ignore next */ []));
135
- this.firstStickyKey = computed(() => {
136
+ return columns[0]?.isSticky ? columns[0].key : undefined;
137
+ }, ...(ngDevMode ? [{ debugName: "stickyStartKey" }] : /* istanbul ignore next */ []));
138
+ this.stickyEndKey = computed(() => {
136
139
  const columns = this.columns();
137
140
  const lastIndex = columns.length - 1;
138
- return columns.find((column, index) => column.isSticky && index !== lastIndex)?.key;
139
- }, ...(ngDevMode ? [{ debugName: "firstStickyKey" }] : /* istanbul ignore next */ []));
140
- /** Real rows, with skeleton placeholders in first-load / load-more states, as one virtual list. */
141
+ // `lastIndex > 0` so a single sticky column pins to the start, not the end.
142
+ return lastIndex > 0 && columns[lastIndex]?.isSticky ? columns[lastIndex].key : undefined;
143
+ }, ...(ngDevMode ? [{ debugName: "stickyEndKey" }] : /* istanbul ignore next */ []));
144
+ this.defaultColumnWidth = DEFAULT_COLUMN_WIDTH;
145
+ this.effectiveMinBufferPx = computed(() => this.minBufferPx() ?? this.rowHeight() * 5, ...(ngDevMode ? [{ debugName: "effectiveMinBufferPx" }] : /* istanbul ignore next */ []));
146
+ /**
147
+ * The `Math.max` is load-bearing: CDK throws in dev mode when `maxBufferPx < minBufferPx`, which a
148
+ * consumer triggers just by raising `minBufferPx` above the derived default.
149
+ */
150
+ this.effectiveMaxBufferPx = computed(() => Math.max(this.maxBufferPx() ?? this.rowHeight() * 10, this.effectiveMinBufferPx()), ...(ngDevMode ? [{ debugName: "effectiveMaxBufferPx" }] : /* istanbul ignore next */ []));
151
+ /** Stable keys for the placeholder columns, so `@for` can track them across renders. */
152
+ this.loadingColumns = computed(() => Array.from({ length: Math.max(0, this.loadingColumnCount()) }, (_, index) => `loading-column-${index}`), ...(ngDevMode ? [{ debugName: "loadingColumns" }] : /* istanbul ignore next */ []));
141
153
  this.renderRows = computed(() => {
142
154
  if (this.loading()) {
143
155
  return Array.from({ length: Math.max(1, this.skeletonRowCount()) }, (_, i) => ({
@@ -147,89 +159,111 @@ class InfiniteScrollTableComponent {
147
159
  }
148
160
  const rows = this.data().map((item, index) => ({ kind: 'data', item, index }));
149
161
  if (this.loadingMore() && this.hasMore()) {
150
- for (let i = 0; i < LOAD_MORE_SKELETON_COUNT; i++) {
162
+ for (let i = 0; i < Math.max(1, this.loadMoreSkeletonCount()); i++) {
151
163
  rows.push({ kind: 'skeleton', id: `skeleton-more-${i}` });
152
164
  }
153
165
  }
154
166
  return rows;
155
167
  }, ...(ngDevMode ? [{ debugName: "renderRows" }] : /* istanbul ignore next */ []));
156
168
  this.hasData = computed(() => this.data().length > 0, ...(ngDevMode ? [{ debugName: "hasData" }] : /* istanbul ignore next */ []));
157
- /** The virtualized body is shown for the first-load skeletons and whenever there are rows. */
158
169
  this.showViewport = computed(() => this.loading() || this.hasData(), ...(ngDevMode ? [{ debugName: "showViewport" }] : /* istanbul ignore next */ []));
159
- this.showEmptyState = computed(() => !this.loading() && !this.error() && !this.hasData(), ...(ngDevMode ? [{ debugName: "showEmptyState" }] : /* istanbul ignore next */ []));
170
+ /**
171
+ * `loadingMore` counts as loading here too: a consumer that retries a failed first page through the same
172
+ * flag it uses for every page would otherwise render "no rows" with `aria-busy` set and the live region
173
+ * announcing a fetch.
174
+ */
175
+ this.showEmptyState = computed(() => !this.loading() && !this.loadingMore() && !this.error() && !this.hasData(), ...(ngDevMode ? [{ debugName: "showEmptyState" }] : /* istanbul ignore next */ []));
160
176
  this.showErrorState = computed(() => !this.loading() && this.error() && !this.hasData(), ...(ngDevMode ? [{ debugName: "showErrorState" }] : /* istanbul ignore next */ []));
161
177
  this.showErrorFooter = computed(() => !this.loading() && this.error() && this.hasData(), ...(ngDevMode ? [{ debugName: "showErrorFooter" }] : /* istanbul ignore next */ []));
162
178
  /**
163
- * Viewport height shrinks to the content and is capped at `maxHeight` (CSS `min()` mixes units).
164
- * When a horizontal scrollbar is present it reserves that scrollbar's measured height on top of the
165
- * content, so a short list doesn't overflow and grow a spurious vertical scrollbar. Still capped at
166
- * `maxHeight`.
179
+ * The negative clauses keep this out of slots other states own, and `!loadingMore()` closes a one-tick
180
+ * flash: a fetch completing and the consumer flipping `hasMore` need not land in the same cycle.
181
+ */
182
+ this.showEndOfList = computed(() => this.hasData() && !this.hasMore() && !this.loading() && !this.loadingMore() && !this.error(), ...(ngDevMode ? [{ debugName: "showEndOfList" }] : /* istanbul ignore next */ []));
183
+ /**
184
+ * CSS `min()` because the cap mixes units. The scrollbar reserve stops a short list from growing a
185
+ * spurious vertical scrollbar; fill mode returns `null` so flex owns the sizing and the reserve is moot.
167
186
  */
168
187
  this.viewportHeight = computed(() => {
188
+ if (this.fillHeight()) {
189
+ return null;
190
+ }
169
191
  const contentHeight = this.renderRows().length * this.rowHeight();
170
192
  const scrollbarReserve = this.horizontalScroll() ? this.hScrollbarHeight() : 0;
171
193
  return `min(${contentHeight + scrollbarReserve}px, ${this.maxHeight()})`;
172
194
  }, ...(ngDevMode ? [{ debugName: "viewportHeight" }] : /* istanbul ignore next */ []));
173
- /** Screen-reader announcement for the busy states. */
174
- this.statusMessage = computed(() => {
195
+ this.translationContext = 'INFINITE_SCROLL_TABLE.';
196
+ /** A translation key, resolved in the template: screen-reader users are not all English speakers. */
197
+ this.statusMessageKey = computed(() => {
175
198
  if (this.loading()) {
176
- return 'Loading';
199
+ return `${this.translationContext}LOADING`;
177
200
  }
178
201
  if (this.loadingMore()) {
179
- return 'Loading more rows';
202
+ return `${this.translationContext}LOADING_MORE`;
180
203
  }
181
204
  return '';
182
- }, ...(ngDevMode ? [{ debugName: "statusMessage" }] : /* istanbul ignore next */ []));
205
+ }, ...(ngDevMode ? [{ debugName: "statusMessageKey" }] : /* istanbul ignore next */ []));
183
206
  this.errorContext = {
184
- $implicit: () => this.onRetry(),
185
- retry: () => this.onRetry(),
207
+ $implicit: () => this.emitRetry(),
208
+ retry: () => this.emitRetry(),
186
209
  };
187
210
  this.renderRowTrackBy = (index, row) => {
188
211
  if (row.kind === 'skeleton') {
189
212
  return row.id;
190
213
  }
191
- const explicitTrackBy = this.trackBy();
192
- if (explicitTrackBy) {
193
- return explicitTrackBy(row.index, row.item);
194
- }
195
- const key = this.rowIdKey();
196
- return key ? row.item[key] : row.index;
214
+ return this.rowIdentity(row.item, row.index);
197
215
  };
198
- // (Re-)attach scroll listeners whenever the viewport enters or leaves the DOM (empty/error states).
199
- // Keyed on `viewport()` only: the setup below reads other signals (through syncHorizontalScroll /
200
- // checkLoadMore), so it runs inside `untracked()` to avoid re-running — and tearing down the
201
- // subscriptions + ResizeObserver — on every data change or loading toggle.
216
+ // (Re-)attaches scroll listeners as the viewport enters or leaves the DOM. The setup reads other
217
+ // signals, so it is `untracked()` otherwise every data change would tear the listeners down.
202
218
  effect(onCleanup => {
203
219
  const viewport = this.viewport();
204
220
  if (!viewport) {
205
221
  return;
206
222
  }
207
223
  untracked(() => {
224
+ // A freshly created CdkVirtualScrollViewport always mounts scrolled to the top, and this field
225
+ // carries across viewport instances (`@if (showViewport())` destroys and recreates one when `data`
226
+ // empties and repopulates). Leaving it stale would let `checkLoadMore` fire off a scroll position
227
+ // belonging to the OLD viewport.
228
+ this.firstVisibleIndex = 0;
208
229
  const element = viewport.elementRef.nativeElement;
209
- // CDK inserts a generic `.cdk-virtual-scroll-content-wrapper` between the viewport
210
- // (role="rowgroup") and the rows (role="row"). Mark it presentational so the rows stay
211
- // owned by the rowgroup and the table's ARIA tree isn't broken for assistive tech.
230
+ // Both patch CDK divs that would otherwise break the ARIA table structure: its content wrapper
231
+ // sits between `role="rowgroup"` and the rows, its spacer is an unlabeled child of the rowgroup.
232
+ // Both selectors are CDK internals, so the optional chaining would swallow a CDK restructure
233
+ // silently. The spec asserts both attributes after mount, which is what turns such a change into a
234
+ // failing test rather than a quietly invalid accessibility tree.
212
235
  element.querySelector('.cdk-virtual-scroll-content-wrapper')?.setAttribute('role', 'presentation');
236
+ element.querySelector('.cdk-virtual-scroll-spacer')?.setAttribute('aria-hidden', 'true');
213
237
  const subscription = new Subscription();
214
- subscription.add(viewport.elementScrolled().subscribe(() => this.syncHorizontalScroll()));
238
+ // Audited to one animation frame, matching CDK's own scroll handler, so `measureScrollOffset`'s
239
+ // forced reflow can't run several times per frame and interleave with CDK's DOM writes.
240
+ subscription.add(viewport
241
+ .elementScrolled()
242
+ .pipe(auditTime(0, animationFrameScheduler))
243
+ .subscribe(() => this.syncHorizontalScroll()));
244
+ // Neither trigger is redundant: an append changes the rendered range without a scroll, and a scroll
245
+ // inside the read-ahead buffer moves `firstVisibleIndex` without changing the range.
215
246
  subscription.add(viewport.renderedRangeStream.subscribe(() => this.checkLoadMore()));
216
- // Keep the measured scrollbar dimensions current: they change when a scrollbar appears or
217
- // disappears (content grows/shrinks) and on viewport resize.
247
+ subscription.add(viewport.scrolledIndexChange.subscribe(index => {
248
+ this.firstVisibleIndex = index;
249
+ this.checkLoadMore();
250
+ }));
251
+ // `offsetWidth - clientWidth` also counts borders: pure scrollbar only because `.ui-ist__body`
252
+ // has none, and it must stay borderless.
218
253
  const measureScrollbar = () => {
219
254
  this.scrollbarWidth.set(element.offsetWidth - element.clientWidth);
220
255
  this.hScrollbarHeight.set(element.offsetHeight - element.clientHeight);
221
- this.updateHeaderScrollMetric();
222
256
  };
223
257
  subscription.add(viewport.renderedRangeStream.subscribe(() => measureScrollbar()));
224
- // The viewport element's box can also change from things no signal captures a flex/grid
225
- // parent resizing, or a relative `maxHeight` (e.g. `70vh`) reacting to a container change.
226
- // CDK caches its measured size, so re-measure the virtual viewport on any box change or it
227
- // under-renders (empty tail band). The signal-driven height changes are covered by the
228
- // dedicated effect below; this catches the rest.
258
+ // Covers box changes no signal captures: a flex parent resizing, or a relative `maxHeight`
259
+ // reacting to its container. Signal-driven height changes are the effect below.
229
260
  let resizeObserver;
230
261
  if (typeof ResizeObserver !== 'undefined') {
231
262
  resizeObserver = new ResizeObserver(() => {
232
263
  measureScrollbar();
264
+ // A resize can cross into or out of horizontal overflow without a scroll event, leaving the
265
+ // sticky shadows and `viewportHeight()`'s scrollbar reserve stale.
266
+ this.syncHorizontalScroll();
233
267
  this.scheduleViewportRemeasure();
234
268
  });
235
269
  resizeObserver.observe(element);
@@ -237,9 +271,8 @@ class InfiniteScrollTableComponent {
237
271
  measureScrollbar();
238
272
  this.syncHorizontalScroll();
239
273
  this.checkLoadMore();
240
- // The first syncHorizontalScroll() runs before the CDK viewport lays out its rows, so the
241
- // overflow measurement (and the sticky end-shadow) is stale. Re-sync after the next frame,
242
- // once layout is settled. Guarded because requestAnimationFrame is browser-only (SSR).
274
+ // The sync above runs before CDK lays out its rows, so the overflow measurement is stale until
275
+ // the next frame. Guarded because requestAnimationFrame is browser-only (SSR).
243
276
  let scrollSyncFrame;
244
277
  if (typeof requestAnimationFrame !== 'undefined') {
245
278
  scrollSyncFrame = requestAnimationFrame(() => this.syncHorizontalScroll());
@@ -247,8 +280,7 @@ class InfiniteScrollTableComponent {
247
280
  onCleanup(() => {
248
281
  subscription.unsubscribe();
249
282
  resizeObserver?.disconnect();
250
- // Cancel still-pending frames so they can't fire after the viewport leaves the DOM
251
- // (e.g. flipping to the empty/error state in the same tick).
283
+ // Pending frames must not fire after the viewport leaves the DOM.
252
284
  if (scrollSyncFrame !== undefined) {
253
285
  cancelAnimationFrame(scrollSyncFrame);
254
286
  }
@@ -259,31 +291,31 @@ class InfiniteScrollTableComponent {
259
291
  });
260
292
  });
261
293
  });
262
- // Re-measure the CDK viewport whenever its height changes from a signal: data growth (more render
263
- // rows), a runtime `rowHeight` change (also changes total content height), or a `maxHeight` change.
264
- // `CdkVirtualScrollViewport` caches its measured size and only re-measures on scroll or window
265
- // resize, so a runtime height increase would otherwise leave the extra space empty with the tail
266
- // rows unrendered until a manual scroll/resize. Reading these signals (not `viewportHeight()`)
267
- // keeps the dependency to genuine height inputs and off horizontal-scroll state.
294
+ // CDK caches its measured size and re-measures only on scroll or window resize, so any signal-driven
295
+ // height change here (`fillHeight` included: it swaps the `min()` cap for a flex-driven height) would
296
+ // leave the new space empty with the tail rows unrendered. Read individually rather than via
297
+ // `viewportHeight()`, to keep the dependency off horizontal-scroll state. `loadingColumnCount` changes
298
+ // content WIDTH not the box, so the ResizeObserver misses it and only the deferred
299
+ // `syncHorizontalScroll()` keeps the horizontal-overflow state current.
268
300
  effect(() => {
269
301
  this.renderRows();
270
302
  this.rowHeight();
271
303
  this.maxHeight();
272
304
  this.columns();
305
+ this.fillHeight();
306
+ this.loadingColumnCount();
307
+ // Adds a 56px checkbox column: content width changes, the box does not.
308
+ this.selectable();
273
309
  this.scheduleViewportRemeasure();
274
310
  });
275
- // Re-arm the loadMore guard whenever the `data` input changes a page arrived, or the set was
276
- // replaced (filter/search reset, even to a shorter or same-length list). It reads `data()` only,
277
- // NOT `loadingMore()`: a failed fetch clears `loadingMore` without changing `data`, so the guard
278
- // stays disarmed and never re-fires `loadMore` in a loop (the `error()` guard is a second layer).
311
+ // A `data` change is the one signal that a requested page landed, so it is where the latch gets its
312
+ // second chance. See `rearmLoadMoreIfUnreachable` for which cases that covers and why.
279
313
  effect(() => {
280
314
  this.data();
281
- this.loadMoreArmed = true;
315
+ untracked(() => this.rearmLoadMoreIfUnreachable());
282
316
  });
283
- // Dev-only: without `trackBy` or `rowIdKey`, row identity falls back to the data index the
284
- // weakest option exactly when rows recycle. If the consumer opts into selection without a stable
285
- // id, surface the sharp edge so selection state can't silently attach to the wrong row after a
286
- // reorder/prepend.
317
+ // Dev-only: with selection on and no stable id, identity falls back to the data index, so selection
318
+ // state can silently attach to the wrong row after a reorder/prepend.
287
319
  effect(() => {
288
320
  if (isDevMode() && this.selectable() && !this.trackBy() && !this.rowIdKey()) {
289
321
  console.warn('[ui-infinite-scroll-table] `selectable` is enabled without `trackBy` or `rowIdKey`. ' +
@@ -291,65 +323,172 @@ class InfiniteScrollTableComponent {
291
323
  'when rows are reordered or prepended. Provide `trackBy` or `rowIdKey` for stable identity.');
292
324
  }
293
325
  });
326
+ // Dev-only: `isSticky` reads like a per-column capability, so name the columns being ignored.
327
+ effect(() => {
328
+ if (!isDevMode()) {
329
+ return;
330
+ }
331
+ const columns = this.columns();
332
+ const lastIndex = columns.length - 1;
333
+ const ignored = columns
334
+ .filter((column, index) => column.isSticky && index !== 0 && index !== lastIndex)
335
+ .map(column => column.key);
336
+ if (ignored.length > 0) {
337
+ console.warn('[ui-infinite-scroll-table] `isSticky` is only honoured on the first and last column. ' +
338
+ `Ignoring it on: ${ignored.join(', ')}.`);
339
+ }
340
+ });
341
+ // Dev-only: the in-place-append tell is a fetch completing with the SAME array reference AND a GROWN
342
+ // length. Length is the discriminator — a failed or empty final fetch keeps the reference but not the
343
+ // growth, and those are legitimate outcomes that must stay silent.
344
+ effect(() => {
345
+ const loadingMore = this.loadingMore();
346
+ const data = this.data();
347
+ const completedFetch = this.wasLoadingMore && !loadingMore;
348
+ const sameReference = this.lastDataReference === data;
349
+ const grew = data.length > this.lastDataLength;
350
+ this.wasLoadingMore = loadingMore;
351
+ this.lastDataReference = data;
352
+ this.lastDataLength = data.length;
353
+ if (isDevMode() && completedFetch && sameReference && grew) {
354
+ console.warn('[ui-infinite-scroll-table] `loadingMore` completed without `data` changing reference. ' +
355
+ 'Append pages with a new array (`[...rows, ...page]`) — a signal input cannot observe an ' +
356
+ 'in-place `push`, so the new rows will not render and `loadMore` will not fire again.');
357
+ }
358
+ });
359
+ // Two ways a reset is recognised. Entering `loading` is the earlier and better one: the body becomes
360
+ // skeletons, so the scroll belongs at the top immediately, and by the time the data lands there is
361
+ // nothing left to jump. The head-identity check then covers a replacement swapped in WITHOUT a loading
362
+ // pass — that one needs `trackBy` or `rowIdKey`, since the index fallback makes the head always `0`.
363
+ // Caveat: a newest-first feed that PREPENDS also resets; those consumers should call `scrollToTop()`.
364
+ effect(onCleanup => {
365
+ onCleanup(() => this.cancelResetScrollFrame());
366
+ const data = this.data();
367
+ const loading = this.loading();
368
+ const startedLoading = loading && !this.wasLoading;
369
+ this.wasLoading = loading;
370
+ const headIdentity = data.length > 0 ? untracked(() => this.rowIdentity(data[0], 0)) : undefined;
371
+ const replaced = this.hasSeenHead && data.length > 0 && headIdentity !== this.previousHeadIdentity;
372
+ this.previousHeadIdentity = headIdentity;
373
+ // Latches once and never clears: a reset usually passes through empty + `loading`, which keeps the
374
+ // viewport (and its scroll offset) alive, so clearing here would make the repopulation look like a
375
+ // first render and skip the reset.
376
+ this.hasSeenHead = this.hasSeenHead || data.length > 0;
377
+ if (!startedLoading && !replaced) {
378
+ // A shrink that isn't recognised as a replacement still strands the index past the new end, where
379
+ // it reads as "at the bottom" and fires `loadMore` for a list the user hasn't seen.
380
+ this.firstVisibleIndex = Math.min(this.firstVisibleIndex, Math.max(0, data.length - 1));
381
+ return;
382
+ }
383
+ // Synchronously, ahead of the deferred scroll: CDK updates its rendered range for the new length
384
+ // during this change detection, and that runs `checkLoadMore` while the index still points into the
385
+ // old, longer list.
386
+ this.firstVisibleIndex = 0;
387
+ // Deferred a frame: CDK clamps a scroll request against the size it has already measured. Tracked so
388
+ // it can be cancelled — a frame must not fire after the viewport leaves the DOM.
389
+ if (typeof requestAnimationFrame === 'undefined') {
390
+ untracked(() => this.scrollToTop());
391
+ return;
392
+ }
393
+ this.cancelResetScrollFrame();
394
+ this.resetScrollFrame = requestAnimationFrame(() => {
395
+ this.resetScrollFrame = undefined;
396
+ untracked(() => this.scrollToTop());
397
+ });
398
+ });
294
399
  }
295
- getCellValue(item, key) {
296
- return item[key];
400
+ cancelResetScrollFrame() {
401
+ if (this.resetScrollFrame !== undefined) {
402
+ cancelAnimationFrame(this.resetScrollFrame);
403
+ this.resetScrollFrame = undefined;
404
+ }
405
+ }
406
+ /** No-ops only when no rows are rendered: the empty state, or an error with no rows. */
407
+ scrollToIndex(index, behavior = 'auto') {
408
+ this.viewport()?.scrollToIndex(index, behavior);
409
+ }
410
+ scrollToTop(behavior = 'auto') {
411
+ this.viewport()?.scrollToIndex(0, behavior);
297
412
  }
298
413
  /**
299
- * The single authoritative width for a column: its declared width, else its min-width, else the
300
- * default. The template applies this identically as `flex-basis`, `min-width` AND `max-width` (with
301
- * `flex-grow:0; flex-shrink:0; overflow:hidden`) to the header cell and every body cell, so a column
302
- * is exactly this width in both containers — content is clipped and can never widen it. This is what
303
- * keeps header and body columns aligned end-to-end regardless of cell content or horizontal scroll.
414
+ * The single place `T` is widened to its structural contract. Every field read on a row goes through
415
+ * here, so the assertion exists once instead of at each call site.
304
416
  */
417
+ asRowLike(item) {
418
+ return item;
419
+ }
420
+ getCellValue(item, key) {
421
+ return this.asRowLike(item)[key];
422
+ }
423
+ /** Applied as `flex-basis`, `min-width` AND `max-width` on header and body cells alike — see the SCSS. */
305
424
  columnWidth(column) {
306
- return column.styles?.width ?? column.styles?.['min-width'] ?? '160px';
425
+ return column.styles?.width ?? DEFAULT_COLUMN_WIDTH;
307
426
  }
308
- onRowClick(row) {
309
- if (row.kind !== 'data') {
427
+ emitRowClick(event, row) {
428
+ if (!this.rowsClickable() || row.kind !== 'data') {
429
+ return;
430
+ }
431
+ if (this.isInteractiveTarget(event) || this.hasTextSelection()) {
310
432
  return;
311
433
  }
312
- this.rowClick.emit({ ...row.item, index: row.index });
434
+ this.rowClick.emit({ row: row.item, index: row.index });
313
435
  }
314
- /**
315
- * Whether a row should show the selected highlight. Mirrors `ui-table`: only when `selectable` is
316
- * enabled and the row's data carries a truthy `selected` property. Skeleton rows are never selected.
317
- */
318
436
  isRowSelected(row) {
319
- return this.selectable() && row.kind === 'data' && Boolean(row.item.selected);
437
+ return this.selectable() && row.kind === 'data' && Boolean(this.asRowLike(row.item).selected);
320
438
  }
321
- /** Emits the new selection state when a row's checkbox is toggled. Consumer-managed, like `ui-table`. */
322
- onSelectionToggle(row, selected) {
439
+ emitSelectionChange(row, selected) {
323
440
  if (row.kind !== 'data') {
324
441
  return;
325
442
  }
326
- this.selectionChange.emit({
327
- ...row.item,
328
- index: row.index,
329
- selected,
330
- });
443
+ this.selectionChange.emit({ row: row.item, index: row.index, selected });
444
+ }
445
+ /** Reads `--ui-ist-checkbox-width` so the offset cannot drift from the checkbox column's own width. */
446
+ stickyStartLeft(column) {
447
+ if (column.key !== this.stickyStartKey()) {
448
+ return null;
449
+ }
450
+ return this.selectable() ? 'var(--ui-ist-checkbox-width)' : '0px';
331
451
  }
332
452
  /**
333
- * Left offset (px) for a sticky-start consumer column: when the checkbox column is present it must
334
- * sit to the right of it, so the first sticky column is pushed over by the checkbox column width.
453
+ * Only the outermost start-pinned cell may cast the sticky shadow: the checkbox cell is pinned too, and
454
+ * shadowing both would drop a shadow across the column beside it.
335
455
  */
336
- stickyStartOffset(column) {
337
- return this.selectable() && column.key === this.firstStickyKey() ? this.checkboxColumnWidth : null;
456
+ isStickyStartEdge(column) {
457
+ return column.key === this.stickyStartKey();
338
458
  }
339
- /** Keyboard equivalent of a row click (Enter/Space) for the currently focused data row. */
340
- onRowActivate(event, row) {
459
+ activateRow(event, row) {
460
+ if (!this.rowsClickable() || row.kind !== 'data') {
461
+ return;
462
+ }
463
+ if (this.isInteractiveTarget(event)) {
464
+ return;
465
+ }
466
+ // Only after the guards, so a non-clickable table never swallows page-scroll-by-space.
341
467
  event.preventDefault();
342
- this.onRowClick(row);
468
+ this.rowClick.emit({ row: row.item, index: row.index });
469
+ }
470
+ /** A clickable row matches `[tabindex]` itself, so `currentTarget` must be excluded explicitly. */
471
+ isInteractiveTarget(event) {
472
+ const target = event.target;
473
+ if (!(target instanceof Element)) {
474
+ return false;
475
+ }
476
+ const interactive = target.closest(INTERACTIVE_TARGET_SELECTOR);
477
+ return interactive !== null && interactive !== event.currentTarget;
478
+ }
479
+ /** Whether the user is selecting text, in which case a click is a selection gesture, not activation. */
480
+ hasTextSelection() {
481
+ const selection = typeof window !== 'undefined' ? window.getSelection() : null;
482
+ return Boolean(selection && !selection.isCollapsed && selection.toString().length > 0);
343
483
  }
344
484
  /**
345
- * Re-measure the CDK virtual viewport after its box may have changed. Deferred to the next frame so
346
- * it reads the settled layout and never fires a "ResizeObserver loop limit exceeded" warning when
347
- * driven from the ResizeObserver; repeated triggers coalesce to a single measure. The re-measure is
348
- * what makes CDK recompute the rendered range to fill a viewport whose height grew at runtime.
485
+ * Deferred a frame to read the settled layout and never trip "ResizeObserver loop limit exceeded";
486
+ * repeated triggers coalesce to one measure.
349
487
  */
350
488
  scheduleViewportRemeasure() {
351
489
  if (typeof requestAnimationFrame === 'undefined') {
352
490
  this.viewport()?.checkViewportSize();
491
+ this.settleLoadMore();
353
492
  return;
354
493
  }
355
494
  if (this.remeasureFrame !== undefined) {
@@ -357,26 +496,42 @@ class InfiniteScrollTableComponent {
357
496
  }
358
497
  this.remeasureFrame = requestAnimationFrame(() => {
359
498
  this.remeasureFrame = undefined;
360
- // `viewport()` is null in the empty/error states and after teardown, so this safely no-ops.
361
499
  this.viewport()?.checkViewportSize();
362
- this.updateHeaderScrollMetric();
500
+ this.syncHorizontalScroll();
501
+ this.settleLoadMore();
363
502
  });
364
503
  }
365
- updateHeaderScrollMetric() {
366
- if (!this.usesScrollTimelineHeaderSync) {
367
- return;
368
- }
369
- const headerScroll = this.headerScroll()?.nativeElement;
370
- if (!headerScroll) {
371
- return;
372
- }
373
- const maxScroll = Math.max(0, headerScroll.scrollWidth - headerScroll.clientWidth);
374
- headerScroll.style.setProperty('--ui-ist-header-scroll-max', `${maxScroll}px`);
504
+ /**
505
+ * The third `checkLoadMore` trigger, and the only one that survives a page landing without a rendered-range
506
+ * change. CDK emits a range only when the virtual-for LENGTH changes, so a page whose row count exactly
507
+ * replaces the load-more skeletons (`data + skeletons` before, `data` after) moves nothing CDK watches —
508
+ * and when the new rows add no scrollable height, no scroll event can ever arrive either. Runs after the
509
+ * re-measure so the zone is tested against the size CDK just settled on.
510
+ */
511
+ settleLoadMore() {
512
+ this.rearmLoadMoreIfUnreachable();
513
+ this.checkLoadMore();
375
514
  }
376
- onRetry() {
515
+ emitRetry() {
377
516
  this.retry.emit();
378
517
  }
379
- /** Keep the header horizontally aligned with the body and update the sticky-shadow state. */
518
+ /**
519
+ * The index fallback is identity-in-name-only — it cannot tell rows apart after a reorder — which is why
520
+ * the automatic scroll reset cannot work without `trackBy` or `rowIdKey`.
521
+ */
522
+ rowIdentity(item, index) {
523
+ const explicitTrackBy = this.trackBy();
524
+ if (explicitTrackBy) {
525
+ return explicitTrackBy(index, item);
526
+ }
527
+ const key = this.rowIdKey();
528
+ return key ? this.asRowLike(item)[key] : index;
529
+ }
530
+ /**
531
+ * Assigning an absolute `scrollLeft` is drift-free by construction: it self-corrects every scroll event
532
+ * and the browser clamps when the header's maximum is momentarily smaller. A progress-normalized CSS
533
+ * scroll-timeline cannot guarantee that, which is why it was removed — do not reintroduce one.
534
+ */
380
535
  syncHorizontalScroll() {
381
536
  const viewport = this.viewport();
382
537
  if (!viewport) {
@@ -385,7 +540,8 @@ class InfiniteScrollTableComponent {
385
540
  const left = viewport.measureScrollOffset('left');
386
541
  const right = viewport.measureScrollOffset('right');
387
542
  const header = this.headerScroll();
388
- if (header && !this.usesScrollTimelineHeaderSync) {
543
+ if (header) {
544
+ // Rounded: columns are pixel-identical, so a sub-pixel offset misaligns every one of them.
389
545
  header.nativeElement.scrollLeft = Math.round(left);
390
546
  }
391
547
  this.scrollStart.set(left <= 1);
@@ -393,13 +549,62 @@ class InfiniteScrollTableComponent {
393
549
  this.horizontalScroll.set(left + right > 1);
394
550
  }
395
551
  /**
396
- * Emits `loadMore` once when the rendered range reaches the trigger zone near the end,
397
- * re-arming when the user scrolls back out. Mirrors the app's `ngx-infinite-scroll` semantics.
552
+ * How many rows the viewport shows. Zero until CDK has measured, which callers must treat as
553
+ * "unknown" rather than "none".
554
+ */
555
+ visibleRowCount() {
556
+ return Math.ceil((this.viewport()?.getViewportSize() ?? 0) / this.rowHeight());
557
+ }
558
+ /**
559
+ * Rows from the end that count as "approaching it". Capped at a screenful: a threshold wider than the
560
+ * viewport cannot mean approaching, and uncapped it made the zone unleavable at every scroll position,
561
+ * which is how an oversized `loadMoreThreshold` used to walk a whole dataset unattended. Clamped at 0 so
562
+ * a negative value can't push the trigger point past the end and disable paging outright.
398
563
  */
564
+ triggerZoneRows(visibleRows) {
565
+ const threshold = Math.max(0, this.loadMoreThreshold());
566
+ // Capped only once the viewport is measured — before that there is no screenful to compare against,
567
+ // and capping at 0 would silently disable paging on the first pass.
568
+ return visibleRows > 0 ? Math.min(threshold, visibleRows) : threshold;
569
+ }
570
+ /**
571
+ * The rendered range includes CDK's read-ahead buffer, so measuring it conflates the user reaching the
572
+ * end with CDK merely pre-rendering it. The visible end has no such slack, so it tracks the user's
573
+ * actual scroll position instead.
574
+ */
575
+ isInTriggerZone(dataLength, visibleRows) {
576
+ return this.firstVisibleIndex + visibleRows >= dataLength - this.triggerZoneRows(visibleRows);
577
+ }
578
+ /**
579
+ * Re-arms the latch when something other than scrolling has to do it. Two cases, both of which used to
580
+ * be terminal stalls:
581
+ *
582
+ * - **The requested page landed.** `data` grew past its length at the last emission, so that emission is
583
+ * spent. Without this, re-arming needed the user to scroll *out* of the zone and back — impossible when
584
+ * the page was smaller than the threshold, and impossible to even attempt when the new rows added no
585
+ * scrollable height.
586
+ * - **The zone can't be left.** `visibleEnd` bottoms out at `visibleRows`, so while that floor sits
587
+ * inside the zone no amount of scrolling re-arms anything. Also covers a not-yet-measured viewport,
588
+ * which must count as unleavable or the first fill never starts.
589
+ *
590
+ * Deliberately not driven by `loadingMore` or `error`: a failed fetch clears `loadingMore` without
591
+ * growing `data`, and must stay disarmed rather than re-fire in a loop.
592
+ */
593
+ rearmLoadMoreIfUnreachable() {
594
+ const dataLength = this.data().length;
595
+ const visibleRows = this.visibleRowCount();
596
+ if (dataLength > this.dataLengthAtEmit) {
597
+ this.isLoadMoreArmed = true;
598
+ return;
599
+ }
600
+ if (visibleRows === 0 || visibleRows >= dataLength - this.triggerZoneRows(visibleRows)) {
601
+ this.isLoadMoreArmed = true;
602
+ }
603
+ }
604
+ /** Emits at most once per page that lands while the user is inside the trigger zone. */
399
605
  checkLoadMore() {
400
606
  const viewport = this.viewport();
401
- // `error()` is part of the guard so a failed page pauses auto-loading until the consumer clears
402
- // the error (e.g. via the retry affordance), instead of immediately re-firing `loadMore`.
607
+ // `error()` pauses auto-loading until the consumer clears it, rather than instantly re-firing.
403
608
  if (!viewport || !this.hasMore() || this.loading() || this.loadingMore() || this.error()) {
404
609
  return;
405
610
  }
@@ -407,34 +612,34 @@ class InfiniteScrollTableComponent {
407
612
  if (dataLength === 0) {
408
613
  return;
409
614
  }
410
- const renderedEnd = viewport.getRenderedRange().end;
411
- // Clamp so a negative threshold can't push the trigger point past the end and silently disable
412
- // loadMore entirely.
413
- const threshold = Math.max(0, this.loadMoreThreshold());
414
- const inTriggerZone = renderedEnd >= dataLength - threshold;
415
- if (!inTriggerZone) {
416
- this.loadMoreArmed = true;
615
+ if (!this.isInTriggerZone(dataLength, this.visibleRowCount())) {
616
+ this.isLoadMoreArmed = true;
417
617
  return;
418
618
  }
419
- if (this.loadMoreArmed) {
420
- this.loadMoreArmed = false;
619
+ if (this.isLoadMoreArmed) {
620
+ this.isLoadMoreArmed = false;
621
+ this.dataLengthAtEmit = dataLength;
421
622
  this.loadMore.emit();
422
623
  }
423
624
  }
424
625
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InfiniteScrollTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
425
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InfiniteScrollTableComponent, isStandalone: true, selector: "ui-infinite-scroll-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: true, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, rowIdKey: { classPropertyName: "rowIdKey", publicName: "rowIdKey", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, isCondensed: { classPropertyName: "isCondensed", publicName: "isCondensed", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: false, transformFunction: null }, skeletonRowCount: { classPropertyName: "skeletonRowCount", publicName: "skeletonRowCount", isSignal: true, isRequired: false, transformFunction: null }, loadMoreThreshold: { classPropertyName: "loadMoreThreshold", publicName: "loadMoreThreshold", isSignal: true, isRequired: false, transformFunction: null }, noDataTemplate: { classPropertyName: "noDataTemplate", publicName: "noDataTemplate", isSignal: true, isRequired: false, transformFunction: null }, errorTemplate: { classPropertyName: "errorTemplate", publicName: "errorTemplate", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadMore: "loadMore", retry: "retry", rowClick: "rowClick", selectionChange: "selectionChange" }, viewQueries: [{ propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }, { propertyName: "headerScroll", first: true, predicate: ["headerScroll"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Header: a separate element above the viewport. It never scrolls vertically (stays pinned).\n The outer element reserves the body's vertical-scrollbar width as right padding, so the reserved\n strip keeps the header background and bottom border; the inner element carries the horizontal\n scrollLeft synced from the body so columns stay aligned. -->\n <div\n class=\"ui-ist__header\"\n [class.ui-ist__header--sticky]=\"stickyHeader()\"\n [style.padding-right.px]=\"scrollbarWidth()\"\n role=\"presentation\"\n >\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- Body: the CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky\n columns pin against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [style.height]=\"viewportHeight()\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-selected]=\"isRowSelected(row) ? true : null\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"onRowClick(row)\"\n (keydown.enter)=\"onRowActivate($event, row)\"\n (keydown.space)=\"onRowActivate($event, row)\"\n >\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"onSelectionToggle(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- Empty and error states render the consumer template in-frame below the header, mirroring how\n ui-table drops `noDataRowTpl` into the table body. Mid-scroll errors sit below the loaded rows. -->\n @if (showErrorFooter() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showErrorState() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showEmptyState() && noDataTemplate(); as emptyTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">{{ statusMessage() }}</span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:#fff}.ui-ist__header--sticky{position:sticky;top:0;z-index:3}.ui-ist__header-scroll{overflow:hidden}@supports (animation-timeline: scroll()){.ui-ist{timeline-scope:--ui-ist-header-x}.ui-ist__body{scroll-timeline:--ui-ist-header-x x}.ui-ist__row--header .ui-ist__cell--header:not(.ui-ist__cell--sticky-start):not(.ui-ist__cell--sticky-end){animation:ui-ist-header-follow linear both;animation-timeline:--ui-ist-header-x}}@keyframes ui-ist-header-follow{to{transform:translate(calc(-1 * var(--ui-ist-header-scroll-max, 0px)))}}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;min-width:160px;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:#fff;font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;left:0;z-index:2}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 56px;min-width:56px;justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: SkeletonComponent, selector: "ui-skeleton", inputs: ["count", "theme", "appearance", "isAiTheme", "applicationTheme"] }, { kind: "component", type: CheckboxComponent, selector: "ui-checkbox", inputs: ["disabled", "checked", "indeterminate", "companyColor", "name", "label", "multiple", "applicationTheme", "ariaLabel", "ariaRequired", "hasError", "hideBuiltInErrors", "hideLabelInErrors", "ariaLabelledby", "ariaDescribedby", "truncateText", "alignment", "tabIndex"], outputs: ["changed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
626
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InfiniteScrollTableComponent, isStandalone: true, selector: "ui-infinite-scroll-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: true, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, rowIdKey: { classPropertyName: "rowIdKey", publicName: "rowIdKey", isSignal: true, isRequired: false, transformFunction: null }, minBufferPx: { classPropertyName: "minBufferPx", publicName: "minBufferPx", isSignal: true, isRequired: false, transformFunction: null }, maxBufferPx: { classPropertyName: "maxBufferPx", publicName: "maxBufferPx", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, fillHeight: { classPropertyName: "fillHeight", publicName: "fillHeight", isSignal: true, isRequired: false, transformFunction: null }, isCondensed: { classPropertyName: "isCondensed", publicName: "isCondensed", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, rowsClickable: { classPropertyName: "rowsClickable", publicName: "rowsClickable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: true, transformFunction: null }, skeletonRowCount: { classPropertyName: "skeletonRowCount", publicName: "skeletonRowCount", isSignal: true, isRequired: false, transformFunction: null }, loadMoreSkeletonCount: { classPropertyName: "loadMoreSkeletonCount", publicName: "loadMoreSkeletonCount", isSignal: true, isRequired: false, transformFunction: null }, loadingColumnCount: { classPropertyName: "loadingColumnCount", publicName: "loadingColumnCount", isSignal: true, isRequired: false, transformFunction: null }, loadMoreThreshold: { classPropertyName: "loadMoreThreshold", publicName: "loadMoreThreshold", isSignal: true, isRequired: false, transformFunction: null }, noDataTemplate: { classPropertyName: "noDataTemplate", publicName: "noDataTemplate", isSignal: true, isRequired: false, transformFunction: null }, errorTemplate: { classPropertyName: "errorTemplate", publicName: "errorTemplate", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, endOfListTemplate: { classPropertyName: "endOfListTemplate", publicName: "endOfListTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadMore: "loadMore", retry: "retry", rowClick: "rowClick", selectionChange: "selectionChange" }, host: { properties: { "class.ui-ist--fill-host": "fillHeight()" } }, viewQueries: [{ propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }, { propertyName: "headerScroll", first: true, predicate: ["headerScroll"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- role=\"table\", not \"grid\": row access is Tab-only with no arrow-key cell traversal, so \"grid\" would\n promise 2D keyboard navigation this does not implement. Selection is conveyed by the row's checkbox,\n not `aria-selected` \u2014 which is valid only on grid/treegrid anyway. -->\n<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [class.ui-ist--fill-height]=\"fillHeight()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Pinned header, outside the viewport so it never scrolls vertically. The outer element reserves the\n body's scrollbar width as padding (keeping the background and border across that strip); the inner\n one carries the horizontal scrollLeft synced from the body. -->\n <div class=\"ui-ist__header\" [style.padding-right.px]=\"scrollbarWidth()\" role=\"presentation\">\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- The CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky columns pin\n against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [minBufferPx]=\"effectiveMinBufferPx()\"\n [maxBufferPx]=\"effectiveMaxBufferPx()\"\n [style.height]=\"viewportHeight()\"\n [style.max-height]=\"fillHeight() ? maxHeight() : null\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"rowsClickable() && row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"rowsClickable() && row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"emitRowClick($event, row)\"\n (keydown.enter)=\"activateRow($event, row)\"\n (keydown.space)=\"activateRow($event, row)\"\n >\n @if (selectable()) {\n <!-- This `stopPropagation` is what actually stops a checkbox click activating the row: it runs\n before the click reaches the row's listener, so `isInteractiveTarget` is never consulted on\n this path. That guard is a real second line of defence (`ui-checkbox` renders a focusable\n wrapper and a native input, both matching INTERACTIVE_TARGET_SELECTOR), not a substitute. -->\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"emitSelectionChange(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- `showErrorFooter()` and `showErrorState()` differ only on `hasData()`, so they are mutually\n exclusive and safe to merge here. Both fall back to a plain-language message, so a failed fetch is\n never a silent blank box. -->\n @if (showErrorFooter() || showErrorState()) {\n <div class=\"ui-ist__state\" role=\"alert\">\n @if (errorTemplate(); as errorTpl) {\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n } @else {\n <span>{{ translationContext + 'LOAD_ERROR' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n @if (showEmptyState()) {\n <div class=\"ui-ist__state\">\n @if (noDataTemplate(); as emptyTpl) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n <span>{{ translationContext + 'NO_ROWS' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n <!-- Opt-in, with no fallback (see `endOfListTemplate`). Not role=\"alert\": reaching the end is not an\n alert, and `aria-rowcount` already conveys completeness to assistive tech. -->\n @if (showEndOfList() && endOfListTemplate(); as endTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"endTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">\n @if (statusMessageKey(); as statusKey) {\n {{ statusKey | uiTranslate | async }}\n }\n </span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{--ui-ist-checkbox-width: 56px;--ui-ist-skeleton-bg: #f4f4f4;display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__header-scroll{overflow:hidden}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable{cursor:pointer}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:var(--ui-ist-header-bg, #ffffff);font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;z-index:2}.ui-ist__cell--checkbox.ui-ist__cell--sticky-start{left:0}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 var(--ui-ist-checkbox-width);min-width:var(--ui-ist-checkbox-width);justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__row--header .ui-ist__cell--spacer{background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start-edge{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}:host(.ui-ist--fill-host){height:100%}.ui-ist--fill-height{height:100%}.ui-ist--fill-height .ui-ist__body{min-height:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: SkeletonComponent, selector: "ui-skeleton", inputs: ["count", "theme", "appearance", "isAiTheme", "applicationTheme"] }, { kind: "component", type: CheckboxComponent, selector: "ui-checkbox", inputs: ["disabled", "checked", "indeterminate", "companyColor", "name", "label", "multiple", "applicationTheme", "ariaLabel", "ariaRequired", "hasError", "hideBuiltInErrors", "hideLabelInErrors", "ariaLabelledby", "ariaDescribedby", "truncateText", "alignment", "tabIndex"], outputs: ["changed"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: UiTranslatePipe, name: "uiTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
426
627
  }
427
628
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InfiniteScrollTableComponent, decorators: [{
428
629
  type: Component,
429
- args: [{ selector: 'ui-infinite-scroll-table', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
630
+ args: [{ selector: 'ui-infinite-scroll-table', changeDetection: ChangeDetectionStrategy.OnPush, host: {
631
+ '[class.ui-ist--fill-host]': 'fillHeight()',
632
+ }, imports: [
633
+ AsyncPipe,
430
634
  NgTemplateOutlet,
431
635
  CdkVirtualScrollViewport,
432
636
  CdkFixedSizeVirtualScroll,
433
637
  CdkVirtualForOf,
434
638
  SkeletonComponent,
435
639
  CheckboxComponent,
436
- ], template: "<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Header: a separate element above the viewport. It never scrolls vertically (stays pinned).\n The outer element reserves the body's vertical-scrollbar width as right padding, so the reserved\n strip keeps the header background and bottom border; the inner element carries the horizontal\n scrollLeft synced from the body so columns stay aligned. -->\n <div\n class=\"ui-ist__header\"\n [class.ui-ist__header--sticky]=\"stickyHeader()\"\n [style.padding-right.px]=\"scrollbarWidth()\"\n role=\"presentation\"\n >\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- Body: the CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky\n columns pin against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [style.height]=\"viewportHeight()\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-selected]=\"isRowSelected(row) ? true : null\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"onRowClick(row)\"\n (keydown.enter)=\"onRowActivate($event, row)\"\n (keydown.space)=\"onRowActivate($event, row)\"\n >\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"onSelectionToggle(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- Empty and error states render the consumer template in-frame below the header, mirroring how\n ui-table drops `noDataRowTpl` into the table body. Mid-scroll errors sit below the loaded rows. -->\n @if (showErrorFooter() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showErrorState() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showEmptyState() && noDataTemplate(); as emptyTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">{{ statusMessage() }}</span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:#fff}.ui-ist__header--sticky{position:sticky;top:0;z-index:3}.ui-ist__header-scroll{overflow:hidden}@supports (animation-timeline: scroll()){.ui-ist{timeline-scope:--ui-ist-header-x}.ui-ist__body{scroll-timeline:--ui-ist-header-x x}.ui-ist__row--header .ui-ist__cell--header:not(.ui-ist__cell--sticky-start):not(.ui-ist__cell--sticky-end){animation:ui-ist-header-follow linear both;animation-timeline:--ui-ist-header-x}}@keyframes ui-ist-header-follow{to{transform:translate(calc(-1 * var(--ui-ist-header-scroll-max, 0px)))}}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;min-width:160px;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:#fff;font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;left:0;z-index:2}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 56px;min-width:56px;justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
437
- }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: true }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], rowIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowIdKey", required: false }] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], isCondensed: [{ type: i0.Input, args: [{ isSignal: true, alias: "isCondensed", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMore", required: false }] }], hasMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasMore", required: false }] }], skeletonRowCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonRowCount", required: false }] }], loadMoreThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMoreThreshold", required: false }] }], noDataTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDataTemplate", required: false }] }], errorTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTemplate", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], retry: [{ type: i0.Output, args: ["retry"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], viewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], headerScroll: [{ type: i0.ViewChild, args: ['headerScroll', { isSignal: true }] }] } });
640
+ UiTranslatePipe,
641
+ ], template: "<!-- role=\"table\", not \"grid\": row access is Tab-only with no arrow-key cell traversal, so \"grid\" would\n promise 2D keyboard navigation this does not implement. Selection is conveyed by the row's checkbox,\n not `aria-selected` \u2014 which is valid only on grid/treegrid anyway. -->\n<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [class.ui-ist--fill-height]=\"fillHeight()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Pinned header, outside the viewport so it never scrolls vertically. The outer element reserves the\n body's scrollbar width as padding (keeping the background and border across that strip); the inner\n one carries the horizontal scrollLeft synced from the body. -->\n <div class=\"ui-ist__header\" [style.padding-right.px]=\"scrollbarWidth()\" role=\"presentation\">\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- The CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky columns pin\n against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [minBufferPx]=\"effectiveMinBufferPx()\"\n [maxBufferPx]=\"effectiveMaxBufferPx()\"\n [style.height]=\"viewportHeight()\"\n [style.max-height]=\"fillHeight() ? maxHeight() : null\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"rowsClickable() && row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"rowsClickable() && row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"emitRowClick($event, row)\"\n (keydown.enter)=\"activateRow($event, row)\"\n (keydown.space)=\"activateRow($event, row)\"\n >\n @if (selectable()) {\n <!-- This `stopPropagation` is what actually stops a checkbox click activating the row: it runs\n before the click reaches the row's listener, so `isInteractiveTarget` is never consulted on\n this path. That guard is a real second line of defence (`ui-checkbox` renders a focusable\n wrapper and a native input, both matching INTERACTIVE_TARGET_SELECTOR), not a substitute. -->\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"emitSelectionChange(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- `showErrorFooter()` and `showErrorState()` differ only on `hasData()`, so they are mutually\n exclusive and safe to merge here. Both fall back to a plain-language message, so a failed fetch is\n never a silent blank box. -->\n @if (showErrorFooter() || showErrorState()) {\n <div class=\"ui-ist__state\" role=\"alert\">\n @if (errorTemplate(); as errorTpl) {\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n } @else {\n <span>{{ translationContext + 'LOAD_ERROR' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n @if (showEmptyState()) {\n <div class=\"ui-ist__state\">\n @if (noDataTemplate(); as emptyTpl) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n <span>{{ translationContext + 'NO_ROWS' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n <!-- Opt-in, with no fallback (see `endOfListTemplate`). Not role=\"alert\": reaching the end is not an\n alert, and `aria-rowcount` already conveys completeness to assistive tech. -->\n @if (showEndOfList() && endOfListTemplate(); as endTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"endTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">\n @if (statusMessageKey(); as statusKey) {\n {{ statusKey | uiTranslate | async }}\n }\n </span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{--ui-ist-checkbox-width: 56px;--ui-ist-skeleton-bg: #f4f4f4;display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__header-scroll{overflow:hidden}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable{cursor:pointer}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:var(--ui-ist-header-bg, #ffffff);font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;z-index:2}.ui-ist__cell--checkbox.ui-ist__cell--sticky-start{left:0}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 var(--ui-ist-checkbox-width);min-width:var(--ui-ist-checkbox-width);justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__row--header .ui-ist__cell--spacer{background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start-edge{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}:host(.ui-ist--fill-host){height:100%}.ui-ist--fill-height{height:100%}.ui-ist--fill-height .ui-ist__body{min-height:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
642
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: true }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], rowIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowIdKey", required: false }] }], minBufferPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "minBufferPx", required: false }] }], maxBufferPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxBufferPx", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], fillHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillHeight", required: false }] }], isCondensed: [{ type: i0.Input, args: [{ isSignal: true, alias: "isCondensed", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], rowsClickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsClickable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMore", required: false }] }], hasMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasMore", required: true }] }], skeletonRowCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonRowCount", required: false }] }], loadMoreSkeletonCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMoreSkeletonCount", required: false }] }], loadingColumnCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingColumnCount", required: false }] }], loadMoreThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMoreThreshold", required: false }] }], noDataTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDataTemplate", required: false }] }], errorTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTemplate", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], endOfListTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "endOfListTemplate", required: false }] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], retry: [{ type: i0.Output, args: ["retry"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], viewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], headerScroll: [{ type: i0.ViewChild, args: ['headerScroll', { isSignal: true }] }] } });
438
643
 
439
644
  class InfiniteScrollTableComponentModule {
440
645
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InfiniteScrollTableComponentModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }