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