@sixbell-telco/sdk
Version:
A collection of reusable components designed for use in Sixbell Telco Angular projects
1,115 lines • 59.6 kB
JavaScript
import { CdkListbox, CdkOption } from '@angular/cdk/listbox';
import * as i1 from '@angular/cdk/overlay';
import { OverlayModule } from '@angular/cdk/overlay';
import { PortalModule } from '@angular/cdk/portal';
import * as i0 from '@angular/core';
import { inject, input, model, output, signal, viewChild, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
import { toObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
import { cn } from '@sixbell-telco/sdk/utils/cn';
import { cva } from 'class-variance-authority';
import { BadgeComponent } from '@sixbell-telco/sdk/components/badge';
import { IconComponent } from '@sixbell-telco/sdk/components/icon';
import { matSearchOutline, matKeyboardArrowDownOutline, matCheckOutline, matCloseOutline } from '@sixbell-telco/sdk/components/icon/material/outline';
import { Subject, switchMap, startWith, debounceTime, distinctUntilChanged } from 'rxjs';
/**
* @fileoverview Combobox Component
*
* A comprehensive combobox component with advanced features including:
* - Single and multiple selection modes
* - Real-time search with debouncing
* - Keyboard navigation support (Arrow keys, Enter, Space)
* - Form validation integration
* - Accessibility compliance with CDK Listbox
* - Customizable styling variants and sizes
* - Animation states for smooth transitions
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
// ==============================================================================
// IMPORTS
// ==============================================================================
// Angular CDK
// ==============================================================================
// COMPONENT STYLES
// ==============================================================================
/**
* Class variance authority configuration for combobox styling
*/
const comboboxComponent = cva(['w-full', 'min-w-fit', 'text-pretty', 'font-body', 'font-normal', 'select', 'leading-normal', 'bg-none'], {
variants: {
variant: {
primary: ['select-primary'],
secondary: ['select-secondary'],
tertiary: ['select-tertiary'],
accent: ['select-accent'],
info: ['select-info'],
success: ['select-success'],
error: ['select-error'],
warning: ['select-warning'],
},
size: {
xs: ['select-xs'],
sm: ['select-sm'],
md: ['select-md'],
lg: ['select-lg'],
xl: ['select-xl'],
},
},
compoundVariants: [],
defaultVariants: {
variant: 'secondary',
size: 'md',
},
});
// ==============================================================================
// COMPONENT
// ==============================================================================
/**
* A customizable combobox component with search functionality, multiple selection modes,
* and keyboard navigation support. Implements Angular's ControlValueAccessor interface
* for seamless integration with reactive forms.
*
* @example
* ```html
* <st-combobox
* [(value)]="selectedValue"
* [options]="availableOptions"
* displayKey="name"
* valueKey="id"
* placeholder="Select an option"
* variant="primary"
* size="md">
* </st-combobox>
* ```
*
* @example
* ```typescript
* // Multiple selection
* <st-combobox
* [(value)]="selectedValues"
* [options]="options"
* selectionType="multiple"
* [allowClear]="true">
* </st-combobox>
* ```
*/
class ComboboxComponent {
// ==============================================================================
// SERVICES
// ==============================================================================
/**
* Angular translation service for internationalization
* @internal
*/
translateService = inject(TranslateService);
// ==============================================================================
// INPUT PROPERTIES
// ==============================================================================
/**
* The visual style variant of the combobox
* @default 'secondary'
*/
variant = input('secondary');
/**
* The size variant of the combobox
* @default 'md'
*/
size = input('md');
/**
* Whether to use ghost (transparent) styling
* @default false
*/
ghost = input(false);
/**
* The HTML name attribute for the underlying form control
*/
name = input(null);
/**
* Placeholder text displayed when no option is selected
*/
placeholder = input('');
/**
* Whether the combobox is disabled
* @default false
*/
disabled = model(false);
/**
* Label text displayed above the combobox
*/
label = input('');
/**
* Parent form group for reactive forms integration
*/
parentForm = input(null);
/**
* The form control name when used within a reactive form
*/
formControlName = input('');
/**
* Whether to show a clear button next to the selected value(s) in the trigger
* Works for both single and multiple selection modes
* @default true
*/
allowClear = input(true);
/**
* Whether to show a "Clear All" button at the bottom of the dropdown for multiple selection
* Only applies when selectionType is 'multiple'
* @default true
*/
allowClearAll = input(true);
/**
* Placeholder text for the search input field
* @default 'Search...'
*/
searchPlaceholder = input('');
/**
* Text displayed when no search results are found
* @default 'No results found'
*/
noResultsText = input('');
/**
* The current selected value(s)
* For single selection: any | null
* For multiple selection: any[]
*/
value = model(null);
/**
* Array of available options to select from
*/
options = input([]);
/**
* The object property to use for display text when options are objects
* Falls back to common property names if not specified
*/
displayKey = input('');
/**
* The object property to use as the value when options are objects
* Uses the entire object if not specified
*/
valueKey = input('');
/**
* Selection mode: 'single' for single selection, 'multiple' for multi-select
* @default 'single'
*/
selectionType = input('single');
/**
* Whether to enable debounced search functionality
* @default false
*/
searchDebounced = input(false);
/**
* Debounce time in milliseconds for search input
* @default 300
*/
searchDebounceTime = input(300);
/**
* Event emitted when combobox loses focus
*/
blurred = output();
/**
* Event emitted when selection changes
*/
valueUpdated = output();
/**
* Event emitted when search query changes with filtered results
*/
searchQuery = output();
// ==============================================================================
// ICONS
// ==============================================================================
/** Search icon for the search input */
iconSearch = matSearchOutline;
/** Dropdown chevron icon */
iconChevronDown = matKeyboardArrowDownOutline;
/** Check icon for selected options */
iconCheck = matCheckOutline;
/** Close/clear icon */
iconClose = matCloseOutline;
// ==============================================================================
// PRIVATE PROPERTIES
// ==============================================================================
/** Internal search input value */
searchValue = signal('');
/** Processed search term after debouncing */
searchTerm = signal('');
/** RxJS subject for search input handling */
searchSubject = new Subject();
/** Current animation state of the overlay */
animationState = signal('closed');
/** Trigger for blur events in form validation */
blurTrigger = signal(0);
/** Active option index for keyboard navigation */
activeOptionIndex = signal(-1);
// ==============================================================================
// VIEW CHILDREN
// ==============================================================================
/** Reference to the trigger button element */
triggerRef = viewChild('trigger');
/** Reference to the search input element */
searchInputRef = viewChild('searchInput');
/** Reference to the options list container */
optionsList = viewChild('optionsList');
/** Reference to the CDK listbox for keyboard navigation */
listbox = viewChild('listbox');
// ==============================================================================
// SIGNALS
// ==============================================================================
/** Width of the trigger button for overlay positioning */
triggerWidth = signal(0);
/** Current active position configuration from CDK overlay */
currentPosition = signal(null);
/**
* Static class mappings for different animation directions
* Maps animation directions to complete Tailwind class names
* All class names are static and detectable by Tailwind at build time
*/
animationClassMap = {
bottom: 'data-[state=opening]:animate-fade-in-up data-[state=open]:animate-fade-in-up data-[state=closing]:animate-fade-out-down',
top: 'data-[state=opening]:animate-fade-in-down data-[state=open]:animate-fade-in-down data-[state=closing]:animate-fade-out-up',
left: 'data-[state=opening]:animate-fade-in-left data-[state=open]:animate-fade-in-left data-[state=closing]:animate-fade-out-right',
right: 'data-[state=opening]:animate-fade-in-right data-[state=open]:animate-fade-in-right data-[state=closing]:animate-fade-out-left',
};
/**
* Computed animation direction based on current overlay position
* Dynamically detects position and returns appropriate direction: 'top', 'bottom', 'left', or 'right'
*/
animationDirection = computed(() => {
const position = this.currentPosition();
if (!position)
return 'bottom'; // default
// Determine direction based on overlay Y and X positions
const overlayY = position?.connectionPair?.overlayY;
const overlayX = position?.connectionPair?.overlayX;
// Priority: check Y first, then X
if (overlayY === 'top') {
return 'top'; // Dropdown appears above
}
else if (overlayY === 'bottom') {
return 'bottom'; // Dropdown appears below
}
else if (overlayX === 'end') {
return 'left'; // Dropdown appears to the left
}
else if (overlayX === 'start') {
return 'right'; // Dropdown appears to the right
}
return 'bottom'; // default
});
/**
* Computed class string for animations
* Applies the correct animation based on position using static class names
*/
animationClasses = computed(() => {
const direction = this.animationDirection();
const baseClasses = this.animationClassMap[direction];
const durationClasses = 'data-[state=closing]:animate-duration-150 data-[state=opening]:animate-duration-150 data-[state=open]:animate-duration-150 overflow-hidden';
return `${baseClasses} ${durationClasses}`;
});
// ==============================================================================
// FORM CONTROL INTEGRATION
// ==============================================================================
/** Form control reference for reactive forms */
formControl = computed(() => this.parentForm()?.get(this.formControlName()));
/** Observable stream of form control */
formControl$ = toObservable(this.formControl);
/** Stream of form control status changes */
statusChanges$ = this.formControl$.pipe(switchMap((control) => control?.statusChanges.pipe(startWith(control.status)) || []));
/** Stream of form control value changes */
stateChanges$ = this.formControl$.pipe(switchMap((control) => control?.valueChanges.pipe(startWith(control.value)) || []));
/** Signal for form control status */
statusSignal = toSignal(this.statusChanges$);
/** Signal for form control state */
stateSignal = toSignal(this.stateChanges$);
// ==============================================================================
// CONTROL VALUE ACCESSOR
// ==============================================================================
/** Callback function for value changes */
onChange = (value) => { };
/** Callback function for touch events */
onTouched = () => { };
// ==============================================================================
// CONSTRUCTOR
// ==============================================================================
constructor() {
this.setupSearchHandling();
this.setupSearchQueryEmission();
}
// ==============================================================================
// COMPUTED PROPERTIES
// ==============================================================================
/**
* Filtered options based on current search term
* @returns Array of options that match the search criteria
*/
filteredOptions = computed(() => {
const options = this.options();
const search = this.searchTerm().toLowerCase().trim();
if (!search)
return options;
return options.filter((option) => {
const displayValue = this.getDisplayValue(option).toLowerCase();
return displayValue.includes(search);
});
});
/**
* Whether the overlay is currently open or in the process of opening/closing
* @returns True if overlay is visible or animating
*/
isOpen = computed(() => {
const state = this.animationState();
return state === 'opening' || state === 'open' || state === 'closing';
});
/**
* Whether the overlay is currently animating to open state
* @returns True if overlay is opening or open
*/
isAnimatingOpen = computed(() => {
const state = this.animationState();
return state === 'opening' || state === 'open';
});
/**
* Current animation state for template binding
* @returns Current animation state
*/
dataState = computed(() => this.animationState());
/**
* Display value for the selected option(s)
* @returns Formatted display string based on selection type and current value
*/
displayValue = computed(() => {
const selectionType = this.selectionType();
const currentValue = this.value();
if (selectionType === 'multiple') {
const selected = Array.isArray(currentValue) ? currentValue : [];
if (selected.length === 0)
return this.translatedPlaceholder();
// Validate that all selected values exist in options
const validSelected = selected.filter((value) => this.isValidValue(value));
if (validSelected.length === 0)
return this.translatedPlaceholder();
if (validSelected.length === 1)
return this.getDisplayValue(validSelected[0]);
return `${validSelected.length} items selected`;
}
else {
// For single selection, only show display value if it's valid
if (!currentValue || !this.isValidValue(currentValue)) {
return this.translatedPlaceholder();
}
return this.getDisplayValue(currentValue);
}
});
/**
* Display information for multiple selection showing first item and additional count
* @returns Object with first item display and additional count, or null for single selection
*/
multipleDisplayInfo = computed(() => {
const selectionType = this.selectionType();
const currentValue = this.value();
if (selectionType === 'multiple') {
const selected = Array.isArray(currentValue) ? currentValue : [];
if (selected.length === 0)
return null;
if (selected.length === 1)
return { firstItem: this.getDisplayValue(selected[0]), additionalCount: 0 };
return {
firstItem: this.getDisplayValue(selected[0]),
additionalCount: selected.length - 1,
};
}
return null;
});
/**
* Whether the clear button should be shown
* @returns True if there's a valid selected value and clear is allowed
*/
showClearButton = computed(() => {
if (!this.allowClear() || this.disabled()) {
return false;
}
const currentValue = this.value();
if (!currentValue)
return false;
if (this.selectionType() === 'multiple') {
// For multiple selection, check if we have any valid values
const selected = Array.isArray(currentValue) ? currentValue : [];
return selected.some((value) => this.isValidValue(value));
}
else {
// For single selection, check if the value is valid
return this.isValidValue(currentValue);
}
});
/**
* Base CSS classes for the combobox component
* @internal
*/
componentClass = computed(() => {
return cn(comboboxComponent({
variant: this.variant(),
size: this.size(),
}), { 'select-ghost': this.ghost() });
});
/**
* Error state CSS classes
* @internal
*/
errorClass = computed(() => {
return cn(comboboxComponent({
variant: 'error',
size: this.size(),
}), { 'select-ghost': this.ghost() });
});
/**
* Success state CSS classes
* @internal
*/
successClass = computed(() => {
return cn(comboboxComponent({
variant: 'success',
size: this.size(),
}), { 'select-ghost': this.ghost() });
});
/**
* Validation-aware CSS classes based on form control state
* @internal
*/
validationClass = computed(() => {
const control = this.formControl();
const trigger = this.blurTrigger();
if (!control)
return this.componentClass();
if (control.pristine && !control.touched) {
return this.componentClass();
}
const isTouched = control.touched || trigger > 0;
this.statusSignal();
this.stateSignal();
if (control.dirty || isTouched) {
if (control.invalid)
return this.errorClass();
if (control.valid)
return this.successClass();
}
return this.componentClass();
});
/**
* CSS classes for the search input field
* @internal
*/
searchInputClass = computed(() => {
return cn('input bg-base-200 w-full border-transparent focus-within:outline-none focus:border-transparent focus:outline-none', {
'input-xs': this.size() === 'xs',
'input-sm': this.size() === 'sm',
'input-md': this.size() === 'md',
'input-lg': this.size() === 'lg',
'input-xl': this.size() === 'xl',
});
});
/**
* CSS classes for the options menu
* @internal
*/
menuClass = computed(() => {
return cn('menu flex-nowrap max-h-72 overflow-y-auto overscroll-contain w-full p-0 focus-within:outline-none focus:outline-none focus-visible:outline-none', {
'menu-xs': this.size() === 'xs',
'menu-sm': this.size() === 'sm',
'menu-md': this.size() === 'md',
'menu-lg': this.size() === 'lg',
'menu-xl': this.size() === 'xl',
});
});
/**
* CSS classes for the badge
* @internal
*/
badgeClass = computed(() => {
const size = this.size();
switch (size) {
case 'xs':
case 'sm':
return 'xs';
case 'md':
return 'sm';
case 'lg':
return 'md';
case 'xl':
return 'lg';
default:
return 'sm';
}
});
/**
* Translated search placeholder text
* @returns Translated placeholder or input value
*/
translatedSearchPlaceholder = computed(() => {
const customPlaceholder = this.searchPlaceholder();
return customPlaceholder || this.translateService.instant('sdk.combobox.searchPlaceholder');
});
/**
* Translated main placeholder text
* @returns Translated placeholder or input value
*/
translatedPlaceholder = computed(() => {
const customPlaceholder = this.placeholder();
return customPlaceholder || this.translateService.instant('sdk.combobox.placeholder');
});
/**
* Translated no results text
* @returns Translated no results text or input value
*/
translatedNoResultsText = computed(() => {
const customNoResults = this.noResultsText();
return customNoResults || this.translateService.instant('sdk.combobox.noResultsFound');
});
/**
* Current search input value for template binding
* @returns Current search value
*/
get currentSearchValue() {
return this.searchValue();
}
// ==============================================================================
// LIFECYCLE METHODS
// ==============================================================================
/**
* Angular lifecycle hook called after view initialization
* Updates the trigger width for proper overlay positioning
*/
ngAfterViewInit() {
this.updateTriggerWidth();
}
// ==============================================================================
// CONTROL VALUE ACCESSOR IMPLEMENTATION
// ==============================================================================
/**
* Writes a new value to the component
* @param value - The new value to set
*/
writeValue(value) {
// Only set the value if it's null/undefined or if it's valid
if (!value) {
this.value.set(value);
return;
}
if (this.selectionType() === 'multiple') {
// For multiple selection, filter out invalid values
if (Array.isArray(value)) {
const validValues = value.filter((v) => this.isValidValue(v));
this.value.set(validValues.length > 0 ? validValues : []);
}
else {
this.value.set([]);
}
}
else {
// For single selection, only set if valid
const isValid = this.isValidValue(value);
this.value.set(isValid ? value : null);
}
}
/**
* Registers a callback function to be called when the value changes
* @param fn - The callback function
*/
registerOnChange(fn) {
this.onChange = fn;
}
/**
* Registers a callback function to be called when the component is touched
* @param fn - The callback function
*/
registerOnTouched(fn) {
this.onTouched = fn;
}
// ==============================================================================
// PUBLIC METHODS
// ==============================================================================
/**
* Toggles the overlay open/closed state
* Handles focus management and animation state transitions
*/
toggleOverlay() {
const currentState = this.animationState();
if (currentState === 'closed') {
this.updateTriggerWidth();
this.animationState.set('opening');
// Focus search input after opening
setTimeout(() => {
this.searchInputRef()?.nativeElement?.focus();
// Set first option as active by default
this.setDefaultActiveOption();
}, 100);
}
else if (currentState === 'open') {
this.animationState.set('closing');
}
}
/**
* Handles animation end events to update state
* @param event - The animation event
*/
onAnimationEnd(event) {
if (event.animationName === 'fade-in-up') {
this.animationState.set('open');
}
else if (event.animationName === 'fade-out-down') {
this.animationState.set('closed');
this.clearSearch();
this.triggerBlur();
// Return focus to trigger button for better keyboard navigation
setTimeout(() => {
this.triggerRef()?.nativeElement?.focus();
}, 0);
}
}
/**
* Handles overlay detachment
* Cleans up state and triggers blur
*/
handleDetach() {
this.animationState.set('closed');
this.clearSearch();
this.triggerBlur();
}
/**
* Handles overlay position changes to determine animation direction
* @param event - The position change event from CDK overlay
*/
onPositionChange(event) {
this.currentPosition.set(event?.connectionPair || null);
}
/**
* Handles clicks outside the component
* @param event - The mouse event
*/
handleClickOutside(event) {
if (this.animationState() === 'open') {
this.animationState.set('closing');
}
event.stopPropagation();
this.triggerBlur();
}
/**
* Handles search input changes
* @param event - The input event
*/
handleSearchInput(event) {
const target = event.target;
const searchValue = target.value;
this.searchValue.set(searchValue);
if (this.searchDebounced()) {
this.searchSubject.next(searchValue);
}
else {
this.searchTerm.set(searchValue);
// Set first option as active after search
setTimeout(() => this.setDefaultActiveOption(), 0);
}
}
/**
* Handles option selection
* @param option - The selected option
*/
handleOptionSelect(option) {
const selectionType = this.selectionType();
const currentValue = this.value();
if (selectionType === 'multiple') {
const currentValues = Array.isArray(currentValue) ? currentValue : [];
const optionValue = this.getOptionValue(option);
const isSelected = this.isOptionSelected(option);
let newValues;
if (isSelected) {
newValues = currentValues.filter((value) => this.getOptionValue(value) !== optionValue);
}
else {
newValues = [...currentValues, option];
}
this.value.set(newValues);
this.onChange(newValues);
this.valueUpdated.emit(newValues);
}
else {
this.value.set(option);
this.onChange(option);
this.valueUpdated.emit(option);
this.animationState.set('closing');
}
this.onTouched();
}
/**
* Handles CDK listbox selection changes
* @param event - The selection change event
*/
handleSelectionChange(event) {
if (event.option) {
this.handleOptionSelect(event.option.value);
}
}
/**
* Handles keyboard events for option selection
* @param event - The keyboard event
*/
handleListboxKeydown(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
const focusedOption = this.optionsList()?.nativeElement.querySelector('.cdk-option-active');
if (focusedOption) {
const optionIndex = parseInt(focusedOption.getAttribute('data-option-index') || '0');
const filteredOptions = this.filteredOptions();
if (optionIndex >= 0 && optionIndex < filteredOptions.length) {
this.handleOptionSelect(filteredOptions[optionIndex]);
}
}
}
}
/**
* Handles keyboard events on the search input for navigation and selection
* @param event - The keyboard event
*/
handleSearchKeydown(event) {
const filteredOptions = this.filteredOptions();
const currentIndex = this.activeOptionIndex();
switch (event.key) {
case 'ArrowDown': {
event.preventDefault();
event.stopPropagation();
const nextIndex = currentIndex < filteredOptions.length - 1 ? currentIndex + 1 : 0;
this.setActiveOption(nextIndex);
break;
}
case 'ArrowUp': {
event.preventDefault();
event.stopPropagation();
const prevIndex = currentIndex > 0 ? currentIndex - 1 : filteredOptions.length - 1;
this.setActiveOption(prevIndex);
break;
}
case 'Enter':
event.preventDefault();
event.stopPropagation();
if (currentIndex >= 0 && currentIndex < filteredOptions.length) {
this.handleOptionSelect(filteredOptions[currentIndex]);
}
break;
case 'Tab': {
// Allow Tab only to clear button in multiple selection mode
const isMultipleSelection = this.selectionType() === 'multiple';
const hasSelectedValues = this.value() && this.isArray(this.value()) && this.value().length > 0;
if (isMultipleSelection && hasSelectedValues) {
// Allow default Tab behavior to go to clear button
break;
}
else {
// Prevent tabbing out of the search input
event.preventDefault();
event.stopPropagation();
}
break;
}
case 'Escape':
event.preventDefault();
event.stopPropagation();
this.animationState.set('closing');
this.activeOptionIndex.set(-1);
break;
default:
// For other keys (including space), set first option as active for typing
setTimeout(() => this.setDefaultActiveOption(), 0);
break;
}
}
/**
* Handles keyboard events on the clear all button
* @param event - The keyboard event
*/
handleClearButtonKeydown(event) {
if (event.key === 'Tab') {
// Prevent default Tab behavior and focus back to search input
event.preventDefault();
event.stopPropagation();
this.searchInputRef()?.nativeElement?.focus();
}
}
/**
* Handles keyboard events on the trigger button
* @param event - The keyboard event
*/
handleTriggerKeydown(event) {
switch (event.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'Enter':
case ' ':
event.preventDefault();
event.stopPropagation();
if (this.animationState() === 'closed') {
this.toggleOverlay();
}
else {
// If already open, handle navigation
this.handleListboxKeydown(event);
}
break;
case 'Escape':
if (this.animationState() === 'open') {
event.preventDefault();
event.stopPropagation();
this.animationState.set('closing');
}
break;
}
}
/**
* Clears the current selection
*/
clearSelection() {
if (this.selectionType() === 'multiple') {
this.value.set([]);
this.onChange([]);
this.valueUpdated.emit([]);
}
else {
this.value.set(null);
this.onChange(null);
this.valueUpdated.emit(null);
}
this.onTouched();
// Focus back to search input after clearing
setTimeout(() => {
this.searchInputRef()?.nativeElement?.focus();
// Set first option as active by default after clearing
this.setDefaultActiveOption();
}, 0);
}
/**
* Handles clear selection button click
* @param event - The click event
*/
handleClearSelection(event) {
event.stopPropagation();
this.clearSelection();
}
/**
* Checks if an option is currently selected
* @param option - The option to check
* @returns True if the option is selected
*/
isOptionSelected(option) {
const selectionType = this.selectionType();
const currentValue = this.value();
const optionValue = this.getOptionValue(option);
if (selectionType === 'multiple') {
const selected = Array.isArray(currentValue) ? currentValue : [];
return selected.some((value) => this.getOptionValue(value) === optionValue);
}
else {
return currentValue && this.getOptionValue(currentValue) === optionValue;
}
}
/**
* Checks if an option is currently active for keyboard navigation
* @param index - The option index
* @returns True if the option is active
*/
isActiveOption(index) {
return this.activeOptionIndex() === index;
}
/**
* Gets the display value for an option
* @param option - The option to get display value for
* @returns The display string for the option
*/
getDisplayValue(option) {
if (!option)
return '';
const displayKey = this.displayKey();
if (displayKey && typeof option === 'object') {
return option[displayKey] || '';
}
if (typeof option === 'object') {
const commonKeys = ['name', 'label', 'text', 'title', 'displayName'];
for (const key of commonKeys) {
if (option[key] !== undefined && option[key] !== null) {
return String(option[key]);
}
}
const keys = Object.keys(option);
for (const key of keys) {
const value = option[key];
if (typeof value === 'string' || typeof value === 'number') {
return String(value);
}
}
return JSON.stringify(option);
}
return String(option);
}
/**
* Type guard to check if a value is an array
* @param value - The value to check
* @returns True if the value is an array
*/
isArray(value) {
return Array.isArray(value);
}
// ==============================================================================
// PRIVATE METHODS
// ==============================================================================
/**
* Updates the trigger button width for overlay positioning
*/
updateTriggerWidth() {
if (this.triggerRef) {
const width = this.triggerRef()?.nativeElement.offsetWidth;
if (!width)
return;
this.triggerWidth.set(width);
}
}
/**
* Gets the value property from an option
* @param option - The option to get value from
* @returns The option value
*/
getOptionValue(option) {
if (!option)
return option;
const valueKey = this.valueKey();
return valueKey && typeof option === 'object' ? option[valueKey] : option;
}
/**
* Checks if a value is valid (exists in options)
* @param value - The value to validate
* @returns True if the value exists in the options array
*/
isValidValue(value) {
if (!value)
return false;
const options = this.options();
return options.some((option) => {
if (typeof option === 'string' && typeof value === 'string') {
return option === value;
}
// For objects, check if the value matches the option or its value property
const optionValue = this.getOptionValue(option);
const currentValue = this.getOptionValue(value);
return optionValue === currentValue || option === value;
});
}
/**
* Triggers blur event for form validation
*/
triggerBlur() {
this.blurTrigger.update((val) => val + 1);
this.onTouched();
this.blurred.emit(this.value());
}
/**
* Clears the search input and term
*/
clearSearch() {
this.searchValue.set('');
this.searchTerm.set('');
this.activeOptionIndex.set(-1);
}
/**
* Sets up reactive search handling with debouncing
* The debounce time updates reactively when the input changes
*/
setupSearchHandling() {
const debounceTime$ = toObservable(this.searchDebounceTime);
debounceTime$
.pipe(switchMap((currentDebounceTime) => {
return this.searchSubject.pipe(debounceTime(currentDebounceTime), distinctUntilChanged());
}), takeUntilDestroyed())
.subscribe((searchValue) => {
this.searchTerm.set(searchValue);
// Set first option as active after debounced search
setTimeout(() => this.setDefaultActiveOption(), 0);
});
}
/**
* Sets the active option for keyboard navigation
* @param index - The index of the option to make active
*/
setActiveOption(index) {
this.activeOptionIndex.set(index);
this.updateActiveOptionVisually();
}
/**
* Sets the first option as active by default
*/
setDefaultActiveOption() {
const filteredOptions = this.filteredOptions();
if (filteredOptions.length > 0) {
this.setActiveOption(0);
}
}
/**
* Updates the visual state of the active option
*/
updateActiveOptionVisually() {
if (!this.optionsList()?.nativeElement)
return;
// Remove active class from all options
const allOptions = this.optionsList()?.nativeElement.querySelectorAll('[data-option-index]');
if (!allOptions)
return;
allOptions.forEach((option) => {
option.classList.remove('cdk-option-active');
});
// Add active class to current option
const currentIndex = this.activeOptionIndex();
if (currentIndex >= 0) {
const activeOption = this.optionsList()?.nativeElement.querySelector(`[data-option-index="${currentIndex}"]`);
if (activeOption) {
activeOption.classList.add('cdk-option-active');
// Scroll into view if needed
activeOption.scrollIntoView({ block: 'nearest' });
}
}
}
/**
* Sets up the search query emission effect
* Emits search results whenever the search term or options change
*/
setupSearchQueryEmission() {
effect(() => {
const query = this.searchTerm();
const results = this.filteredOptions();
// Emit the search query event with current query and filtered results
this.searchQuery.emit({ query, results });
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ComboboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: ComboboxComponent, isStandalone: true, selector: "st-combobox", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, ghost: { classPropertyName: "ghost", publicName: "ghost", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, parentForm: { classPropertyName: "parentForm", publicName: "parentForm", isSignal: true, isRequired: false, transformFunction: null }, formControlName: { classPropertyName: "formControlName", publicName: "formControlName", isSignal: true, isRequired: false, transformFunction: null }, allowClear: { classPropertyName: "allowClear", publicName: "allowClear", isSignal: true, isRequired: false, transformFunction: null }, allowClearAll: { classPropertyName: "allowClearAll", publicName: "allowClearAll", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, displayKey: { classPropertyName: "displayKey", publicName: "displayKey", isSignal: true, isRequired: false, transformFunction: null }, valueKey: { classPropertyName: "valueKey", publicName: "valueKey", isSignal: true, isRequired: false, transformFunction: null }, selectionType: { classPropertyName: "selectionType", publicName: "selectionType", isSignal: true, isRequired: false, transformFunction: null }, searchDebounced: { classPropertyName: "searchDebounced", publicName: "searchDebounced", isSignal: true, isRequired: false, transformFunction: null }, searchDebounceTime: { classPropertyName: "searchDebounceTime", publicName: "searchDebounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", value: "valueChange", blurred: "blurred", valueUpdated: "valueUpdated", searchQuery: "searchQuery" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: ComboboxComponent,
multi: true,
},
], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "searchInputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "optionsList", first: true, predicate: ["optionsList"], descendants: true, isSignal: true }, { propertyName: "listbox", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], ngImport: i0, template: "@let isOpen = this.isOpen(); @let isAnimatingOpen = this.isAnimatingOpen(); @let triggerWidth = this.triggerWidth();\n@let dataState = this.dataState(); @let filteredOptions = this.filteredOptions(); @let displayValue = this.displayValue();\n@let multipleDisplayInfo = this.multipleDisplayInfo(); @let label = this.label();\n\n<fieldset class=\"fieldset p-0\">\n\t@if (label) {\n\t\t<legend class=\"fieldset-legend font-body pt-0 text-pretty\">{{ label }}</legend>\n\t}\n\n\t<button\n\t\t#trigger\n\t\t[class]=\"validationClass()\"\n\t\t(click)=\"toggleOverlay()\"\n\t\t(keydown)=\"handleTriggerKeydown($event)\"\n\t\ttype=\"button\"\n\t\tcdkOverlayOrigin\n\t\taria-haspopup=\"true\"\n\t\trole=\"combobox\"\n\t\t[attr.aria-controls]=\"'combobox-list'\"\n\t\t[attr.aria-expanded]=\"isOpen\"\n\t\t[disabled]=\"disabled()\"\n\t>\n\t\t<span class=\"flex-1 truncate text-left\">\n\t\t\t@if (selectionType() === 'multiple' && multipleDisplayInfo) {\n\t\t\t\t<span class=\"inline-flex items-center gap-2\">\n\t\t\t\t\t<span class=\"truncate\">{{ multipleDisplayInfo.firstItem }}</span>\n\t\t\t\t\t@if (multipleDisplayInfo.additionalCount > 0) {\n\t\t\t\t\t\t<st-badge [size]=\"badgeClass()\">+{{ multipleDisplayInfo.additionalCount }}</st-badge>\n\t\t\t\t\t}\n\t\t\t\t</span>\n\t\t\t} @else {\n\t\t\t\t{{ displayValue }}\n\t\t\t}\n\t\t</span>\n\t\t<div class=\"inline-flex items-center justify-items-end gap-2\">\n\t\t\t<!-- Clear button for both single and multiple selection -->\n\t\t\t@if (showClearButton()) {\n\t\t\t\t<div class=\"grid translate-x-full place-content-center\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"btn font-body btn-sm btn-secondary h-[1.5em] w-[1.5em] items-center p-0 align-top [font-size:inherit] leading-normal font-medium text-pretty\"\n\t\t\t\t\t\t(click)=\"handleClearSelection($event)\"\n\t\t\t\t\t\t(keydown.enter)=\"handleClearSelection($event)\"\n\t\t\t\t\t\t(keydown.space)=\"handleClearSelection($event)\"\n\t\t\t\t\t\t[attr.aria-label]=\"'sdk.combobox.clearSelection' | translate\"\n\t\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<st-icon [icon]=\"iconClose\"></st-icon>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t}\n\t\t\t<st-icon\n\t\t\t\tclass=\"h-[1.5em] w-[1.5em] flex-0 translate-x-full transition-transform duration-200\"\n\t\t\t\t[class.rotate-180]=\"isAnimatingOpen\"\n\t\t\t\t[icon]=\"iconChevronDown\"\n\t\t\t></st-icon>\n\t\t</div>\n\t</button>\n\n\t<ng-template\n\t\tcdkConnectedOverlay\n\t\t[cdkConnectedOverlayOrigin]=\"trigger\"\n\t\t[cdkConnectedOverlayOpen]=\"isOpen\"\n\t\t[cdkConnectedOverlayWidth]=\"triggerWidth\"\n\t\t[cdkConnectedOverlayMinWidth]=\"triggerWidth\"\n\t\t(detach)=\"handleDetach()\"\n\t\t(overlayOutsideClick)=\"handleClickOutside($event)\"\n\t\t(positionChange)=\"onPositionChange($event)\"\n\t>\n\t\t<div\n\t\t\t[class]=\"'bg-base-200 rounded-box shadow-main border-neutral ' + animationClasses() + ' mt-1 w-full border border-solid'\"\n\t\t\t(animationend)=\"onAnimationEnd($event)\"\n\t\t\t[attr.data-state]=\"dataState\"\n\t\t>\n\t\t\t<!-- Search Input -->\n\t\t\t<div class=\"border-neutral border-b\">\n\t\t\t\t@let searchInputClass = this.searchInputClass();\n\t\t\t\t<div [class]=\"searchInputClass\">\n\t\t\t\t\t<st-icon class=\"text-base-placeholder\" [icon]=\"iconSearch\"></st-icon>\n\t\t\t\t\t<input\n\t\t\t\t\t\t#searchInput\n\t\t\t\t\t\tclass=\"placeholder:text-base-placeholder w-full border-transparent py-0 focus-within:outline-none focus:border-transparent focus:outline-none\"\n\t\t\t\t\t\t[value]=\"currentSearchValue\"\n\t\t\t\t\t\t(input)=\"handleSearchInput($event)\"\n\t\t\t\t\t\t(keydown)=\"handleSearchKeydown($event)\"\n\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t[placeholder]=\"translatedSearchPlaceholder()\"\n\t\t\t\t\t\t[attr.aria-label]=\"'sdk.combobox.searchOptions' | translate\"\n\t\t\t\t\t\t[disabled]=\"!isOpen\"\n\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Options List -->\n\t\t\t@let menuClass = this.menuClass();\n\t\t\t<ul\n\t\t\t\t[class]=\"menuClass\"\n\t\t\t\tcdkListbox\n\t\t\t\tcdkListboxUseActiveDescendant\n\t\t\t\t[cdkListboxMultiple]=\"selectionType() === 'multiple'\"\n\t\t\t\taria-labelledby=\"Combobox options\"\n\t\t\t\trole=\"listbox\"\n\t\t\t\t#optionsList\n\t\t\t\t[tabindex]=\"-1\"\n\t\t\t\t(keydown)=\"handleListboxKeydown($event)\"\n\t\t\t\t(selectionChange)=\"handleSelectionChange($event)\"\n\t\t\t>\n\t\t\t\t@for (option of filteredOptions; track $index) {\n\t\t\t\t\t<li [cdkOption]=\"option\" class=\"group\" [attr.data-option-index]=\"$index\" [class.cdk-option-active]=\"isActiveOption($index)\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"group-[.cdk-option-active]:bg-base-300 font-body flex w-full items-center justify-between rounded-none px-4 py-2 text-left leading-normal font-normal text-pretty transition-colors duration-150 ease-in-out\"\n\t\t\t\t\t\t\t(click)=\"handleOptionSelect(option)\"\n\t\t\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span class=\"flex-1 truncate\">{{ getDisplayValue(option) }}</span>\n\n\t\t\t\t\t\t\t@if (isOptionSelected(option)) {\n\t\t\t\t\t\t\t\t<st-icon class=\"h-4 w-4 shrink-0\" [icon]=\"iconCheck\"></st-icon>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\n\t\t\t\t<!-- Empty state -->\n\t\t\t\t@if (filteredOptions.length === 0) {\n\t\t\t\t\t<li class=\"text-base-content/60 px-4 py-2 text-center\">\n\t\t\t\t\t\t<span>{{ translatedNoResultsText() }}</span>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\n\t\t\t<!-- Clear All button for multiple selection -->\n\t\t\t@if (selectionType() === 'multiple' && allowClearAll() && value() && isArray(value()) && value().length > 0) {\n\t\t\t\t<div class=\"border-base-300 border-t p-2\">\n\t\t\t\t\t<button type=\"button\" class=\"btn btn-outline btn-sm w-full\" (click)=\"clearSelection()\" (keydown)=\"handleClearButtonKeydown($event)\">\n\t\t\t\t\t\t{{ 'sdk.combobox.clearAll' | translate }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\t</ng-template>\n\n\t<!-- Hidden label for accessibility -->\n\t<label class=\"sr-only\" [for]=\"name()\">{{ label }}</label>\n\n\t<!-- Additional content -->\n\t<ng-content></ng-content>\n</fieldset>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "directive", type: CdkListbox, selector: "[cdkListbox]", inputs: ["id", "tabindex", "cdkListboxValue", "cdkListboxMultiple", "cdkListboxDisabled", "cdkListboxUseActiveDescendant", "cdkListboxOrientation", "cdkListboxCompareWith", "cdkListboxNavigationWrapDisabled", "cdkListboxNavigatesDisabledOptions"], outputs: ["cdkListboxValueChange"], exportAs: ["cdkListbox"] }, { kind: "directive", type: CdkOption, selector: "[cdkOption]", inputs: ["id", "cdkOption", "cdkOptionTypeaheadLabel", "cdkOptionDisabled", "tabindex"], exportAs: ["cdkOption"] }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "component", type: BadgeComponent, selector: "st-badge", inputs: ["variant", "size", "outline", "soft", "dash", "ghost", "icon", "iconPosition"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ComboboxComponent, decorators: [{
type: Component,
args: [{ selector: 'st-combobox', imports: [FormsModule, ReactiveFormsModule, PortalModule, OverlayModule, CdkListbox, CdkOption, IconComponent, BadgeComponent, TranslatePipe], providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: ComboboxComponent,
multi: true,
},
], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let isOpen = this.isOpen(); @let isAnimatingOpen = this.isAnimatingOpen(); @let triggerWidth = this.triggerWidth();\n@let dataState = this.dataState(); @let filteredOptions = this.filteredOptions(); @let displayValue = this.displayValue();\n@let multipleDisplayInfo = this.multipleDisplayInfo(); @let label = this.label();\n\n<fieldset class=\"fieldset p-0\">\n\t@if (label) {\n\t\t<legend class=\"fieldset-legend font-body pt-0 text-pretty\">{{ label }}</legend>\n\t}\n\n\t<button\n\t\t#trigger\n\t\t[class]=\"validationClass()\"\n\t\t(click)=\"toggleOverlay()\"\n\t\t(keydown)=\"handleTriggerKeydown($event)\"\n\t\ttype=\"button\"\n\t\tcdkOverlayOrigin\n\t\taria-haspopup=\"true\"\n\t\trole=\"combobox\"\n\t\t[attr.aria-controls]=\"'combobox-list'\"\n\t\t[attr.aria-expanded]=\"isOpen\"\n\t\t[disabled]=\"disabled()\"\n\t>\n\t\t<span class=\"flex-1 truncate text-left\">\n\t\t\t@if (selectionType() === 'multiple' && multipleDisplayInfo) {\n\t\t\t\t<span class=\"inline-flex items-center gap-2\">\n\t\t\t\t\t<span class=\"truncate\">{{ multipleDisplayInfo.firstItem }}</span>\n\t\t\t\t\t@if (multipleDisplayInfo.additionalCount > 0) {\n\t\t\t\t\t\t<st-badge [size]=\"badgeClass()\">+{{ multipleDisplayInfo.additionalCount }}</st-badge>\n\t\t\t\t\t}\n\t\t\t\t</span>\n\t\t\t} @else {\n\t\t\t\t{{ displayValue }}\n\t\t\t}\n\t\t</span>\n\t\t<div class=\"inline-flex items-center justify-items-end gap-2\">\n\t\t\t<!-- Clear button for both single and multiple selection -->\n\t\t\t@if (showClearButton()) {\n\t\t\t\t<div class=\"grid translate-x-full place-content-center\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"btn font-body btn-sm btn-secondary h-[1.5em] w-[1.5em] items-center p-0 align-top [font-size:inherit] leading-normal font-medium text-pretty\"\n\t\t\t\t\t\t(click)=\"handleClearSelection($event)\"\n\t\t\t\t\t\t(keydown.enter)=\"handleClearSelection($event)\"\n\t\t\t\t\t\t(keydown.space)=\"handleClearSelection($event)\"\n\t\t\t\t\t\t[attr.aria-label]=\"'sdk.combobox.clearSelection' | translate\"\n\t\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<st-icon [icon]=\"iconClose\"></st-icon>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t}\n\t\t\t<st-icon\n\t\t\t\tclass=\"h-[1.5em] w-[1.5em] flex-0 translate-x-full transition-transform duration-200\"\n\t\t\t\t[class.rotate-180]=\"isAnimatingOpen\"\n\t\t\t\t[icon]=\"iconChevronDown\"\n\t\t\t></st-icon>\n\t\t</div>\n\t</button>\n\n\t<ng-template\n\t\tcdkConnectedOverlay\n\t\t[cdkConnectedOverlayOrigin]=\"trigger\"\n\t\t[cdkConnectedOverlayOpen]=\"isOpen\"\n\t\t[cdkConnectedOverlayWidth]=\"triggerWidth\"\n\t\t[cdkConnectedOverlayMinWidth]=\"triggerWidth\"\n\t\t(detach)=\"handleDetach()\"\n\t\t(overlayOutsideClick)=\"handleClickOutside($event)\"\n\t\t(positionChange)=\"onPositionChange($event)\"\n\t>\n\t\t<div\n\t\t\t[class]=\"'bg-base-200 rounded-box shadow-main border-neutral ' + animationClasses() + ' mt-1 w-full border border-solid'\"\n\t\t\t(animationend)=\"onAnimationEnd($event)\"\n\t\t\t[attr.data-state]=\"dataState\"\n\t\t>\n\t\t\t<!-- Search Input -->\n\t\t\t<div class=\"border-neutral border-b\">\n\t\t\t\t@let searchInputClass = this.searchInputClass();\n\t\t\t\t<div [class]=\"searchInputClass\">\n\t\t\t\t\t<st-icon class=\"text-base-placeholder\" [icon]=\"iconSearch\"></st-icon>\n\t\t\t\t\t<input\n\t\t\t\t\t\t#searchInput\n\t\t\t\t\t\tclass=\"placeholder:text-base-placeholder w-full border-transparent py-0 focus-within:outline-none focus:border-transparent focus:outline-none\"\n\t\t\t\t\t\t[value]=\"currentSearchValue\"\n\t\t\t\t\t\t(input)=\"handleSearchInput($event)\"\n\t\t\t\t\t\t(keydown)=\"handleSearchKeydown($event)\"\n\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t[placeholder]=\"translatedSearchPlaceholder()\"\n\t\t\t\t\t\t[attr.aria-label]=\"'sdk.combobox.searchOptions' | translate\"\n\t\t\t\t\t\t[disabled]=\"!isOpen\"\n\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Options List -->\n\t\t\t@let menuClass = this.menuClass();\n\t\t\t<ul\n\t\t\t\t[class]=\"menuClass\"\n\t\t\t\tcdkListbox\n\t\t\t\tcdkListboxUseActiveDescendant\n\t\t\t\t[cdkListboxMultiple]=\"selectionType() === 'multiple'\"\n\t\t\t\taria-labelledby=\"Combobox options\"\n\t\t\t\trole=\"listbox\"\n\t\t\t\t#optionsList\n\t\t\t\t[tabindex]=\"-1\"\n\t\t\t\t(keydown)=\"handleListboxKeydown($event)\"\n\t\t\t\t(selectionChange)=\"handleSelectionChange($event)\"\n\t\t\t>\n\t\t\t\t@for (option of filteredOptions; track $index) {\n\t\t\t\t\t<li [cdkOption]=\"option\" class=\"group\" [attr.data-option-index]=\"$index\" [class.cdk-option-active]=\"isActiveOption($index)\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"group-[.cdk-option-active]:bg-base-300 font-body flex w-full items-center justify-between rounded-none px-4 py-2 text-left leading-normal font-normal text-pretty transition-colors duration-150 ease-in-out\"\n\t\t\t\t\t\t\t(click)=\"handleOptionSelect(option)\"\n\t\t\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span class=\"flex-1 truncate\">{{ getDisplayValue(option) }}</span>\n\n\t\t\t\t\t\t\t@if (isOptionSelected(option)) {\n\t\t\t\t\t\t\t\t<st-icon class=\"h-4 w-4 shrink-0\" [icon]=\"iconCheck\"></st-icon>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\n\t\t\t\t<!-- Empty state -->\n\t\t\t\t@if (filteredOptions.length === 0) {\n\t\t\t\t\t<li class=\"text-base-content/60 px-4 py-2 text-center\">\n\t\t\t\t\t\t<span>{{ translatedNoResultsText() }}</span>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\n\t\t\t<!-- Clear All button for multiple selection -->\n\t\t\t@if (selectionType() === 'multiple' && allowClearAll() && value() && isArray(value()) && value().length > 0) {\n\t\t\t\t<div class=\"border-base-300 border-t p-2\">\n\t\t\t\t\t<button type=\"button\" class=\"btn btn-outline btn-sm w-full\" (click)=\"clearSelection()\" (keydown)=\"handleClearButtonKeydown($event)\">\n\t\t\t\t\t\t{{ 'sdk.combobox.clearAll' | translate }}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\t</ng-template>\n\n\t<!-- Hidden label for accessibility -->\n\t<label class=\"sr-only\" [for]=\"name()\">{{ label }}</label>\n\n\t<!-- Additional content -->\n\t<ng-content></ng-content>\n</fieldset>\n" }]
}], ctorParameters: () => [] });
/**
* Generated bundle index. Do not edit.
*/
export { ComboboxComponent };
//# sourceMappingURL=sixbell-telco-sdk-components-forms-combobox.mjs.map