@sixbell-telco/sdk
Version:
A collection of reusable components designed for use in Sixbell Telco Angular projects
793 lines (789 loc) • 42.2 kB
JavaScript
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