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