diff --git a/apps/layout-test/src/app/app.component.html b/apps/layout-test/src/app/app.component.html index d48f12ae..cfd4bd51 100644 --- a/apps/layout-test/src/app/app.component.html +++ b/apps/layout-test/src/app/app.component.html @@ -13,4 +13,7 @@
+ + + diff --git a/apps/layout-test/src/app/app.component.ts b/apps/layout-test/src/app/app.component.ts index f3011260..8d524428 100644 --- a/apps/layout-test/src/app/app.component.ts +++ b/apps/layout-test/src/app/app.component.ts @@ -1,7 +1,10 @@ import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { tap } from 'rxjs'; +import { ModalComponent } from '../modal/modal.component'; import { NgxMediaQueryService } from '@ngx/utils'; +import { NgxModalService } from '@ngx/inform'; @Component({ selector: 'app-root', @@ -10,7 +13,10 @@ import { NgxMediaQueryService } from '@ngx/utils'; imports: [RouterModule], }) export class AppComponent { - constructor(private readonly mediaService: NgxMediaQueryService) { + constructor( + private readonly mediaService: NgxMediaQueryService, + private readonly modalService: NgxModalService + ) { // Wouter: To see these in action, navigate to '/queries' in the browser this.mediaService.registerMediaQueries( ['small', '(max-width: 500px)'], @@ -18,4 +24,31 @@ export class AppComponent { ['large', '(max-width: 700px)'] ); } + + public sayHello(): void { + this.modalService + .open({ + component: ModalComponent, + label: 'Modal', + role: 'dialog', + }) + .pipe( + tap((action) => { + if (action === 'Test') { + console.log('Hello!'); + } + }) + ) + .subscribe(); + } + + confirm(): void { + this.modalService + .open({ + type: 'confirm', + describedById: 'id', + labelledById: 'hello', + }) + .subscribe(); + } } diff --git a/apps/layout-test/src/app/app.config.ts b/apps/layout-test/src/app/app.config.ts index efce3793..e5f4ccf2 100644 --- a/apps/layout-test/src/app/app.config.ts +++ b/apps/layout-test/src/app/app.config.ts @@ -8,9 +8,10 @@ import { import { TourItemComponent } from '../tour/tour.component'; import { routes } from '../routes'; import { TooltipComponent } from '../tooltip/tooltip.component'; +import { ConfirmModalComponent } from '../modal/confirm.component'; import { provideNgxDisplayContentConfiguration } from '@ngx/layout'; import { provideNgxTourConfiguration } from '@ngx/tour'; -import { provideNgxTooltipConfiguration } from '@ngx/inform'; +import { provideNgxModalConfiguration, provideNgxTooltipConfiguration } from '@ngx/inform'; export const appConfig: ApplicationConfig = { providers: [ @@ -25,6 +26,14 @@ export const appConfig: ApplicationConfig = { }), provideNgxTourConfiguration(TourItemComponent), provideNgxTooltipConfiguration({ component: TooltipComponent }), + provideNgxModalConfiguration({ + modals: { + confirm: { + component: ConfirmModalComponent, + role: 'alertdialog', + }, + }, + }), provideRouter(routes), ], }; diff --git a/apps/layout-test/src/modal/confirm.component.ts b/apps/layout-test/src/modal/confirm.component.ts new file mode 100644 index 00000000..72b47c48 --- /dev/null +++ b/apps/layout-test/src/modal/confirm.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { NgxModalAbstractComponent } from '@ngx/inform'; + +@Component({ + selector: 'confirm-modal', + template: ` + Confirm this please + + + `, + styleUrl: './modal.component.scss', + standalone: true, +}) +export class ConfirmModalComponent extends NgxModalAbstractComponent<'Confirm'> {} diff --git a/apps/layout-test/src/modal/modal.component.scss b/apps/layout-test/src/modal/modal.component.scss new file mode 100644 index 00000000..e017738b --- /dev/null +++ b/apps/layout-test/src/modal/modal.component.scss @@ -0,0 +1,6 @@ +:host { + display: block; + background: white; + border: 1px solid black; + padding: 15px; +} diff --git a/apps/layout-test/src/modal/modal.component.ts b/apps/layout-test/src/modal/modal.component.ts new file mode 100644 index 00000000..fd730be1 --- /dev/null +++ b/apps/layout-test/src/modal/modal.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { NgxModalAbstractComponent } from '@ngx/inform'; + +@Component({ + selector: 'test-modal', + template: ` + Hello world! + + + `, + styleUrl: './modal.component.scss', + standalone: true, +}) +export class ModalComponent extends NgxModalAbstractComponent<'Test'> {} diff --git a/libs/inform/README.md b/libs/inform/README.md index a5e9659f..24665d69 100644 --- a/libs/inform/README.md +++ b/libs/inform/README.md @@ -21,9 +21,11 @@ For more information about the build process, authors, contributions and issues, ## Concept -`ngx-inform` is a package to help facilitate common user information use-cases such as tooltips, toasts and snackbars. +`ngx-inform` is a package to help facilitate common user information use-cases such as tooltips, toasts and snackbars. -Currently the package provides a `ngxTooltip` directive which can be used to attach a customizable ARIA compliant tooltip to any component. +At its core, `ngx-inform` is build to be WCAG and ARIA compliant, and will throw errors whenever the necessary setup has not been provided to ensure said compliancy. + +Currently the package provides a `ngxTooltip` directive which can be used to attach a customizable ARIA compliant tooltip to any component and the `ngxModalService` which allows for both custom and predefined global modals to be used throughout the application. ## Directives @@ -84,4 +86,106 @@ On top of these two inputs, we have two additional inputs, being `ngxTooltipComp

Title 1

``` -When you wish to disable a tooltip and thus prevent it from being shown, you can use the `ngxTooltipDisabled` property. By default, this property is false. +When you wish to disable a tooltip and thus prevent it from being shown, you can use the `ngxTooltipDisabled` property. By default, this property is `false`. + +## Services + +### NgxModalService + +The `NgxModalService` provides a WCAG/ARIA compliant approach to the Angular CDK `Dialog` service. It is important to understand that, unlike the Dialog service of the CDK, `ngx-inform` **will enforce WCAG/ARIA compliance**. Because of that, certain configuration of the CDK Dialog becomes mandatory and other options that would result in incompliance have been disabled. + +### Setup + +You can use the `NgxModalService` without any prior setup, but the `ngx-inform` package does provide the ability to provide a global configuration that can be applied for all modals. On top of that, you can provide default modals with your specific configuration. + +``` ts + provideNgxModalConfiguration({ + closeOnNavigation: true, + autoClose: true + modals: { + confirm: { + component: ConfirmModalComponent, + role: 'alertdialog', + panelClass: 'panel-confirm', + }, + }, + }), +``` + +Using the above configuration, we can set several properties that will be applied to the modals globally. These properties are: +| Property | | +| ------------------------- | --------------------------------------------------------------------------------------------------------------- | +| closeOnNavigation | Whether the modal closes on navigation. By default, this is `true` | +| direction | The reading direction of the modal. | +| hasBackdrop | Whether or not we wish to set a backdrop. By default, this is `true`. | +| panelClass | A class set to the `overlay` element. By default, this is an empty array. | +| autoClose | Whether the modal automatically closes after the initial interaction emitted by the `action` output. By default, this is `true`. | + +On top of that, by passing a `modals` record, we can define a set preset modals we can use throughout the entire application. We can setup default modals for confirmation, navigating away from a route, etc. Next to overwrites of the global properties above, we can also provide the following properties: + +| Property | | +| ------------------------- | --------------------------------------------------------------------------------------------------------------- | +| role | The ARIA role of the modal, either `dialog` or `alertDialog` | +| component | An implementation of the `NgxModalAbstractComponent` | +| data | Any data we wish to provide to the component. | + +### Implementation + +The `NgxModalService` allows for two ways of opening a modal. Either by opening a predefined modal we set in the configuration, or a custom modal by passing a new modal component. + +To make the modals ARIA compliant either a `label` or a `labelledById` must be provided. If the role of a modal was set to `alertdialog`, the `describedById` property is also required. + +When opening a modal we can overwrite all the globally and modal-specific configuration using the same properties as mentioned earlier. On top of that, we can set several other properties. These properties are: + +| Property | | +| ------------------------- | --------------------------------------------------------------------------------------------------------------- | +| injector | Injector used for the instantiation of the component to be attached. If provided, takes precedence over the injector indirectly provided by ViewContainerRef. | +| viewContainerRef | Where the attached component should live in Angular's logical component tree. This affects what is available for injection and the change detection order for the component instantiated inside of the dialog. This does not affect where the dialog content will be rendered. | +| restoreFocus | Whether the dialog should restore focus to the previously-focused element upon closing. By default, this is `true`| +| autoFocus | Where the dialog should focus on open. By default, this is `true` | + +#### Predefined modal + +If we set a predefined modal, we can now call said modal using the `open` method on `NgxModalService`. + +```ts + this.modalService + .open<'Confirm' | 'Deny'>({ + type: 'confirm', + describedById: 'confirm-button', + labelledById: 'confirm-label', + data: { + title: 'Please confirm your actions!' + } + }) + .pipe( + tap(action => { + if(action === 'Confirm') { + // Perform confirm logic + } + + // Perform non-confirm logic + }) + ) + .subscribe(); +``` +#### Custom modal + +We can always create a custom modal for feature-specific use-cases. We do this by providing a component. + +``` ts + this.modalService + .open<'Test'>({ + component: ModalComponent, + label: 'Modal', + role: 'dialog', + }) + .pipe( + tap((action) => { + if (action === 'Test') { + console.log('Hello!'); + } + }) + ) + .subscribe(); +``` diff --git a/libs/inform/package.json b/libs/inform/package.json index 6afc1bb3..96417ac6 100644 --- a/libs/inform/package.json +++ b/libs/inform/package.json @@ -7,9 +7,13 @@ "angular2", "inform", "ARIA", + "WCAG", "tooltip", "tip", - "accessibility" + "accessibility", + "modal", + "dialog", + "alertdialog" ], "homepage": "https://github.com/studiohyperdrive/ngx-tools/tree/master/libs/inform", "license": "MIT", diff --git a/libs/inform/src/lib/abstracts/index.ts b/libs/inform/src/lib/abstracts/index.ts index 9bdfb27d..74bd5138 100644 --- a/libs/inform/src/lib/abstracts/index.ts +++ b/libs/inform/src/lib/abstracts/index.ts @@ -1 +1,2 @@ export * from './tooltip/tooltip.abstract.component'; +export * from './modal/modal.abstract.component'; diff --git a/libs/inform/src/lib/abstracts/modal/modal.abstract.component.ts b/libs/inform/src/lib/abstracts/modal/modal.abstract.component.ts new file mode 100644 index 00000000..550d9c8d --- /dev/null +++ b/libs/inform/src/lib/abstracts/modal/modal.abstract.component.ts @@ -0,0 +1,29 @@ +import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core'; + +/** + * An abstract for the NgxModalService + */ +@Directive() +export class NgxModalAbstractComponent { + /** + * Remove the modal on escape pressed + */ + @HostListener('document:keydown.escape') private onEscape() { + this.close.emit(); + } + + /** + * Optional data that can be passed to the modal + */ + @Input() public data: DataType; + + /** + * An emitter that will emit an action we can later respond to + */ + @Output() public action: EventEmitter = new EventEmitter(); + + /** + * An emitter that will emit if the modal is closed + */ + @Output() public close: EventEmitter = new EventEmitter(); +} diff --git a/libs/inform/src/lib/abstracts/tooltip/tooltip.abstract.component.ts b/libs/inform/src/lib/abstracts/tooltip/tooltip.abstract.component.ts index af2ade43..cf84046d 100644 --- a/libs/inform/src/lib/abstracts/tooltip/tooltip.abstract.component.ts +++ b/libs/inform/src/lib/abstracts/tooltip/tooltip.abstract.component.ts @@ -3,6 +3,9 @@ import { Directive, HostBinding, HostListener, Input } from '@angular/core'; import { NgxTooltipPosition, NgxTooltipPositionClass } from '../../types'; import { NgxTooltipService } from '../../services'; +/** + * An abstract for the NgxTooltipDirective + */ @Directive() export abstract class NgxTooltipAbstractComponent { /** diff --git a/libs/inform/src/lib/index.ts b/libs/inform/src/lib/index.ts index fad6b672..4b02abd1 100644 --- a/libs/inform/src/lib/index.ts +++ b/libs/inform/src/lib/index.ts @@ -3,3 +3,4 @@ export * from './directives'; export * from './providers'; export * from './abstracts'; export * from './tokens'; +export { NgxModalService } from './services'; diff --git a/libs/inform/src/lib/providers/index.ts b/libs/inform/src/lib/providers/index.ts index 142f52c1..757b094c 100644 --- a/libs/inform/src/lib/providers/index.ts +++ b/libs/inform/src/lib/providers/index.ts @@ -1 +1,2 @@ export * from './tooltip/tooltip-configuration.provider'; +export * from './modal/modal-configuration.provider'; diff --git a/libs/inform/src/lib/providers/modal/modal-configuration.provider.ts b/libs/inform/src/lib/providers/modal/modal-configuration.provider.ts new file mode 100644 index 00000000..55ac9d48 --- /dev/null +++ b/libs/inform/src/lib/providers/modal/modal-configuration.provider.ts @@ -0,0 +1,15 @@ +import { Provider } from '@angular/core'; +import { NgxModalConfiguration } from '../../types'; +import { NgxModalConfigurationToken } from '../../tokens'; + +/** + * Provides the configuration for the NgxModalService + * + * @param configuration - The required configuration + */ +export const provideNgxModalConfiguration = (configuration: NgxModalConfiguration): Provider => { + return { + provide: NgxModalConfigurationToken, + useValue: configuration, + }; +}; diff --git a/libs/inform/src/lib/services/index.ts b/libs/inform/src/lib/services/index.ts index 2e0ac997..d08d37d7 100644 --- a/libs/inform/src/lib/services/index.ts +++ b/libs/inform/src/lib/services/index.ts @@ -1 +1,2 @@ -export * from './tooltip-service/tooltip.service'; +export * from './tooltip/tooltip.service'; +export * from './modal/modal.service'; diff --git a/libs/inform/src/lib/services/modal/modal.service.ts b/libs/inform/src/lib/services/modal/modal.service.ts new file mode 100644 index 00000000..0ca0132a --- /dev/null +++ b/libs/inform/src/lib/services/modal/modal.service.ts @@ -0,0 +1,244 @@ +import { Inject, Injectable, Optional, Type } from '@angular/core'; +import { Dialog } from '@angular/cdk/dialog'; +import { + combineLatest, + filter, + map, + NEVER, + Observable, + startWith, + Subject, + takeUntil, + tap, +} from 'rxjs'; + +import { NgxModalConfiguration, NgxModalOptions } from '../../types'; +import { NgxModalConfigurationToken } from '../../tokens'; +import { NgxModalAbstractComponent } from '../../abstracts'; + +/** + * A wrapper service to Angular CDK Dialog that provides a WCAG/ARIA compliant implementation of modals + * + * @export + * @class NgxModalService + */ +@Injectable({ providedIn: 'root' }) +export class NgxModalService { + /** + * A subject to hold the close event + */ + private modalClosedSubject: Subject; + + constructor( + @Optional() + @Inject(NgxModalConfigurationToken) + private readonly configuration: NgxModalConfiguration, + private readonly dialogService: Dialog + ) {} + + /** + * Opens a modal based on the provided options + * + * @param {NgxModalOptions} options - The modal options + */ + public open( + options: NgxModalOptions + ): Observable { + // Iben: If there still is an active subject running, the modal is still active and we early exit + if (!this.modalClosedSubject?.closed) { + console.warn( + 'NgxInform: An active modal is currently displayed, close the active modal before opening a new one' + ); + + return NEVER; + } + + // Iben: Create a new subject + this.modalClosedSubject = new Subject(); + + // Iben: Fetch the type of component we wish to show + const configuration = this.configuration?.modals?.[options.type]; + const component: Type> = + options.component || + (this.configuration?.modals?.[options.type].component as Type< + NgxModalAbstractComponent + >); + + // Iben: Check if all the correct parameters are set and return NEVER when they're not correctly set + if (!this.runARIAChecks(options, component)) { + return NEVER; + } + + // Iben: Render the modal + const modal = this.createModalComponent(options, component); + + // Iben: Return the modal action + return combineLatest([ + // Iben: Set the start value to undefined so both actions at least emit once + modal.action.pipe(startWith(undefined)), + modal.close.pipe( + // Iben: Map so we can keep the emit value void, but can work with the filter later down the line + map(() => 'NgxModalClose'), + // Iben: Set the start value to undefined so both actions at least emit once + startWith(undefined) + ), + ]).pipe( + // Iben: Only emit if one of the two actions actually has an emit + filter(([action, closed]: [ActionType, 'NgxModalClose']) => { + return Boolean(action) || Boolean(closed); + }), + map(([action, closed]: [ActionType, 'NgxModalClose']) => { + return closed || action; + }), + tap((action: ActionType | 'NgxModalClose') => { + // Iben: If the autoClose is specifically set to false, we early exit unless we're running in a close event + if ( + action !== 'NgxModalClose' && + ((options.autoClose !== undefined && options.autoClose === false) || + (configuration?.autoClose !== undefined && + configuration.autoClose === false)) + ) { + return; + } + + // Iben: Close the modal + this.close(options.onClose); + }), + // Iben: Map the action back to the ActionType + map((action: ActionType | 'NgxModalClose') => { + return action === 'NgxModalClose' ? undefined : (action as ActionType); + }), + takeUntil(this.modalClosedSubject) + ); + } + + /** + * Closes the currently active modal + * + * * @param onClose - An optional onClose function + */ + public close(onClose?: () => void): void { + // Iben: Close the modal + this.dialogService.closeAll(); + + // Iben: Mark the modal as closed + this.modalClosedSubject.next(); + this.modalClosedSubject.complete(); + + // Iben: Run an optional onClose function + if (onClose) { + onClose(); + } + } + + /** + * Checks if all the necessary preconditions are met + * + * @param options - The options of the modal + * @param component - The component we wish to render + */ + private runARIAChecks( + options: NgxModalOptions, + component: Type> + ): boolean { + // Iben: If no component was found, we return NEVER and throw an error + if (!component) { + console.error( + 'NgxInform: No component was provided or found in the configuration to render.' + ); + + return false; + } + + // Iben: If no description was provided when required, we return NEVER and throw an error + if (!this.hasRequiredDescription(options)) { + console.error( + 'NgxInform: The role of the modal was set to "alertdialog" but no "describedById" was provided.' + ); + + return false; + } + + return true; + } + + /** + * Creates the modal component + * + * @param options - The options of the modal + * @param component - The component we wish to render + */ + private createModalComponent( + options: NgxModalOptions, + component: Type> + ): NgxModalAbstractComponent { + const configuration = this.configuration?.modals?.[options.type]; + + // Iben: Create the modal and render it + const dialogRef = this.dialogService.open(component, { + role: configuration?.role || options.role, + ariaLabel: options.label, + ariaLabelledBy: options.labelledById, + ariaDescribedBy: options.describedById, + disableClose: true, + restoreFocus: this.getValue(undefined, options.restoreFocus, true), + autoFocus: this.getValue(undefined, options.autoFocus, true), + viewContainerRef: options.viewContainerRef, + direction: configuration?.direction || options.direction, + hasBackdrop: this.getValue(configuration?.hasBackdrop, options.hasBackdrop, true), + panelClass: this.getValue(configuration?.panelClass, options.panelClass, []), + closeOnNavigation: this.getValue( + configuration?.closeOnNavigation, + options.closeOnNavigation, + true + ), + closeOnDestroy: true, + closeOnOverlayDetachments: true, + }); + const modal = dialogRef.componentInstance; + + // Iben: Set the data of the modal + modal.data = this.getValue(configuration?.data, options.data, undefined); + + return modal; + } + + /** + * Checks if the description is provided when the role requires it + * + * @param options - The options of the modal + */ + private hasRequiredDescription( + options: NgxModalOptions + ): boolean { + // Iben: If the options has provided a default type, we check based on the configuration role + if (options.type) { + const configuration = this.configuration?.modals[options.type]; + + return !(configuration.role === 'alertdialog' && !options.describedById); + } + + // Iben: Check based on the options role + return !(options.role === 'alertdialog' && !options.describedById); + } + + /** + * Returns a value based on whether one of the overwrites is defined + * + * @private + * @param configurationValue - The overwrite on configuration level + * @param optionsValue - The overwrite on options level + * @param defaultValue - The default value if no overwrite was defined + */ + private getValue(configurationValue: any, optionsValue: any, defaultValue: any): any { + if (configurationValue === undefined && optionsValue === undefined) { + return defaultValue; + } + + if (optionsValue !== undefined) { + return optionsValue; + } + + return configurationValue; + } +} diff --git a/libs/inform/src/lib/services/tooltip-service/tooltip.service.spec.ts b/libs/inform/src/lib/services/tooltip/tooltip.service.spec.ts similarity index 100% rename from libs/inform/src/lib/services/tooltip-service/tooltip.service.spec.ts rename to libs/inform/src/lib/services/tooltip/tooltip.service.spec.ts diff --git a/libs/inform/src/lib/services/tooltip-service/tooltip.service.ts b/libs/inform/src/lib/services/tooltip/tooltip.service.ts similarity index 95% rename from libs/inform/src/lib/services/tooltip-service/tooltip.service.ts rename to libs/inform/src/lib/services/tooltip/tooltip.service.ts index f97d460f..2688d815 100644 --- a/libs/inform/src/lib/services/tooltip-service/tooltip.service.ts +++ b/libs/inform/src/lib/services/tooltip/tooltip.service.ts @@ -169,12 +169,14 @@ export class NgxTooltipService implements OnDestroy { /** * Removes the tooltip. */ - public removeToolTip() { - // Iben: Unset the active tooltip - this.activeTooltip = undefined; + public removeToolTip(): void { + if (this.activeTooltip) { + // Iben: Unset the active tooltip + this.activeTooltip = undefined; - // Iben: Remove the active tooltip from view - this.overlayRef.detach(); + // Iben: Remove the active tooltip from view + this.overlayRef.detach(); + } } /** @@ -182,7 +184,7 @@ export class NgxTooltipService implements OnDestroy { * * @param event - A tooltip event */ - public setToolTipEvent(event: NgxTooltipEvent) { + public setToolTipEvent(event: NgxTooltipEvent): void { // Iben: We add a delay so that the user can navigate between the tooltip and the element setTimeout( () => { diff --git a/libs/inform/src/lib/tokens/index.ts b/libs/inform/src/lib/tokens/index.ts index dfa6c94b..70e97564 100644 --- a/libs/inform/src/lib/tokens/index.ts +++ b/libs/inform/src/lib/tokens/index.ts @@ -1 +1,2 @@ export * from './tooltip/tooltip-configuration.token'; +export * from './modal/modal-configuration.token'; diff --git a/libs/inform/src/lib/tokens/modal/modal-configuration.token.ts b/libs/inform/src/lib/tokens/modal/modal-configuration.token.ts new file mode 100644 index 00000000..696870a0 --- /dev/null +++ b/libs/inform/src/lib/tokens/modal/modal-configuration.token.ts @@ -0,0 +1,11 @@ +import { InjectionToken } from '@angular/core'; + +import { NgxModalConfiguration } from '../../types'; + +/** + * A token to provide the optional configuration to the NgxModalService + */ +// This is exported due to testing frameworks (like Storybook) not being able to resolve the InjectionToken in the `provideNgxTooltipConfiguration`. +export const NgxModalConfigurationToken = new InjectionToken( + 'NgxModalConfiguration' +); diff --git a/libs/inform/src/lib/tokens/tooltip/tooltip-configuration.token.ts b/libs/inform/src/lib/tokens/tooltip/tooltip-configuration.token.ts index f56e616e..e7ddfda3 100644 --- a/libs/inform/src/lib/tokens/tooltip/tooltip-configuration.token.ts +++ b/libs/inform/src/lib/tokens/tooltip/tooltip-configuration.token.ts @@ -8,5 +8,5 @@ import { NgxTooltipConfiguration } from '../../types'; * `provideNgxTooltipConfiguration`. */ export const NgxTooltipConfigurationToken = new InjectionToken( - 'NgxTooltipConfigurationConfiguration' + 'NgxTooltipConfiguration' ); diff --git a/libs/inform/src/lib/types/index.ts b/libs/inform/src/lib/types/index.ts index 2eb8b548..d00b1b32 100644 --- a/libs/inform/src/lib/types/index.ts +++ b/libs/inform/src/lib/types/index.ts @@ -1 +1,2 @@ export * from './tooltip.types'; +export * from './modal.types'; diff --git a/libs/inform/src/lib/types/modal.types.ts b/libs/inform/src/lib/types/modal.types.ts new file mode 100644 index 00000000..65694b79 --- /dev/null +++ b/libs/inform/src/lib/types/modal.types.ts @@ -0,0 +1,88 @@ +import { Injector, Type, ViewContainerRef } from '@angular/core'; + +import { Direction } from '@angular/cdk/bidi'; +import { AutoFocusTarget } from '@angular/cdk/dialog'; +import { NgxModalAbstractComponent } from '../abstracts'; + +export type NgxModalRole = 'dialog' | 'alertdialog'; + +// Aria configuration +interface NgxModalAriaLabelBaseOptions { + label?: string; + labelledById?: string; +} + +interface NgxModalAriaLabelOptions extends NgxModalAriaLabelBaseOptions { + label: string; + labelledById?: undefined; +} + +interface NgxModalAriaLabelledOptions extends NgxModalAriaLabelBaseOptions { + label?: undefined; + labelledById: string; +} +type NgxModalLabelAriaOptions = NgxModalAriaLabelOptions | NgxModalAriaLabelledOptions; + +// CDKDialog configuration + +interface NgxModalGlobalCDKConfiguration { + closeOnNavigation?: boolean; + direction?: Direction; + hasBackdrop?: boolean; + panelClass?: string | string[]; + autoClose?: boolean; +} + +interface NgxModalCDKModalConfiguration { + injector?: Injector; + viewContainerRef?: ViewContainerRef; + restoreFocus?: boolean | string | HTMLElement; + autoFocus?: AutoFocusTarget | string | boolean; +} + +// Global configuration + +export interface NgxModalComponentConfiguration { + component: Type; + role: NgxModalRole; + data?: DataType; +} + +interface NgxModalBaseConfiguration { + modals?: Record; +} + +export type NgxModalConfiguration = NgxModalBaseConfiguration & NgxModalGlobalCDKConfiguration; + +// Modal options +interface NgxModalBaseOptions { + type?: string; + component?: Type>; + data?: DataType; + onClose?: () => void; + describedById?: string; +} + +interface NgxModalTypeOptions + extends NgxModalBaseOptions { + type: string; + component?: undefined; + role?: undefined; +} + +interface NgxModalComponentOptions + extends NgxModalBaseOptions { + type?: undefined; + component: Type>; + role: NgxModalRole; +} + +export type NgxModalOptions = + | (NgxModalTypeOptions & + NgxModalLabelAriaOptions & + NgxModalGlobalCDKConfiguration & + NgxModalCDKModalConfiguration) + | (NgxModalComponentOptions & + NgxModalLabelAriaOptions & + NgxModalGlobalCDKConfiguration & + NgxModalCDKModalConfiguration);