ngx-com 0.1.2 → 0.1.4

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { viewChild, input, output, computed, ChangeDetectionStrategy, Component, inject, DestroyRef, signal, TemplateRef, Directive, ElementRef, ViewContainerRef, contentChild, model, linkedSignal, forwardRef } from '@angular/core';
2
+ import { viewChild, input, output, computed, ChangeDetectionStrategy, Component, inject, DestroyRef, signal, TemplateRef, Directive, ElementRef, ViewContainerRef, contentChild, model, linkedSignal, effect, forwardRef } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { NgTemplateOutlet } from '@angular/common';
5
5
  import { NgForm, FormGroupDirective, NgControl } from '@angular/forms';
@@ -10,9 +10,10 @@ import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf }
10
10
  import { TemplatePortal } from '@angular/cdk/portal';
11
11
  import { LiveAnnouncer } from '@angular/cdk/a11y';
12
12
  import { cva } from 'class-variance-authority';
13
- import { mergeClasses } from 'ngx-com/utils';
13
+ import { mergeClasses, throttle } from 'ngx-com/utils';
14
14
  import { Subject } from 'rxjs';
15
15
  import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
16
+ import { ComSpinner } from 'ngx-com/components/spinner';
16
17
 
17
18
  /**
18
19
  * CVA variants for the dropdown trigger button.
@@ -437,6 +438,28 @@ const dropdownChevronVariants = cva([
437
438
  open: false,
438
439
  },
439
440
  });
441
+ /**
442
+ * CVA variants for the loading indicator container.
443
+ *
444
+ * @tokens `--color-muted-foreground`
445
+ */
446
+ const dropdownLoadingVariants = cva([
447
+ 'flex',
448
+ 'items-center',
449
+ 'justify-center',
450
+ 'text-muted-foreground',
451
+ ], {
452
+ variants: {
453
+ size: {
454
+ sm: ['px-2', 'py-3', 'text-xs'],
455
+ default: ['px-3', 'py-3', 'text-sm'],
456
+ lg: ['px-4', 'py-4', 'text-base'],
457
+ },
458
+ },
459
+ defaultVariants: {
460
+ size: 'default',
461
+ },
462
+ });
440
463
 
441
464
  /**
442
465
  * A single option in the dropdown list.
@@ -1282,6 +1305,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1282
1305
  }]
1283
1306
  }] });
1284
1307
 
1308
+ /**
1309
+ * Directive to mark a template as the custom loading indicator template.
1310
+ *
1311
+ * @tokens none
1312
+ *
1313
+ * @example
1314
+ * ```html
1315
+ * <com-dropdown [options]="users()" [loading]="isLoading()">
1316
+ * <ng-template comDropdownLoading>
1317
+ * <div class="flex items-center gap-2 p-3">
1318
+ * <com-spinner size="xs" color="primary" />
1319
+ * <span class="text-sm text-muted-foreground">Fetching more results...</span>
1320
+ * </div>
1321
+ * </ng-template>
1322
+ * </com-dropdown>
1323
+ * ```
1324
+ */
1325
+ class ComDropdownLoadingTpl {
1326
+ /** Reference to the template. */
1327
+ templateRef = inject(TemplateRef);
1328
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownLoadingTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1329
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownLoadingTpl, isStandalone: true, selector: "ng-template[comDropdownLoading]", ngImport: i0 });
1330
+ }
1331
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownLoadingTpl, decorators: [{
1332
+ type: Directive,
1333
+ args: [{
1334
+ selector: 'ng-template[comDropdownLoading]',
1335
+ }]
1336
+ }] });
1337
+
1285
1338
  /**
1286
1339
  * Default compare function for primitive values.
1287
1340
  * @param a First value.
@@ -1392,6 +1445,8 @@ class ComDropdown {
1392
1445
  groupTemplate = contentChild(ComDropdownGroupTpl, ...(ngDevMode ? [{ debugName: "groupTemplate" }] : []));
1393
1446
  /** Content query for custom tag template. */
1394
1447
  tagTemplate = contentChild(ComDropdownTagTpl, ...(ngDevMode ? [{ debugName: "tagTemplate" }] : []));
1448
+ /** Content query for custom loading template. */
1449
+ loadingTemplate = contentChild(ComDropdownLoadingTpl, ...(ngDevMode ? [{ debugName: "loadingTemplate" }] : []));
1395
1450
  /** Overlay reference. */
1396
1451
  overlayRef = null;
1397
1452
  /** Unique ID for the dropdown. */
@@ -1447,6 +1502,8 @@ class ComDropdown {
1447
1502
  maxVisibleTags = input(2, ...(ngDevMode ? [{ debugName: "maxVisibleTags" }] : []));
1448
1503
  /** Custom error state matcher for determining when to show errors. */
1449
1504
  errorStateMatcher = input(...(ngDevMode ? [undefined, { debugName: "errorStateMatcher" }] : []));
1505
+ /** Whether the dropdown is currently loading data. */
1506
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
1450
1507
  // Signal Forms inputs — set automatically by [formField] via setInputOnDirectives
1451
1508
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : []));
1452
1509
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : []));
@@ -1458,6 +1515,8 @@ class ComDropdown {
1458
1515
  opened = output();
1459
1516
  /** Emitted when panel closes. */
1460
1517
  closed = output();
1518
+ /** Emitted when user scrolls near the bottom of the option list. */
1519
+ loadMore = output();
1461
1520
  // ============ INTERNAL STATE ============
1462
1521
  /** Whether the panel is open. */
1463
1522
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
@@ -1471,6 +1530,12 @@ class ComDropdown {
1471
1530
  internalValue = linkedSignal(() => this.value() ?? null, ...(ngDevMode ? [{ debugName: "internalValue" }] : []));
1472
1531
  /** Live announcements for screen readers. */
1473
1532
  liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : []));
1533
+ /** Flag to prevent duplicate loadMore emissions per scroll-to-bottom. */
1534
+ loadMoreEmitted = false;
1535
+ /** Subscription for virtual scroll elementScrolled. */
1536
+ scrollSubscription = null;
1537
+ /** Throttled handler for standard scroll events. */
1538
+ throttledScrollHandler = null;
1474
1539
  /** IDs for aria-describedby (set by form-field). */
1475
1540
  _describedByIds = signal('', ...(ngDevMode ? [{ debugName: "_describedByIds" }] : []));
1476
1541
  /** Form field appearance (set by form-field). */
@@ -1655,6 +1720,10 @@ class ComDropdown {
1655
1720
  overflowBadgeClasses = computed(() => {
1656
1721
  return dropdownOverflowBadgeVariants({ size: this.size() });
1657
1722
  }, ...(ngDevMode ? [{ debugName: "overflowBadgeClasses" }] : []));
1723
+ /** Computed loading container classes. */
1724
+ loadingContainerClasses = computed(() => {
1725
+ return dropdownLoadingVariants({ size: this.size() });
1726
+ }, ...(ngDevMode ? [{ debugName: "loadingContainerClasses" }] : []));
1658
1727
  // ============ CVA CALLBACKS ============
1659
1728
  onChange = () => { };
1660
1729
  onTouched = () => { };
@@ -1662,6 +1731,11 @@ class ComDropdown {
1662
1731
  if (this.ngControl) {
1663
1732
  this.ngControl.valueAccessor = this;
1664
1733
  }
1734
+ // Reset loadMoreEmitted when options change (new data arrived)
1735
+ effect(() => {
1736
+ this.options(); // track
1737
+ this.loadMoreEmitted = false;
1738
+ });
1665
1739
  }
1666
1740
  // ============ CVA IMPLEMENTATION ============
1667
1741
  writeValue(value) {
@@ -1727,12 +1801,15 @@ class ComDropdown {
1727
1801
  }
1728
1802
  // Announce opening
1729
1803
  this.announce(`${this.placeholder()} dropdown opened, ${this.filteredOptions().length} options available`);
1804
+ // Set up scroll detection for loadMore
1805
+ this.setupScrollDetection();
1730
1806
  }
1731
1807
  /** Closes the dropdown panel. */
1732
1808
  close() {
1733
1809
  if (!this.isOpen()) {
1734
1810
  return;
1735
1811
  }
1812
+ this.cleanupScrollDetection();
1736
1813
  this.destroyOverlay();
1737
1814
  this.isOpen.set(false);
1738
1815
  this.searchQuery.set('');
@@ -2102,8 +2179,67 @@ class ComDropdown {
2102
2179
  this.liveAnnouncement.set(message);
2103
2180
  this.liveAnnouncer.announce(message, 'polite');
2104
2181
  }
2182
+ /** Sets up scroll detection for the loadMore output. SSR-safe: only called from user interaction. */
2183
+ setupScrollDetection() {
2184
+ this.loadMoreEmitted = false;
2185
+ this.cleanupScrollDetection();
2186
+ const viewport = this.virtualViewport();
2187
+ if (viewport) {
2188
+ // Virtual scroll branch — use viewport's elementScrolled observable
2189
+ this.scrollSubscription = viewport.elementScrolled()
2190
+ .pipe(takeUntilDestroyed(this.destroyRef))
2191
+ .subscribe(() => {
2192
+ if (this.loading() || this.loadMoreEmitted)
2193
+ return;
2194
+ const offset = viewport.measureScrollOffset('bottom');
2195
+ if (offset < 50) {
2196
+ this.loadMoreEmitted = true;
2197
+ this.loadMore.emit();
2198
+ }
2199
+ else {
2200
+ // Reset flag when user scrolls back up
2201
+ this.loadMoreEmitted = false;
2202
+ }
2203
+ });
2204
+ }
2205
+ else {
2206
+ // Standard scroll branch — throttled scroll event on the scroll container
2207
+ const panelEl = this.overlayRef?.overlayElement;
2208
+ const scrollContainer = panelEl?.querySelector('[data-scroll-container]');
2209
+ if (!scrollContainer)
2210
+ return;
2211
+ this.throttledScrollHandler = throttle(() => {
2212
+ if (this.loading() || this.loadMoreEmitted)
2213
+ return;
2214
+ const { scrollTop, clientHeight, scrollHeight } = scrollContainer;
2215
+ if (scrollTop + clientHeight >= scrollHeight - 50) {
2216
+ this.loadMoreEmitted = true;
2217
+ this.loadMore.emit();
2218
+ }
2219
+ else {
2220
+ // Reset flag when user scrolls back up
2221
+ this.loadMoreEmitted = false;
2222
+ }
2223
+ }, 100);
2224
+ scrollContainer.addEventListener('scroll', this.throttledScrollHandler, { passive: true });
2225
+ }
2226
+ }
2227
+ /** Cleans up scroll detection listeners. */
2228
+ cleanupScrollDetection() {
2229
+ if (this.scrollSubscription) {
2230
+ this.scrollSubscription.unsubscribe();
2231
+ this.scrollSubscription = null;
2232
+ }
2233
+ if (this.throttledScrollHandler) {
2234
+ const panelEl = this.overlayRef?.overlayElement;
2235
+ const scrollContainer = panelEl?.querySelector('[data-scroll-container]');
2236
+ scrollContainer?.removeEventListener('scroll', this.throttledScrollHandler);
2237
+ this.throttledScrollHandler.cancel();
2238
+ this.throttledScrollHandler = null;
2239
+ }
2240
+ }
2105
2241
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
2106
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdown, isStandalone: true, selector: "com-dropdown", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, searchDebounceMs: { classPropertyName: "searchDebounceMs", publicName: "searchDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollThreshold: { classPropertyName: "virtualScrollThreshold", publicName: "virtualScrollThreshold", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollItemSize: { classPropertyName: "virtualScrollItemSize", publicName: "virtualScrollItemSize", isSignal: true, isRequired: false, transformFunction: null }, maxVisibleTags: { classPropertyName: "maxVisibleTags", publicName: "maxVisibleTags", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, sfErrors: { classPropertyName: "sfErrors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", touched: "touchedChange", searchChange: "searchChange", opened: "opened", closed: "closed" }, host: { properties: { "class.com-dropdown-disabled": "disabled()", "class.com-dropdown-open": "isOpen()" }, classAttribute: "com-dropdown-host inline-block" }, providers: [{ provide: FormFieldControl, useExisting: forwardRef(() => ComDropdown) }], queries: [{ propertyName: "optionTemplate", first: true, predicate: ComDropdownOptionTpl, descendants: true, isSignal: true }, { propertyName: "selectedTemplate", first: true, predicate: ComDropdownSelectedTpl, descendants: true, isSignal: true }, { propertyName: "emptyTemplate", first: true, predicate: ComDropdownEmptyTpl, descendants: true, isSignal: true }, { propertyName: "groupTemplate", first: true, predicate: ComDropdownGroupTpl, descendants: true, isSignal: true }, { propertyName: "tagTemplate", first: true, predicate: ComDropdownTagTpl, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["triggerElement"], descendants: true, isSignal: true }, { propertyName: "panelTemplateRef", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }, { propertyName: "virtualViewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }], exportAs: ["comDropdown"], ngImport: i0, template: `
2242
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdown, isStandalone: true, selector: "com-dropdown", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, searchDebounceMs: { classPropertyName: "searchDebounceMs", publicName: "searchDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollThreshold: { classPropertyName: "virtualScrollThreshold", publicName: "virtualScrollThreshold", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollItemSize: { classPropertyName: "virtualScrollItemSize", publicName: "virtualScrollItemSize", isSignal: true, isRequired: false, transformFunction: null }, maxVisibleTags: { classPropertyName: "maxVisibleTags", publicName: "maxVisibleTags", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, sfErrors: { classPropertyName: "sfErrors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", touched: "touchedChange", searchChange: "searchChange", opened: "opened", closed: "closed", loadMore: "loadMore" }, host: { properties: { "class.com-dropdown-disabled": "disabled()", "class.com-dropdown-open": "isOpen()" }, classAttribute: "com-dropdown-host inline-block" }, providers: [{ provide: FormFieldControl, useExisting: forwardRef(() => ComDropdown) }], queries: [{ propertyName: "optionTemplate", first: true, predicate: ComDropdownOptionTpl, descendants: true, isSignal: true }, { propertyName: "selectedTemplate", first: true, predicate: ComDropdownSelectedTpl, descendants: true, isSignal: true }, { propertyName: "emptyTemplate", first: true, predicate: ComDropdownEmptyTpl, descendants: true, isSignal: true }, { propertyName: "groupTemplate", first: true, predicate: ComDropdownGroupTpl, descendants: true, isSignal: true }, { propertyName: "tagTemplate", first: true, predicate: ComDropdownTagTpl, descendants: true, isSignal: true }, { propertyName: "loadingTemplate", first: true, predicate: ComDropdownLoadingTpl, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["triggerElement"], descendants: true, isSignal: true }, { propertyName: "panelTemplateRef", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }, { propertyName: "virtualViewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }], exportAs: ["comDropdown"], ngImport: i0, template: `
2107
2243
  <!-- Trigger button -->
2108
2244
  <button
2109
2245
  #triggerElement
@@ -2251,11 +2387,23 @@ class ComDropdown {
2251
2387
  (hover)="onOptionHover(option.id)"
2252
2388
  />
2253
2389
  </cdk-virtual-scroll-viewport>
2390
+
2391
+ <!-- Loading indicator (virtual scroll) -->
2392
+ @if (loading()) {
2393
+ @if (loadingTemplate()) {
2394
+ <ng-container [ngTemplateOutlet]="loadingTemplate()!.templateRef" />
2395
+ } @else {
2396
+ <div [class]="loadingContainerClasses()">
2397
+ <com-spinner size="sm" color="muted" />
2398
+ </div>
2399
+ }
2400
+ }
2254
2401
  } @else {
2255
2402
  <!-- Standard rendering for small lists and grouped options -->
2256
2403
  <div
2257
2404
  class="overflow-auto"
2258
2405
  [style.maxHeight]="maxHeight()"
2406
+ data-scroll-container
2259
2407
  >
2260
2408
  @if (groupedOptions().length > 0) {
2261
2409
  @for (group of groupedOptions(); track group.key) {
@@ -2301,8 +2449,8 @@ class ComDropdown {
2301
2449
  (hover)="onOptionHover(option.id)"
2302
2450
  />
2303
2451
  }
2304
- } @else {
2305
- <!-- Empty state -->
2452
+ } @else if (!loading()) {
2453
+ <!-- Empty state (suppressed when loading) -->
2306
2454
  @if (emptyTemplate()) {
2307
2455
  <ng-container
2308
2456
  [ngTemplateOutlet]="emptyTemplate()!.templateRef"
@@ -2318,6 +2466,17 @@ class ComDropdown {
2318
2466
  </div>
2319
2467
  }
2320
2468
  }
2469
+
2470
+ <!-- Loading indicator (standard) -->
2471
+ @if (loading()) {
2472
+ @if (loadingTemplate()) {
2473
+ <ng-container [ngTemplateOutlet]="loadingTemplate()!.templateRef" />
2474
+ } @else {
2475
+ <div [class]="loadingContainerClasses()">
2476
+ <com-spinner size="sm" color="muted" />
2477
+ </div>
2478
+ }
2479
+ }
2321
2480
  </div>
2322
2481
  }
2323
2482
  </div>
@@ -2327,7 +2486,7 @@ class ComDropdown {
2327
2486
  <div class="sr-only" aria-live="polite" aria-atomic="true">
2328
2487
  {{ liveAnnouncement() }}
2329
2488
  </div>
2330
- `, isInline: true, styles: [".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: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.ɵɵCdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1.ɵɵCdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1.ɵɵCdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "component", type: ComDropdownOption, selector: "com-dropdown-option", inputs: ["value", "displayText", "id", "index", "selected", "active", "disabled", "size", "optionTemplate", "class"], outputs: ["select", "hover"], exportAs: ["comDropdownOption"] }, { kind: "component", type: ComDropdownSearch, selector: "com-dropdown-search", inputs: ["placeholder", "ariaLabel", "disabled", "debounceMs", "size", "class"], outputs: ["searchChange", "keyNav"], exportAs: ["comDropdownSearch"] }, { kind: "component", type: ComDropdownTag, selector: "com-dropdown-tag", inputs: ["value", "displayText", "index", "disabled", "size", "variant", "class", "tagTemplate"], outputs: ["remove"], exportAs: ["comDropdownTag"] }, { kind: "component", type: ComDropdownGroup, selector: "com-dropdown-group", inputs: ["label", "count", "expanded", "showCount", "size", "class", "groupTemplate"], exportAs: ["comDropdownGroup"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2489
+ `, isInline: true, styles: [".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: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.ɵɵCdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1.ɵɵCdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1.ɵɵCdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "component", type: ComDropdownOption, selector: "com-dropdown-option", inputs: ["value", "displayText", "id", "index", "selected", "active", "disabled", "size", "optionTemplate", "class"], outputs: ["select", "hover"], exportAs: ["comDropdownOption"] }, { kind: "component", type: ComDropdownSearch, selector: "com-dropdown-search", inputs: ["placeholder", "ariaLabel", "disabled", "debounceMs", "size", "class"], outputs: ["searchChange", "keyNav"], exportAs: ["comDropdownSearch"] }, { kind: "component", type: ComDropdownTag, selector: "com-dropdown-tag", inputs: ["value", "displayText", "index", "disabled", "size", "variant", "class", "tagTemplate"], outputs: ["remove"], exportAs: ["comDropdownTag"] }, { kind: "component", type: ComDropdownGroup, selector: "com-dropdown-group", inputs: ["label", "count", "expanded", "showCount", "size", "class", "groupTemplate"], exportAs: ["comDropdownGroup"] }, { kind: "component", type: ComSpinner, selector: "com-spinner", inputs: ["label", "labelPosition", "size", "color"], exportAs: ["comSpinner"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2331
2490
  }
2332
2491
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdown, decorators: [{
2333
2492
  type: Component,
@@ -2479,11 +2638,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
2479
2638
  (hover)="onOptionHover(option.id)"
2480
2639
  />
2481
2640
  </cdk-virtual-scroll-viewport>
2641
+
2642
+ <!-- Loading indicator (virtual scroll) -->
2643
+ @if (loading()) {
2644
+ @if (loadingTemplate()) {
2645
+ <ng-container [ngTemplateOutlet]="loadingTemplate()!.templateRef" />
2646
+ } @else {
2647
+ <div [class]="loadingContainerClasses()">
2648
+ <com-spinner size="sm" color="muted" />
2649
+ </div>
2650
+ }
2651
+ }
2482
2652
  } @else {
2483
2653
  <!-- Standard rendering for small lists and grouped options -->
2484
2654
  <div
2485
2655
  class="overflow-auto"
2486
2656
  [style.maxHeight]="maxHeight()"
2657
+ data-scroll-container
2487
2658
  >
2488
2659
  @if (groupedOptions().length > 0) {
2489
2660
  @for (group of groupedOptions(); track group.key) {
@@ -2529,8 +2700,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
2529
2700
  (hover)="onOptionHover(option.id)"
2530
2701
  />
2531
2702
  }
2532
- } @else {
2533
- <!-- Empty state -->
2703
+ } @else if (!loading()) {
2704
+ <!-- Empty state (suppressed when loading) -->
2534
2705
  @if (emptyTemplate()) {
2535
2706
  <ng-container
2536
2707
  [ngTemplateOutlet]="emptyTemplate()!.templateRef"
@@ -2546,6 +2717,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
2546
2717
  </div>
2547
2718
  }
2548
2719
  }
2720
+
2721
+ <!-- Loading indicator (standard) -->
2722
+ @if (loading()) {
2723
+ @if (loadingTemplate()) {
2724
+ <ng-container [ngTemplateOutlet]="loadingTemplate()!.templateRef" />
2725
+ } @else {
2726
+ <div [class]="loadingContainerClasses()">
2727
+ <com-spinner size="sm" color="muted" />
2728
+ </div>
2729
+ }
2730
+ }
2549
2731
  </div>
2550
2732
  }
2551
2733
  </div>
@@ -2565,12 +2747,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
2565
2747
  ComDropdownSearch,
2566
2748
  ComDropdownTag,
2567
2749
  ComDropdownGroup,
2750
+ ComSpinner,
2568
2751
  ], providers: [{ provide: FormFieldControl, useExisting: forwardRef(() => ComDropdown) }], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2569
2752
  class: 'com-dropdown-host inline-block',
2570
2753
  '[class.com-dropdown-disabled]': 'disabled()',
2571
2754
  '[class.com-dropdown-open]': 'isOpen()',
2572
2755
  }, styles: [".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"] }]
2573
- }], ctorParameters: () => [], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['triggerElement', { isSignal: true }] }], panelTemplateRef: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], virtualViewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], optionTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownOptionTpl), { isSignal: true }] }], selectedTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownSelectedTpl), { isSignal: true }] }], emptyTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownEmptyTpl), { isSignal: true }] }], groupTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownGroupTpl), { isSignal: true }] }], tagTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownTagTpl), { isSignal: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], filterWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterWith", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], searchDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchDebounceMs", required: false }] }], virtualScrollThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollThreshold", required: false }] }], virtualScrollItemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollItemSize", required: false }] }], maxVisibleTags: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisibleTags", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], sfErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
2756
+ }], ctorParameters: () => [], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['triggerElement', { isSignal: true }] }], panelTemplateRef: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], virtualViewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], optionTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownOptionTpl), { isSignal: true }] }], selectedTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownSelectedTpl), { isSignal: true }] }], emptyTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownEmptyTpl), { isSignal: true }] }], groupTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownGroupTpl), { isSignal: true }] }], tagTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownTagTpl), { isSignal: true }] }], loadingTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownLoadingTpl), { isSignal: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], filterWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterWith", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], searchDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchDebounceMs", required: false }] }], virtualScrollThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollThreshold", required: false }] }], virtualScrollItemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollItemSize", required: false }] }], maxVisibleTags: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisibleTags", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], sfErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }] } });
2574
2757
 
2575
2758
  /**
2576
2759
  * The overlay panel containing the dropdown options.
@@ -2801,5 +2984,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
2801
2984
  * Generated bundle index. Do not edit.
2802
2985
  */
2803
2986
 
2804
- export { ComDropdown, ComDropdownEmptyTpl, ComDropdownGroup, ComDropdownGroupTpl, ComDropdownOption, ComDropdownOptionTpl, ComDropdownPanel, ComDropdownSearch, ComDropdownSelectedTpl, ComDropdownTag, ComDropdownTagTpl, defaultCompareWith, defaultDisplayWith, defaultFilterWith, dropdownChevronVariants, dropdownClearVariants, dropdownEmptyVariants, dropdownGroupVariants, dropdownOptionVariants, dropdownOverflowBadgeVariants, dropdownPanelVariants, dropdownSearchVariants, dropdownTagRemoveVariants, dropdownTagVariants, dropdownTriggerVariants, generateDropdownId };
2987
+ export { ComDropdown, ComDropdownEmptyTpl, ComDropdownGroup, ComDropdownGroupTpl, ComDropdownLoadingTpl, ComDropdownOption, ComDropdownOptionTpl, ComDropdownPanel, ComDropdownSearch, ComDropdownSelectedTpl, ComDropdownTag, ComDropdownTagTpl, defaultCompareWith, defaultDisplayWith, defaultFilterWith, dropdownChevronVariants, dropdownClearVariants, dropdownEmptyVariants, dropdownGroupVariants, dropdownLoadingVariants, dropdownOptionVariants, dropdownOverflowBadgeVariants, dropdownPanelVariants, dropdownSearchVariants, dropdownTagRemoveVariants, dropdownTagVariants, dropdownTriggerVariants, generateDropdownId };
2805
2988
  //# sourceMappingURL=ngx-com-components-dropdown.mjs.map