UNPKG

@sixbell-telco/sdk

Version:

A collection of reusable components designed for use in Sixbell Telco Angular projects

793 lines (789 loc) 42.2 kB
import * as i0 from '@angular/core'; import { inject, input, model, output, signal, viewChild, computed, ChangeDetectionStrategy, Component } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { cn } from '@sixbell-telco/sdk/utils/cn'; import { cva } from 'class-variance-authority'; import { switchMap, startWith } from 'rxjs'; 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 { IconComponent } from '@sixbell-telco/sdk/components/icon'; import { matKeyboardArrowDownOutline, matCheckOutline, matCloseOutline } from '@sixbell-telco/sdk/components/icon/material/outline'; /* eslint-disable @typescript-eslint/no-explicit-any */ // ============================================================================== // COMPONENT STYLES // ============================================================================== /** * @internal * Generates base select classes with style variants */ const selectComponent = 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 select dropdown component with CDK overlay integration * * @remarks * Uses the same visual design as the combobox but simplified for single selection only. * No search functionality or multi-select support. Implements ControlValueAccessor * for Angular form compatibility. * * @example * ```html * <!-- Basic usage with primitive values --> * <st-select * [options]="['Option 1', 'Option 2']" * [(value)]="selectedValue" * ></st-select> * ``` * * @example * ```html * <!-- Object options with custom keys --> * <st-select * [options]="users" * valueKey="id" * displayKey="name" * label="Select user" * [parentForm]="userForm" * formControlName="selectedUser" * ></st-select> * ``` */ class SelectComponent { // ============================================================================== // SERVICES // ============================================================================== /** @internal Translation service instance */ translateService = inject(TranslateService); // ============================================================================== // INPUT PROPERTIES // ============================================================================== /** * Select dropdown style variant * @defaultValue 'secondary' */ variant = input('secondary'); /** * Select dropdown size variant * @defaultValue 'md' */ size = input('md'); /** * Whether to use ghost (transparent) styling * @defaultValue false */ ghost = input(false); /** * HTML name attribute for the select */ name = input(null); /** * Placeholder text when no option is selected */ placeholder = input(''); /** * Label text displayed above the select */ label = input(''); /** * Two-way bindable selected value */ value = model(); /** * Array of available options (primitives or objects) */ 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(''); /** * Whether to show a clear button next to the selected value * @default true */ allowClear = input(true); /** * Parent form group for reactive forms */ parentForm = input(null); /** * Form control name for reactive forms */ formControlName = input(''); /** * Whether the select is disabled * @defaultValue false */ disabled = model(false); /** * Event emitted when select loses focus */ blurred = output(); /** * Event emitted when selection changes */ valueUpdated = output(); // ============================================================================== // ICONS // ============================================================================== /** Dropdown chevron icon */ iconChevronDown = matKeyboardArrowDownOutline; /** Check icon for selected options */ iconCheck = matCheckOutline; /** Close/clear icon */ iconClose = matCloseOutline; // ============================================================================== // PRIVATE PROPERTIES // ============================================================================== /** 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 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); /** Tracks the overlay position to determine animation direction */ 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 overlay positioning * Detects actual position from CDK overlay and returns appropriate direction */ animationDirection = computed(() => { const position = this.currentPosition(); if (!position) return 'bottom'; // default const overlayY = position?.connectionPair?.overlayY; const overlayX = position?.connectionPair?.overlayX; if (overlayY === 'top') return 'top'; else if (overlayY === 'bottom') return 'bottom'; else if (overlayX === 'end') return 'left'; else if (overlayX === 'start') return 'right'; return 'bottom'; }); /** * 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 = () => { }; // ============================================================================== // COMPUTED PROPERTIES // ============================================================================== /** * 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 * @returns Formatted display string for the current selection */ displayValue = computed(() => { const currentValue = this.value(); if (!currentValue) return this.translatedPlaceholder(); // If the current value is a string that matches one of the options, display it const options = this.options(); const matchingOption = options.find((option) => { if (typeof option === 'string' && typeof currentValue === 'string') { return option === currentValue; } // For objects, check if the current value matches the option or its value property const optionValue = this.getOptionValue(option); const currentOptionValue = this.getOptionValue(currentValue); return optionValue === currentOptionValue || option === currentValue; }); if (matchingOption) { return this.getDisplayValue(matchingOption); } // If no matching option found, show placeholder (invalid value) return this.translatedPlaceholder(); }); /** * 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; // Only show clear button if the value is valid (exists in options) const options = this.options(); return options.some((option) => { if (typeof option === 'string' && typeof currentValue === 'string') { return option === currentValue; } // For objects, check if the current value matches the option or its value property const optionValue = this.getOptionValue(option); const currentOptionValue = this.getOptionValue(currentValue); return optionValue === currentOptionValue || option === currentValue; }); }); /** * Translated placeholder text * @returns Translated placeholder or input value */ translatedPlaceholder = computed(() => { const customPlaceholder = this.placeholder(); return customPlaceholder || this.translateService.instant('sdk.select.placeholder'); }); /** * @internal * Computed base select classes */ componentClass = computed(() => { return cn(selectComponent({ variant: this.variant(), size: this.size(), }), { 'select-ghost': this.ghost() }); }); /** * @internal * Computed error state classes */ errorClass = computed(() => { return cn(selectComponent({ variant: 'error', size: this.size(), }), { 'select-ghost': this.ghost() }); }); /** * @internal * Computed success state classes */ successClass = computed(() => { return cn(selectComponent({ 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(); 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 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', }); }); // ============================================================================== // 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 exists in the options if (!value) { this.value.set(value); return; } const options = this.options(); const isValidValue = 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; }); // Only set the value if it's valid, otherwise set null this.value.set(isValidValue ? 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; } /** * Sets the disabled state of the component * @param isDisabled - Whether the component should be disabled */ setDisabledState(isDisabled) { this.disabled.set(isDisabled); } // ============================================================================== // 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'); // Set first option as active and focus listbox after opening setTimeout(() => { this.setDefaultActiveOption(); // Focus the listbox for keyboard navigation this.optionsList()?.nativeElement?.focus(); }, 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.activeOptionIndex.set(-1); this.triggerBlur(); // Return focus to trigger setTimeout(() => { this.triggerRef()?.nativeElement?.focus(); }, 0); } } /** * Handles overlay detachment * Cleans up state and triggers blur */ handleDetach() { this.animationState.set('closed'); this.activeOptionIndex.set(-1); this.triggerBlur(); // Return focus to trigger setTimeout(() => { this.triggerRef()?.nativeElement?.focus(); }, 0); } /** * 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 option selection * @param option - The selected option */ handleOptionSelect(option) { this.value.set(option); this.onChange(option); this.onTouched(); this.valueUpdated.emit(option); this.animationState.set('closing'); } /** * 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) { const options = this.options(); const currentIndex = this.activeOptionIndex(); switch (event.key) { case 'ArrowDown': { event.preventDefault(); event.stopPropagation(); const nextIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0; this.setActiveOption(nextIndex); break; } case 'ArrowUp': { event.preventDefault(); event.stopPropagation(); const prevIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1; this.setActiveOption(prevIndex); break; } case 'Enter': case ' ': event.preventDefault(); event.stopPropagation(); if (currentIndex >= 0 && currentIndex < options.length) { this.handleOptionSelect(options[currentIndex]); } break; case 'Escape': event.preventDefault(); event.stopPropagation(); this.animationState.set('closing'); this.activeOptionIndex.set(-1); break; } } /** * 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() { this.value.set(null); this.onChange(null); this.onTouched(); this.valueUpdated.emit(null); } /** * 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 currentValue = this.value(); if (!currentValue) return false; // Direct comparison for strings if (typeof option === 'string' && typeof currentValue === 'string') { return option === currentValue; } // For objects or mixed types, compare values const optionValue = this.getOptionValue(option); const currentValueForComparison = this.getOptionValue(currentValue); // Also check direct object equality return optionValue === currentValueForComparison || option === currentValue; } /** * 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); } // ============================================================================== // 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; } /** * Triggers blur event for form validation */ triggerBlur() { this.blurTrigger.update((val) => val + 1); this.onTouched(); this.blurred.emit(this.value()); } /** * 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 options = this.options(); if (options.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; for (const option of Array.from(allOptions)) { 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' }); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: SelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: SelectComponent, isStandalone: true, selector: "st-select", 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 }, label: { classPropertyName: "label", publicName: "label", 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 }, allowClear: { classPropertyName: "allowClear", publicName: "allowClear", 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 }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", blurred: "blurred", valueUpdated: "valueUpdated" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: SelectComponent, multi: true, }, ], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], 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 options = this.options(); @let displayValue = this.displayValue();\n@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]=\"'select-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{{ displayValue }}\n\t\t</span>\n\t\t<div class=\"inline-flex items-center justify-items-end gap-2\">\n\t\t\t<!-- Clear button -->\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]=\"'Clear selection'\"\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<!-- 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\taria-labelledby=\"Select options\"\n\t\t\t\trole=\"listbox\"\n\t\t\t\t#optionsList\n\t\t\t\t[tabindex]=\"0\"\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 options; 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 (options.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>No options available</span>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\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: 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: "ngmodule", type: PortalModule }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: SelectComponent, decorators: [{ type: Component, args: [{ selector: 'st-select', imports: [FormsModule, OverlayModule, PortalModule, CdkListbox, CdkOption, IconComponent], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: SelectComponent, multi: true, }, ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let isOpen = this.isOpen(); @let isAnimatingOpen = this.isAnimatingOpen(); @let triggerWidth = this.triggerWidth();\n@let dataState = this.dataState(); @let options = this.options(); @let displayValue = this.displayValue();\n@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]=\"'select-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{{ displayValue }}\n\t\t</span>\n\t\t<div class=\"inline-flex items-center justify-items-end gap-2\">\n\t\t\t<!-- Clear button -->\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]=\"'Clear selection'\"\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<!-- 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\taria-labelledby=\"Select options\"\n\t\t\t\trole=\"listbox\"\n\t\t\t\t#optionsList\n\t\t\t\t[tabindex]=\"0\"\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 options; 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 (options.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>No options available</span>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\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" }] }] }); /** * Generated bundle index. Do not edit. */ export { SelectComponent }; //# sourceMappingURL=sixbell-telco-sdk-components-forms-select.mjs.map