feat(dialog): add dialog content elements (#2090)

* 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.

* Rename the dialog directives file.

* Add the selectors for Material 1 compatibility.

* Remove the closeTop method and use the dialogRef instead.

* Add an aria-label to the close button and simplify the testing setup.

* Remove redundant element roles.

* Use the computed value on the dialog title font size.

* Remove the letter spacing override on the dialog title.

* Add a comment regarding the negative bottom margin on the dialog actions.
This commit is contained in:
Kristiyan Kostadinov
2016-12-16 21:34:04 +01:00
committed by Jeremy Elbourn
parent 20ee360912
commit cac72aa30c
11 changed files with 294 additions and 74 deletions
+3 -1
View File
@@ -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,
+2 -1
View File
@@ -1,6 +1,7 @@
<h1>Dialog demo</h1>
<button md-raised-button color="primary" (click)="open()" [disabled]="dialogRef">Open dialog</button>
<button md-raised-button color="primary" (click)="openJazz()" [disabled]="dialogRef">Open dialog</button>
<button md-raised-button color="accent" (click)="openContentElement()">Open dialog with content elements</button>
<md-card class="demo-dialog-card">
<md-card-content>
+46 -1
View File
@@ -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<JazzDialog>) { }
}
@Component({
selector: 'demo-content-element-dialog',
styles: [
`img {
max-width: 100%;
}`
],
template: `
<h2 md-dialog-title>Neptune</h2>
<md-dialog-content>
<img src="https://upload.wikimedia.org/wikipedia/commons/5/56/Neptune_Full.jpg"/>
<p>
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.
</p>
</md-dialog-content>
<md-dialog-actions>
<button
md-raised-button
color="primary"
md-dialog-close>Close</button>
<a
md-button
color="primary"
href="https://en.wikipedia.org/wiki/Neptune"
target="_blank">Read more on Wikipedia</a>
</md-dialog-actions>
`
})
export class ContentElementDialog { }
+21 -8
View File
@@ -4,15 +4,16 @@ MdDialog is a service, which opens dialogs components in the view.
### Methods
| Name | Description |
| --- | --- |
| Name | Description |
| ---- | ----------- |
| `open(component: ComponentType<T>, config: MdDialogConfig): MdDialogRef<T>` | 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<any>` | 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: `
<button type="button" (click)="dialogRef.close('yes')">Yes</button>
<button type="button" (click)="dialogRef.close('no')">No</button>
<h1 md-dialog-title>Would you like to order pizza?</h1>
<md-dialog-actions>
<button (click)="dialogRef.close('yes')">Yes</button>
<button md-dialog-close>No</button>
</md-dialog-actions>
`
})
export class PizzaDialog {
-24
View File
@@ -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;
}
}
+1 -1
View File
@@ -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',
@@ -0,0 +1,47 @@
import {Directive, Input} from '@angular/core';
import {MdDialogRef} from './dialog-ref';
/**
* Button that will close the current dialog.
*/
@Directive({
selector: 'button[md-dialog-close], button[mat-dialog-close]',
host: {
'(click)': 'dialogRef.close()',
'[attr.aria-label]': 'ariaLabel'
}
})
export class MdDialogClose {
/** Screenreader label for the button. */
@Input('aria-label') ariaLabel: string = 'Close dialog';
constructor(public dialogRef: MdDialogRef<any>) { }
}
/**
* Title of a dialog element. Stays fixed to the top of the dialog when scrolling.
*/
@Directive({
selector: '[md-dialog-title], [mat-dialog-title]'
})
export class MdDialogTitle { }
/**
* Scrollable content container of a dialog.
*/
@Directive({
selector: '[md-dialog-content], md-dialog-content, [mat-dialog-content], mat-dialog-content'
})
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, [mat-dialog-actions], mat-dialog-actions'
})
export class MdDialogActions { }
+54
View File
@@ -0,0 +1,54 @@
@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], mat-dialog-content, [mat-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], [mat-dialog-title] {
font-size: 20px;
font-weight: bold;
margin: 0 0 20px;
display: block;
}
md-dialog-actions, [md-dialog-actions], mat-dialog-actions, [mat-dialog-actions] {
padding: $md-dialog-padding / 2 0;
display: block;
&:last-child {
// If the actions are the last element in a dialog, we need to pull them down
// over the dialog padding, in order to avoid the action's padding stacking
// with the dialog's.
margin-bottom: -$md-dialog-padding;
}
}
+59 -3
View File
@@ -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';
@@ -308,6 +309,41 @@ describe('MdDialog', () => {
.toBe('dialog-trigger', 'Expected that the trigger was refocused after dialog close');
}));
});
describe('dialog content elements', () => {
let dialogRef: MdDialogRef<ContentElementDialog>;
beforeEach(() => {
dialogRef = dialog.open(ContentElementDialog);
viewContainerFixture.detectChanges();
});
it('should close the dialog when clicking on the close button', () => {
expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(1);
(overlayContainerElement.querySelector('button[md-dialog-close]') as HTMLElement).click();
expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(0);
});
it('should not close the dialog if [md-dialog-close] is applied on a non-button node', () => {
expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(1);
(overlayContainerElement.querySelector('div[md-dialog-close]') as HTMLElement).click();
expect(overlayContainerElement.querySelectorAll('.md-dialog-container').length).toBe(1);
});
it('should allow for a user-specified aria-label on the close button', () => {
let button = overlayContainerElement.querySelector('button[md-dialog-close]');
dialogRef.componentInstance.closeButtonAriaLabel = 'Best close button ever';
viewContainerFixture.detectChanges();
expect(button.getAttribute('aria-label')).toBe('Best close button ever');
});
});
});
@@ -334,13 +370,33 @@ class PizzaMsg {
constructor(public dialogRef: MdDialogRef<PizzaMsg>) { }
}
@Component({
template: `
<h1 md-dialog-title>This is the title</h1>
<md-dialog-content>Lorem ipsum dolor sit amet.</md-dialog-content>
<md-dialog-actions>
<button md-dialog-close [aria-label]="closeButtonAriaLabel">Close</button>
<div md-dialog-close>Should not close</div>
</md-dialog-actions>
`
})
class ContentElementDialog {
closeButtonAriaLabel: string;
}
// 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 { }
+6 -35
View File
@@ -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
@@ -184,18 +170,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],
};
}
}
+55
View File
@@ -1,4 +1,59 @@
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-content-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-content-directives';
export * from './dialog-config';
export * from './dialog-ref';