All files / autocomplete si-autocomplete-listbox.directive.ts

94.28% Statements 33/35
76.92% Branches 10/13
100% Functions 10/10
94.28% Lines 33/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119                                                              1x 1x   125x               125x   125x         125x   125x   125x 125x       125x 125x     125x 232x 81x       125x 155x 155x             125x 125x 125x   125x 125x         236x 236x 236x           71x         71x 71x 7x   7x   71x           117x     117x      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
import {
  ChangeDetectorRef,
  DestroyRef,
  Directive,
  inject,
  input,
  OnInit,
  output,
  contentChildren,
  INJECTOR,
  effect
} from '@angular/core';
 
import { SiAutocompleteOptionDirective } from './si-autocomplete-option.directive';
import { SiAutocompleteDirective } from './si-autocomplete.directive';
import { AUTOCOMPLETE_LISTBOX } from './si-autocomplete.model';
 
@Directive({
  selector: '[siAutocompleteListboxFor]',
  providers: [{ provide: AUTOCOMPLETE_LISTBOX, useExisting: SiAutocompleteListboxDirective }],
  host: {
    role: 'listbox',
    '[id]': 'id()'
  },
  exportAs: 'siAutocompleteListbox'
})
export class SiAutocompleteListboxDirective<T> implements OnInit {
  private static idCounter = 0;
 
  private readonly options = contentChildren(SiAutocompleteOptionDirective, { descendants: true });
 
  /**
   * @defaultValue
   * ```
   * `__si-autocomplete-listbox-${SiAutocompleteListboxDirective.idCounter++}`
   * ```
   */
  readonly id = input(`__si-autocomplete-listbox-${SiAutocompleteListboxDirective.idCounter++}`);
 
  readonly autocomplete = input.required<SiAutocompleteDirective<T>>({
    alias: 'siAutocompleteListboxFor'
  });
 
  /** @defaultValue 0 */
  readonly siAutocompleteDefaultIndex = input(0);
 
  readonly siAutocompleteOptionSubmitted = output<T | undefined>();
 
  private injector = inject(INJECTOR);
  private keyManager = new ActiveDescendantKeyManager(this.options, this.injector)
    .withWrap(true)
    .withVerticalOrientation(true);
 
  private changeDetectorRef = inject(ChangeDetectorRef);
  private destroyRef = inject(DestroyRef);
 
  constructor() {
    effect(() => {
      if (this.siAutocompleteDefaultIndex() >= 0 && !this.keyManager.activeItem) {
        this.setActiveItem();
      }
    });
 
    effect(() => {
      if (this.options()) {
        this.setActiveItem();
      }
    });
  }
 
  ngOnInit(): void {
    // For some reason, this is needed sometimes. Otherwise, one may get ExpressionChangedAfterItHasBeenCheckedError.
    queueMicrotask(() => {
      this.changeDetectorRef.markForCheck();
      this.autocomplete().listbox = this;
    });
    this.destroyRef.onDestroy(() => {
      this.autocomplete().listbox = undefined;
    });
  }
 
  private setActiveItem(): void {
    queueMicrotask(() => {
      this.keyManager.setActiveItem(this.siAutocompleteDefaultIndex());
      this.changeDetectorRef.markForCheck();
    });
  }
 
  /** @internal */
  onKeydown(event: KeyboardEvent): void {
    Iif (event.ctrlKey && event.key === 'Enter') {
      // [ctrl + enter] should submit and not select an option.
      // Mainly needed for filtered-search.
      return;
    }
    this.keyManager!.onKeydown(event);
    if (event.key === 'Enter' && this.keyManager!.activeItem) {
      this.siAutocompleteOptionSubmitted.emit(this.keyManager!.activeItem.value());
      // Something was selected. This should prevent everything else from happening, especially submitting the form.
      event.stopImmediatePropagation();
    }
    this.changeDetectorRef.markForCheck();
  }
 
  get active(): SiAutocompleteOptionDirective<T> | null {
    // NOTE: We must not return `this.keyManager.activeItem` here, because its not updating
    // activeItem reference when options change.
    Iif (this.keyManager.activeItemIndex === null) {
      return null;
    }
    return this.options().at(this.keyManager.activeItemIndex) ?? null;
  }
}