From 33cb7418dddee6eb9ce5280806ebb2b79c7192ea Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 5 Dec 2016 23:05:37 +0100 Subject: [PATCH] feat(dialog): add dialog content elements Adds the following dialog-specific directives: * `md-dialog-close` - Closes the current dialog. * `md-dialog-title` - Title of a dialog. * `md-dialog-content` - Scrollable content for a dialog. * `md-dialog-actions` - Container for the bottom buttons in a dialog. Fixes #1624. Fixes #2042. --- src/demo-app/demo-app-module.ts | 4 +- src/demo-app/dialog/dialog-demo.html | 3 +- src/demo-app/dialog/dialog-demo.ts | 47 ++++++++++++++++- src/lib/dialog/README.md | 29 ++++++++--- src/lib/dialog/dialog-container.scss | 24 --------- src/lib/dialog/dialog-container.ts | 2 +- src/lib/dialog/dialog-directives.ts | 49 +++++++++++++++++ src/lib/dialog/dialog.scss | 52 +++++++++++++++++++ src/lib/dialog/dialog.spec.ts | 78 ++++++++++++++++++++++++++-- src/lib/dialog/dialog.ts | 52 ++++++------------- src/lib/dialog/index.ts | 50 ++++++++++++++++++ 11 files changed, 316 insertions(+), 74 deletions(-) delete mode 100644 src/lib/dialog/dialog-container.scss create mode 100644 src/lib/dialog/dialog-directives.ts create mode 100644 src/lib/dialog/dialog.scss diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 0a85b5da9475..e9d9cde47819 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -7,7 +7,7 @@ import {RouterModule} from '@angular/router'; import {MaterialModule} from '@angular/material'; import {DEMO_APP_ROUTES} from './demo-app/routes'; import {ProgressBarDemo} from './progress-bar/progress-bar-demo'; -import {JazzDialog, DialogDemo} from './dialog/dialog-demo'; +import {JazzDialog, ContentElementDialog, DialogDemo} from './dialog/dialog-demo'; import {RippleDemo} from './ripple/ripple-demo'; import {IconDemo} from './icon/icon-demo'; import {GesturesDemo} from './gestures/gestures-demo'; @@ -65,6 +65,7 @@ import {InputContainerDemo} from './input/input-container-demo'; InputDemo, InputContainerDemo, JazzDialog, + ContentElementDialog, ListDemo, LiveAnnouncerDemo, MdCheckboxDemoNestedChecklist, @@ -96,6 +97,7 @@ import {InputContainerDemo} from './input/input-container-demo'; entryComponents: [ DemoApp, JazzDialog, + ContentElementDialog, RotiniPanel, ScienceJoke, SpagettiPanel, diff --git a/src/demo-app/dialog/dialog-demo.html b/src/demo-app/dialog/dialog-demo.html index fe71fed7f98f..491e8031393f 100644 --- a/src/demo-app/dialog/dialog-demo.html +++ b/src/demo-app/dialog/dialog-demo.html @@ -1,6 +1,7 @@

Dialog demo

- + + diff --git a/src/demo-app/dialog/dialog-demo.ts b/src/demo-app/dialog/dialog-demo.ts index c2ffc0dad2ee..7323d8f67173 100644 --- a/src/demo-app/dialog/dialog-demo.ts +++ b/src/demo-app/dialog/dialog-demo.ts @@ -24,7 +24,7 @@ export class DialogDemo { constructor(public dialog: MdDialog) { } - open() { + openJazz() { this.dialogRef = this.dialog.open(JazzDialog, this.config); this.dialogRef.afterClosed().subscribe(result => { @@ -32,6 +32,10 @@ export class DialogDemo { this.dialogRef = null; }); } + + openContentElement() { + this.dialog.open(ContentElementDialog, this.config); + } } @@ -48,3 +52,44 @@ export class JazzDialog { constructor(public dialogRef: MdDialogRef) { } } + + +@Component({ + selector: 'demo-content-element-dialog', + styles: [ + `img { + max-width: 100%; + }` + ], + template: ` +

Neptune

+ + + + +

+ Neptune is the eighth and farthest known planet from the Sun in the Solar System. In the + Solar System, it is the fourth-largest planet by diameter, the third-most-massive planet, + and the densest giant planet. Neptune is 17 times the mass of Earth and is slightly more + massive than its near-twin Uranus, which is 15 times the mass of Earth and slightly larger + than Neptune. Neptune orbits the Sun once every 164.8 years at an average distance of 30.1 + astronomical units (4.50×109 km). It is named after the Roman god of the sea and has the + astronomical symbol ♆, a stylised version of the god Neptune's trident. +

+
+ + + + + Read more on Wikipedia + + ` +}) +export class ContentElementDialog { } diff --git a/src/lib/dialog/README.md b/src/lib/dialog/README.md index d68f89f54aff..194b8fe0b969 100644 --- a/src/lib/dialog/README.md +++ b/src/lib/dialog/README.md @@ -4,15 +4,16 @@ MdDialog is a service, which opens dialogs components in the view. ### Methods -| Name | Description | -| --- | --- | +| Name | Description | +| ---- | ----------- | | `open(component: ComponentType, config: MdDialogConfig): MdDialogRef` | Creates and opens a dialog matching material spec. | | `closeAll(): void` | Closes all of the dialogs that are currently open. | +| `closeTop(): void` | Closes the topmost of the open dialogs. | ### Config -| Key | Description | -| --- | --- | +| Key | Description | +| --- | ------------ | | `role: DialogRole = 'dialog'` | The ARIA role of the dialog element. Possible values are `dialog` and `alertdialog`. Optional. | | `disableClose: boolean = false` | Whether to prevent the user from closing a dialog by clicking on the backdrop or pressing escape. Optional. | | `width: string = ''` | Width of the dialog. Takes any valid CSS value. Optional. | @@ -26,11 +27,19 @@ A reference to the dialog created by the MdDialog `open` method. ### Methods -| Name | Description | -| --- | --- | +| Name | Description | +| ---- | ----------- | | `close(dialogResult?: any)` | Closes the dialog, pushing a value to the afterClosed observable. | | `afterClosed(): Observable` | Returns an observable which will emit the dialog result, passed to the `close` method above. | +### Directives +| Name | Description | +| --- | ------------ | +| `md-dialog-title` | Marks the title of the dialog. +| `md-dialog-content` | Scrollable content of the dialog. +| `md-dialog-close` | When added to a `button`, makes the element act as a close button for the dialog. +| `md-dialog-actions` | Wrapper for the set of actions at the bottom of a dialog. Typically contains buttons. + ## Example The service can be injected in a component. @@ -62,8 +71,12 @@ export class PizzaComponent { @Component({ selector: 'pizza-dialog', template: ` - - +

Would you like to order pizza?

+ + + + + ` }) export class PizzaDialog { diff --git a/src/lib/dialog/dialog-container.scss b/src/lib/dialog/dialog-container.scss deleted file mode 100644 index c7675efbbbcc..000000000000 --- a/src/lib/dialog/dialog-container.scss +++ /dev/null @@ -1,24 +0,0 @@ -@import '../core/style/elevation'; -@import '../core/a11y/a11y'; - - -$md-dialog-padding: 24px !default; -$md-dialog-border-radius: 2px !default; - -md-dialog-container { - @include md-elevation(24); - - display: block; - padding: $md-dialog-padding; - border-radius: $md-dialog-border-radius; - box-sizing: border-box; - overflow: auto; - - // The dialog container should completely fill its parent overlay element. - width: 100%; - height: 100%; - - @include md-high-contrast { - outline: solid 1px; - } -} diff --git a/src/lib/dialog/dialog-container.ts b/src/lib/dialog/dialog-container.ts index 39a2db89cc50..c9726d9499c3 100644 --- a/src/lib/dialog/dialog-container.ts +++ b/src/lib/dialog/dialog-container.ts @@ -22,7 +22,7 @@ import 'rxjs/add/operator/first'; moduleId: module.id, selector: 'md-dialog-container, mat-dialog-container', templateUrl: 'dialog-container.html', - styleUrls: ['dialog-container.css'], + styleUrls: ['dialog.css'], host: { 'class': 'md-dialog-container', '[attr.role]': 'dialogConfig?.role', diff --git a/src/lib/dialog/dialog-directives.ts b/src/lib/dialog/dialog-directives.ts new file mode 100644 index 000000000000..3c201a65d6aa --- /dev/null +++ b/src/lib/dialog/dialog-directives.ts @@ -0,0 +1,49 @@ +import {Directive} from '@angular/core'; +import {MdDialog} from './dialog'; + + +/** + * Button that will close the current dialog. + */ +@Directive({ + selector: 'button[md-dialog-close]', + host: { + '(click)': 'dialog.closeTop()' + } +}) +export class MdDialogClose { + constructor(public dialog: MdDialog) { } +} + +/** + * Title of a dialog element. Stays fixed to the top of the dialog when scrolling. + */ +@Directive({ + selector: '[md-dialog-title]', + host: { + role: 'heading' + } +}) +export class MdDialogTitle { } + + +/** + * Scrollable content container of a dialog. + */ +@Directive({ + selector: '[md-dialog-content], md-dialog-content', + host: { + role: 'main' + } +}) +export class MdDialogContent { } + + +/** + * Container for the bottom action buttons in a dialog. + * Stays fixed to the bottom when scrolling. + */ +@Directive({ + selector: '[md-dialog-actions], md-dialog-actions' +}) +export class MdDialogActions { } diff --git a/src/lib/dialog/dialog.scss b/src/lib/dialog/dialog.scss new file mode 100644 index 000000000000..c7e7a5fce6af --- /dev/null +++ b/src/lib/dialog/dialog.scss @@ -0,0 +1,52 @@ +@import '../core/style/elevation'; +@import '../core/a11y/a11y'; + + +$md-dialog-padding: 24px !default; +$md-dialog-border-radius: 2px !default; +$md-dialog-max-width: 80vw !default; +$md-dialog-max-height: 65vh !default; + +md-dialog-container { + @include md-elevation(24); + + display: block; + padding: $md-dialog-padding; + border-radius: $md-dialog-border-radius; + box-sizing: border-box; + overflow: auto; + max-width: $md-dialog-max-width; + + // The dialog container should completely fill its parent overlay element. + width: 100%; + height: 100%; + + @include md-high-contrast { + outline: solid 1px; + } +} + +md-dialog-content, [md-dialog-content] { + display: block; + margin: 0 $md-dialog-padding * -1; + padding: 0 $md-dialog-padding; + max-height: $md-dialog-max-height; + overflow: auto; +} + +[md-dialog-title] { + font-size: rem(2); + font-weight: bold; + letter-spacing: 0.005em; + margin: 0 0 rem(2); + display: block; +} + +md-dialog-actions, [md-dialog-actions] { + padding: $md-dialog-padding / 2 0; + display: block; + + &:last-child { + margin-bottom: -$md-dialog-padding; + } +} diff --git a/src/lib/dialog/dialog.spec.ts b/src/lib/dialog/dialog.spec.ts index 34855db3f59a..be6d5a9b48a6 100644 --- a/src/lib/dialog/dialog.spec.ts +++ b/src/lib/dialog/dialog.spec.ts @@ -9,7 +9,8 @@ import { } from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {NgModule, Component, Directive, ViewChild, ViewContainerRef} from '@angular/core'; -import {MdDialog, MdDialogModule} from './dialog'; +import {MdDialogModule} from './index'; +import {MdDialog} from './dialog'; import {OverlayContainer} from '../core'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContainer} from './dialog-container'; @@ -226,6 +227,21 @@ describe('MdDialog', () => { expect(overlayContainerElement.querySelectorAll('md-dialog-container').length).toBe(0); }); + it('should close the top dialog', () => { + dialog.open(PizzaMsg); + dialog.open(PizzaMsg); + + expect(overlayContainerElement.querySelectorAll('md-dialog-container').length).toBe(2); + + dialog.closeTop(); + + expect(overlayContainerElement.querySelectorAll('md-dialog-container').length).toBe(1); + + dialog.closeTop(); + + expect(overlayContainerElement.querySelectorAll('md-dialog-container').length).toBe(0); + }); + describe('disableClose option', () => { it('should prevent closing via clicks on the backdrop', () => { dialog.open(PizzaMsg, { @@ -308,6 +324,44 @@ describe('MdDialog', () => { .toBe('dialog-trigger', 'Expected that the trigger was refocused after dialog close'); })); }); + + describe('dialog content elements', () => { + let fixture: ComponentFixture; + let dialogElement: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(ContentElementDialog); + dialogElement = fixture.debugElement.nativeElement; + dialog.open(ContentElementDialog); + fixture.detectChanges(); + }); + + it('close the dialog when clicking on the close button', () => { + expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(1); + + (dialogElement.querySelector('button[md-dialog-close]') as HTMLElement).click(); + + expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(0); + }); + + it('close not close the dialog if [md-dialog-close] is applied on a non-button node', () => { + expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(1); + + (dialogElement.querySelector('div[md-dialog-close]') as HTMLElement).click(); + + expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(1); + }); + + it('should add a role to the dialog title', () => { + let header = dialogElement.querySelector('[md-dialog-title]'); + expect(header.getAttribute('role')).toBe('heading'); + }); + + it('should add a role to the dialog content', () => { + let content = dialogElement.querySelector('md-dialog-content'); + expect(content.getAttribute('role')).toBe('main'); + }); + }); }); @@ -334,13 +388,31 @@ class PizzaMsg { constructor(public dialogRef: MdDialogRef) { } } +@Component({ + template: ` +

This is the title

+ Lorem ipsum dolor sit amet. + + +
Should not close
+
+ ` +}) +class ContentElementDialog {} + // Create a real (non-test) NgModule as a workaround for // https://github.com/angular/angular/issues/10760 -const TEST_DIRECTIVES = [ComponentWithChildViewContainer, PizzaMsg, DirectiveWithViewContainer]; +const TEST_DIRECTIVES = [ + ComponentWithChildViewContainer, + PizzaMsg, + DirectiveWithViewContainer, + ContentElementDialog +]; + @NgModule({ imports: [MdDialogModule], exports: TEST_DIRECTIVES, declarations: TEST_DIRECTIVES, - entryComponents: [ComponentWithChildViewContainer, PizzaMsg], + entryComponents: [ComponentWithChildViewContainer, PizzaMsg, ContentElementDialog], }) class DialogTestModule { } diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index fe15864617af..26c0f7150de1 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -1,29 +1,15 @@ -import {NgModule, ModuleWithProviders, Injector, ComponentRef, Injectable} from '@angular/core'; -import { - Overlay, - OverlayModule, - PortalModule, - OverlayRef, - OverlayState, - ComponentPortal, - OVERLAY_PROVIDERS, - ComponentType, - A11yModule, - InteractivityChecker, - MdPlatform, - DefaultStyleCompatibilityModeModule, -} from '../core'; +import {Injector, ComponentRef, Injectable} from '@angular/core'; + +import {Overlay, OverlayRef, ComponentType, OverlayState, ComponentPortal} from '../core'; +import {extendObject} from '../core/util/object-extend'; + +import {DialogInjector} from './dialog-injector'; import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; -import {DialogInjector} from './dialog-injector'; import {MdDialogContainer} from './dialog-container'; -import {extendObject} from '../core/util/object-extend'; -export {MdDialogConfig} from './dialog-config'; -export {MdDialogRef} from './dialog-ref'; // TODO(jelbourn): add support for opening with a TemplateRef -// TODO(jelbourn): dialog content directives (e.g., md-dialog-header) // TODO(jelbourn): animations @@ -71,6 +57,17 @@ export class MdDialog { } } + /** + * Closes the top dialog + */ + closeTop(): void { + let dialogs = this._openDialogs.length; + + if (dialogs) { + this._openDialogs[dialogs - 1].close(); + } + } + /** * Creates the overlay into which the dialog will be loaded. * @param dialogConfig The dialog configuration. @@ -184,18 +181,3 @@ function _applyConfigDefaults(dialogConfig: MdDialogConfig): MdDialogConfig { return extendObject(new MdDialogConfig(), dialogConfig); } - -@NgModule({ - imports: [OverlayModule, PortalModule, A11yModule, DefaultStyleCompatibilityModeModule], - exports: [MdDialogContainer, DefaultStyleCompatibilityModeModule], - declarations: [MdDialogContainer], - entryComponents: [MdDialogContainer], -}) -export class MdDialogModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: MdDialogModule, - providers: [MdDialog, OVERLAY_PROVIDERS, InteractivityChecker, MdPlatform], - }; - } -} diff --git a/src/lib/dialog/index.ts b/src/lib/dialog/index.ts index bac800313647..4f805e945f7f 100644 --- a/src/lib/dialog/index.ts +++ b/src/lib/dialog/index.ts @@ -1,4 +1,54 @@ +import {NgModule, ModuleWithProviders} from '@angular/core'; +import { + OverlayModule, + PortalModule, + OVERLAY_PROVIDERS, + A11yModule, + InteractivityChecker, + MdPlatform, + DefaultStyleCompatibilityModeModule, +} from '../core'; + +import {MdDialog} from './dialog'; +import {MdDialogContainer} from './dialog-container'; +import {MdDialogClose, MdDialogContent, MdDialogTitle, MdDialogActions} from './dialog-directives'; + + +@NgModule({ + imports: [ + OverlayModule, + PortalModule, + A11yModule, + DefaultStyleCompatibilityModeModule + ], + exports: [ + MdDialogContainer, + MdDialogClose, + MdDialogTitle, + MdDialogContent, + MdDialogActions, + DefaultStyleCompatibilityModeModule + ], + declarations: [ + MdDialogContainer, + MdDialogClose, + MdDialogTitle, + MdDialogActions, + MdDialogContent + ], + entryComponents: [MdDialogContainer], +}) +export class MdDialogModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: MdDialogModule, + providers: [MdDialog, OVERLAY_PROVIDERS, InteractivityChecker, MdPlatform], + }; + } +} + export * from './dialog'; export * from './dialog-container'; +export * from './dialog-directives'; export * from './dialog-config'; export * from './dialog-ref';