UNPKG

@sixbell-telco/sdk

Version:

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

357 lines (349 loc) 19.3 kB
import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { input, computed, ChangeDetectionStrategy, Component, signal, contentChild, model, output, inject, viewChild, effect } from '@angular/core'; import { cn } from '@sixbell-telco/sdk/utils/cn'; import { TypographyDirective } from '@sixbell-telco/sdk/directives/typography'; import { Dialog } from '@angular/cdk/dialog'; import { Overlay } from '@angular/cdk/overlay'; import { cva } from 'class-variance-authority'; import { Subscription } from 'rxjs'; class DialogActionsComponent { // Base table properties alignment = input('left'); componentClass = computed(() => cn('modal-action text-pretty font-body leading-normal', { 'justify-start': this.alignment() === 'left', 'justify-end': this.alignment() === 'right', 'justify-center': this.alignment() === 'center', })); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogActionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: DialogActionsComponent, isStandalone: true, selector: "st-dialog-actions", inputs: { alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div [class]=\"componentClass()\">\n\t<ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogActionsComponent, decorators: [{ type: Component, args: [{ selector: 'st-dialog-actions', imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"componentClass()\">\n\t<ng-content></ng-content>\n</div>\n" }] }] }); class DialogContentComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: DialogContentComponent, isStandalone: true, selector: "st-dialog-content", ngImport: i0, template: "<ng-content></ng-content>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogContentComponent, decorators: [{ type: Component, args: [{ selector: 'st-dialog-content', imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>\n" }] }] }); class DialogSubTitleComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogSubTitleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: DialogSubTitleComponent, isStandalone: true, selector: "st-dialog-sub-title", ngImport: i0, template: "<div class=\"text-base-content/80 mb-3\">\n\t<p typography tyFontWeight=\"normal\" tyColor=\"inherit\" tyVariant=\"h6\">\n\t\t<ng-content></ng-content>\n\t</p>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogSubTitleComponent, decorators: [{ type: Component, args: [{ selector: 'st-dialog-sub-title', imports: [CommonModule, TypographyDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"text-base-content/80 mb-3\">\n\t<p typography tyFontWeight=\"normal\" tyColor=\"inherit\" tyVariant=\"h6\">\n\t\t<ng-content></ng-content>\n\t</p>\n</div>\n" }] }] }); class DialogTitleComponent { /** Internal signal that indicates whether a subtitle is present in the dialog */ hasSubTitle = signal(false); setHasSubTitle(value) { this.hasSubTitle.set(value); } componentClass = computed(() => cn({ 'mb-3': !this.hasSubTitle(), 'mb-1': this.hasSubTitle(), })); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogTitleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: DialogTitleComponent, isStandalone: true, selector: "st-dialog-title", ngImport: i0, template: "<div [class]=\"componentClass()\">\n\t<h2 typography tyFontWeight=\"medium\" tyColor=\"base\" tyVariant=\"h2\">\n\t\t<ng-content></ng-content>\n\t</h2>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogTitleComponent, decorators: [{ type: Component, args: [{ selector: 'st-dialog-title', imports: [CommonModule, TypographyDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"componentClass()\">\n\t<h2 typography tyFontWeight=\"medium\" tyColor=\"base\" tyVariant=\"h2\">\n\t\t<ng-content></ng-content>\n\t</h2>\n</div>\n" }] }] }); /** * CSS class generator for dialog component variants * Uses carefully selected DaisyUI classes that don't conflict with CDK positioning * @internal */ const dialogComponent = cva([ 'bg-base-200', 'rounded-box', 'shadow-backdrop', 'p-6', 'text-base-content', 'relative', 'data-[state=closed]:animate-exit data-[state=open]:animate-enter', ], { variants: { width: { xs: ['w-64'], sm: ['w-80'], md: ['w-96'], lg: ['w-[32rem]'], xl: ['w-[36rem]'], '2xl': ['w-[42rem]'], '3xl': ['w-[48rem]'], '4xl': ['w-[56rem]'], '5xl': ['w-[64rem]'], }, }, compoundVariants: [], defaultVariants: { width: 'md', }, }); /** * A customizable dialog component using Angular CDK Dialog for declarative usage * * @remarks * This component provides a declarative way to create dialogs with content projection. * Uses CDK Dialog under the hood to avoid z-index issues with native dialog elements. * * @example * ```html * <st-dialog * [(opened)]="isDialogOpen" * width="lg" * alignment="center" * (closed)="handleDialogClosed()"> * <st-dialog-title>Confirm Action</st-dialog-title> * <st-dialog-content> * Are you sure you want to proceed? * </st-dialog-content> * <st-dialog-actions> * <st-button (click)="confirmAction()">Confirm</st-button> * <st-button variant="secondary" (click)="closeDialog()">Cancel</st-button> * </st-dialog-actions> * </st-dialog> * ``` */ class DialogComponent { /** @internal Content children to detect what's projected */ titleComponent = contentChild(DialogTitleComponent); subTitleComponent = contentChild(DialogSubTitleComponent); /** @internal Signals for conditional styling */ hasTitle = signal(false); hasSubTitle = signal(false); ngAfterContentInit() { const titleInstance = typeof this.titleComponent === 'function' ? this.titleComponent() : this.titleComponent; const subTitleInstance = typeof this.subTitleComponent === 'function' ? this.subTitleComponent() : this.subTitleComponent; const hasSub = !!subTitleInstance; this.hasTitle.set(!!titleInstance); this.hasSubTitle.set(hasSub); titleInstance?.setHasSubTitle?.(hasSub); } /** * Specifies the maximum width of the dialog content * @defaultValue 'md' */ width = input('md'); /** * Controls the horizontal positioning of the dialog (x-axis) * @defaultValue 'center' */ xPosition = input('center'); /** * Controls the vertical positioning of the dialog (y-axis) * @defaultValue 'center' */ yPosition = input('center'); /** * Controls the visibility state of the dialog * Supports two-way binding via Angular's model */ opened = model(false); /** Event emitted when the dialog closes */ closed = output(); /** Event emitted when clicking outside the dialog content */ clickOutside = output(); /** @internal CDK Dialog service */ dialog = inject(Dialog); /** @internal CDK Overlay service for positioning */ overlay = inject(Overlay); /** @internal Reference to the dialog template */ dialogTemplate = viewChild('dialogTemplate'); /** @internal Reference to the current open dialog */ dialogRef = null; /** @internal Subscriptions for CDK dialog observables so we can clean up on destroy */ subs = new Subscription(); /** @internal Animation state signal for controlling CSS animations */ animationState = signal('closed'); /** @internal Flag to track if we're in the process of closing */ isClosing = false; /** * Computes the data-state attribute value for CSS animations * @returns Current animation state * @internal */ dataState = computed(() => this.animationState()); /** * Computes CSS classes for dialog content based on current configuration * @returns Combined Tailwind classes string * @internal */ componentClass = computed(() => { return cn(dialogComponent({ width: this.width(), })); }); /** @internal Handles open/close state changes */ openedEffect = effect(() => { if (this.opened()) { this.openDialog(); } else { this.closeDialog(); } }); /** * Computed CDK Dialog configuration based on x/y axis signals * Uses CDK's overlay positioning system for full axis control * @internal */ dialogConfig = computed(() => { const x = this.xPosition(); const y = this.yPosition(); const baseConfig = { hasBackdrop: true, disableClose: true, // Prevent automatic close, we handle it manually for animations autoFocus: true, restoreFocus: true, backdropClass: ['bg-backdrop'], panelClass: ['st-dialog-panel'], // Use noop scroll strategy to prevent body scroll changes that affect sticky navbar scrollStrategy: this.overlay.scrollStrategies.noop(), }; let positionStrategy = this.overlay.position().global(); // X axis switch (x) { case 'start': positionStrategy = positionStrategy.left('2rem'); break; case 'end': positionStrategy = positionStrategy.right('2rem'); break; default: positionStrategy = positionStrategy.centerHorizontally(); break; } // Y axis switch (y) { case 'top': positionStrategy = positionStrategy.top('2rem'); break; case 'bottom': positionStrategy = positionStrategy.bottom('2rem'); break; default: positionStrategy = positionStrategy.centerVertically(); break; } return { ...baseConfig, positionStrategy, }; }); /** * Opens the dialog using CDK Dialog * @internal */ openDialog() { if (this.dialogRef || !this.dialogTemplate()) { return; } this.dialogRef = this.dialog.open(this.dialogTemplate(), this.dialogConfig()); // Set open state for animation this.animationState.set('open'); this.isClosing = false; // Handle dialog close events this.subs.add(this.dialogRef.closed.subscribe(() => { // tear down current subscriptions to avoid re-entrancy or emits after destroy this.subs.unsubscribe(); // recreate the subscription container so future opens can re-subscribe this.subs = new Subscription(); this.dialogRef = null; this.animationState.set('closed'); this.isClosing = false; if (this.opened()) { this.opened.set(false); this.closed.emit(); } })); // Handle backdrop clicks this.subs.add(this.dialogRef.backdropClick.subscribe(() => { this.clickOutside.emit(); this.close(); })); // Handle escape key this.subs.add(this.dialogRef.keydownEvents.subscribe((event) => { if (event.key === 'Escape') { this.close(); } })); } /** * Public method to close the dialog * Can be called directly from parent components or child components */ close() { // If we have an active dialogRef, close it immediately so consumers (and tests) // that spy on dialogRef.close() observe the call. if (this.dialogRef) { this.dialogRef.close(); this.dialogRef = null; } // Ensure opened flag is synchronized and notify listeners that the dialog closed this.opened.set(false); this.closed.emit(); } /** * Closes the dialog using CDK Dialog * @internal */ closeDialog() { if (this.dialogRef && !this.isClosing) { // Set closing flag and closed state for animation this.isClosing = true; this.animationState.set('closed'); // The actual CDK close will be triggered by the animation end event } } /** * Handles animation end events to trigger the actual dialog close * @internal */ onAnimationEnd(event) { const target = event.target; // Only handle animations on the main dialog element if (target.hasAttribute('data-state') && this.isClosing && this.dialogRef) { // Animation finished, now actually close the CDK dialog this.dialogRef.close(); } } /** * Clean up CDK subscriptions and ensure the overlay is closed when the component is destroyed */ ngOnDestroy() { // Unsubscribe from any dialog-related observables to prevent handlers running after destroy this.subs.unsubscribe(); // If a dialog is still open, close it now. We call close after unsubscribing to avoid // receiving the closed event while this component is being torn down. if (this.dialogRef) { this.dialogRef.close?.(); this.dialogRef = null; } } /** * Handles the close button click * @internal */ handleCloseClick() { this.close(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.0", type: DialogComponent, isStandalone: true, selector: "st-dialog", inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, xPosition: { classPropertyName: "xPosition", publicName: "xPosition", isSignal: true, isRequired: false, transformFunction: null }, yPosition: { classPropertyName: "yPosition", publicName: "yPosition", isSignal: true, isRequired: false, transformFunction: null }, opened: { classPropertyName: "opened", publicName: "opened", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "openedChange", closed: "closed", clickOutside: "clickOutside" }, queries: [{ propertyName: "titleComponent", first: true, predicate: DialogTitleComponent, descendants: true, isSignal: true }, { propertyName: "subTitleComponent", first: true, predicate: DialogSubTitleComponent, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "dialogTemplate", first: true, predicate: ["dialogTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Dialog template for CDK Dialog -->\n<ng-template #dialogTemplate>\n\t<!-- Dialog content with data-driven animations -->\n\t<div [class]=\"componentClass()\" [attr.data-state]=\"dataState()\" (animationend)=\"onAnimationEnd($event)\">\n\t\t<!-- Close button -->\n\t\t<div class=\"absolute top-2 right-2\">\n\t\t\t<button class=\"btn btn-circle btn-ghost btn-sm\" (click)=\"handleCloseClick()\" aria-label=\"Close dialog\">\u2715</button>\n\t\t</div>\n\n\t\t<!-- Content projection -->\n\t\t<ng-content select=\"st-dialog-title\"></ng-content>\n\t\t<ng-content select=\"st-dialog-sub-title\"></ng-content>\n\t\t<ng-content select=\"st-dialog-content\"></ng-content>\n\n\t\t<!-- Actions section with spacing -->\n\t\t<div class=\"flex justify-end gap-2 pt-4\">\n\t\t\t<ng-content select=\"st-dialog-actions\"></ng-content>\n\t\t</div>\n\t</div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DialogComponent, decorators: [{ type: Component, args: [{ selector: 'st-dialog', imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Dialog template for CDK Dialog -->\n<ng-template #dialogTemplate>\n\t<!-- Dialog content with data-driven animations -->\n\t<div [class]=\"componentClass()\" [attr.data-state]=\"dataState()\" (animationend)=\"onAnimationEnd($event)\">\n\t\t<!-- Close button -->\n\t\t<div class=\"absolute top-2 right-2\">\n\t\t\t<button class=\"btn btn-circle btn-ghost btn-sm\" (click)=\"handleCloseClick()\" aria-label=\"Close dialog\">\u2715</button>\n\t\t</div>\n\n\t\t<!-- Content projection -->\n\t\t<ng-content select=\"st-dialog-title\"></ng-content>\n\t\t<ng-content select=\"st-dialog-sub-title\"></ng-content>\n\t\t<ng-content select=\"st-dialog-content\"></ng-content>\n\n\t\t<!-- Actions section with spacing -->\n\t\t<div class=\"flex justify-end gap-2 pt-4\">\n\t\t\t<ng-content select=\"st-dialog-actions\"></ng-content>\n\t\t</div>\n\t</div>\n</ng-template>\n" }] }] }); /** * Generated bundle index. Do not edit. */ export { DialogActionsComponent, DialogComponent, DialogContentComponent, DialogSubTitleComponent, DialogTitleComponent, dialogComponent }; //# sourceMappingURL=sixbell-telco-sdk-components-dialog.mjs.map