diff --git a/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.html b/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.html index c74884de1e..2d76dfbc47 100644 --- a/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.html +++ b/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.html @@ -1,19 +1,10 @@ - - - + diff --git a/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.ts b/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.ts index f59e154079..0bfed3abdc 100644 --- a/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.ts +++ b/apps/docs-app/src/app/content/components/component-demos/dialogs/demos/dialogs-demo-basic/dialogs-demo-basic.component.ts @@ -38,4 +38,16 @@ export class DialogsDemoBasicComponent { acceptButton: 'Ok', }); } + + openStatus(): void { + this._dialogService.openStatus({ + title: 'Status dialog', + disableClose: true, + closeButton: 'Close', + state: 'error', + details: 'Additional information about the error.', + message: + 'This is how simple it is to create a status dialog with this wrapper service.', + }); + } } diff --git a/apps/docs-app/src/app/content/components/components.ts b/apps/docs-app/src/app/content/components/components.ts index 9595df9dae..cb4a7991ae 100644 --- a/apps/docs-app/src/app/content/components/components.ts +++ b/apps/docs-app/src/app/content/components/components.ts @@ -77,7 +77,7 @@ export const createComponentDetails: IComponentDetails[] = [ name: 'Dialogs', id: 'dialogs', description: - 'Quick way to use alert, confirm, prompt, and draggable dialogs', + 'Quick way to use alert, confirm, prompt, draggable and status dialogs', apiDocUrl: 'libs/angular/dialogs/README.md', icon: 'open_in_browser', category: buttons.name, diff --git a/libs/angular/dialogs/README.md b/libs/angular/dialogs/README.md index d3144a11be..542de9a042 100644 --- a/libs/angular/dialogs/README.md +++ b/libs/angular/dialogs/README.md @@ -16,6 +16,8 @@ Note: if no [ViewContainerRef] is provided, [TdDialogService] will throw an erro - Opens a confirm dialog with the provided config. - openPrompt: function(IPromptConfig): MatDialogRef - Opens a prompt dialog with the provided config. +- openStatus: function(IStatusConfig): MatDialogRef + - Opens a status dialog with the provided config. - open: function(component: ComponentType, config: MatDialogConfig): MatDialogRef - Wrapper function over the open() method in MatDialog. Opens a modal dialog containing the given component. - openDraggable: function(IDraggableConfig): MatDialogRef @@ -87,6 +89,28 @@ export class Demo { }); } + openStatus(): void { + this._dialogService.openStatus({ + closeButton: 'Close', //OPTIONAL, defaults to 'CLOSE' + details: 'Additional information about the error.', //OPTIONAL, additional details to be displayed in the message + detailsLabels: { + showDetailsLabel: 'Show more details', + hideDetailsLabel: 'Hide more details' + }, //OPTIONAL, defaults to 'Show details' and 'Hide details' + disableClose: true, // defaults to false + message: + 'This is how simple it is to create a status dialog with this wrapper service.', + state: 'error', // represents the state ('error' | 'positive' | 'warning') of the dialog, defaults to 'error' + title: 'Status dialog', //OPTIONAL, hides if not provided + }).afterClosed().subscribe((newValue: string) => { + if (newValue) { + // DO SOMETHING + } else { + // DO SOMETHING ELSE + } + });; + } + openDraggable(): void { this._dialogService.openDraggable({ component: DraggableDemoComponent, @@ -150,25 +174,15 @@ A utility to make a draggable dialog resizable. ``` ```ts -const { - matDialogRef, - dragRefSubject, -}: IDraggableRefs = this._dialogService.openDraggable( - { - component: DraggableResizableDialogComponent, - // CSS selectors of element(s) inside the component meant to be drag handle(s) - dragHandleSelectors: ['.drag-handle'], - } -); +const { matDialogRef, dragRefSubject }: IDraggableRefs = this._dialogService.openDraggable({ + component: DraggableResizableDialogComponent, + // CSS selectors of element(s) inside the component meant to be drag handle(s) + dragHandleSelectors: ['.drag-handle'], +}); let resizableDraggableDialog: ResizableDraggableDialog; dragRefSubject.subscribe((dragRf: DragRef) => { - resizableDraggableDialog = new ResizableDraggableDialog( - this._document, - this._renderer2, - matDialogRef, - dragRf - ); + resizableDraggableDialog = new ResizableDraggableDialog(this._document, this._renderer2, matDialogRef, dragRf); }); // Detach resize-ability event listeners after dialog closes @@ -200,12 +214,7 @@ A component that can be utilized to create a dialog with a toolbar ```ts @Component({ template: ` - +

Comes with a handy toolbar

`, @@ -216,8 +225,7 @@ export class DraggableResizableWindowDialogComponent { ``` ```ts -const matDialogRef: MatDialogRef = - this._dialogService.open(DraggableResizableWindowDialogComponent); +const matDialogRef: MatDialogRef = this._dialogService.open(DraggableResizableWindowDialogComponent); // listen to close event matDialogRef.componentInstance.closed.subscribe(() => matDialogRef.close()); ``` diff --git a/libs/angular/dialogs/_dialog-theme.scss b/libs/angular/dialogs/_dialog-theme.scss index a0ca394838..752ae61ea7 100644 --- a/libs/angular/dialogs/_dialog-theme.scss +++ b/libs/angular/dialogs/_dialog-theme.scss @@ -2,7 +2,8 @@ @import '../common/styles/typography-functions'; @mixin td-dialog-typography($config) { - .td-dialog-title { + .td-dialog-title, + .td-status-dialog-title { font: { family: td-font-family($config); size: td-font-size($config, title); @@ -10,6 +11,11 @@ } } + .td-status-dialog-title, + .td-status-dialog-title .td-dialog-message { + line-height: td-line-height($config, subheading-1); + } + .td-dialog-message { font: { family: td-font-family($config); diff --git a/libs/angular/dialogs/src/dialog.component.html b/libs/angular/dialogs/src/dialog.component.html index 507b9aa4c0..8f8676ed8b 100644 --- a/libs/angular/dialogs/src/dialog.component.html +++ b/libs/angular/dialogs/src/dialog.component.html @@ -1,10 +1,24 @@ -
- +
+ +
+
+ +
+ + + + + + + +
- - - - - - - diff --git a/libs/angular/dialogs/src/dialog.component.scss b/libs/angular/dialogs/src/dialog.component.scss index 64a011e822..82f4bea83c 100644 --- a/libs/angular/dialogs/src/dialog.component.scss +++ b/libs/angular/dialogs/src/dialog.component.scss @@ -1,3 +1,11 @@ +.td-dialog { + width: 100%; +} + +.td-dialog-wrapper { + display: flex; +} + .td-dialog-actions { // [layout="row"] flex-direction: row; @@ -22,4 +30,14 @@ padding-right: 8px; min-width: 64px; } + + ::ng-deep .td-status-dialog___button { + padding: 9px 16px; + } +} + +@media screen and (max-width: 480px) { + .td-dialog-wrapper { + flex-direction: column; + } } diff --git a/libs/angular/dialogs/src/dialog.component.ts b/libs/angular/dialogs/src/dialog.component.ts index 0e76a58252..7d2eab4616 100644 --- a/libs/angular/dialogs/src/dialog.component.ts +++ b/libs/angular/dialogs/src/dialog.component.ts @@ -15,6 +15,9 @@ export class TdDialogContentDirective {} @Directive({ selector: '[tdDialogActions]' }) export class TdDialogActionsDirective {} +@Directive({ selector: '[tdDialogStatus]' }) +export class TdDialogStatusDirective {} + @Component({ selector: 'td-dialog', templateUrl: './dialog.component.html', @@ -27,6 +30,8 @@ export class TdDialogComponent implements AfterContentInit { dialogContent!: QueryList; @ContentChildren(TdDialogActionsDirective, { descendants: true }) dialogActions!: QueryList; + @ContentChildren(TdDialogStatusDirective, { descendants: true }) + dialogStatus!: QueryList; ngAfterContentInit(): void { if (this.dialogTitle.length > 1) { @@ -38,5 +43,8 @@ export class TdDialogComponent implements AfterContentInit { if (this.dialogActions.length > 1) { throw new Error('Duplicate td-dialog-actions component at in td-dialog.'); } + if (this.dialogStatus.length > 1) { + throw new Error('Duplicate td-dialog-status component at in td-dialog.'); + } } } diff --git a/libs/angular/dialogs/src/dialogs.module.ts b/libs/angular/dialogs/src/dialogs.module.ts index 5df39952b5..5620cb1f50 100644 --- a/libs/angular/dialogs/src/dialogs.module.ts +++ b/libs/angular/dialogs/src/dialogs.module.ts @@ -12,10 +12,12 @@ import { TdDialogTitleDirective, TdDialogActionsDirective, TdDialogContentDirective, + TdDialogStatusDirective, } from './dialog.component'; import { TdAlertDialogComponent } from './alert-dialog/alert-dialog.component'; import { TdConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; import { TdPromptDialogComponent } from './prompt-dialog/prompt-dialog.component'; +import { TdStatusDialogComponent } from './status-dialog/status-dialog.component'; import { TdDialogService } from './services/dialog.service'; import { TdWindowDialogComponent } from './window-dialog/window-dialog.component'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -27,6 +29,7 @@ const TD_DIALOGS: Type[] = [ TdConfirmDialogComponent, TdPromptDialogComponent, TdDialogComponent, + TdDialogStatusDirective, TdDialogTitleDirective, TdDialogActionsDirective, TdDialogContentDirective, @@ -34,6 +37,7 @@ const TD_DIALOGS: Type[] = [ TdAlertDialogComponent, TdConfirmDialogComponent, TdPromptDialogComponent, + TdStatusDialogComponent, ]; @NgModule({ diff --git a/libs/angular/dialogs/src/public_api.ts b/libs/angular/dialogs/src/public_api.ts index b8521cbe28..03005e57a8 100644 --- a/libs/angular/dialogs/src/public_api.ts +++ b/libs/angular/dialogs/src/public_api.ts @@ -3,6 +3,7 @@ export * from './dialog.component'; export * from './alert-dialog/alert-dialog.component'; export * from './confirm-dialog/confirm-dialog.component'; export * from './prompt-dialog/prompt-dialog.component'; +export * from './status-dialog/status-dialog.component'; export * from './services/dialog.service'; export * from './resizable-draggable-dialog/resizable-draggable-dialog'; export * from './window-dialog/window-dialog.component'; diff --git a/libs/angular/dialogs/src/services/dialog.service.ts b/libs/angular/dialogs/src/services/dialog.service.ts index 03db37f62c..3d926c3fd1 100644 --- a/libs/angular/dialogs/src/services/dialog.service.ts +++ b/libs/angular/dialogs/src/services/dialog.service.ts @@ -12,6 +12,11 @@ import { TdPromptDialogComponent } from '../prompt-dialog/prompt-dialog.componen import { DragDrop, DragRef } from '@angular/cdk/drag-drop'; import { DOCUMENT } from '@angular/common'; import { Subject } from 'rxjs'; +import { + TdStatusDialogStates, + TdStatusDialogComponent, + TdStatusDialogDetailsLabels, +} from '../status-dialog/status-dialog.component'; export interface IDialogConfig extends MatDialogConfig { title?: string; @@ -32,6 +37,12 @@ export interface IPromptConfig extends IConfirmConfig { value?: string; } +export interface IStatusConfig extends IAlertConfig { + state?: TdStatusDialogStates; + details?: string; + detailsLabels?: TdStatusDialogDetailsLabels; +} + export interface IDraggableConfig { component: ComponentType; config?: MatDialogConfig; @@ -246,4 +257,44 @@ export class TdDialogService { Object.assign(dialogConfig, config); return dialogConfig; } + + /** + * params: + * - config: IStatusConfig { + * closeButton?: string; + * details?: string; + * detailsLabels?: TdStatusDialogDetailsLabels; + * message: string; + * state?: 'error' | 'positive' | 'warning' + * title?: string; + * viewContainerRef?: ViewContainerRef; + * } + * + * Opens a status dialog with the provided config. + * Returns an MatDialogRef object. + */ + public openStatus( + config: IStatusConfig + ): MatDialogRef { + config.panelClass = 'td-status-dialog'; + config.autoFocus = false; + config.width = '575px'; + config.maxWidth = '96vw'; + const dialogConfig: MatDialogConfig = this._createConfig(config); + const dialogRef: MatDialogRef = + this._dialogService.open(TdStatusDialogComponent, dialogConfig); + const statusDialogComponent: TdStatusDialogComponent = + dialogRef.componentInstance; + statusDialogComponent.title = config.title; + statusDialogComponent.message = config.message; + statusDialogComponent.state = config.state; + statusDialogComponent.details = config.details; + if (config.detailsLabels) { + statusDialogComponent.detailsLabels = config.detailsLabels; + } + if (config.closeButton) { + statusDialogComponent.closeButton = config.closeButton; + } + return dialogRef; + } } diff --git a/libs/angular/dialogs/src/status-dialog/status-dialog.component.html b/libs/angular/dialogs/src/status-dialog/status-dialog.component.html new file mode 100644 index 0000000000..1ec71b8a71 --- /dev/null +++ b/libs/angular/dialogs/src/status-dialog/status-dialog.component.html @@ -0,0 +1,60 @@ + + +
+ + {{ getStatusIcon() }} + +
+ + +
+ {{ title }} + +
+
+ + + + {{ message }} +
+ {{ + showDetails + ? detailsLabels?.hideDetailsLabel + : detailsLabels?.showDetailsLabel + }} + arrow_drop_down +
+
{{ details }}
+
+
+ + + +
diff --git a/libs/angular/dialogs/src/status-dialog/status-dialog.component.scss b/libs/angular/dialogs/src/status-dialog/status-dialog.component.scss new file mode 100644 index 0000000000..15d730cce9 --- /dev/null +++ b/libs/angular/dialogs/src/status-dialog/status-dialog.component.scss @@ -0,0 +1,151 @@ +::ng-deep { + .mat-mdc-dialog-container .mdc-dialog__surface { + background-color: var(--cv-theme-surface-container-lowest); + border-radius: 12px; + } + + .td-status-dialog-title { + align-items: flex-start; + display: inline-flex; + justify-content: space-between; + width: 100%; + -webkit-font-smoothing: antialiased; + + :first-child { + padding-right: 40px; + } + } + + [mat-icon-button].td-status-dialog__icon-button { + --mdc-icon-button-state-layer-size: 40px; + + padding: 8px; + position: absolute; + right: 8px; + top: 10px; + } + + .td-status-dialog___button { + border-radius: 8px; + } + + .mat-mdc-dialog-container .mdc-dialog__title { + padding: 18px 16px; + line-height: var(--mdc-dialog-supporting-text-line-height); + + &::before { + display: none; + } + } + + .mat-mdc-icon-button .mat-mdc-button-base { + padding: 8px; + } + + .mdc-dialog .mdc-dialog__content { + padding: 0 16px; + -webkit-font-smoothing: antialiased; + } + + .mdc-dialog__actions { + padding: 16px; + } +} + +.td-status-dialog { + width: auto; + + &::before { + padding: 14px 16px 0; + } + + .td-dialog-message { + font-feature-settings: 'clig' off, 'liga' off; + color: var(--cv-theme-on-surface-variant); + line-height: var(--mdc-dialog-supporting-text-line-height); + } +} + +.td-status-dialog-state { + padding: 14px 16px 0; + + .mat-icon { + font-size: var(--mat-toolbar-title-text-line-height); + height: var(--mat-toolbar-title-text-line-height); + width: var(--mat-toolbar-title-text-line-height); + font-variation-settings: 'FILL' 1; + } + + &.error { + background-color: var(--cv-theme-negative-8); + + .mat-icon { + color: var(--cv-theme-negative); + } + } + + &.positive { + background-color: var(--cv-theme-positive-8); + + .mat-icon { + color: var(--cv-theme-positive); + } + } + + &.warning { + background-color: var(--cv-theme-caution-8); + + .mat-icon { + color: var(--cv-theme-caution); + } + } +} + +.td-status-dialog__toggle-details { + align-items: center; + color: var(--cv-theme-primary); + cursor: pointer; + display: flex; + font-size: var(--mat-expansion-container-text-size); + letter-spacing: 0.25px; + line-height: var(--mat-expansion-container-text-line-height); + padding: 16px 0; +} + +.td-status-dialog__arrow-icon { + margin-top: 2px; + + &.open { + transform: rotate(0deg); + transition: transform 250ms ease-out; + } + + &.close { + transform: rotate(180deg); + transition: transform 250ms ease-in; + } +} + +@media screen and (max-width: 480px) { + :host { + .td-status-dialog-state { + padding: 4px; + + .mat-icon { + display: none; + } + + &.error { + background-color: var(--cv-theme-negative); + } + + &.positive { + background-color: var(--cv-theme-positive); + } + + &.warning { + background-color: var(--cv-theme-caution); + } + } + } +} diff --git a/libs/angular/dialogs/src/status-dialog/status-dialog.component.spec.ts b/libs/angular/dialogs/src/status-dialog/status-dialog.component.spec.ts new file mode 100644 index 0000000000..48c374c6c7 --- /dev/null +++ b/libs/angular/dialogs/src/status-dialog/status-dialog.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TdStatusDialogComponent } from './status-dialog.component'; +import { CovalentDialogsModule } from '../dialogs.module'; +import { MatDialogRef } from '@angular/material/dialog'; + +class MatDialogRefMock { + close(): null { + return null; + } +} + +describe('TdStatusDialogComponent', () => { + let component: TdStatusDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CovalentDialogsModule], + declarations: [TdStatusDialogComponent], + providers: [{ provide: MatDialogRef, useClass: MatDialogRefMock }], + }).compileComponents(); + + fixture = TestBed.createComponent(TdStatusDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should get the status icon based on the state', () => { + component.state = 'positive'; + const icon = component.getStatusIcon(); + expect(icon).toBe('check'); + }); + + it('should toggle additional details section', () => { + component.showDetails = false; + component.toggleDetails(); + expect(component.showDetails).toBeTruthy(); + }); + + it('should close the dialog', () => { + const closeSpy = jest.spyOn(component['_dialogRef'], 'close'); + component.showDetails = false; + component.close(); + expect(closeSpy).toHaveBeenCalled(); + }); +}); diff --git a/libs/angular/dialogs/src/status-dialog/status-dialog.component.ts b/libs/angular/dialogs/src/status-dialog/status-dialog.component.ts new file mode 100644 index 0000000000..cabddfd765 --- /dev/null +++ b/libs/angular/dialogs/src/status-dialog/status-dialog.component.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; + +export type TdStatusDialogStates = 'error' | 'positive' | 'warning'; + +export type TdStatusDialogDetailsLabels = { + showDetailsLabel: string; + hideDetailsLabel: string; +}; + +@Component({ + selector: 'td-status-dialog', + templateUrl: './status-dialog.component.html', + styleUrls: ['./status-dialog.component.scss'], +}) +export class TdStatusDialogComponent { + // Label of the close button in the footer + closeButton?: string = 'CLOSE'; + // Message to be displayed in the dialog + message?: string; + // State of the status dialog + state?: TdStatusDialogStates = 'error'; + // Title of the status dialog + title?: string; + // Additional details to be displayed after the dialog message + details?: string; + // Toggles the additional details section + showDetails? = false; + // Labels for the toggle details link + detailsLabels?: TdStatusDialogDetailsLabels = { + showDetailsLabel: 'Show details', + hideDetailsLabel: 'Hide details', + }; + + constructor(private _dialogRef: MatDialogRef) {} + + close(): void { + this._dialogRef.close(); + } + + getStatusIcon(): string { + switch (this.state) { + case 'positive': + return 'check'; + case 'error': + case 'warning': + return this.state; + default: + return 'error'; + } + } + + toggleDetails(): void { + this.showDetails = !this.showDetails; + } +} diff --git a/libs/angular/theming/_teradata-theme.scss b/libs/angular/theming/_teradata-theme.scss index 8b73aab90c..681091c2bb 100644 --- a/libs/angular/theming/_teradata-theme.scss +++ b/libs/angular/theming/_teradata-theme.scss @@ -397,6 +397,12 @@ $td-dark-theme: mat.private-deep-merge-all( ) ); +@mixin css-variable-theme-tokens($theme, $prefix: 'cv') { + @each $key, $value in $theme { + --#{$prefix}-theme-#{$key}: #{map-get($theme, $key)}; + } +} + @mixin teradata-brand($theme) { $primary: map-get($theme, primary); $accent: map-get($theme, accent); @@ -418,6 +424,12 @@ $td-dark-theme: mat.private-deep-merge-all( $background, disabled-button )}; + + @include css-variable-theme-tokens($td-light-colors); + } + + .theme-dark { + @include css-variable-theme-tokens($td-dark-colors); } // Logo alignment diff --git a/libs/components/src/status-dialog/status-dialog.scss b/libs/components/src/status-dialog/status-dialog.scss index 289631600d..ebe05ea52e 100644 --- a/libs/components/src/status-dialog/status-dialog.scss +++ b/libs/components/src/status-dialog/status-dialog.scss @@ -193,7 +193,7 @@ } &.positive .status-dialog__state { - background-color: var(-cv-theme-positive); + background-color: var(--cv-theme-positive); } &.warning .status-dialog__state {