From 2cc454424644df121d6f94a0aad2c2b81d3b540d Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 21 Oct 2019 13:26:18 +0200 Subject: [PATCH 01/16] feat(business): implement dialog component Implement dialog component, create dialog examples and documentation --- .../business-examples.module.ts | 22 +- .../dialog-showcase-content-1.component.html | 21 ++ .../dialog-showcase-content-2.component.html | 99 +++++++ .../dialog-showcase-content-3.component.html | 33 +++ .../dialog-showcase.component.html | 41 +++ .../dialog-showcase.component.scss | 31 ++ .../dialog-showcase.component.ts | 123 ++++++++ .../business/business/business.component.ts | 7 +- .../angular-business/dialog/dialog.md | 166 +++++++++++ .../sbb-esta/angular-business/dialog/index.ts | 1 + .../angular-business/dialog/package.json | 19 ++ .../angular-business/dialog/src/_dialog.scss | 242 +++++++++++++++ .../dialog/src/dialog.module.ts | 43 +++ .../dialog/src/dialog/dialog-animations.ts | 25 ++ .../dialog/src/dialog/dialog-config.ts | 54 ++++ .../dialog/dialog-container.component.html | 1 + .../dialog/dialog-container.component.scss | 48 +++ .../src/dialog/dialog-container.component.ts | 231 +++++++++++++++ .../dialog/src/dialog/dialog-content.ts | 270 +++++++++++++++++ .../dialog/src/dialog/dialog-ref.ts | 150 ++++++++++ .../dialog/src/dialog/dialog.service.ts | 280 ++++++++++++++++++ .../angular-business/dialog/src/public_api.ts | 7 + .../sbb-esta/angular-business/public-api.ts | 1 + 23 files changed, 1911 insertions(+), 4 deletions(-) create mode 100644 projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html create mode 100644 projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html create mode 100644 projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html create mode 100644 projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.html create mode 100644 projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.scss create mode 100644 projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts create mode 100644 projects/sbb-esta/angular-business/dialog/dialog.md create mode 100644 projects/sbb-esta/angular-business/dialog/index.ts create mode 100644 projects/sbb-esta/angular-business/dialog/package.json create mode 100644 projects/sbb-esta/angular-business/dialog/src/_dialog.scss create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog.module.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-animations.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.html create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-ref.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog.service.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/public_api.ts diff --git a/projects/angular-showcase/src/app/business/business-examples/business-examples.module.ts b/projects/angular-showcase/src/app/business/business-examples/business-examples.module.ts index 4d1a28346f..37e8696754 100644 --- a/projects/angular-showcase/src/app/business/business-examples/business-examples.module.ts +++ b/projects/angular-showcase/src/app/business/business-examples/business-examples.module.ts @@ -5,13 +5,23 @@ import { RouterModule } from '@angular/router'; import { ButtonModule } from '@sbb-esta/angular-business/button'; import { CheckboxModule } from '@sbb-esta/angular-business/checkbox'; import { ContextmenuModule } from '@sbb-esta/angular-business/contextmenu'; +import { DialogModule } from '@sbb-esta/angular-business/dialog'; import { FieldModule } from '@sbb-esta/angular-business/field'; import { HeaderModule } from '@sbb-esta/angular-business/header'; import { ProcessflowModule } from '@sbb-esta/angular-business/processflow'; +import { RadioButtonModule } from '@sbb-esta/angular-business/radio-button'; import { TooltipModule } from '@sbb-esta/angular-business/tooltip'; import { UserMenuModule } from '@sbb-esta/angular-business/usermenu'; import { IconCollectionModule } from '@sbb-esta/angular-icons'; +import { + DialogShowcaseComponent, + DialogShowcaseExample2Component, + DialogShowcaseExample2ContentComponent, + DialogShowcaseExample3Component, + DialogShowcaseExampleComponent, + DialogShowcaseExampleContentComponent +} from './dialog-showcase/dialog-showcase.component'; import { SimpleContextmenuComponent } from './simple-contextmenu/simple-contextmenu.component'; import { SkippableProcessflowComponent } from './skippable-processflow/skippable-processflow.component'; import { TooltipShowcaseComponent } from './tooltip-showcase/tooltip-showcase.component'; @@ -21,7 +31,13 @@ const exampleComponents = [ SimpleContextmenuComponent, SkippableProcessflowComponent, TooltipShowcaseComponent, - UsermenuShowcaseComponent + UsermenuShowcaseComponent, + DialogShowcaseComponent, + DialogShowcaseExampleComponent, + DialogShowcaseExampleContentComponent, + DialogShowcaseExample2Component, + DialogShowcaseExample2ContentComponent, + DialogShowcaseExample3Component ]; @NgModule({ @@ -41,7 +57,9 @@ const exampleComponents = [ HeaderModule, ProcessflowModule, TooltipModule, - UserMenuModule + UserMenuModule, + DialogModule, + RadioButtonModule ] }) export class BusinessExamplesModule {} diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html new file mode 100644 index 0000000000..1de0f1fd2a --- /dev/null +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html @@ -0,0 +1,21 @@ +
+
+

Hi {{ data.name }}

+
+
+
+ What's your favorite animal? + + + +
+
+
+ + +
+
diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html new file mode 100644 index 0000000000..a5c0a3d8c7 --- /dev/null +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html @@ -0,0 +1,99 @@ +
+
+

Terms and conditions

+
+
+
+

+ Learn one way to build applications with Angular and reuse your code and abilities to build + apps for any deployment target. For web, mobile web, native mobile and native desktop. +

+ +

Speed & Performance

+

+ Achieve the maximum speed possible on the Web Platform today, and take it further, via Web + Workers and server-side rendering. Angular puts you in control over scalability. Meet huge + data requirements by building data models on RxJS, Immutable.js or another push-model. +

+ +

Incredible tooling

+

+ Build features quickly with simple, declarative templates. Extend the template language with + your own components and use a wide array of existing components. Get immediate + Angular-specific help and feedback with nearly every IDE and editor. All this comes together + so you can focus on building amazing apps rather than trying to make the code work. +

+ +

Loved by millions

+

+ From prototype through global deployment, Angular delivers the productivity and scalable + infrastructure that supports Google's largest applications. +

+ +

What is Angular?

+ +

+ Angular is a platform that makes it easy to build applications with the web. Angular + combines declarative templates, dependency injection, end to end tooling, and integrated + best practices to solve development challenges. Angular empowers developers to build + applications that live on the web, mobile, or the desktop +

+ +

Architecture overview

+ +

+ Angular is a platform and framework for building client applications in HTML and TypeScript. + Angular is itself written in TypeScript. It implements core and optional functionality as a + set of TypeScript libraries that you import into your apps. +

+ +

+ The basic building blocks of an Angular application are NgModules, which provide a + compilation context for components. NgModules collect related code into functional sets; an + Angular app is defined by a set of NgModules. An app always has at least a root module that + enables bootstrapping, and typically has many more feature modules. +

+ +

+ Components define views, which are sets of screen elements that Angular can choose among and + modify according to your program logic and data. Every app has at least a root component. +

+ +

+ Components use services, which provide specific functionality not directly related to views. + Service providers can be injected into components as dependencies, making your code modular, + reusable, and efficient. +

+ +

+ Both components and services are simply classes, with decorators that mark their type and + provide metadata that tells Angular how to use them. +

+ +

+ The metadata for a component class associates it with a template that defines a view. A + template combines ordinary HTML with Angular directives and binding markup that allow + Angular to modify the HTML before rendering it for display. +

+ +

+ The metadata for a service class provides the information Angular needs to make it available + to components through Dependency Injection (DI). +

+ +

+ An app's components typically define many views, arranged hierarchically. Angular provides + the Router service to help you define navigation paths among views. The router provides + sophisticated in-browser navigational capabilities. END +

+
+
+
+ + +
+
diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html new file mode 100644 index 0000000000..05494f0c1b --- /dev/null +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html @@ -0,0 +1,33 @@ +
+ +
+ + +
+
+

Terms and conditions

+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+
+
+
+ + +
+
+
diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.html new file mode 100644 index 0000000000..ae4d4a4054 --- /dev/null +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.html @@ -0,0 +1,41 @@ +
+
+
+

Dialog sharing data

+ +
+
+

Description

+

+ This implementation shows how to share data between parent component and dialog component. +

+
+
+ +
+
+

Dialog with content loaded from Component

+ +
+
+

Description

+

+ This dialog load its content from a component and shows custom content inside the header, + scrollable content and footer with its 3 positioning options. +

+
+
+ +
+
+

Dialog with content loaded from Template

+ +
+
+

Description

+

+ Dialog with content loaded from a TemplateRef. +

+
+
+
diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.scss b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.scss new file mode 100644 index 0000000000..9bddd46b39 --- /dev/null +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.scss @@ -0,0 +1,31 @@ +.sbbsc-dialog-example-icon-1, +.sbbsc-dialog-example-icon-2 { + height: 20px; + vertical-align: middle; +} + +.sbbsc-dialog-example-icon-1 { + width: 20px; +} + +.sbbsc-dialog-example-icon-2 { + width: 60px; + fill: #fff; + background: #eb0000; +} + +.sbbsc-lb-disableclose-c-1 { + text-align: center; +} + +.sbbsc-lb-disableclose-c-2 { + text-align: center; + + h3 { + margin-bottom: 1em; + } + + button[mode='ghost'] { + margin-right: 8px; + } +} diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts new file mode 100644 index 0000000000..a48c8244be --- /dev/null +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts @@ -0,0 +1,123 @@ +import { Component, Inject, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Dialog, DIALOG_DATA, DialogRef } from '@sbb-esta/angular-business/dialog'; + +export interface DialogData { + animal: string; + name: string; +} + +/** + * Dialog sharing data + */ +@Component({ + selector: 'sbb-dialog-showcase-content-1', + templateUrl: 'dialog-showcase-content-1.component.html' +}) +export class DialogShowcaseExampleContentComponent { + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) public data: DialogData + ) {} + + noThanks(): void { + this.dialogRef.close(); + } +} + +@Component({ + selector: 'sbb-dialog-showcase-example', + template: ` +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + You chose: {{ animal }} +
  6. +
+ ` +}) +export class DialogShowcaseExampleComponent { + animal: string; + name: string; + + constructor(public dialog: Dialog) {} + + openDialog(): void { + const dialogRef = this.dialog.open(DialogShowcaseExampleContentComponent, { + data: { name: this.name, animal: this.animal } + }); + + dialogRef.afterClosed().subscribe(result => { + console.log('Dialog sharing data was closed'); + this.animal = result; + }); + } +} + +/** + * Dialog with content loaded from component, footer button bar + */ +@Component({ + selector: 'sbb-dialog-showcase-content-2', + templateUrl: 'dialog-showcase-content-2.component.html' +}) +export class DialogShowcaseExample2ContentComponent {} + +/** + * @title Dialog with header, scrollable content and actions + */ +@Component({ + selector: 'sbb-dialog-showcase-example-2', + template: ` +
+ +
+ ` +}) +export class DialogShowcaseExample2Component { + constructor(public dialog: Dialog) {} + + openDialog() { + const dialogRef = this.dialog.open(DialogShowcaseExample2ContentComponent); + + dialogRef.afterClosed().subscribe(result => { + console.log(`Dialog result: ${result}`); + }); + } +} + +/** + * Dialog with content loaded from Template + */ +@Component({ + selector: 'sbb-dialog-showcase-example-3', + templateUrl: 'dialog-showcase-content-3.component.html' +}) +export class DialogShowcaseExample3Component { + @ViewChild('sampleDialogTemplate', { static: true }) sampleDialogTemplate: TemplateRef; + constructor(public dialog: Dialog) {} + + openDialog() { + const dialogRef = this.dialog.open(this.sampleDialogTemplate); + + dialogRef.afterClosed().subscribe(result => { + console.log(`Dialog result: ${result}`); + }); + } +} + +@Component({ + selector: 'sbb-dialog-showcase', + templateUrl: 'dialog-showcase.component.html', + styleUrls: ['dialog-showcase.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DialogShowcaseComponent {} diff --git a/projects/angular-showcase/src/app/business/business/business.component.ts b/projects/angular-showcase/src/app/business/business/business.component.ts index 038f3effa8..645db01b9c 100644 --- a/projects/angular-showcase/src/app/business/business/business.component.ts +++ b/projects/angular-showcase/src/app/business/business/business.component.ts @@ -2,6 +2,7 @@ import { ComponentPortal } from '@angular/cdk/portal'; import { Component } from '@angular/core'; import { ExampleProvider } from '../../shared/example-provider'; +import { DialogShowcaseComponent } from '../business-examples/dialog-showcase/dialog-showcase.component'; import { SimpleContextmenuComponent } from '../business-examples/simple-contextmenu/simple-contextmenu.component'; import { SkippableProcessflowComponent } from '../business-examples/skippable-processflow/skippable-processflow.component'; import { TooltipShowcaseComponent } from '../business-examples/tooltip-showcase/tooltip-showcase.component'; @@ -37,7 +38,8 @@ export class BusinessComponent implements ExampleProvider { contextmenu: 'Contextmenu' }; popupsAndModals = { - tooltip: 'Tooltip' + tooltip: 'Tooltip', + dialog: 'Dialog' }; private _examples: { [component: string]: { [name: string]: ComponentPortal } } = { processflow: { @@ -51,7 +53,8 @@ export class BusinessComponent implements ExampleProvider { }, usermenu: { 'usermenu-showcase': new ComponentPortal(UsermenuShowcaseComponent) - } + }, + dialog: { 'dialog-showcase': new ComponentPortal(DialogShowcaseComponent) } }; resolveExample( diff --git a/projects/sbb-esta/angular-business/dialog/dialog.md b/projects/sbb-esta/angular-business/dialog/dialog.md new file mode 100644 index 0000000000..12ce7637e0 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/dialog.md @@ -0,0 +1,166 @@ +The dialog can be used to seek confirmation as see below + +```html +
+
+

Hi {{ data.name }}

+
+
+
+ What's your favorite animal? + + + +
+
+
+ + +
+
+``` + +### Sharing data with the Dialog component + +A dialog is opened by calling the `open` method and if you want to share data with your dialog, +you can use the `data` option to pass information to the dialog component. + +```ts +const dialogRef = this.dialog.open(DialogShowcaseExampleContentComponent, { + data: { name: this.name, animal: this.animal } +}); +``` + +Components created via `Dialog` can use `DialogRef` to close the dialog in which they are +contained. To access data in your dialog component, you have to use the `DialogData` injection +token. When closing, the data result value is provided. This result value is forwarded as the +result of the `afterClosed` promise. + +```ts +@Component({ + selector: 'sbb-dialog-showcase-content-1', + templateUrl: 'dialog-showcase-content-1.component.html' +}) +export class DialogShowcaseExampleContentComponent { + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) public data: DialogData + ) {} + + close(): void { + this.dialogRef.close(); + } +} +``` + +```ts +dialogRef.afterClosed().subscribe(result => { + console.log('Dialog sharing data was closed'); + this.animal = result; +}); +``` + +

Dialog with content loaded from Template

+ +You can use `Dialog` to load content from a TemplateRef by calling `open` method and +passing it the template reference: + +```ts +@Component({ + selector: 'sbb-dialog-showcase-example-3', + templateUrl: 'dialog-showcase-content-3.component.html' +}) +export class DialogShowcaseExample3Component { + @ViewChild('sampleDialogTemplate', { static: true }) sampleDialogTemplate: TemplateRef; + constructor(public dialog: Dialog) {} + + openDialog() { + const dialogRef = this.dialog.open(this.sampleDialogTemplate); + + dialogRef.afterClosed().subscribe(result => { + console.log(`Dialog result: ${result}`); + }); + } +} +``` + +```html + +
+
+

Terms and conditions

+
+
+
+

Lorem ipsum dolor sit amet...

+
+
+
+ + +
+
+
+``` + +- You can also use the disableClose property on `Dialog` to close the dialog manually and + listening changes with `manualCloseAction` method of DialogRef istance: + +```ts +@Component({ + selector: 'sbb-dialog-showcase-example-5', + template: ` +
+ +
+ ` +}) +export class DialogShowcaseExample5Component { + constructor(public dialog: Dialog) {} + openDialog() { + const dialogRef = this.dialog.open(DialogShowcaseExample5ContentComponent, { + disableClose: true + }); + dialogRef.afterClosed().subscribe(() => { + console.log(`Dialog confirmed`); + }); + } +} +``` + +```ts +export class DialogShowcaseExample5ContentComponent implements OnInit { + constructor( + private _dialogRef: DialogRef, + public dialog: Dialog + ) {} + ngOnInit() { + this._lightBoxRef.manualCloseAction.subscribe(() => { + this.dialog.open(DialogShowcaseExample6ContentComponent); + }); + } +} + +export class DialogShowcaseExample6ContentComponent { + constructor( + private _lightBoxRef: DialogRef, + public dialog: Dialog + ) {} + closeThisDialog() { + this._lightBoxRef.close(); + } + closeAllDialog() { + this.dialog.closeAll(); + } +} +``` diff --git a/projects/sbb-esta/angular-business/dialog/index.ts b/projects/sbb-esta/angular-business/dialog/index.ts new file mode 100644 index 0000000000..decc72d85b --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/index.ts @@ -0,0 +1 @@ +export * from './src/public_api'; diff --git a/projects/sbb-esta/angular-business/dialog/package.json b/projects/sbb-esta/angular-business/dialog/package.json new file mode 100644 index 0000000000..28982fec69 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/package.json @@ -0,0 +1,19 @@ +{ + "ngPackage": { + "lib": { + "umdModuleIds": { + "@sbb-esta/angular-core": "sbbAngularCore", + "@sbb-esta/angular-core/base": "sbbAngularCoreBase", + "@sbb-esta/angular-core/breakpoints": "sbbAngularCoreBreakpoints", + "@sbb-esta/angular-core/common-behaviors": "sbbAngularCoreCommonBehaviors", + "@sbb-esta/angular-core/datetime": "sbbAngularCoreDatetime", + "@sbb-esta/angular-core/error": "sbbAngularCoreError", + "@sbb-esta/angular-core/forms": "sbbAngularCoreForms", + "@sbb-esta/angular-core/icon-directive": "sbbAngularCoreIconDirective", + "@sbb-esta/angular-core/models": "sbbAngularCoreModels", + "@sbb-esta/angular-icons": "sbbAngularIcons", + "ngx-perfect-scrollbar": "ngxPerfectScrollbar" + } + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss new file mode 100644 index 0000000000..4e9ea8d5ba --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss @@ -0,0 +1,242 @@ +@import '../../../angular-core/styles/common/variants'; +@import '../../../angular-core/styles/common/colors'; +@import '../../../angular-core/styles/common/functions'; +@import '../../../angular-core/styles/common/mediaqueries'; +@import '../../../angular-core/styles/common/mixins'; +@import '../../../angular-core/styles/common/button'; + +$dialog-bgcolor: $sbbColorWhite; +$dialog-min-width: 560; +$dialog-max-width: 800; + +$dialog-header-height: 52; +$dialog-header-y-padding: 8; +$dialog-header-x-padding-mobile: 16; +$dialog-header-x-padding-tablet: 16; +$dialog-header-x-padding: 16; +$dialog-header-border-color: $sbbColorCloud; + +$dialog-content-y-padding-mobile: 24; +$dialog-content-y-padding: 24; +$dialog-content-x-padding-mobile: 16; +$dialog-content-x-padding-tablet: 16; +$dialog-content-x-padding: 16; + +$dialog-close-button-size: 24; +$dialog-close-icon-size: 24; +$dialog-close-button-color: $sbbColorStorm; +$dialog-close-button-color-hover: $sbbColorRed125; +$dialog-close-icon-color: $sbbColorGrey; + +$dialog-footer-height-mobile: 52; +$dialog-footer-height-tablet: 52; +$dialog-footer-padding: 0; + +@function getContentMaxHeightOffset($mode, $viewport: mobile) { + @if ($viewport == mobile) and ($mode == withHeaderAndFooter) { + @return $dialog-header-height + $dialog-footer-height-mobile; + } @else if ($viewport == tabletPortrait) and ($mode == withHeaderAndFooter) { + @return $dialog-header-height + $dialog-footer-height-tablet; + } @else if ($viewport == mobile) and ($mode == withHeader) { + @return $dialog-header-height; + } @else if ($viewport == mobile) and ($mode == withFooter) { + @return $dialog-footer-height-mobile; + } @else if ($viewport == tabletPortrait) and ($mode == withFooter) { + @return $dialog-footer-height-tablet; + } +} + +@mixin dialogContainer() { + display: flex; + align-items: center; + justify-content: center; + background-color: $dialog-bgcolor; + position: relative; + outline: 0; + width: 100%; + height: 100%; +} + +@mixin dialog() { + border: 1px solid $sbbColorGranite; + min-width: pxToEm($dialog-min-width); + max-width: pxToEm($dialog-max-width); +} + +@mixin dialogHeader() { + display: flex; + align-items: center; + height: pxToEm($dialog-header-height); + padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding-mobile); + border-bottom: 1px solid $dialog-header-border-color; + overflow: hidden; + + @include mq($from: tabletPortrait) { + padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding-tablet); + } + + @include mq($from: desktop) { + padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding); + } +} + +@mixin dialogCloseBtn() { + @include buttonResetFrameless(); + @include svgIconColor($dialog-close-icon-color); + + margin-left: auto; + position: relative; + width: pxToEm($dialog-close-button-size); + height: pxToEm($dialog-close-button-size); + + &::before { + content: ''; + position: absolute; + display: block; + width: 100%; + height: 100%; + top: 0; + left: 0; + transition: border 0.3s; + } + + svg { + @include absoluteCenterXY(); + + width: pxToEm($dialog-close-icon-size); + height: pxToEm($dialog-close-icon-size); + } + + &:hover, + &:focus { + @include svgIconColor($dialog-close-button-color-hover); + cursor: pointer; + + &::before { + border-color: $dialog-close-button-color-hover; + } + } +} + +@mixin dialogContentHeight($mode: default) { + perfect-scrollbar { + @if ($mode == default) { + max-height: 100vh; + } @else if ($mode == withHeader) { + max-height: calc(100vh - #{pxToEm(getContentMaxHeightOffset(withHeader))}); + } @else { + max-height: calc(100vh - #{pxToEm(getContentMaxHeightOffset($mode, mobile))}); + + @include mq($from: tabletPortrait) { + max-height: calc(100vh - #{pxToEm(getContentMaxHeightOffset($mode, tabletPortrait))}); + } + } + } +} + +@mixin dialogContent() { + display: block; + + perfect-scrollbar { + .ps-content { + padding: pxToEm($dialog-content-y-padding-mobile) pxToEm($dialog-content-x-padding-mobile); + + @include mq($from: tabletPortrait) { + padding: pxToEm($dialog-content-y-padding) pxToEm($dialog-content-x-padding-tablet); + } + + @include mq($from: desktop) { + padding: pxToEm($dialog-content-y-padding) pxToEm($dialog-content-x-padding); + } + } + } +} + +@mixin dialogFooter() { + perfect-scrollbar { + bottom: 0; + left: 0; + width: 100%; + max-height: pxToEm($dialog-footer-height-mobile + 2); + background-color: $dialog-bgcolor; + border-top: 1px solid $dialog-header-border-color; + + @include mq($from: tabletPortrait) { + max-height: pxToEm($dialog-footer-height-tablet + 2); + } + + .ps-content { + display: flex; + flex-direction: column; + align-items: center; + min-height: pxToEm($dialog-footer-height-mobile); + padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding-mobile); + + @include mq($from: tabletPortrait) { + flex-direction: row; + min-height: pxToEm($dialog-footer-height-tablet); + padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding-tablet); + } + + @include mq($from: desktop) { + padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding); + } + + button { + margin-bottom: pxToEm(10); + + @include mq($from: tabletPortrait) { + margin-bottom: 0; + } + } + } + } + + &-align-start { + perfect-scrollbar .ps-content { + justify-content: flex-start; + + button { + @include mq($from: tabletPortrait) { + margin-right: pxToEm(10); + } + } + } + } + + &-align-end { + perfect-scrollbar .ps-content { + justify-content: flex-end; + + button { + @include mq($from: tabletPortrait) { + margin-left: pxToEm(10); + } + } + } + } + + &-align-center { + perfect-scrollbar .ps-content { + justify-content: space-around; + } + } +} + +@mixin dialogTitle() { + text-align: center; + font-size: 28px; + font-family: $fontSbbThin; + margin-top: toPx(48 - $dialog-content-y-padding-mobile); + margin-bottom: 36px; + + @include mq($from: tabletPortrait) { + font-size: 30px; + margin-top: toPx(80 - $dialog-content-y-padding); + margin-bottom: 48px; + } + + @include mq($from: desktop) { + font-size: 40px; + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts new file mode 100644 index 0000000000..6bc13b194f --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts @@ -0,0 +1,43 @@ +import { OverlayModule } from '@angular/cdk/overlay'; +import { PortalModule } from '@angular/cdk/portal'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ScrollingModule } from '@sbb-esta/angular-core/scrolling'; +import { IconCrossModule } from '@sbb-esta/angular-icons'; + +import { DialogContainerComponent } from './dialog/dialog-container.component'; +import { + DialogCloseDirective, + DialogComponent, + DialogContentComponent, + DialogFooterComponent, + DialogHeaderComponent, + DialogTitleDirective +} from './dialog/dialog-content'; +import { Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER } from './dialog/dialog.service'; + +@NgModule({ + imports: [CommonModule, IconCrossModule, OverlayModule, PortalModule, ScrollingModule], + exports: [ + DialogContainerComponent, + DialogComponent, + DialogCloseDirective, + DialogHeaderComponent, + DialogContentComponent, + DialogFooterComponent, + DialogTitleDirective + ], + declarations: [ + DialogContainerComponent, + DialogComponent, + DialogCloseDirective, + DialogComponent, + DialogHeaderComponent, + DialogFooterComponent, + DialogContentComponent, + DialogTitleDirective + ], + providers: [Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER], + entryComponents: [DialogContainerComponent] +}) +export class DialogModule {} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-animations.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-animations.ts new file mode 100644 index 0000000000..f0cb511937 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-animations.ts @@ -0,0 +1,25 @@ +import { + animate, + AnimationTriggerMetadata, + state, + style, + transition, + trigger +} from '@angular/animations'; + +/** Animations used by Dialog. */ +export const DIALOG_ANIMATIONS: { + readonly slideDialog: AnimationTriggerMetadata; +} = { + /** Animation that slides the Dialog in and out of view and fades the opacity. */ + slideDialog: trigger('slideDialog', [ + // Note: The `enter` animation doesn't transition to something like `translate3d(0, 0, 0) + // scale(1)`, because for some reason specifying the transform explicitly, causes IE both + // to blur the dialog content and decimate the animation performance. Leaving it as `none` + // solves both issues. + state('enter', style({ transform: 'none', opacity: 1 })), + state('void', style({ transform: 'translate3d(0, 25%, 0) scale(0.9)', opacity: 0 })), + state('exit', style({ transform: 'translate3d(0, 25%, 0)', opacity: 0 })), + transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')) + ]) +}; diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts new file mode 100644 index 0000000000..95e545bbe7 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts @@ -0,0 +1,54 @@ +import { ScrollStrategy } from '@angular/cdk/overlay'; +import { ViewContainerRef } from '@angular/core'; + +/** Valid ARIA roles for a Dialog element. */ +export type DialogRole = 'dialog' | 'alertdialog'; + +/** + * Configuration for opening a modal dialog with the Dialog service. + */ +export class DialogConfig { + /** + * 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. + */ + viewContainerRef?: ViewContainerRef; + + /** ID for the Dialog. If omitted, a unique one will be generated. */ + id?: string; + + /** The ARIA role of the Dialog element. */ + role?: DialogRole = 'dialog'; + + /** Custom class for the overlay pane. */ + panelClass?: string | string[] = ''; + + /** Whether the user can use escape or clicking on the backdrop to close the modal. */ + disableClose? = false; + + /** Width of the Dialog. */ + width? = '100vw'; + + /** Height of the Dialog. */ + height? = '100vh'; + + /** Data being injected into the child component. */ + data?: D | null = null; + + /** ID of the element that describes the Dialog. */ + ariaDescribedBy?: string | null = null; + + /** Aria label to assign to the Dialog element */ + ariaLabel?: string | null = null; + + /** Whether the Dialog should focus the first focusable element on open. */ + autoFocus? = true; + + /** Scroll strategy to be used for the dialog. */ + scrollStrategy?: ScrollStrategy; + + /** Whether the Dialog should close when the user goes backwards/forwards in history. */ + closeOnNavigation? = true; +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.html b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.html new file mode 100644 index 0000000000..215f0d9c55 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.html @@ -0,0 +1 @@ + diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss new file mode 100644 index 0000000000..9446e710f3 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss @@ -0,0 +1,48 @@ +@import '../dialog'; + +sbb-dialog-container { + @include dialogContainer(); + + .sbb-dialog { + @include dialog(); + } + + .sbb-dialog-content { + @include dialogContent(); + @include dialogContentHeight(); + } + + .sbb-dialog-header { + @include dialogHeader(); + } + + .sbb-dialog-footer { + @include dialogFooter(); + } + + .sbb-dialog-close-btn { + @include dialogCloseBtn(); + } + + .sbb-dialog-title { + @include dialogTitle(); + } + + &.sbb-dialog-with-header { + .sbb-dialog-content { + @include dialogContentHeight(withHeader); + } + } + + &.sbb-dialog-with-footer { + .sbb-dialog-content { + @include dialogContentHeight(withFooter); + } + } + + &.sbb-dialog-with-header.sbb-dialog-with-footer { + .sbb-dialog-content { + @include dialogContentHeight(withHeaderAndFooter); + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.ts new file mode 100644 index 0000000000..bf8675f0ba --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.ts @@ -0,0 +1,231 @@ +import { AnimationEvent } from '@angular/animations'; +import { FocusTrap, FocusTrapFactory } from '@angular/cdk/a11y'; +import { + BasePortalOutlet, + CdkPortalOutlet, + ComponentPortal, + TemplatePortal +} from '@angular/cdk/portal'; +import { DOCUMENT } from '@angular/common'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentRef, + ElementRef, + EmbeddedViewRef, + EventEmitter, + HostBinding, + HostListener, + Inject, + Optional, + ViewChild, + ViewEncapsulation +} from '@angular/core'; + +import { DIALOG_ANIMATIONS } from './dialog-animations'; +import { DialogConfig } from './dialog-config'; + +/** + * Throws an exception for the case when a ComponentPortal is + * attached to a DomPortalOutlet without an origin. + * @docs-private + */ +export function throwDialogContentAlreadyAttachedError() { + throw Error('Attempting to attach dialog content after content is already attached'); +} + +/** + * Internal component that wraps user-provided dialog content. + * @docs-private + */ +@Component({ + selector: 'sbb-dialog-container', + templateUrl: 'dialog-container.component.html', + styleUrls: ['dialog-container.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [DIALOG_ANIMATIONS.slideDialog] +}) +export class DialogContainerComponent extends BasePortalOutlet { + /** The portal outlet inside of this container into which the dialog content will be loaded. */ + @ViewChild(CdkPortalOutlet, { static: true }) portalOutlet: CdkPortalOutlet; + + @HostBinding('class.sbb-dialog-container') + containerClass = true; + + @HostBinding('attr.tabindex') + tabIndex = '-1'; + + @HostBinding('attr.aria-modal') + arialModal = 'true'; + + @HostBinding('attr.id') + get dialogContainerID() { + return this.id; + } + + @HostBinding('attr.role') + get role() { + return this.config.role; + } + + @HostBinding('attr.aria-labelledby') + get ariaLabelledbyAttr() { + return this.config.ariaLabel ? null : this.ariaLabelledBy; + } + + @HostBinding('attr.aria-label') + get ariaLabel() { + return this.config.ariaLabel; + } + + @HostBinding('attr.aria-describedby') + get describeDby() { + return this.config.ariaDescribedBy || null; + } + + @HostBinding('@slideDialog') + get slideDialogAnimation() { + return this.state; + } + + @HostBinding('class.sbb-dialog-with-header') + get hasHeaderClass() { + return this.hasHeader; + } + + @HostBinding('class.sbb-dialog-with-footer') + get hasFooterClass() { + return this.hasFooter; + } + + /** The class that traps and manages focus within the dialog. */ + private _focusTrap: FocusTrap; + + /** Element that was focused before the dialog was opened. Save this to restore upon close. */ + private _elementFocusedBeforeDialogWasOpened: HTMLElement | null = null; + + /** State of the dialog animation. */ + state: 'void' | 'enter' | 'exit' = 'enter'; + + /** Emits when an animation state changes. */ + animationStateChanged = new EventEmitter(); + + /** ID of the element that should be considered as the dialog's label. */ + ariaLabelledBy: string | null = null; + + /** ID for the container DOM element. */ + id: string; + + hasHeader: boolean | null = null; + + hasFooter: boolean | null = null; + + constructor( + private _elementRef: ElementRef, + private _focusTrapFactory: FocusTrapFactory, + private _changeDetectorRef: ChangeDetectorRef, + @Optional() @Inject(DOCUMENT) private _document: any, + /** The dialog configuration. */ + public config: DialogConfig + ) { + super(); + } + + /** + * Attach a ComponentPortal as content to this dialog container. + * @param portal Portal to be attached as the dialog content. + */ + attachComponentPortal(portal: ComponentPortal): ComponentRef { + if (this.portalOutlet.hasAttached()) { + throwDialogContentAlreadyAttachedError(); + } + + this._savePreviouslyFocusedElement(); + return this.portalOutlet.attachComponentPortal(portal); + } + + /** + * Attach a TemplatePortal as content to this dialog container. + * @param portal Portal to be attached as the dialog content. + */ + attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef { + if (this.portalOutlet.hasAttached()) { + throwDialogContentAlreadyAttachedError(); + } + + this._savePreviouslyFocusedElement(); + return this.portalOutlet.attachTemplatePortal(portal); + } + + /** Moves the focus inside the focus trap. */ + private _trapFocus() { + if (!this._focusTrap) { + this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); + } + + // If were to attempt to focus immediately, then the content of the dialog would not yet be + // ready in instances where change detection has to run first. To deal with this, we simply + // wait for the microtask queue to be empty. + if (this.config.autoFocus) { + this._focusTrap.focusInitialElementWhenReady(); + } + } + + /** Restores focus to the element that was focused before the dialog opened. */ + private _restoreFocus() { + const toFocus = this._elementFocusedBeforeDialogWasOpened; + + // We need the extra check, because IE can set the `activeElement` to null in some cases. + if (toFocus && typeof toFocus.focus === 'function') { + toFocus.focus(); + } + + if (this._focusTrap) { + this._focusTrap.destroy(); + } + } + + /** Saves a reference to the element that was focused before the dialog was opened. */ + private _savePreviouslyFocusedElement() { + if (this._document) { + this._elementFocusedBeforeDialogWasOpened = this._document.activeElement as HTMLElement; + + // Note that there is no focus method when rendering on the server. + if (this._elementRef.nativeElement.focus) { + // Move focus onto the dialog immediately in order to prevent the user from accidentally + // opening multiple dialoges at the same time. Needs to be async, because the element + // may not be focusable immediately. + Promise.resolve().then(() => this._elementRef.nativeElement.focus()); + } + } + } + + /** Callback, invoked whenever an animation on the host completes. */ + @HostListener('@slideDialog.done', ['$event']) + onAnimationDone(event: AnimationEvent) { + if (event.toState === 'enter') { + this._trapFocus(); + } else if (event.toState === 'exit') { + this._restoreFocus(); + } + + this.animationStateChanged.emit(event); + } + + /** Callback, invoked when an animation on the host starts. */ + @HostListener('@slideDialog.start', ['$event']) + onAnimationStart(event: AnimationEvent) { + this.animationStateChanged.emit(event); + } + + /** Starts the dialog exit animation. */ + startExitAnimation(): void { + this.state = 'exit'; + + // Mark the container for check so it can react if the + // view container is using OnPush change detection. + this._changeDetectorRef.markForCheck(); + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts new file mode 100644 index 0000000000..0eaca12e26 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts @@ -0,0 +1,270 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Directive, + ElementRef, + HostBinding, + HostListener, + Input, + OnInit, + Optional +} from '@angular/core'; + +import { DialogRef } from './dialog-ref'; +import { Dialog } from './dialog.service'; + +/** Counter used to generate unique IDs for dialog elements. */ +let dialogElementUid = 0; + +/** + * Button that will close the current dialog. + */ +@Directive({ + selector: `button[sbbDialogClose]`, + exportAs: 'sbbDialogClose' +}) +export class DialogCloseDirective implements OnInit { + /** Screenreader label for the button. */ + @HostBinding('attr.aria-label') + ariaLabel = 'Close dialog'; + + /** Prevents accidental form submits. **/ + @HostBinding('attr.type') + btnType = 'button'; + + /** dialog close input **/ + // tslint:disable-next-line:no-input-rename + @Input('sbbDialogClose') + dialogResult: any; + + constructor( + /** Reference of dialog. */ + @Optional() public dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog + ) {} + + ngOnInit() { + if (!this.dialogRef) { + // When this directive is included in a dialog via TemplateRef (rather than being + // in a Component), the dialogRef isn't available via injection because embedded + // views cannot be given a custom injector. Instead, we look up the dialogRef by + // ID. This must occur in `onInit`, as the ID binding for the dialog container won't + // be resolved at constructor time. + this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); + } + } + + @HostListener('click') + onCloseClick() { + this.dialogRef.close(this.dialogResult); + } +} + +/** + * Header of a dialog element. Stays fixed to the top of the dialog when scrolling. + */ +@Component({ + selector: 'sbb-dialog, [sbbDialog]', + template: ` +
+ + + +
+ `, + exportAs: 'sbbDialog', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogComponent {} + +/** + * Header of a dialog element. Stays fixed to the top of the dialog when scrolling. + */ +@Component({ + selector: 'sbb-dialog-header, [sbbDialogHeader]', + template: ` + + + + `, + exportAs: 'sbbDialogHeader', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogHeaderComponent implements OnInit { + /** Disables dialog header when dialog is closed. */ + isCloseDisabled: boolean; + /** Class attribute on dialog header. */ + @HostBinding('class.sbb-dialog-header') + dialogHeaderClass = true; + + constructor( + @Optional() private _dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog, + private _changeDetectorRef: ChangeDetectorRef + ) {} + + ngOnInit() { + if (!this._dialogRef) { + this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); + } + + if (this._dialogRef) { + Promise.resolve().then(() => { + const container = this._dialogRef.containerInstance; + + if (container) { + container.hasHeader = true; + this.isCloseDisabled = container.config.disableClose; + this._changeDetectorRef.markForCheck(); + } + }); + } + } + + emitManualCloseAction() { + if (this._dialogRef) { + this._dialogRef.manualCloseAction.next(null); + } + } +} + +@Directive({ + selector: `[sbbDialogTitle]` +}) +export class DialogTitleDirective implements OnInit { + /** Identifier of dialog title. */ + @Input() + @HostBinding('attr.id') + id = `sbb-dialog-title-${dialogElementUid++}`; + /** Class attribute for dialog title. */ + @HostBinding('class.sbb-dialog-title') + dialogTitleClass = true; + + constructor( + @Optional() private _dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog + ) {} + + ngOnInit() { + if (!this._dialogRef) { + this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); + } + + if (this._dialogRef) { + Promise.resolve().then(() => { + const container = this._dialogRef.containerInstance; + + if (container && !container.ariaLabelledBy) { + container.ariaLabelledBy = this.id; + } + }); + } + } +} + +/** + * Scrollable content container of a dialog. + */ +@Component({ + selector: `sbb-dialog-content, [sbbDialogContent]`, + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogContentComponent { + /** Class attribute for dialog content */ + @HostBinding('class.sbb-dialog-content') + dialogContentClass = true; +} + +/** + * Container for the bottom action buttons in a dialog. + * Stays fixed to the bottom when scrolling. + */ +@Component({ + selector: `sbb-dialog-footer, [sbbDialogFooter]`, + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogFooterComponent implements OnInit { + /** Class attribute for the footer. */ + @HostBinding('class.sbb-dialog-footer') + dialogFooterClass = true; + /** Types of alignment. */ + @Input() alignment: 'left' | 'center' | 'right' = 'right'; + + /** Alignment to left position. */ + @HostBinding('class.sbb-dialog-footer-align-start') + get alignmentStartClass() { + return this.alignment === 'left'; + } + + /** Alignment to center position. */ + @HostBinding('class.sbb-dialog-footer-align-center') + get alignmentCenterClass() { + return this.alignment === 'center'; + } + + /** Alignment to right position. */ + @HostBinding('class.sbb-dialog-footer-align-end') + get alignmentEndClass() { + return this.alignment === 'right'; + } + + constructor( + @Optional() private _dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog + ) {} + + ngOnInit() { + if (!this._dialogRef) { + this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); + } + + if (this._dialogRef) { + Promise.resolve().then(() => { + const container = this._dialogRef.containerInstance; + + if (container) { + container.hasFooter = true; + } + }); + } + } +} + +/** + * Finds the closest DialogRef to an element by looking at the DOM. + * @param element Element relative to which to look for a dialog. + * @param openDialogs References to the currently-open dialogs. + */ +function getClosestDialog(element: ElementRef, openDialogs: DialogRef[]) { + let parent: HTMLElement | null = element.nativeElement.parentElement; + + while (parent && !parent.classList.contains('sbb-dialog-container')) { + parent = parent.parentElement; + } + + return parent ? openDialogs.find(dialog => dialog.id === parent.id) : null; +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-ref.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-ref.ts new file mode 100644 index 0000000000..7ccf223ce2 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-ref.ts @@ -0,0 +1,150 @@ +import { ESCAPE } from '@angular/cdk/keycodes'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { Location } from '@angular/common'; +import { Observable, Subject, Subscription, SubscriptionLike } from 'rxjs'; +import { filter, first } from 'rxjs/operators'; + +import { DialogContainerComponent } from './dialog-container.component'; + +// Counter for unique dialog ids. +let uniqueId = 0; + +/** + * Reference to a dialog opened via the Dialog service. + */ +export class DialogRef { + /** The instance of component opened into the dialog. */ + componentInstance: T; + + /** Whether the user is allowed to close the dialog. */ + disableClose: boolean | undefined = this.containerInstance.config.disableClose; + /** Observable to close manually a dialog. */ + manualCloseAction = new Subject(); + + /** Subject for notifying the user that the dialog has finished opening. */ + private readonly _afterOpen = new Subject(); + + /** Subject for notifying the user that the dialog has finished closing. */ + private readonly _afterClosed = new Subject(); + + /** Subject for notifying the user that the dialog has started closing. */ + private readonly _beforeClose = new Subject(); + + /** Result to be passed to afterClosed. */ + private _result: R | undefined; + + /** Subscription to changes in the user's location. */ + private _locationChanges: SubscriptionLike = Subscription.EMPTY; + + constructor( + /** The instance of the container component. */ + public containerInstance: DialogContainerComponent, + /** Identifier of dialog. */ + readonly id: string = `sbb-dialog-${uniqueId++}`, + private _overlayRef: OverlayRef, + location?: Location + ) { + // Pass the id along to the container. + containerInstance.id = id; + + // Emit when opening animation completes + containerInstance.animationStateChanged + .pipe( + filter(event => event.phaseName === 'done' && event.toState === 'enter'), + first() + ) + .subscribe(() => { + this._afterOpen.next(); + this._afterOpen.complete(); + }); + + // Dispose overlay when closing animation is complete + containerInstance.animationStateChanged + .pipe( + filter(event => event.phaseName === 'done' && event.toState === 'exit'), + first() + ) + .subscribe(() => this._overlayRef.dispose()); + + _overlayRef.detachments().subscribe(() => { + this._beforeClose.next(this._result); + this._beforeClose.complete(); + this._locationChanges.unsubscribe(); + this._afterClosed.next(this._result); + this._afterClosed.complete(); + this.componentInstance = null; + this._overlayRef.dispose(); + }); + + _overlayRef + .keydownEvents() + .pipe(filter(event => event.keyCode === ESCAPE && !this.disableClose)) + .subscribe(() => this.close()); + + _overlayRef + .keydownEvents() + .pipe(filter(event => event.keyCode === ESCAPE && this.disableClose)) + .subscribe(() => this.manualCloseAction.next(null)); + + if (location) { + // Close the dialog when the user goes forwards/backwards in history or when the location + // hash changes. Note that this usually doesn't include clicking on links (unless the user + // is using the `HashLocationStrategy`). + this._locationChanges = location.subscribe(() => { + if (this.containerInstance.config.closeOnNavigation) { + this.close(); + } + }); + } + } + + /** + * Close the dialog. + * @param dialogResult Optional result to return to the dialog opener. + */ + close(dialogResult?: R): void { + this._result = dialogResult; + + // Transition the backdrop in parallel to the dialog. + this.containerInstance.animationStateChanged + .pipe( + filter(event => event.phaseName === 'start'), + first() + ) + .subscribe(() => { + this._beforeClose.next(dialogResult); + this._beforeClose.complete(); + this._overlayRef.detachBackdrop(); + }); + + this.containerInstance.startExitAnimation(); + } + + /** + * Gets an observable that is notified when the dialog is finished opening. + */ + afterOpen(): Observable { + return this._afterOpen.asObservable(); + } + + /** + * Gets an observable that is notified when the dialog is finished closing. + */ + afterClosed(): Observable { + return this._afterClosed.asObservable(); + } + + /** + * Gets an observable that is notified when the dialog has started closing. + */ + beforeClose(): Observable { + return this._beforeClose.asObservable(); + } + + /** + * Gets an observable that emits when keydown events are targeted on the overlay. + */ + keydownEvents(): Observable { + return this._overlayRef.keydownEvents(); + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog.service.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog.service.ts new file mode 100644 index 0000000000..2b5ae7e105 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog.service.ts @@ -0,0 +1,280 @@ +import { Overlay, OverlayConfig, OverlayRef, ScrollStrategy } from '@angular/cdk/overlay'; +import { + ComponentPortal, + ComponentType, + PortalInjector, + TemplatePortal +} from '@angular/cdk/portal'; +import { Location } from '@angular/common'; +import { + Inject, + Injectable, + InjectionToken, + Injector, + Optional, + SkipSelf, + TemplateRef +} from '@angular/core'; +import { defer, Observable, Subject } from 'rxjs'; +import { startWith } from 'rxjs/operators'; + +import { DialogConfig } from './dialog-config'; +import { DialogContainerComponent } from './dialog-container.component'; +import { DialogRef } from './dialog-ref'; + +/** Injection token that can be used to access the data that was passed in to a dialog. */ +export const DIALOG_DATA = new InjectionToken('DialogData'); + +/** Injection token that can be used to specify default dialog options. */ +export const DIALOG_DEFAULT_OPTIONS = new InjectionToken('DialogDefaultOptions'); + +/** Injection token that determines the scroll handling while the dialog is open. */ +export const DIALOG_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>( + 'DialogScrollStrategy' +); + +/** @docs-private */ +export function SBB_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY( + overlay: Overlay +): () => ScrollStrategy { + return () => overlay.scrollStrategies.block(); +} + +/** @docs-private */ +export const DIALOG_SCROLL_STRATEGY_PROVIDER = { + provide: DIALOG_SCROLL_STRATEGY, + deps: [Overlay], + useFactory: SBB_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY +}; + +/** + * Service to open SBB Design modal dialogs. + */ +@Injectable({ + providedIn: 'root' +}) +export class Dialog { + private _openDialogsAtThisLevel: DialogRef[] = []; + private readonly _afterAllClosedAtThisLevel = new Subject(); + private readonly _afterOpenAtThisLevel = new Subject>(); + + /** Keeps track of the currently-open dialogs. */ + get openDialogs(): DialogRef[] { + return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel; + } + + /** Stream that emits when a dialog has been opened. */ + get afterOpen(): Subject> { + return this._parentDialog ? this._parentDialog.afterOpen : this._afterOpenAtThisLevel; + } + + get _afterAllClosed(): Subject { + const parent = this._parentDialog; + return parent ? parent._afterAllClosed : this._afterAllClosedAtThisLevel; + } + + /** + * Stream that emits when all open dialog have finished closing. + * Will emit on subscribe if there are no open dialogs to begin with. + */ + readonly afterAllClosed: Observable = defer(() => + this.openDialogs.length ? this._afterAllClosed : this._afterAllClosed.pipe(startWith(undefined)) + ); + + constructor( + private _overlay: Overlay, + private _injector: Injector, + @Optional() private _location: Location, + @Optional() @Inject(DIALOG_DEFAULT_OPTIONS) private _defaultOptions, + @Inject(DIALOG_SCROLL_STRATEGY) private _scrollStrategy, + @Optional() @SkipSelf() private _parentDialog: Dialog + ) {} + + /** + * Opens a modal dialog containing the given component. + * @param componentOrTemplateRef Type of the component to load into the dialog, + * or a TemplateRef to instantiate as the dialog content. + * @param config Extra configuration options. + * @returns Reference to the newly-opened dialog. + */ + open( + componentOrTemplateRef: ComponentType | TemplateRef, + config?: DialogConfig + ): DialogRef { + config = { ...(this._defaultOptions || new DialogConfig()), ...config }; + if (config.id && this.getDialogById(config.id)) { + throw Error(`dialog with id "${config.id}" exists already. The dialog id must be unique.`); + } + + const overlayRef = this._createOverlay(config); + const dialogContainer = this._attachDialogContainer(overlayRef, config); + const dialogRef = this._attachDialogContent( + componentOrTemplateRef, + dialogContainer, + overlayRef, + config + ); + + this.openDialogs.push(dialogRef); + dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef)); + this.afterOpen.next(dialogRef); + + return dialogRef; + } + + /** + * Closes all of the currently-open dialogs. + */ + closeAll(): void { + let i = this.openDialogs.length; + + while (i--) { + // The `openDialogs` property isn't updated after close until the rxjs subscription + // runs on the next microtask, in addition to modifying the array as we're going + // through it. We loop through all of them and call close without assuming that + // they'll be removed from the list instantaneously. + this.openDialogs[i].close(); + } + } + + /** + * Finds an open dialog by its id. + * @param id ID to use when looking up the dialog. + * @returns Dialog reference associated to the input id. + */ + getDialogById(id: string): DialogRef | undefined { + return this.openDialogs.find(dialog => dialog.id === id); + } + + /** + * Creates the overlay into which the dialog will be loaded. + * @param config The dialog configuration. + * @returns A promise resolving to the OverlayRef for the created overlay. + */ + private _createOverlay(config: DialogConfig): OverlayRef { + const overlayConfig = this._getOverlayConfig(config); + return this._overlay.create(overlayConfig); + } + + /** + * Creates an overlay config from a dialog config. + * @param dialogConfig The dialog configuration. + * @returns The overlay configuration. + */ + private _getOverlayConfig(dialogConfig: DialogConfig): OverlayConfig { + return new OverlayConfig({ + positionStrategy: this._overlay.position().global(), + scrollStrategy: dialogConfig.scrollStrategy || this._scrollStrategy(), + panelClass: dialogConfig.panelClass, + width: dialogConfig.width, + height: dialogConfig.height + }); + } + + /** + * Attaches an DialogContainer to a dialog's already-created overlay. + * @param overlay Reference to the dialog's underlying overlay. + * @param config The dialog configuration. + * @returns A promise resolving to a ComponentRef for the attached container. + */ + private _attachDialogContainer( + overlay: OverlayRef, + config: DialogConfig + ): DialogContainerComponent { + const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; + const injector = new PortalInjector( + userInjector || this._injector, + new WeakMap([[DialogConfig, config]]) + ); + const containerPortal = new ComponentPortal( + DialogContainerComponent, + config.viewContainerRef, + injector + ); + const containerRef = overlay.attach(containerPortal); + + return containerRef.instance; + } + + /** + * Attaches the user-provided component to the already-created DialogContainer. + * @param componentOrTemplateRef The type of component being loaded into the dialog, + * or a TemplateRef to instantiate as the content. + * @param dialogContainer Reference to the wrapping DialogContainer. + * @param overlayRef Reference to the overlay in which the dialog resides. + * @param config The dialog configuration. + * @returns A promise resolving to the DialogRef that should be returned to the user. + */ + private _attachDialogContent( + componentOrTemplateRef: ComponentType | TemplateRef, + dialogContainer: DialogContainerComponent, + overlayRef: OverlayRef, + config: DialogConfig + ): DialogRef { + // Create a reference to the dialog we're creating in order to give the user a handle + // to modify and close it. + const dialogRef = new DialogRef(dialogContainer, config.id, overlayRef, this._location); + + if (componentOrTemplateRef instanceof TemplateRef) { + dialogContainer.attachTemplatePortal( + new TemplatePortal(componentOrTemplateRef, null, { + $implicit: config.data, + dialogRef + }) + ); + } else { + const injector = this._createInjector(config, dialogRef, dialogContainer); + const contentRef = dialogContainer.attachComponentPortal( + new ComponentPortal(componentOrTemplateRef, undefined, injector) + ); + dialogRef.componentInstance = contentRef.instance; + } + + return dialogRef; + } + + /** + * Creates a custom injector to be used inside the dialog. This allows a component loaded inside + * of a dialog to close itself and, optionally, to return a value. + * @param config Config object that is used to construct the dialog. + * @param dialogRef Reference to the dialog. + * @param dialogContainer dialog container element that wraps all of the contents. + * @returns The custom injector that can be used inside the dialog. + */ + private _createInjector( + config: DialogConfig, + dialogRef: DialogRef, + dialogContainer: DialogContainerComponent + ): PortalInjector { + const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; + + // The DialogContainer is injected in the portal as the Dialog and the dialog's + // content are created out of the same ViewContainerRef and as such, are siblings for injector + // purposes. To allow the hierarchy that is expected, the DialogContainer is explicitly + // added to the injection tokens. + const injectionTokens = new WeakMap([ + [DialogContainerComponent, dialogContainer], + [DIALOG_DATA, config.data], + [DialogRef, dialogRef] + ]); + + return new PortalInjector(userInjector || this._injector, injectionTokens); + } + + /** + * Removes a dialog from the array of open dialogs. + * @param dialogRef dialog to be removed. + */ + private _removeOpenDialog(dialogRef: DialogRef) { + const index = this.openDialogs.indexOf(dialogRef); + + if (index > -1) { + this.openDialogs.splice(index, 1); + + // emit to the `afterAllClosed` stream. + if (!this.openDialogs.length) { + this._afterAllClosed.next(); + } + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/public_api.ts b/projects/sbb-esta/angular-business/dialog/src/public_api.ts new file mode 100644 index 0000000000..e319f01a1b --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/public_api.ts @@ -0,0 +1,7 @@ +export * from './dialog.module'; +export * from './dialog/dialog.service'; +export * from './dialog/dialog-container.component'; +export * from './dialog/dialog-content'; +export * from './dialog/dialog-config'; +export * from './dialog/dialog-ref'; +export * from './dialog/dialog-animations'; diff --git a/projects/sbb-esta/angular-business/public-api.ts b/projects/sbb-esta/angular-business/public-api.ts index 30c77358b7..c164fc58e6 100644 --- a/projects/sbb-esta/angular-business/public-api.ts +++ b/projects/sbb-esta/angular-business/public-api.ts @@ -7,6 +7,7 @@ export * from '@sbb-esta/angular-business/button'; export * from '@sbb-esta/angular-business/checkbox'; export * from '@sbb-esta/angular-business/contextmenu'; export * from '@sbb-esta/angular-business/datepicker'; +export * from '@sbb-esta/angular-business/dialog'; export * from '@sbb-esta/angular-business/field'; export * from '@sbb-esta/angular-business/header'; export * from '@sbb-esta/angular-business/option'; From 2921c514d1f1e205d110e7e64cd764d47609379a Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 28 Oct 2019 08:39:53 +0100 Subject: [PATCH 02/16] feat(business): implement dialog component Improve showcase text --- .../dialog-showcase/dialog-showcase-content-2.component.html | 2 +- .../dialog-showcase/dialog-showcase-content-3.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html index a5c0a3d8c7..262957b63b 100644 --- a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html @@ -1,6 +1,6 @@
-

Terms and conditions

+

Install Angular

diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html index 05494f0c1b..f1630305d5 100644 --- a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html @@ -7,7 +7,7 @@
-

Terms and conditions

+

Lorem Ipsum

From 7a59a5e9aeb1fed1f095cfc1d0d799eca28f1804 Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 28 Oct 2019 08:40:50 +0100 Subject: [PATCH 03/16] feat(business): implement dialog component Fix layering for dialog component --- projects/angular-showcase/src/app/app.component.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/angular-showcase/src/app/app.component.scss b/projects/angular-showcase/src/app/app.component.scss index f6f2441786..b38094193d 100644 --- a/projects/angular-showcase/src/app/app.component.scss +++ b/projects/angular-showcase/src/app/app.component.scss @@ -21,7 +21,7 @@ top: 0; height: 48px; border-bottom: solid 1px #cccccc; - z-index: 2000; + z-index: 980; a { height: 48px; @@ -45,7 +45,7 @@ width: 300px; height: 100%; top: 0; - z-index: 1000; + z-index: 980; transition: all 0.3s ease; overflow-y: auto; @@ -101,7 +101,7 @@ transform: rotate(0deg); transition: all 0.3s ease; cursor: pointer; - z-index: 3000; + z-index: 990; span { display: block; From 47527768b4e9cb96a136de0009c766cc750e421e Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 28 Oct 2019 08:41:34 +0100 Subject: [PATCH 04/16] feat(business): implement dialog component Improve imports on dialog styling --- projects/sbb-esta/angular-business/dialog/src/_dialog.scss | 1 - .../dialog/src/dialog/dialog-container.component.scss | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss index 4e9ea8d5ba..79a76de29b 100644 --- a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss +++ b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss @@ -1,4 +1,3 @@ -@import '../../../angular-core/styles/common/variants'; @import '../../../angular-core/styles/common/colors'; @import '../../../angular-core/styles/common/functions'; @import '../../../angular-core/styles/common/mediaqueries'; diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss index 9446e710f3..f81ac9f45a 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss @@ -1,3 +1,4 @@ +@import '../../../../angular-core/styles/common/variants'; @import '../dialog'; sbb-dialog-container { From 26c310a4b3d4b295b7f81457bbc98d7360c9a968 Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 28 Oct 2019 08:57:13 +0100 Subject: [PATCH 05/16] feat(business): implement dialog component Improve documentation --- projects/sbb-esta/angular-business/dialog/dialog.md | 4 ++-- .../angular-business/dialog/src/dialog/dialog-content.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/sbb-esta/angular-business/dialog/dialog.md b/projects/sbb-esta/angular-business/dialog/dialog.md index 12ce7637e0..eca212b009 100644 --- a/projects/sbb-esta/angular-business/dialog/dialog.md +++ b/projects/sbb-esta/angular-business/dialog/dialog.md @@ -36,7 +36,7 @@ const dialogRef = this.dialog.open(DialogShowcaseExampleContentComponent, { ``` Components created via `Dialog` can use `DialogRef` to close the dialog in which they are -contained. To access data in your dialog component, you have to use the `DialogData` injection +contained. To access data in your dialog component, you have to use the `DIALOG_DATA` injection token. When closing, the data result value is provided. This result value is forwarded as the result of the `afterClosed` promise. @@ -64,7 +64,7 @@ dialogRef.afterClosed().subscribe(result => { }); ``` -

Dialog with content loaded from Template

+### Dialog with content loaded from Template You can use `Dialog` to load content from a TemplateRef by calling `open` method and passing it the template reference: diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts index 0eaca12e26..c6c68a5c28 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts @@ -63,7 +63,7 @@ export class DialogCloseDirective implements OnInit { } /** - * Header of a dialog element. Stays fixed to the top of the dialog when scrolling. + * Dialog wrapper element. Contains the dialog's header, content and footer. */ @Component({ selector: 'sbb-dialog, [sbbDialog]', From e52fc2d1e9b5a42d2f767985c5504ac8c24fe375 Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 28 Oct 2019 08:58:24 +0100 Subject: [PATCH 06/16] feat(business): implement dialog component Remove double declaration --- projects/sbb-esta/angular-business/dialog/src/dialog.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts index 6bc13b194f..8ad8597bc1 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts @@ -31,7 +31,6 @@ import { Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER } from './dialog/dialog.service DialogContainerComponent, DialogComponent, DialogCloseDirective, - DialogComponent, DialogHeaderComponent, DialogFooterComponent, DialogContentComponent, From 0047f1e44fdb75fc946aeb98f9acd518df2316b9 Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 28 Oct 2019 09:20:18 +0100 Subject: [PATCH 07/16] feat(business): implement dialog component Add background opacity --- projects/sbb-esta/angular-business/dialog/src/_dialog.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss index 79a76de29b..eff58d3dd3 100644 --- a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss +++ b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss @@ -49,7 +49,7 @@ $dialog-footer-padding: 0; display: flex; align-items: center; justify-content: center; - background-color: $dialog-bgcolor; + background-color: rgba(255, 255, 255, 0.7); position: relative; outline: 0; width: 100%; @@ -58,6 +58,7 @@ $dialog-footer-padding: 0; @mixin dialog() { border: 1px solid $sbbColorGranite; + background-color: $dialog-bgcolor; min-width: pxToEm($dialog-min-width); max-width: pxToEm($dialog-max-width); } From aeccd75ec15ab9c8056489c83ce4047a3af995de Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 28 Oct 2019 10:25:33 +0100 Subject: [PATCH 08/16] feat(business): implement dialog component Extract components and directives into separate files --- .../dialog/src/dialog.module.ts | 14 +- .../src/dialog/dialog-close.directive.ts | 62 ++++ .../src/dialog/dialog-content.component.ts | 19 ++ .../dialog/src/dialog/dialog-content.ts | 270 ------------------ .../src/dialog/dialog-footer.component.ts | 78 +++++ .../src/dialog/dialog-header.component.ts | 78 +++++ .../src/dialog/dialog-helper.service.ts | 21 ++ .../src/dialog/dialog-title.directive.ts | 47 +++ .../dialog/src/dialog/dialog.component.ts | 18 ++ .../angular-business/dialog/src/public_api.ts | 5 +- 10 files changed, 333 insertions(+), 279 deletions(-) create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-close.directive.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.component.ts delete mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer.component.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header.component.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-helper.service.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-title.directive.ts create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog.component.ts diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts index 8ad8597bc1..c87aaab14f 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts @@ -5,15 +5,13 @@ import { NgModule } from '@angular/core'; import { ScrollingModule } from '@sbb-esta/angular-core/scrolling'; import { IconCrossModule } from '@sbb-esta/angular-icons'; +import { DialogCloseDirective } from './dialog/dialog-close.directive'; import { DialogContainerComponent } from './dialog/dialog-container.component'; -import { - DialogCloseDirective, - DialogComponent, - DialogContentComponent, - DialogFooterComponent, - DialogHeaderComponent, - DialogTitleDirective -} from './dialog/dialog-content'; +import { DialogContentComponent } from './dialog/dialog-content.component'; +import { DialogFooterComponent } from './dialog/dialog-footer.component'; +import { DialogHeaderComponent } from './dialog/dialog-header.component'; +import { DialogTitleDirective } from './dialog/dialog-title.directive'; +import { DialogComponent } from './dialog/dialog.component'; import { Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER } from './dialog/dialog.service'; @NgModule({ diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-close.directive.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-close.directive.ts new file mode 100644 index 0000000000..cdc262b3e3 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-close.directive.ts @@ -0,0 +1,62 @@ +import { + Directive, + ElementRef, + HostBinding, + HostListener, + Input, + OnInit, + Optional +} from '@angular/core'; + +import { DialogHelperService } from './dialog-helper.service'; +import { DialogRef } from './dialog-ref'; +import { Dialog } from './dialog.service'; + +/** + * Button that will close the current dialog. + */ +@Directive({ + selector: `button[sbbDialogClose]`, + exportAs: 'sbbDialogClose' +}) +export class DialogCloseDirective implements OnInit { + /** Screenreader label for the button. */ + @HostBinding('attr.aria-label') + ariaLabel = 'Close dialog'; + + /** Prevents accidental form submits. **/ + @HostBinding('attr.type') + btnType = 'button'; + + /** dialog close input **/ + // tslint:disable-next-line:no-input-rename + @Input('sbbDialogClose') + dialogResult: any; + + constructor( + /** Reference of dialog. */ + @Optional() public dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog, + private _dialogHelperService: DialogHelperService + ) {} + + ngOnInit() { + if (!this.dialogRef) { + // When this directive is included in a dialog via TemplateRef (rather than being + // in a Component), the dialogRef isn't available via injection because embedded + // views cannot be given a custom injector. Instead, we look up the dialogRef by + // ID. This must occur in `onInit`, as the ID binding for the dialog container won't + // be resolved at constructor time. + this.dialogRef = this._dialogHelperService.getClosestDialog( + this._elementRef, + this._dialog.openDialogs + ); + } + } + + @HostListener('click') + onCloseClick() { + this.dialogRef.close(this.dialogResult); + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.component.ts new file mode 100644 index 0000000000..f2baea83ba --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; + +/** + * Scrollable content container of a dialog. + */ +@Component({ + selector: `sbb-dialog-content, [sbbDialogContent]`, + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogContentComponent { + /** Class attribute for dialog content */ + @HostBinding('class.sbb-dialog-content') + dialogContentClass = true; +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts deleted file mode 100644 index c6c68a5c28..0000000000 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Directive, - ElementRef, - HostBinding, - HostListener, - Input, - OnInit, - Optional -} from '@angular/core'; - -import { DialogRef } from './dialog-ref'; -import { Dialog } from './dialog.service'; - -/** Counter used to generate unique IDs for dialog elements. */ -let dialogElementUid = 0; - -/** - * Button that will close the current dialog. - */ -@Directive({ - selector: `button[sbbDialogClose]`, - exportAs: 'sbbDialogClose' -}) -export class DialogCloseDirective implements OnInit { - /** Screenreader label for the button. */ - @HostBinding('attr.aria-label') - ariaLabel = 'Close dialog'; - - /** Prevents accidental form submits. **/ - @HostBinding('attr.type') - btnType = 'button'; - - /** dialog close input **/ - // tslint:disable-next-line:no-input-rename - @Input('sbbDialogClose') - dialogResult: any; - - constructor( - /** Reference of dialog. */ - @Optional() public dialogRef: DialogRef, - private _elementRef: ElementRef, - private _dialog: Dialog - ) {} - - ngOnInit() { - if (!this.dialogRef) { - // When this directive is included in a dialog via TemplateRef (rather than being - // in a Component), the dialogRef isn't available via injection because embedded - // views cannot be given a custom injector. Instead, we look up the dialogRef by - // ID. This must occur in `onInit`, as the ID binding for the dialog container won't - // be resolved at constructor time. - this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); - } - } - - @HostListener('click') - onCloseClick() { - this.dialogRef.close(this.dialogResult); - } -} - -/** - * Dialog wrapper element. Contains the dialog's header, content and footer. - */ -@Component({ - selector: 'sbb-dialog, [sbbDialog]', - template: ` -
- - - -
- `, - exportAs: 'sbbDialog', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DialogComponent {} - -/** - * Header of a dialog element. Stays fixed to the top of the dialog when scrolling. - */ -@Component({ - selector: 'sbb-dialog-header, [sbbDialogHeader]', - template: ` - - - - `, - exportAs: 'sbbDialogHeader', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DialogHeaderComponent implements OnInit { - /** Disables dialog header when dialog is closed. */ - isCloseDisabled: boolean; - /** Class attribute on dialog header. */ - @HostBinding('class.sbb-dialog-header') - dialogHeaderClass = true; - - constructor( - @Optional() private _dialogRef: DialogRef, - private _elementRef: ElementRef, - private _dialog: Dialog, - private _changeDetectorRef: ChangeDetectorRef - ) {} - - ngOnInit() { - if (!this._dialogRef) { - this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); - } - - if (this._dialogRef) { - Promise.resolve().then(() => { - const container = this._dialogRef.containerInstance; - - if (container) { - container.hasHeader = true; - this.isCloseDisabled = container.config.disableClose; - this._changeDetectorRef.markForCheck(); - } - }); - } - } - - emitManualCloseAction() { - if (this._dialogRef) { - this._dialogRef.manualCloseAction.next(null); - } - } -} - -@Directive({ - selector: `[sbbDialogTitle]` -}) -export class DialogTitleDirective implements OnInit { - /** Identifier of dialog title. */ - @Input() - @HostBinding('attr.id') - id = `sbb-dialog-title-${dialogElementUid++}`; - /** Class attribute for dialog title. */ - @HostBinding('class.sbb-dialog-title') - dialogTitleClass = true; - - constructor( - @Optional() private _dialogRef: DialogRef, - private _elementRef: ElementRef, - private _dialog: Dialog - ) {} - - ngOnInit() { - if (!this._dialogRef) { - this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); - } - - if (this._dialogRef) { - Promise.resolve().then(() => { - const container = this._dialogRef.containerInstance; - - if (container && !container.ariaLabelledBy) { - container.ariaLabelledBy = this.id; - } - }); - } - } -} - -/** - * Scrollable content container of a dialog. - */ -@Component({ - selector: `sbb-dialog-content, [sbbDialogContent]`, - template: ` - - - - `, - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DialogContentComponent { - /** Class attribute for dialog content */ - @HostBinding('class.sbb-dialog-content') - dialogContentClass = true; -} - -/** - * Container for the bottom action buttons in a dialog. - * Stays fixed to the bottom when scrolling. - */ -@Component({ - selector: `sbb-dialog-footer, [sbbDialogFooter]`, - template: ` - - - - `, - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DialogFooterComponent implements OnInit { - /** Class attribute for the footer. */ - @HostBinding('class.sbb-dialog-footer') - dialogFooterClass = true; - /** Types of alignment. */ - @Input() alignment: 'left' | 'center' | 'right' = 'right'; - - /** Alignment to left position. */ - @HostBinding('class.sbb-dialog-footer-align-start') - get alignmentStartClass() { - return this.alignment === 'left'; - } - - /** Alignment to center position. */ - @HostBinding('class.sbb-dialog-footer-align-center') - get alignmentCenterClass() { - return this.alignment === 'center'; - } - - /** Alignment to right position. */ - @HostBinding('class.sbb-dialog-footer-align-end') - get alignmentEndClass() { - return this.alignment === 'right'; - } - - constructor( - @Optional() private _dialogRef: DialogRef, - private _elementRef: ElementRef, - private _dialog: Dialog - ) {} - - ngOnInit() { - if (!this._dialogRef) { - this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs); - } - - if (this._dialogRef) { - Promise.resolve().then(() => { - const container = this._dialogRef.containerInstance; - - if (container) { - container.hasFooter = true; - } - }); - } - } -} - -/** - * Finds the closest DialogRef to an element by looking at the DOM. - * @param element Element relative to which to look for a dialog. - * @param openDialogs References to the currently-open dialogs. - */ -function getClosestDialog(element: ElementRef, openDialogs: DialogRef[]) { - let parent: HTMLElement | null = element.nativeElement.parentElement; - - while (parent && !parent.classList.contains('sbb-dialog-container')) { - parent = parent.parentElement; - } - - return parent ? openDialogs.find(dialog => dialog.id === parent.id) : null; -} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer.component.ts new file mode 100644 index 0000000000..485611fbfa --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer.component.ts @@ -0,0 +1,78 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + HostBinding, + Input, + OnInit, + Optional +} from '@angular/core'; + +import { DialogHelperService } from './dialog-helper.service'; +import { DialogRef } from './dialog-ref'; +import { Dialog } from './dialog.service'; + +/** + * Container for the bottom action buttons in a dialog. + * Stays fixed to the bottom when scrolling. + */ +@Component({ + selector: `sbb-dialog-footer, [sbbDialogFooter]`, + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogFooterComponent implements OnInit { + /** Class attribute for the footer. */ + @HostBinding('class.sbb-dialog-footer') + dialogFooterClass = true; + /** Types of alignment. */ + @Input() alignment: 'left' | 'center' | 'right' = 'right'; + + /** Alignment to left position. */ + @HostBinding('class.sbb-dialog-footer-align-start') + get alignmentStartClass() { + return this.alignment === 'left'; + } + + /** Alignment to center position. */ + @HostBinding('class.sbb-dialog-footer-align-center') + get alignmentCenterClass() { + return this.alignment === 'center'; + } + + /** Alignment to right position. */ + @HostBinding('class.sbb-dialog-footer-align-end') + get alignmentEndClass() { + return this.alignment === 'right'; + } + + constructor( + @Optional() private _dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog, + private _dialogHelperService: DialogHelperService + ) {} + + ngOnInit() { + if (!this._dialogRef) { + this._dialogRef = this._dialogHelperService.getClosestDialog( + this._elementRef, + this._dialog.openDialogs + ); + } + + if (this._dialogRef) { + Promise.resolve().then(() => { + const container = this._dialogRef.containerInstance; + + if (container) { + container.hasFooter = true; + } + }); + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header.component.ts new file mode 100644 index 0000000000..121888c34a --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header.component.ts @@ -0,0 +1,78 @@ +/** + * Header of a dialog element. Stays fixed to the top of the dialog when scrolling. + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + OnInit, + Optional +} from '@angular/core'; + +import { DialogHelperService } from './dialog-helper.service'; +import { DialogRef } from './dialog-ref'; +import { Dialog } from './dialog.service'; + +@Component({ + selector: 'sbb-dialog-header, [sbbDialogHeader]', + template: ` + + + + `, + exportAs: 'sbbDialogHeader', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogHeaderComponent implements OnInit { + /** Disables dialog header when dialog is closed. */ + isCloseDisabled: boolean; + /** Class attribute on dialog header. */ + @HostBinding('class.sbb-dialog-header') + dialogHeaderClass = true; + + constructor( + @Optional() private _dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog, + private _changeDetectorRef: ChangeDetectorRef, + private _dialogHelperService: DialogHelperService + ) {} + + ngOnInit() { + if (!this._dialogRef) { + this._dialogRef = this._dialogHelperService.getClosestDialog( + this._elementRef, + this._dialog.openDialogs + ); + } + + if (this._dialogRef) { + Promise.resolve().then(() => { + const container = this._dialogRef.containerInstance; + + if (container) { + container.hasHeader = true; + this.isCloseDisabled = container.config.disableClose; + this._changeDetectorRef.markForCheck(); + } + }); + } + } + + emitManualCloseAction() { + if (this._dialogRef) { + this._dialogRef.manualCloseAction.next(null); + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-helper.service.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-helper.service.ts new file mode 100644 index 0000000000..3e537764ea --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-helper.service.ts @@ -0,0 +1,21 @@ +import { ElementRef, Injectable } from '@angular/core'; + +import { DialogRef } from './dialog-ref'; + +@Injectable({ providedIn: 'root' }) +export class DialogHelperService { + /** + * Finds the closest DialogRef to an element by looking at the DOM. + * @param element Element relative to which to look for a dialog. + * @param openDialogs References to the currently-open dialogs. + */ + getClosestDialog(element: ElementRef, openDialogs: DialogRef[]) { + let parent: HTMLElement | null = element.nativeElement.parentElement; + + while (parent && !parent.classList.contains('sbb-dialog-container')) { + parent = parent.parentElement; + } + + return parent ? openDialogs.find(dialog => dialog.id === parent.id) : null; + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-title.directive.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-title.directive.ts new file mode 100644 index 0000000000..eb521b1657 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-title.directive.ts @@ -0,0 +1,47 @@ +import { Directive, ElementRef, HostBinding, Input, OnInit, Optional } from '@angular/core'; + +import { DialogHelperService } from './dialog-helper.service'; +import { DialogRef } from './dialog-ref'; +import { Dialog } from './dialog.service'; + +/** Counter used to generate unique IDs for dialog elements. */ +let dialogElementUid = 0; + +@Directive({ + selector: `[sbbDialogTitle]` +}) +export class DialogTitleDirective implements OnInit { + /** Identifier of dialog title. */ + @Input() + @HostBinding('attr.id') + id = `sbb-dialog-title-${dialogElementUid++}`; + /** Class attribute for dialog title. */ + @HostBinding('class.sbb-dialog-title') + dialogTitleClass = true; + + constructor( + @Optional() private _dialogRef: DialogRef, + private _elementRef: ElementRef, + private _dialog: Dialog, + private _dialogHelperService: DialogHelperService + ) {} + + ngOnInit() { + if (!this._dialogRef) { + this._dialogRef = this._dialogHelperService.getClosestDialog( + this._elementRef, + this._dialog.openDialogs + ); + } + + if (this._dialogRef) { + Promise.resolve().then(() => { + const container = this._dialogRef.containerInstance; + + if (container && !container.ariaLabelledBy) { + container.ariaLabelledBy = this.id; + } + }); + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog.component.ts new file mode 100644 index 0000000000..5379f3f2c2 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * Dialog wrapper element. Contains the dialog's header, content and footer. + */ +@Component({ + selector: 'sbb-dialog, [sbbDialog]', + template: ` +
+ + + +
+ `, + exportAs: 'sbbDialog', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogComponent {} diff --git a/projects/sbb-esta/angular-business/dialog/src/public_api.ts b/projects/sbb-esta/angular-business/dialog/src/public_api.ts index e319f01a1b..08aaf1f955 100644 --- a/projects/sbb-esta/angular-business/dialog/src/public_api.ts +++ b/projects/sbb-esta/angular-business/dialog/src/public_api.ts @@ -1,7 +1,10 @@ export * from './dialog.module'; export * from './dialog/dialog.service'; export * from './dialog/dialog-container.component'; -export * from './dialog/dialog-content'; +export * from './dialog/dialog.component'; +export * from './dialog/dialog-header.component'; +export * from './dialog/dialog-content.component'; +export * from './dialog/dialog-footer.component'; export * from './dialog/dialog-config'; export * from './dialog/dialog-ref'; export * from './dialog/dialog-animations'; From f1028d6710d3e61cc560e2de5e1aaba7055ae202 Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 4 Nov 2019 15:39:03 +0100 Subject: [PATCH 09/16] feat(business): implement dialog component Add positioning and size configuration, separate styles to components --- .../dialog-showcase-content-1.component.html | 36 ++-- .../dialog-showcase-content-2.component.html | 164 +++++++++--------- .../dialog-showcase-content-3.component.html | 2 +- .../dialog-showcase.component.ts | 4 +- .../angular-business/dialog/src/_dialog.scss | 88 ++-------- .../dialog/src/dialog.module.ts | 11 +- .../dialog/src/dialog/dialog-config.ts | 34 +++- .../dialog-container.component.html | 0 .../dialog-container.component.scss | 31 ++-- .../dialog-container.component.ts | 4 +- .../dialog-content.component.scss | 15 ++ .../dialog-content.component.ts | 6 +- .../dialog-footer.component.scss | 43 +++++ .../dialog-footer.component.ts | 13 +- .../dialog-header.component.scss | 25 +++ .../dialog-header.component.ts | 7 +- .../dialog/src/dialog/dialog-ref.ts | 34 +++- .../dialog/src/dialog/dialog.component.ts | 18 -- .../dialog/src/dialog/dialog.service.ts | 10 +- .../angular-business/dialog/src/public_api.ts | 9 +- 20 files changed, 309 insertions(+), 245 deletions(-) rename projects/sbb-esta/angular-business/dialog/src/dialog/{ => dialog-container}/dialog-container.component.html (100%) rename projects/sbb-esta/angular-business/dialog/src/dialog/{ => dialog-container}/dialog-container.component.scss (73%) rename projects/sbb-esta/angular-business/dialog/src/dialog/{ => dialog-container}/dialog-container.component.ts (98%) create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content/dialog-content.component.scss rename projects/sbb-esta/angular-business/dialog/src/dialog/{ => dialog-content}/dialog-content.component.ts (61%) create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer/dialog-footer.component.scss rename projects/sbb-esta/angular-business/dialog/src/dialog/{ => dialog-footer}/dialog-footer.component.ts (84%) create mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header/dialog-header.component.scss rename projects/sbb-esta/angular-business/dialog/src/dialog/{ => dialog-header}/dialog-header.component.ts (90%) delete mode 100644 projects/sbb-esta/angular-business/dialog/src/dialog/dialog.component.ts diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html index 1de0f1fd2a..47770731d1 100644 --- a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-1.component.html @@ -1,21 +1,19 @@ -
-
-

Hi {{ data.name }}

-
-
-
- What's your favorite animal? - - - -
-
-
- - +
+

Hi {{ data.name }}

+
+
+
+ What's your favorite animal? + + +
+
+ + +
diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html index 262957b63b..cb5e1616f6 100644 --- a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-2.component.html @@ -1,99 +1,97 @@ -
-
-

Install Angular

-
-
-
-

- Learn one way to build applications with Angular and reuse your code and abilities to build - apps for any deployment target. For web, mobile web, native mobile and native desktop. -

+
+

Install Angular

+
+
+
+

+ Learn one way to build applications with Angular and reuse your code and abilities to build + apps for any deployment target. For web, mobile web, native mobile and native desktop. +

-

Speed & Performance

-

- Achieve the maximum speed possible on the Web Platform today, and take it further, via Web - Workers and server-side rendering. Angular puts you in control over scalability. Meet huge - data requirements by building data models on RxJS, Immutable.js or another push-model. -

+

Speed & Performance

+

+ Achieve the maximum speed possible on the Web Platform today, and take it further, via Web + Workers and server-side rendering. Angular puts you in control over scalability. Meet huge + data requirements by building data models on RxJS, Immutable.js or another push-model. +

-

Incredible tooling

-

- Build features quickly with simple, declarative templates. Extend the template language with - your own components and use a wide array of existing components. Get immediate - Angular-specific help and feedback with nearly every IDE and editor. All this comes together - so you can focus on building amazing apps rather than trying to make the code work. -

+

Incredible tooling

+

+ Build features quickly with simple, declarative templates. Extend the template language with + your own components and use a wide array of existing components. Get immediate + Angular-specific help and feedback with nearly every IDE and editor. All this comes together + so you can focus on building amazing apps rather than trying to make the code work. +

-

Loved by millions

-

- From prototype through global deployment, Angular delivers the productivity and scalable - infrastructure that supports Google's largest applications. -

+

Loved by millions

+

+ From prototype through global deployment, Angular delivers the productivity and scalable + infrastructure that supports Google's largest applications. +

-

What is Angular?

+

What is Angular?

-

- Angular is a platform that makes it easy to build applications with the web. Angular - combines declarative templates, dependency injection, end to end tooling, and integrated - best practices to solve development challenges. Angular empowers developers to build - applications that live on the web, mobile, or the desktop -

+

+ Angular is a platform that makes it easy to build applications with the web. Angular combines + declarative templates, dependency injection, end to end tooling, and integrated best practices + to solve development challenges. Angular empowers developers to build applications that live + on the web, mobile, or the desktop +

-

Architecture overview

+

Architecture overview

-

- Angular is a platform and framework for building client applications in HTML and TypeScript. - Angular is itself written in TypeScript. It implements core and optional functionality as a - set of TypeScript libraries that you import into your apps. -

+

+ Angular is a platform and framework for building client applications in HTML and TypeScript. + Angular is itself written in TypeScript. It implements core and optional functionality as a + set of TypeScript libraries that you import into your apps. +

-

- The basic building blocks of an Angular application are NgModules, which provide a - compilation context for components. NgModules collect related code into functional sets; an - Angular app is defined by a set of NgModules. An app always has at least a root module that - enables bootstrapping, and typically has many more feature modules. -

+

+ The basic building blocks of an Angular application are NgModules, which provide a compilation + context for components. NgModules collect related code into functional sets; an Angular app is + defined by a set of NgModules. An app always has at least a root module that enables + bootstrapping, and typically has many more feature modules. +

-

- Components define views, which are sets of screen elements that Angular can choose among and - modify according to your program logic and data. Every app has at least a root component. -

+

+ Components define views, which are sets of screen elements that Angular can choose among and + modify according to your program logic and data. Every app has at least a root component. +

-

- Components use services, which provide specific functionality not directly related to views. - Service providers can be injected into components as dependencies, making your code modular, - reusable, and efficient. -

+

+ Components use services, which provide specific functionality not directly related to views. + Service providers can be injected into components as dependencies, making your code modular, + reusable, and efficient. +

-

- Both components and services are simply classes, with decorators that mark their type and - provide metadata that tells Angular how to use them. -

+

+ Both components and services are simply classes, with decorators that mark their type and + provide metadata that tells Angular how to use them. +

-

- The metadata for a component class associates it with a template that defines a view. A - template combines ordinary HTML with Angular directives and binding markup that allow - Angular to modify the HTML before rendering it for display. -

+

+ The metadata for a component class associates it with a template that defines a view. A + template combines ordinary HTML with Angular directives and binding markup that allow Angular + to modify the HTML before rendering it for display. +

-

- The metadata for a service class provides the information Angular needs to make it available - to components through Dependency Injection (DI). -

+

+ The metadata for a service class provides the information Angular needs to make it available + to components through Dependency Injection (DI). +

-

- An app's components typically define many views, arranged hierarchically. Angular provides - the Router service to help you define navigation paths among views. The router provides - sophisticated in-browser navigational capabilities. END -

-
-
-
- - +

+ An app's components typically define many views, arranged hierarchically. Angular provides the + Router service to help you define navigation paths among views. The router provides + sophisticated in-browser navigational capabilities. END +

+
+ + +
diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html index f1630305d5..8535ecbaa9 100644 --- a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase-content-3.component.html @@ -5,7 +5,7 @@
-
+

Lorem Ipsum

diff --git a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts index a48c8244be..ba93ffdbc5 100644 --- a/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts +++ b/projects/angular-showcase/src/app/business/business-examples/dialog-showcase/dialog-showcase.component.ts @@ -50,7 +50,8 @@ export class DialogShowcaseExampleComponent { openDialog(): void { const dialogRef = this.dialog.open(DialogShowcaseExampleContentComponent, { - data: { name: this.name, animal: this.animal } + data: { name: this.name, animal: this.animal }, + position: { top: '10px' } }); dialogRef.afterClosed().subscribe(result => { @@ -103,6 +104,7 @@ export class DialogShowcaseExample2Component { }) export class DialogShowcaseExample3Component { @ViewChild('sampleDialogTemplate', { static: true }) sampleDialogTemplate: TemplateRef; + constructor(public dialog: Dialog) {} openDialog() { diff --git a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss index eff58d3dd3..e1f5b90481 100644 --- a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss +++ b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss @@ -45,41 +45,22 @@ $dialog-footer-padding: 0; } } +@mixin overlay() { + background-color: rgba(255, 255, 255, 0.7); + align-items: center; +} + @mixin dialogContainer() { display: flex; align-items: center; justify-content: center; - background-color: rgba(255, 255, 255, 0.7); + background-color: $sbbColorWhite; position: relative; outline: 0; width: 100%; height: 100%; } -@mixin dialog() { - border: 1px solid $sbbColorGranite; - background-color: $dialog-bgcolor; - min-width: pxToEm($dialog-min-width); - max-width: pxToEm($dialog-max-width); -} - -@mixin dialogHeader() { - display: flex; - align-items: center; - height: pxToEm($dialog-header-height); - padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding-mobile); - border-bottom: 1px solid $dialog-header-border-color; - overflow: hidden; - - @include mq($from: tabletPortrait) { - padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding-tablet); - } - - @include mq($from: desktop) { - padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding); - } -} - @mixin dialogCloseBtn() { @include buttonResetFrameless(); @include svgIconColor($dialog-close-icon-color); @@ -136,62 +117,13 @@ $dialog-footer-padding: 0; @mixin dialogContent() { display: block; - - perfect-scrollbar { - .ps-content { - padding: pxToEm($dialog-content-y-padding-mobile) pxToEm($dialog-content-x-padding-mobile); - - @include mq($from: tabletPortrait) { - padding: pxToEm($dialog-content-y-padding) pxToEm($dialog-content-x-padding-tablet); - } - - @include mq($from: desktop) { - padding: pxToEm($dialog-content-y-padding) pxToEm($dialog-content-x-padding); - } - } - } + border-left: 1px solid $sbbColorGranite; + border-right: 1px solid $sbbColorGranite; + flex: 1 1 auto; + @include dialogContentHeight(); } @mixin dialogFooter() { - perfect-scrollbar { - bottom: 0; - left: 0; - width: 100%; - max-height: pxToEm($dialog-footer-height-mobile + 2); - background-color: $dialog-bgcolor; - border-top: 1px solid $dialog-header-border-color; - - @include mq($from: tabletPortrait) { - max-height: pxToEm($dialog-footer-height-tablet + 2); - } - - .ps-content { - display: flex; - flex-direction: column; - align-items: center; - min-height: pxToEm($dialog-footer-height-mobile); - padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding-mobile); - - @include mq($from: tabletPortrait) { - flex-direction: row; - min-height: pxToEm($dialog-footer-height-tablet); - padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding-tablet); - } - - @include mq($from: desktop) { - padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding); - } - - button { - margin-bottom: pxToEm(10); - - @include mq($from: tabletPortrait) { - margin-bottom: 0; - } - } - } - } - &-align-start { perfect-scrollbar .ps-content { justify-content: flex-start; diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts index c87aaab14f..63b469bfc2 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog.module.ts @@ -6,19 +6,17 @@ import { ScrollingModule } from '@sbb-esta/angular-core/scrolling'; import { IconCrossModule } from '@sbb-esta/angular-icons'; import { DialogCloseDirective } from './dialog/dialog-close.directive'; -import { DialogContainerComponent } from './dialog/dialog-container.component'; -import { DialogContentComponent } from './dialog/dialog-content.component'; -import { DialogFooterComponent } from './dialog/dialog-footer.component'; -import { DialogHeaderComponent } from './dialog/dialog-header.component'; +import { DialogContainerComponent } from './dialog/dialog-container/dialog-container.component'; +import { DialogContentComponent } from './dialog/dialog-content/dialog-content.component'; +import { DialogFooterComponent } from './dialog/dialog-footer/dialog-footer.component'; +import { DialogHeaderComponent } from './dialog/dialog-header/dialog-header.component'; import { DialogTitleDirective } from './dialog/dialog-title.directive'; -import { DialogComponent } from './dialog/dialog.component'; import { Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER } from './dialog/dialog.service'; @NgModule({ imports: [CommonModule, IconCrossModule, OverlayModule, PortalModule, ScrollingModule], exports: [ DialogContainerComponent, - DialogComponent, DialogCloseDirective, DialogHeaderComponent, DialogContentComponent, @@ -27,7 +25,6 @@ import { Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER } from './dialog/dialog.service ], declarations: [ DialogContainerComponent, - DialogComponent, DialogCloseDirective, DialogHeaderComponent, DialogFooterComponent, diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts index 95e545bbe7..a436ef7119 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts @@ -4,6 +4,21 @@ import { ViewContainerRef } from '@angular/core'; /** Valid ARIA roles for a Dialog element. */ export type DialogRole = 'dialog' | 'alertdialog'; +/** Possible overrides for a dialog's position. */ +export interface DialogPosition { + /** Override for the Dialog's top position. */ + top?: string; + + /** Override for the Dialog's bottom position. */ + bottom?: string; + + /** Override for the Dialog's left position. */ + left?: string; + + /** Override for the Dialog's right position. */ + right?: string; +} + /** * Configuration for opening a modal dialog with the Dialog service. */ @@ -28,12 +43,27 @@ export class DialogConfig { /** Whether the user can use escape or clicking on the backdrop to close the modal. */ disableClose? = false; - /** Width of the Dialog. */ + /** Width of the Dialog overlay. */ width? = '100vw'; - /** Height of the Dialog. */ + /** Height of the Dialog overlay. */ height? = '100vh'; + /** Min-width of the Dialog. If a number is provided, assumes pixel units. */ + minWidth?: number | string; + + /** Min-height of the Dialog. If a number is provided, assumes pixel units. */ + minHeight?: number | string; + + /** Max-width of the Dialog. If a number is provided, assumes pixel units. Defaults to 80vw. */ + maxWidth?: number | string = '80vw'; + + /** Max-height of the Dialog. If a number is provided, assumes pixel units. */ + maxHeight?: number | string; + + /** Position overrides. */ + position?: DialogPosition; + /** Data being injected into the child component. */ data?: D | null = null; diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.html b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.html similarity index 100% rename from projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.html rename to projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.html diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.scss similarity index 73% rename from projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss rename to projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.scss index f81ac9f45a..5d1e43ff49 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.scss +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.scss @@ -1,11 +1,22 @@ -@import '../../../../angular-core/styles/common/variants'; -@import '../dialog'; +@import '../../../../../angular-core/styles/common/variants'; +@import '../../dialog'; + +.sbb-overlay-background { + @include overlay(); +} sbb-dialog-container { @include dialogContainer(); - .sbb-dialog { - @include dialog(); + & > * { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + } + + .sbb-dialog-title { + @include dialogTitle(); } .sbb-dialog-content { @@ -13,22 +24,10 @@ sbb-dialog-container { @include dialogContentHeight(); } - .sbb-dialog-header { - @include dialogHeader(); - } - .sbb-dialog-footer { @include dialogFooter(); } - .sbb-dialog-close-btn { - @include dialogCloseBtn(); - } - - .sbb-dialog-title { - @include dialogTitle(); - } - &.sbb-dialog-with-header { .sbb-dialog-content { @include dialogContentHeight(withHeader); diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.ts similarity index 98% rename from projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.ts rename to projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.ts index bf8675f0ba..b339b35852 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container.component.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-container/dialog-container.component.ts @@ -23,8 +23,8 @@ import { ViewEncapsulation } from '@angular/core'; -import { DIALOG_ANIMATIONS } from './dialog-animations'; -import { DialogConfig } from './dialog-config'; +import { DIALOG_ANIMATIONS } from '../dialog-animations'; +import { DialogConfig } from '../dialog-config'; /** * Throws an exception for the case when a ComponentPortal is diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content/dialog-content.component.scss b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content/dialog-content.component.scss new file mode 100644 index 0000000000..94c011a617 --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content/dialog-content.component.scss @@ -0,0 +1,15 @@ +@import '../../dialog'; + +.sbb-dialog-content-scrollbar { + .ps-content { + padding: pxToEm($dialog-content-y-padding-mobile) pxToEm($dialog-content-x-padding-mobile); + + @include mq($from: tabletPortrait) { + padding: pxToEm($dialog-content-y-padding) pxToEm($dialog-content-x-padding-tablet); + } + + @include mq($from: desktop) { + padding: pxToEm($dialog-content-y-padding) pxToEm($dialog-content-x-padding); + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content/dialog-content.component.ts similarity index 61% rename from projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.component.ts rename to projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content/dialog-content.component.ts index f2baea83ba..dbbb95fec5 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content.component.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-content/dialog-content.component.ts @@ -1,12 +1,14 @@ -import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostBinding, ViewEncapsulation } from '@angular/core'; /** * Scrollable content container of a dialog. */ @Component({ selector: `sbb-dialog-content, [sbbDialogContent]`, + styleUrls: ['./dialog-content.component.scss'], + encapsulation: ViewEncapsulation.None, template: ` - + `, diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer/dialog-footer.component.scss b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer/dialog-footer.component.scss new file mode 100644 index 0000000000..ae5957bc6c --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer/dialog-footer.component.scss @@ -0,0 +1,43 @@ +@import '../../dialog'; + +.sbb-dialog-footer-scrollbar { + bottom: 0; + left: 0; + width: 100%; + max-height: pxToEm($dialog-footer-height-mobile + 2); + background-color: $dialog-bgcolor; + border-left: 1px solid $sbbColorGranite; + border-right: 1px solid $sbbColorGranite; + border-bottom: 1px solid $sbbColorGranite; + border-top: 1px solid $dialog-header-border-color; + + @include mq($from: tabletPortrait) { + max-height: pxToEm($dialog-footer-height-tablet + 2); + } + + .ps-content { + display: flex; + flex-direction: column; + align-items: center; + min-height: pxToEm($dialog-footer-height-mobile); + padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding-mobile); + + @include mq($from: tabletPortrait) { + flex-direction: row; + min-height: pxToEm($dialog-footer-height-tablet); + padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding-tablet); + } + + @include mq($from: desktop) { + padding: pxToEm($dialog-footer-padding) pxToEm($dialog-header-x-padding); + } + + button { + margin-bottom: pxToEm(10); + + @include mq($from: tabletPortrait) { + margin-bottom: 0; + } + } + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer/dialog-footer.component.ts similarity index 84% rename from projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer.component.ts rename to projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer/dialog-footer.component.ts index 485611fbfa..d614c137f6 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer.component.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-footer/dialog-footer.component.ts @@ -5,12 +5,13 @@ import { HostBinding, Input, OnInit, - Optional + Optional, + ViewEncapsulation } from '@angular/core'; -import { DialogHelperService } from './dialog-helper.service'; -import { DialogRef } from './dialog-ref'; -import { Dialog } from './dialog.service'; +import { DialogHelperService } from '../dialog-helper.service'; +import { DialogRef } from '../dialog-ref'; +import { Dialog } from '../dialog.service'; /** * Container for the bottom action buttons in a dialog. @@ -18,8 +19,10 @@ import { Dialog } from './dialog.service'; */ @Component({ selector: `sbb-dialog-footer, [sbbDialogFooter]`, + styleUrls: ['./dialog-footer.component.scss'], + encapsulation: ViewEncapsulation.None, template: ` - + `, diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header/dialog-header.component.scss b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header/dialog-header.component.scss new file mode 100644 index 0000000000..7c6a80221a --- /dev/null +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header/dialog-header.component.scss @@ -0,0 +1,25 @@ +@import '../../_dialog.scss'; + +:host { + display: flex; + align-items: center; + height: pxToEm($dialog-header-height); + padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding-mobile); + border-top: 1px solid $sbbColorGranite; + border-left: 1px solid $sbbColorGranite; + border-right: 1px solid $sbbColorGranite; + border-bottom: 1px solid $dialog-header-border-color; + overflow: hidden; + + @include mq($from: tabletPortrait) { + padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding-tablet); + } + + @include mq($from: desktop) { + padding: pxToEm($dialog-header-y-padding) pxToEm($dialog-header-x-padding); + } + + .sbb-dialog-close-btn { + @include dialogCloseBtn(); + } +} diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header.component.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header/dialog-header.component.ts similarity index 90% rename from projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header.component.ts rename to projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header/dialog-header.component.ts index 121888c34a..bd483aad51 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header.component.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-header/dialog-header.component.ts @@ -11,12 +11,13 @@ import { Optional } from '@angular/core'; -import { DialogHelperService } from './dialog-helper.service'; -import { DialogRef } from './dialog-ref'; -import { Dialog } from './dialog.service'; +import { DialogHelperService } from '../dialog-helper.service'; +import { DialogRef } from '../dialog-ref'; +import { Dialog } from '../dialog.service'; @Component({ selector: 'sbb-dialog-header, [sbbDialogHeader]', + styleUrls: ['dialog-header.component.scss'], template: `
From ea4ab15369ebadfd5ceb415645903af4934d187d Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 11 Nov 2019 09:23:39 +0100 Subject: [PATCH 12/16] feat(business): implement dialog component Set default dialog max-height --- projects/sbb-esta/angular-business/dialog/src/_dialog.scss | 1 - .../angular-business/dialog/src/dialog/dialog-config.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss index fc5c9b0939..71b6fa9991 100644 --- a/projects/sbb-esta/angular-business/dialog/src/_dialog.scss +++ b/projects/sbb-esta/angular-business/dialog/src/_dialog.scss @@ -58,7 +58,6 @@ $dialog-footer-padding: 0; position: relative; outline: 0; width: 100%; - height: 100%; } @mixin dialogCloseBtn() { diff --git a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts index 6e72c954a8..65810df183 100644 --- a/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts +++ b/projects/sbb-esta/angular-business/dialog/src/dialog/dialog-config.ts @@ -59,7 +59,7 @@ export class DialogConfig { maxWidth?: number | string = '80vw'; /** Max-height of the Dialog. If a number is provided, assumes pixel units. */ - maxHeight?: number | string; + maxHeight?: number | string = '96vh'; /** Position overrides. */ position?: DialogPosition; From add53d73b411f499c647c7f20ee5da687f077874 Mon Sep 17 00:00:00 2001 From: vlanz Date: Mon, 11 Nov 2019 09:29:08 +0100 Subject: [PATCH 13/16] feat(business): implement dialog component Remove unnecessary initial focus --- projects/sbb-esta/angular-business/dialog/dialog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/sbb-esta/angular-business/dialog/dialog.md b/projects/sbb-esta/angular-business/dialog/dialog.md index eca212b009..dace12f91a 100644 --- a/projects/sbb-esta/angular-business/dialog/dialog.md +++ b/projects/sbb-esta/angular-business/dialog/dialog.md @@ -14,7 +14,7 @@ The dialog can be used to seek confirmation as see below
-