UNPKG

@sixbell-telco/sdk

Version:

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

833 lines (820 loc) 45.6 kB
import * as i0 from '@angular/core'; import { input, output, signal, computed, Component, inject, viewChild, contentChild, model, effect } from '@angular/core'; import { IconComponent } from '@sixbell-telco/sdk/components/icon'; import { matKeyboardArrowDown } from '@sixbell-telco/sdk/components/icon/material/baseline'; import { cn } from '@sixbell-telco/sdk/utils/cn'; import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay'; import { NgTemplateOutlet } from '@angular/common'; /** * DropdownTrigger - Trigger element for opening dropdown content * * @remarks * This component acts as the trigger that opens/closes the dropdown content. * It can contain any content (buttons, avatars, text, icons, etc.). * Uses CdkConnectedOverlay for positioning and management. * * @example * ```html * <st-dropdown> * <st-dropdown-trigger> * <button class="btn btn-primary"> * Open Menu * <st-icon [icon]="chevronDown"></st-icon> * </button> * </st-dropdown-trigger> * <st-dropdown-content> * <st-dropdown-item>Item 1</st-dropdown-item> * </st-dropdown-content> * </st-dropdown> * ``` */ class DropdownTriggerComponent { /** Offset X in pixels */ offsetX = input(0); /** Offset Y in pixels */ offsetY = input(0); /** Origin X position */ originX = input('start'); /** Origin Y position */ originY = input('bottom'); /** Overlay X position */ overlayX = input('start'); /** Overlay Y position */ overlayY = input('top'); /** Fallback positions configuration */ fallbacks = input({ enabled: true }); /** Event emitted when dropdown is opened */ opened = output(); /** Event emitted when dropdown is closed */ closed = output(); /** Current open state */ isOpen = signal(false); /** Internal state to control overlay visibility (keeps overlay open during close animation) */ overlayOpen = signal(false); /** Current animation state */ animationState = signal('closed'); /** Current active position configuration */ currentPosition = signal(null); /** * Computed animation direction based on current position * Returns the 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 // overlayY indicates vertical position (top/bottom/center) // overlayX indicates horizontal position (start/end/center) const overlayY = position.overlayY; const overlayX = position.overlayX; // Priority: check Y first, then X if (overlayY === 'top') { return 'top'; // Dropdown appears above } if (overlayY === 'bottom') { return 'bottom'; // Dropdown appears below } if (overlayX === 'end') { return 'left'; // Dropdown appears to the left (end aligns to left side of trigger) } if (overlayX === 'start') { return 'right'; // Dropdown appears to the right (start aligns to right side of trigger) } return 'bottom'; // default }); /** * 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 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}`; }); /** Content template reference */ contentTemplate = null; /** * Computed positions array based on signal inputs */ positions = computed(() => { // Get current values and ensure they are strings const originX = String(this.originX()); const originY = String(this.originY()); const overlayX = String(this.overlayX()); const overlayY = String(this.overlayY()); const offsetX = Number(this.offsetX()); const offsetY = Number(this.offsetY()); // Primary position using direct signal values const primary = { originX, originY, overlayX, overlayY, offsetX, offsetY, }; // If fallbacks are disabled, return only primary position const fallbackConfig = this.fallbacks(); if (!fallbackConfig.enabled) { return [primary]; } // Use custom fallback positions if provided if (fallbackConfig.positions && fallbackConfig.positions.length > 0) { return [primary, ...fallbackConfig.positions]; } // Generate default fallback positions const fallbacks = this.generateDefaultFallbacks(primary); return [primary, ...fallbacks]; }); /** * Generate default fallback positions based on the primary position * Provides comprehensive fallback coverage for all space constraints */ generateDefaultFallbacks(primary) { const fallbacks = []; const primaryOriginX = primary.originX; const primaryOriginY = primary.originY; const primaryOverlayX = primary.overlayX; const primaryOverlayY = primary.overlayY; // Add basic axis flips this.addAxisFlips(fallbacks, primary, primaryOriginX, primaryOriginY, primaryOverlayX, primaryOverlayY); // Add standard dropdown positions this.addStandardPositions(fallbacks, primary); // Add center alignment fallbacks this.addCenterFallbacks(fallbacks, primary, primaryOriginX, primaryOriginY, primaryOverlayX, primaryOverlayY); return fallbacks; } /** * Helper to flip Y position */ getOppositeY(position) { if (position === 'top') return 'bottom'; if (position === 'bottom') return 'top'; return 'center'; } /** * Helper to flip X position */ getOppositeX(position) { if (position === 'start') return 'end'; if (position === 'end') return 'start'; return 'center'; } /** * Helper to create position variant */ createPosition(primary, originX, originY, overlayX, overlayY) { return { ...primary, originX, originY, overlayX, overlayY, }; } /** * Add axis flip fallbacks */ addAxisFlips(fallbacks, primary, primaryOriginX, primaryOriginY, primaryOverlayX, primaryOverlayY) { // Flip Y axis const oppositeY = this.getOppositeY(primaryOriginY); const oppositeOverlayY = this.getOppositeY(primaryOverlayY); fallbacks.push(this.createPosition(primary, primaryOriginX, oppositeY, primaryOverlayX, oppositeOverlayY)); // Flip X axis const oppositeX = this.getOppositeX(primaryOriginX); const oppositeOverlayX = this.getOppositeX(primaryOverlayX); fallbacks.push(this.createPosition(primary, oppositeX, primaryOriginY, oppositeOverlayX, primaryOverlayY)); // Flip both axes fallbacks.push(this.createPosition(primary, oppositeX, oppositeY, oppositeOverlayX, oppositeOverlayY)); } /** * Add standard dropdown positions */ addStandardPositions(fallbacks, primary) { const standardPositions = [ this.createPosition(primary, 'start', 'bottom', 'start', 'top'), // Classic dropdown this.createPosition(primary, 'start', 'top', 'start', 'bottom'), // Dropup this.createPosition(primary, 'end', 'top', 'start', 'top'), // Left side this.createPosition(primary, 'start', 'top', 'end', 'top'), // Right side this.createPosition(primary, 'center', 'bottom', 'center', 'top'), // Center dropdown this.createPosition(primary, 'center', 'top', 'center', 'bottom'), // Center dropup ]; for (const pos of standardPositions) { if (!this.isPositionDuplicate(pos, [primary, ...fallbacks])) { fallbacks.push(pos); } } } /** * Add center alignment fallbacks */ addCenterFallbacks(fallbacks, primary, primaryOriginX, primaryOriginY, primaryOverlayX, primaryOverlayY) { // Center horizontally if (primaryOriginX !== 'center' || primaryOverlayX !== 'center') { fallbacks.push(this.createPosition(primary, 'center', primaryOriginY, 'center', primaryOverlayY)); } // Center vertically if (primaryOriginY !== 'center' || primaryOverlayY !== 'center') { fallbacks.push(this.createPosition(primary, primaryOriginX, 'center', primaryOverlayX, 'center')); } // Fully centered as last resort const fullyCentered = this.createPosition(primary, 'center', 'center', 'center', 'center'); if (!this.isPositionDuplicate(fullyCentered, [primary, ...fallbacks])) { fallbacks.push(fullyCentered); } } /** * Check if position is duplicate */ isPositionDuplicate(position, existing) { return existing.some((pos) => pos.originX === position.originX && pos.originY === position.originY && pos.overlayX === position.overlayX && pos.overlayY === position.overlayY); } /** * Set the content template to be displayed in the overlay */ setContentTemplate(template) { this.contentTemplate = template; } /** * Toggle the dropdown open/close state */ toggle() { if (this.isOpen()) { this.close(); return; } this.open(); } /** * Open the dropdown * Sets up overlay with animation and emits opened event */ open() { if (this.isOpen() || !this.contentTemplate) { return; } this.isOpen.set(true); this.overlayOpen.set(true); this.animationState.set('opening'); this.opened.emit(); } /** * Close the dropdown * Triggers closing animation and emits closed event */ close() { if (!this.isOpen()) { return; } this.animationState.set('closing'); this.closed.emit(); } /** * Handle animation end events * Completes state transitions when animations finish */ onAnimationEnd(event) { const currentState = this.animationState(); // Opening animation complete if (currentState === 'opening' && (event.animationName.includes('fade-in') || event.animationName.includes('morph-in'))) { this.animationState.set('open'); return; } // Closing animation complete if (currentState === 'closing' && event.animationName.includes('fade-out')) { this.animationState.set('closed'); this.isOpen.set(false); this.overlayOpen.set(false); return; } } /** * Handle position changes from CDK overlay */ onPositionChange(position) { if (position?.connectionPair) { this.currentPosition.set(position.connectionPair); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownTriggerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: DropdownTriggerComponent, isStandalone: true, selector: "st-dropdown-trigger", inputs: { offsetX: { classPropertyName: "offsetX", publicName: "offsetX", isSignal: true, isRequired: false, transformFunction: null }, offsetY: { classPropertyName: "offsetY", publicName: "offsetY", isSignal: true, isRequired: false, transformFunction: null }, originX: { classPropertyName: "originX", publicName: "originX", isSignal: true, isRequired: false, transformFunction: null }, originY: { classPropertyName: "originY", publicName: "originY", isSignal: true, isRequired: false, transformFunction: null }, overlayX: { classPropertyName: "overlayX", publicName: "overlayX", isSignal: true, isRequired: false, transformFunction: null }, overlayY: { classPropertyName: "overlayY", publicName: "overlayY", isSignal: true, isRequired: false, transformFunction: null }, fallbacks: { classPropertyName: "fallbacks", publicName: "fallbacks", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "opened", closed: "closed" }, ngImport: i0, template: "<div\n\tcdkOverlayOrigin\n\t#trigger=\"cdkOverlayOrigin\"\n\tclass=\"inline-flex cursor-pointer items-center\"\n\t(click)=\"toggle()\"\n\t(keydown.enter)=\"toggle()\"\n\t(keydown.space)=\"toggle(); $event.preventDefault()\"\n\t(keydown.escape)=\"close()\"\n\t[attr.aria-expanded]=\"isOpen()\"\n\t[attr.aria-haspopup]=\"true\"\n\ttabindex=\"0\"\n\trole=\"button\"\n>\n\t<ng-content></ng-content>\n</div>\n\n<ng-template\n\tcdkConnectedOverlay\n\t[cdkConnectedOverlayPositions]=\"positions()\"\n\t[cdkConnectedOverlayOrigin]=\"trigger\"\n\t[cdkConnectedOverlayOpen]=\"overlayOpen()\"\n\t[cdkConnectedOverlayHasBackdrop]=\"true\"\n\t[cdkConnectedOverlayBackdropClass]=\"'cdk-overlay-transparent-backdrop'\"\n\t[cdkConnectedOverlayDisableClose]=\"true\"\n\t(backdropClick)=\"close()\"\n\t(positionChange)=\"onPositionChange($event)\"\n>\n\t<div [class]=\"animationClasses()\" (animationend)=\"onAnimationEnd($event)\" [attr.data-state]=\"animationState()\">\n\t\t<ng-container *ngTemplateOutlet=\"contentTemplate\"></ng-container>\n\t</div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: 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: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownTriggerComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-trigger', standalone: true, imports: [CdkConnectedOverlay, CdkOverlayOrigin, NgTemplateOutlet], template: "<div\n\tcdkOverlayOrigin\n\t#trigger=\"cdkOverlayOrigin\"\n\tclass=\"inline-flex cursor-pointer items-center\"\n\t(click)=\"toggle()\"\n\t(keydown.enter)=\"toggle()\"\n\t(keydown.space)=\"toggle(); $event.preventDefault()\"\n\t(keydown.escape)=\"close()\"\n\t[attr.aria-expanded]=\"isOpen()\"\n\t[attr.aria-haspopup]=\"true\"\n\ttabindex=\"0\"\n\trole=\"button\"\n>\n\t<ng-content></ng-content>\n</div>\n\n<ng-template\n\tcdkConnectedOverlay\n\t[cdkConnectedOverlayPositions]=\"positions()\"\n\t[cdkConnectedOverlayOrigin]=\"trigger\"\n\t[cdkConnectedOverlayOpen]=\"overlayOpen()\"\n\t[cdkConnectedOverlayHasBackdrop]=\"true\"\n\t[cdkConnectedOverlayBackdropClass]=\"'cdk-overlay-transparent-backdrop'\"\n\t[cdkConnectedOverlayDisableClose]=\"true\"\n\t(backdropClick)=\"close()\"\n\t(positionChange)=\"onPositionChange($event)\"\n>\n\t<div [class]=\"animationClasses()\" (animationend)=\"onAnimationEnd($event)\" [attr.data-state]=\"animationState()\">\n\t\t<ng-container *ngTemplateOutlet=\"contentTemplate\"></ng-container>\n\t</div>\n</ng-template>\n" }] }] }); /** * DropdownChevron - Chevron indicator for dropdown triggers * Automatically rotates based on dropdown state and positions itself correctly */ class DropdownChevronComponent { /** * Inject parent trigger component to access its isOpen signal * Use SkipSelf to get the immediate parent trigger */ trigger = inject(DropdownTriggerComponent, { optional: true, skipSelf: true }); /** * Custom icon to use instead of default arrow down */ icon = input(matKeyboardArrowDown); /** * Additional CSS classes */ class = input(''); /** * Size of the chevron using Icon component size types */ size = input('md'); /** * Color of the chevron using Icon component color types */ color = input('inherit'); /** * Position relative to content */ position = input('right'); /** * Whether the chevron is open (rotated) * Derives from the parent trigger's isOpen signal */ isOpen = computed(() => this.trigger?.isOpen() ?? false); /** * Computed classes for the chevron */ chevronClasses = computed(() => { const marginClasses = { left: 'mr-2', right: 'ml-2', }; return cn(marginClasses[this.position()], 'transition-transform duration-200 ease-in-out', this.isOpen() && 'rotate-180', this.class()); }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownChevronComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: DropdownChevronComponent, isStandalone: true, selector: "st-dropdown-chevron", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<st-icon [icon]=\"icon()\" [size]=\"size()\" [color]=\"color()\" [class]=\"chevronClasses()\"></st-icon>\n", dependencies: [{ kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownChevronComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-chevron', standalone: true, imports: [IconComponent], template: "<st-icon [icon]=\"icon()\" [size]=\"size()\" [color]=\"color()\" [class]=\"chevronClasses()\"></st-icon>\n" }] }] }); /** * DropdownContent - Container for dropdown menu items * * @remarks * This component contains the dropdown menu items and provides them as a template * for the trigger component to display in the overlay. * * @example * ```html * <st-dropdown-content> * <st-dropdown-item>Menu Item 1</st-dropdown-item> * <st-dropdown-item>Menu Item 2</st-dropdown-item> * <div class="divider"></div> * <st-dropdown-item>Menu Item 3</st-dropdown-item> * </st-dropdown-content> * ``` */ class DropdownContentComponent { template = viewChild.required('contentTemplate'); /** Custom CSS classes */ class = input(''); /** Size variant */ size = input('md'); /** Whether to apply shadow */ shadow = input(true); /** Event emitted when close is requested */ closeRequested = output(); /** * Computed CSS classes for the content */ contentClass = () => cn('menu', 'bg-base-200', 'rounded-box', 'border', 'border-neutral', 'min-w-full', 'max-h-72', 'overflow-y-auto', 'overscroll-contain', 'flex-nowrap', { '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', 'shadow-main': this.shadow(), }, this.class()); /** * Request to close the dropdown * This will be handled by the parent dropdown component */ requestClose() { this.closeRequested.emit(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.0", type: DropdownContentComponent, isStandalone: true, selector: "st-dropdown-content", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeRequested: "closeRequested" }, viewQueries: [{ propertyName: "template", first: true, predicate: ["contentTemplate"], descendants: true, isSignal: true }], exportAs: ["dropdownContent"], ngImport: i0, template: "<ng-template #contentTemplate>\n\t<ul [class]=\"contentClass()\">\n\t\t<ng-content></ng-content>\n\t</ul>\n</ng-template>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownContentComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-content', standalone: true, exportAs: 'dropdownContent', template: "<ng-template #contentTemplate>\n\t<ul [class]=\"contentClass()\">\n\t\t<ng-content></ng-content>\n\t</ul>\n</ng-template>\n" }] }] }); /** * DropdownGroup - Groups related dropdown items together * Provides semantic grouping for dropdown items without rendering a wrapper element */ class DropdownGroupComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: DropdownGroupComponent, isStandalone: true, selector: "st-dropdown-group", host: { styleAttribute: "display: contents" }, ngImport: i0, template: "<ng-content></ng-content>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownGroupComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-group', standalone: true, imports: [], host: { style: 'display: contents', }, template: "<ng-content></ng-content>\n" }] }] }); /** * Dropdown - Main container component for dropdown menus * * @remarks * This is the root container that provides a wrapper for trigger and content components. * The trigger and content components coordinate themselves through this parent component. * * @example * ```html * <st-dropdown> * <st-dropdown-trigger [showChevron]="true"> * <button class="btn btn-primary">Open Menu</button> * </st-dropdown-trigger> * <st-dropdown-content> * <st-dropdown-item>Item 1</st-dropdown-item> * <st-dropdown-item>Item 2</st-dropdown-item> * </st-dropdown-content> * </st-dropdown> * ``` * * @example * ```html * <!-- Listen to open/close events --> * <st-dropdown (opened)="onOpened()" (closed)="onClosed()"> * ... * </st-dropdown> * * <!-- Programmatic control with model signal --> * <st-dropdown [(open)]="isDropdownOpen"> * ... * <button (click)="isDropdownOpen.set(true)">Open</button> * <button (click)="isDropdownOpen.set(false)">Close</button> * <div>Is open: {{ isDropdownOpen() }}</div> * </st-dropdown> * ``` */ class DropdownComponent { // Use contentChild() signal instead of @ContentChild decorator trigger = contentChild.required(DropdownTriggerComponent); content = contentChild.required(DropdownContentComponent); /** Model signal for open/close state - allows two-way binding [(open)]="isDropdownOpen" */ open = model(false); /** Event emitted when dropdown is opened - exposed from trigger */ opened = output(); /** Event emitted when dropdown is closed - exposed from trigger */ closed = output(); constructor() { const _prevState = { trigger: null, content: null }; /** * One-time template wiring effect * Fires once when trigger and content become available * Sets up template injection and subscribes to trigger outputs exactly once */ effect(() => { const trigger = this.trigger(); const content = this.content(); if (trigger && content && !_prevState.trigger) { trigger.setContentTemplate(content.template()); _prevState.trigger = trigger; _prevState.content = content; if (trigger.opened) { _prevState.openUnsub = trigger.opened.subscribe(() => { this.opened.emit(); }); } if (trigger.closed) { _prevState.closedUnsub = trigger.closed.subscribe(() => { this.closed.emit(); }); } } }); /** * Model-to-trigger sync effect * Tracks last synced model value to detect external changes * Only syncs when model changes from what was previously synced * Prevents control loops when showcase updates model in response to trigger events */ const _lastSyncedModel = signal(this.open()); effect(() => { const currentModel = this.open(); const lastSynced = _lastSyncedModel(); if (currentModel === lastSynced) { return; } const trigger = this.trigger(); if (!trigger) { return; } const triggerIsOpen = trigger.isOpen(); if (currentModel && !triggerIsOpen) { trigger.open(); } else if (!currentModel && triggerIsOpen) { trigger.close(); } _lastSyncedModel.set(currentModel); }); } /** * Get the current open state */ isOpen = computed(() => this.open()); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.0", type: DropdownComponent, isStandalone: true, selector: "st-dropdown", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", opened: "opened", closed: "closed" }, queries: [{ propertyName: "trigger", first: true, predicate: DropdownTriggerComponent, descendants: true, isSignal: true }, { propertyName: "content", first: true, predicate: DropdownContentComponent, descendants: true, isSignal: true }], exportAs: ["stDropdown"], ngImport: i0, template: "<ng-content></ng-content>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown', standalone: true, exportAs: 'stDropdown', template: "<ng-content></ng-content>\n" }] }], ctorParameters: () => [] }); /** * DropdownItemCheckbox - Checkbox dropdown item * Provides checkbox functionality for dropdown selections */ class DropdownItemCheckboxComponent { dropdown = inject(DropdownComponent, { optional: true }); /** * Whether the checkbox is checked */ checked = input(false); /** * Whether the menu item is disabled */ disabled = input(false); /** * Custom indicator character or text */ indicator = input(''); /** * Whether to show the indicator */ showIndicator = input(true); /** * Custom CSS classes */ class = input(''); /** * Whether to close the dropdown when the item is selected * @defaultValue true */ closeOnSelect = input(false); /** * Emitted when the checkbox state changes */ checkedChanged = output(); /** * Emitted when the menu item is triggered/clicked */ triggered = output(); /** * Computed data state for ARIA */ dataState = computed(() => { return this.checked() ? 'checked' : 'unchecked'; }); /** * Handle item click - toggle checkbox state */ onItemClick() { if (!this.disabled()) { const newChecked = !this.checked(); this.checkedChanged.emit(newChecked); this.triggered.emit(); if (this.closeOnSelect()) { this.dropdown?.trigger()?.close(); } } } /** * Computed CSS classes for the menu item */ itemClass = () => cn('group', 'transition-colors', 'duration-200', { 'menu-disabled pointer-events-none opacity-50': this.disabled(), 'menu-active': this.checked(), }, this.class()); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownItemCheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DropdownItemCheckboxComponent, isStandalone: true, selector: "st-dropdown-item-checkbox", inputs: { checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, indicator: { classPropertyName: "indicator", publicName: "indicator", isSignal: true, isRequired: false, transformFunction: null }, showIndicator: { classPropertyName: "showIndicator", publicName: "showIndicator", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checkedChanged: "checkedChanged", triggered: "triggered" }, ngImport: i0, template: "<li\n\t[class]=\"itemClass()\"\n\t[attr.data-state]=\"dataState()\"\n\t(click)=\"onItemClick()\"\n\t(keydown.enter)=\"onItemClick()\"\n\t(keydown.space)=\"onItemClick()\"\n\t[attr.tabindex]=\"disabled() ? -1 : 0\"\n\trole=\"menuitemcheckbox\"\n\t[attr.aria-checked]=\"checked()\"\n\t[attr.aria-disabled]=\"disabled()\"\n>\n\t<a class=\"flex items-center justify-between\">\n\t\t<span class=\"flex-1\">\n\t\t\t<ng-content></ng-content>\n\t\t</span>\n\t\t@if (showIndicator()) {\n\t\t\t<span\n\t\t\t\tclass=\"ml-auto text-xs transition-opacity duration-300 ease-in-out group-aria-[checked=false]:opacity-0 group-aria-[checked=true]:opacity-100\"\n\t\t\t>\n\t\t\t\t{{ indicator() }}\n\t\t\t</span>\n\t\t}\n\t</a>\n</li>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownItemCheckboxComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-item-checkbox', standalone: true, imports: [], template: "<li\n\t[class]=\"itemClass()\"\n\t[attr.data-state]=\"dataState()\"\n\t(click)=\"onItemClick()\"\n\t(keydown.enter)=\"onItemClick()\"\n\t(keydown.space)=\"onItemClick()\"\n\t[attr.tabindex]=\"disabled() ? -1 : 0\"\n\trole=\"menuitemcheckbox\"\n\t[attr.aria-checked]=\"checked()\"\n\t[attr.aria-disabled]=\"disabled()\"\n>\n\t<a class=\"flex items-center justify-between\">\n\t\t<span class=\"flex-1\">\n\t\t\t<ng-content></ng-content>\n\t\t</span>\n\t\t@if (showIndicator()) {\n\t\t\t<span\n\t\t\t\tclass=\"ml-auto text-xs transition-opacity duration-300 ease-in-out group-aria-[checked=false]:opacity-0 group-aria-[checked=true]:opacity-100\"\n\t\t\t>\n\t\t\t\t{{ indicator() }}\n\t\t\t</span>\n\t\t}\n\t</a>\n</li>\n" }] }] }); /** * DropdownItemRadio - Radio dropdown item * Provides radio button functionality for dropdown selections */ class DropdownItemRadioComponent { dropdown = inject(DropdownComponent, { optional: true }); /** * Whether the radio is checked */ checked = input(false); /** * The value of this radio item */ value = input.required(); /** * Whether the menu item is disabled */ disabled = input(false); /** * Custom indicator character or text */ indicator = input(''); /** * Whether to show the indicator */ showIndicator = input(true); /** * Custom CSS classes */ class = input(''); /** * Whether to close the dropdown when the item is selected * @defaultValue true */ closeOnSelect = input(true); /** * Emitted when the radio state changes */ checkedChanged = output(); /** * Emitted when the menu item is triggered/clicked */ triggered = output(); /** * Computed data state for ARIA */ dataState = computed(() => { return this.checked() ? 'checked' : 'unchecked'; }); /** * Handle item click - select this radio option */ onItemClick() { if (!this.disabled() && !this.checked()) { this.checkedChanged.emit(this.value()); this.triggered.emit(); if (this.closeOnSelect()) { this.dropdown?.trigger()?.close(); } } } /** * Computed CSS classes for the menu item */ itemClass = () => cn('group', 'transition-colors', 'duration-200', { 'menu-disabled pointer-events-none opacity-50': this.disabled(), 'menu-active': this.checked(), }, this.class()); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownItemRadioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DropdownItemRadioComponent, isStandalone: true, selector: "st-dropdown-item-radio", inputs: { checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, indicator: { classPropertyName: "indicator", publicName: "indicator", isSignal: true, isRequired: false, transformFunction: null }, showIndicator: { classPropertyName: "showIndicator", publicName: "showIndicator", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checkedChanged: "checkedChanged", triggered: "triggered" }, ngImport: i0, template: "<li\n\t[class]=\"itemClass()\"\n\t[attr.data-state]=\"dataState()\"\n\t(click)=\"onItemClick()\"\n\t(keydown.enter)=\"onItemClick()\"\n\t(keydown.space)=\"onItemClick()\"\n\t[attr.tabindex]=\"disabled() ? -1 : 0\"\n\trole=\"menuitemradio\"\n\t[attr.aria-checked]=\"checked()\"\n\t[attr.aria-disabled]=\"disabled()\"\n>\n\t<a class=\"flex items-center justify-between\">\n\t\t<span class=\"flex-1\">\n\t\t\t<ng-content></ng-content>\n\t\t</span>\n\t\t@if (showIndicator()) {\n\t\t\t<span\n\t\t\t\tclass=\"ml-auto text-xs transition-opacity duration-300 ease-in-out group-aria-[checked=false]:opacity-0 group-aria-[checked=true]:opacity-100\"\n\t\t\t>\n\t\t\t\t{{ indicator() }}\n\t\t\t</span>\n\t\t}\n\t</a>\n</li>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownItemRadioComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-item-radio', standalone: true, imports: [], template: "<li\n\t[class]=\"itemClass()\"\n\t[attr.data-state]=\"dataState()\"\n\t(click)=\"onItemClick()\"\n\t(keydown.enter)=\"onItemClick()\"\n\t(keydown.space)=\"onItemClick()\"\n\t[attr.tabindex]=\"disabled() ? -1 : 0\"\n\trole=\"menuitemradio\"\n\t[attr.aria-checked]=\"checked()\"\n\t[attr.aria-disabled]=\"disabled()\"\n>\n\t<a class=\"flex items-center justify-between\">\n\t\t<span class=\"flex-1\">\n\t\t\t<ng-content></ng-content>\n\t\t</span>\n\t\t@if (showIndicator()) {\n\t\t\t<span\n\t\t\t\tclass=\"ml-auto text-xs transition-opacity duration-300 ease-in-out group-aria-[checked=false]:opacity-0 group-aria-[checked=true]:opacity-100\"\n\t\t\t>\n\t\t\t\t{{ indicator() }}\n\t\t\t</span>\n\t\t}\n\t</a>\n</li>\n" }] }] }); /** * A selectable item component for use within dropdown menus * * @remarks * Designed to be used inside `<st-dropdown-content>` components. Handles selection events * and automatically closes the parent dropdown when clicked. * * @example * ```html * <st-dropdown-item (selected)="handleItemSelect()"> * <st-icon icon="check"></st-icon> * <span typography>Select Item</span> * </st-dropdown-item> * ``` * * @example * ```html * <st-dropdown-item class="custom-item-style" (selected)="logSelection()" [disabled]="true"> * Disabled item * </st-dropdown-item> * ``` */ class DropdownItemComponent { dropdown = inject(DropdownComponent, { optional: true }); /** * Whether the menu item is active */ active = input(false); /** * Custom CSS classes */ class = input(''); /** * Whether the item is disabled * @defaultValue false */ disabled = input(false); /** * Whether to close the dropdown when the item is selected * @defaultValue true */ closeOnSelect = input(true); /** * Event emitted when the item is selected */ selected = output(); /** * @internal * Handles item selection and dropdown state */ handleSelect() { if (this.disabled()) return; this.selected.emit(); if (this.closeOnSelect()) { this.dropdown?.trigger()?.close(); } } /** * Computed CSS classes for the menu item */ itemClass = () => cn('transition-colors', 'duration-200', { 'menu-disabled pointer-events-none opacity-50': this.disabled(), 'menu-active': this.active(), }, this.class()); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: DropdownItemComponent, isStandalone: true, selector: "st-dropdown-item", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, ngImport: i0, template: "<li\n\t[class]=\"itemClass()\"\n\t(click)=\"handleSelect()\"\n\t(keydown.enter)=\"handleSelect()\"\n\t(keydown.space)=\"handleSelect(); $event.preventDefault()\"\n\t[attr.tabindex]=\"disabled() ? -1 : 0\"\n\trole=\"menuitem\"\n\t[attr.aria-disabled]=\"disabled()\"\n>\n\t<a>\n\t\t<ng-content></ng-content>\n\t</a>\n</li>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownItemComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-item', standalone: true, template: "<li\n\t[class]=\"itemClass()\"\n\t(click)=\"handleSelect()\"\n\t(keydown.enter)=\"handleSelect()\"\n\t(keydown.space)=\"handleSelect(); $event.preventDefault()\"\n\t[attr.tabindex]=\"disabled() ? -1 : 0\"\n\trole=\"menuitem\"\n\t[attr.aria-disabled]=\"disabled()\"\n>\n\t<a>\n\t\t<ng-content></ng-content>\n\t</a>\n</li>\n" }] }] }); /** * DropdownLabel - Label for dropdown sections * Provides section headers for dropdown content */ class DropdownLabelComponent { /** * Custom CSS classes */ class = input(''); /** * Computed CSS classes for the label */ labelClass = () => cn('menu-title', this.class()); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: DropdownLabelComponent, isStandalone: true, selector: "st-dropdown-label", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div [class]=\"labelClass()\">\n\t<ng-content></ng-content>\n</div>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownLabelComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-label', standalone: true, template: "<div [class]=\"labelClass()\">\n\t<ng-content></ng-content>\n</div>\n" }] }] }); /** * DropdownSeparator - Visual separator for dropdown menu sections * * @remarks * A simple horizontal divider to separate groups of dropdown items. * * @example * ```html * <st-dropdown-content> * <st-dropdown-item>Item 1</st-dropdown-item> * <st-dropdown-item>Item 2</st-dropdown-item> * <st-dropdown-separator></st-dropdown-separator> * <st-dropdown-item>Item 3</st-dropdown-item> * </st-dropdown-content> * ``` */ class DropdownSeparatorComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownSeparatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: DropdownSeparatorComponent, isStandalone: true, selector: "st-dropdown-separator", ngImport: i0, template: "<li class=\"pointer-events-none top-0 left-0 mx-0 my-1 w-full p-0\"></li>\n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropdownSeparatorComponent, decorators: [{ type: Component, args: [{ selector: 'st-dropdown-separator', standalone: true, template: "<li class=\"pointer-events-none top-0 left-0 mx-0 my-1 w-full p-0\"></li>\n" }] }] }); /** * Generated bundle index. Do not edit. */ export { DropdownChevronComponent, DropdownComponent, DropdownContentComponent, DropdownGroupComponent, DropdownItemCheckboxComponent, DropdownItemComponent, DropdownItemRadioComponent, DropdownLabelComponent, DropdownSeparatorComponent, DropdownTriggerComponent }; //# sourceMappingURL=sixbell-telco-sdk-components-dropdown.mjs.map