Skip to content

Commit bc6cf6e

Browse files
crisbetotinayuangao
authored andcommitted
feat(dialog): add configurable width, height and position (#1848)
* Adds the ability to set a dialog's `width` and `height`. * Adds the ability to set a dialog's position. If only the position in one axis is overridden, the other axis will stay centered. * Fixes the `GlobalPositionStrategy` adding an unnecessary `0px` transform. * Makes the dialog scrollable. * Adds more options to the dialog demo so it's easier to test them out. Fixes #1698.
1 parent 5ac29dd commit bc6cf6e

File tree

10 files changed

+253
-23
lines changed

10 files changed

+253
-23
lines changed

src/demo-app/dialog/dialog-demo.html

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
11
<h1>Dialog demo</h1>
22

3-
<button (click)="open()" [disabled]="dialogRef">Open dialog</button>
3+
<button md-raised-button color="primary" (click)="open()" [disabled]="dialogRef">Open dialog</button>
44

5-
<p>
6-
Last close result: {{lastCloseResult}}
7-
</p>
5+
<md-card class="demo-dialog-card">
6+
<md-card-content>
7+
<h2>Dialog dimensions</h2>
8+
9+
<p>
10+
<md-input [(ngModel)]="config.width" placeholder="Width"></md-input>
11+
<md-input [(ngModel)]="config.height" placeholder="Height"></md-input>
12+
</p>
13+
14+
<h2>Dialog position</h2>
15+
16+
<p>
17+
<md-input [(ngModel)]="config.position.top" (change)="config.position.bottom = ''" placeholder="Top"></md-input>
18+
<md-input [(ngModel)]="config.position.bottom" (change)="config.position.top = ''" placeholder="Bottom"></md-input>
19+
</p>
20+
21+
<p>
22+
<md-input [(ngModel)]="config.position.left" (change)="config.position.right = ''" placeholder="Left"></md-input>
23+
<md-input [(ngModel)]="config.position.right" (change)="config.position.left = ''" placeholder="Right"></md-input>
24+
</p>
25+
26+
<h2>Other options</h2>
27+
28+
<md-checkbox [(ngModel)]="config.disableClose">Disable close</md-checkbox>
29+
</md-card-content>
30+
</md-card>
31+
32+
<p>Last close result: {{lastCloseResult}}</p>

src/demo-app/dialog/dialog-demo.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
.demo-dialog {
22
color: rebeccapurple;
33
}
4+
5+
.demo-dialog-card {
6+
max-width: 350px;
7+
margin: 20px 0;
8+
}

src/demo-app/dialog/dialog-demo.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component} from '@angular/core';
2-
import {MdDialog, MdDialogRef} from '@angular/material';
2+
import {MdDialog, MdDialogRef, MdDialogConfig} from '@angular/material';
33

44
@Component({
55
moduleId: module.id,
@@ -10,11 +10,22 @@ import {MdDialog, MdDialogRef} from '@angular/material';
1010
export class DialogDemo {
1111
dialogRef: MdDialogRef<JazzDialog>;
1212
lastCloseResult: string;
13+
config: MdDialogConfig = {
14+
disableClose: false,
15+
width: '',
16+
height: '',
17+
position: {
18+
top: '',
19+
bottom: '',
20+
left: '',
21+
right: ''
22+
}
23+
};
1324

1425
constructor(public dialog: MdDialog) { }
1526

1627
open() {
17-
this.dialogRef = this.dialog.open(JazzDialog);
28+
this.dialogRef = this.dialog.open(JazzDialog, this.config);
1829

1930
this.dialogRef.afterClosed().subscribe(result => {
2031
this.lastCloseResult = result;

src/lib/core/overlay/position/global-position-strategy.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,40 @@ describe('GlobalPositonStrategy', () => {
100100

101101
expect(element.style.position).toBe('fixed');
102102
}));
103+
104+
it('should set the element width', fakeAsync(() => {
105+
strategy.width('100px').apply(element);
106+
107+
flushMicrotasks();
108+
109+
expect(element.style.width).toBe('100px');
110+
}));
111+
112+
it('should set the element height', fakeAsync(() => {
113+
strategy.height('100px').apply(element);
114+
115+
flushMicrotasks();
116+
117+
expect(element.style.height).toBe('100px');
118+
}));
119+
120+
it('should reset the horizontal position and offset when the width is 100%', fakeAsync(() => {
121+
strategy.centerHorizontally().width('100%').apply(element);
122+
123+
flushMicrotasks();
124+
125+
expect(element.style.left).toBe('0px');
126+
expect(element.style.transform).toBe('');
127+
}));
128+
129+
it('should reset the vertical position and offset when the height is 100%', fakeAsync(() => {
130+
strategy.centerVertically().height('100%').apply(element);
131+
132+
flushMicrotasks();
133+
134+
expect(element.style.top).toBe('0px');
135+
expect(element.style.transform).toBe('');
136+
}));
103137
});
104138

105139
function fakeAsyncTest(fn: () => void) {

src/lib/core/overlay/position/global-position-strategy.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export class GlobalPositionStrategy implements PositionStrategy {
1212
private _bottom: string = '';
1313
private _left: string = '';
1414
private _right: string = '';
15+
private _width: string = '';
16+
private _height: string = '';
1517

1618
/** Array of individual applications of translateX(). Currently only for centering. */
1719
private _translateX: string[] = [];
@@ -63,25 +65,61 @@ export class GlobalPositionStrategy implements PositionStrategy {
6365
return this;
6466
}
6567

68+
/** Sets the overlay width and clears any previously set width. */
69+
width(value: string) {
70+
this._width = value;
71+
72+
// When the width is 100%, we should reset the `left` and the offset,
73+
// in order to ensure that the element is flush against the viewport edge.
74+
if (value === '100%') {
75+
this.left('0px');
76+
}
77+
78+
return this;
79+
}
80+
81+
/** Sets the overlay height and clears any previously set height. */
82+
height(value: string) {
83+
this._height = value;
84+
85+
// When the height is 100%, we should reset the `top` and the offset,
86+
// in order to ensure that the element is flush against the viewport edge.
87+
if (value === '100%') {
88+
this.top('0px');
89+
}
90+
91+
return this;
92+
}
93+
6694
/**
6795
* Centers the overlay horizontally with an optional offset.
6896
* Clears any previously set horizontal position.
6997
*/
70-
centerHorizontally(offset = '0px') {
98+
centerHorizontally(offset = '') {
7199
this._left = '50%';
72100
this._right = '';
73-
this._translateX = ['-50%', offset];
101+
this._translateX = ['-50%'];
102+
103+
if (offset) {
104+
this._translateX.push(offset);
105+
}
106+
74107
return this;
75108
}
76109

77110
/**
78111
* Centers the overlay vertically with an optional offset.
79112
* Clears any previously set vertical position.
80113
*/
81-
centerVertically(offset = '0px') {
114+
centerVertically(offset = '') {
82115
this._top = '50%';
83116
this._bottom = '';
84-
this._translateY = ['-50%', offset];
117+
this._translateY = ['-50%'];
118+
119+
if (offset) {
120+
this._translateY.push(offset);
121+
}
122+
85123
return this;
86124
}
87125

@@ -95,13 +133,15 @@ export class GlobalPositionStrategy implements PositionStrategy {
95133
element.style.left = this._left;
96134
element.style.bottom = this._bottom;
97135
element.style.right = this._right;
136+
element.style.width = this._width;
137+
element.style.height = this._height;
98138

99139
// TODO(jelbourn): we don't want to always overwrite the transform property here,
100140
// because it will need to be used for animations.
101-
let tranlateX = this._reduceTranslateValues('translateX', this._translateX);
141+
let translateX = this._reduceTranslateValues('translateX', this._translateX);
102142
let translateY = this._reduceTranslateValues('translateY', this._translateY);
103143

104-
applyCssTransform(element, `${tranlateX} ${translateY}`);
144+
applyCssTransform(element, `${translateX} ${translateY}`);
105145

106146
return Promise.resolve(null);
107147
}

src/lib/dialog/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ MdDialog is a service, which opens dialogs components in the view.
1212

1313
| Key | Description |
1414
| --- | --- |
15-
| `role: DialogRole = 'dialog'` | The ARIA role of the dialog element. Possible values are `dialog` and `alertdialog`. Defaults to `dialog`. |
16-
| `disableClose: boolean = false` | Whether to prevent the user from closing a dialog by clicking on the backdrop or pressing escape. Defaults to `false`. |
15+
| `role: DialogRole = 'dialog'` | The ARIA role of the dialog element. Possible values are `dialog` and `alertdialog`. Optional. |
16+
| `disableClose: boolean = false` | Whether to prevent the user from closing a dialog by clicking on the backdrop or pressing escape. Optional. |
17+
| `width: string = ''` | Width of the dialog. Takes any valid CSS value. Optional. |
18+
| `height: string = ''` | Height of the dialog. Takes any valid CSS value. Optional. |
19+
| `position: { top?: string, bottom?: string, left?: string, right?: string }` | Position of the dialog that overrides the default centering in it's axis. Optional. |
1720
| `viewContainerRef: ViewContainerRef` | The view container ref to attach the dialog to. Optional. |
1821

1922
## MdDialogRef

src/lib/dialog/dialog-config.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import {ViewContainerRef} from '@angular/core';
22

33
/** Valid ARIA roles for a dialog element. */
4-
export type DialogRole = 'dialog' | 'alertdialog'
4+
export type DialogRole = 'dialog' | 'alertdialog';
55

6+
/** Possible overrides for a dialog's position. */
7+
export interface DialogPosition {
8+
top?: string;
9+
bottom?: string;
10+
left?: string;
11+
right?: string;
12+
};
613

714

815
/**
@@ -17,5 +24,14 @@ export class MdDialogConfig {
1724
/** Whether the user can use escape or clicking outside to close a modal. */
1825
disableClose?: boolean = false;
1926

20-
// TODO(jelbourn): add configuration for size, lifecycle hooks, ARIA labelling.
27+
/** Width of the dialog. */
28+
width?: string = '';
29+
30+
/** Height of the dialog. */
31+
height?: string = '';
32+
33+
/** Position overrides. */
34+
position?: DialogPosition;
35+
36+
// TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling.
2137
}

src/lib/dialog/dialog-container.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ md-dialog-container {
88
@include md-elevation(24);
99

1010
display: block;
11-
overflow: hidden;
1211
padding: $md-dialog-padding;
1312
border-radius: $md-dialog-border-radius;
13+
box-sizing: border-box;
14+
overflow: auto;
15+
16+
// The dialog container should completely fill its parent overlay element.
17+
width: 100%;
18+
height: 100%;
1419
}

src/lib/dialog/dialog.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,86 @@ describe('MdDialog', () => {
134134
expect(overlayContainerElement.querySelector('md-dialog-container')).toBeFalsy();
135135
});
136136

137+
it('should should override the width of the overlay pane', () => {
138+
dialog.open(PizzaMsg, {
139+
width: '500px'
140+
});
141+
142+
viewContainerFixture.detectChanges();
143+
144+
let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement;
145+
146+
expect(overlayPane.style.width).toBe('500px');
147+
});
148+
149+
it('should should override the height of the overlay pane', () => {
150+
dialog.open(PizzaMsg, {
151+
height: '100px'
152+
});
153+
154+
viewContainerFixture.detectChanges();
155+
156+
let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement;
157+
158+
expect(overlayPane.style.height).toBe('100px');
159+
});
160+
161+
it('should should override the top offset of the overlay pane', () => {
162+
dialog.open(PizzaMsg, {
163+
position: {
164+
top: '100px'
165+
}
166+
});
167+
168+
viewContainerFixture.detectChanges();
169+
170+
let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement;
171+
172+
expect(overlayPane.style.top).toBe('100px');
173+
});
174+
175+
it('should should override the bottom offset of the overlay pane', () => {
176+
dialog.open(PizzaMsg, {
177+
position: {
178+
bottom: '200px'
179+
}
180+
});
181+
182+
viewContainerFixture.detectChanges();
183+
184+
let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement;
185+
186+
expect(overlayPane.style.bottom).toBe('200px');
187+
});
188+
189+
it('should should override the left offset of the overlay pane', () => {
190+
dialog.open(PizzaMsg, {
191+
position: {
192+
left: '250px'
193+
}
194+
});
195+
196+
viewContainerFixture.detectChanges();
197+
198+
let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement;
199+
200+
expect(overlayPane.style.left).toBe('250px');
201+
});
202+
203+
it('should should override the right offset of the overlay pane', () => {
204+
dialog.open(PizzaMsg, {
205+
position: {
206+
right: '125px'
207+
}
208+
});
209+
210+
viewContainerFixture.detectChanges();
211+
212+
let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement;
213+
214+
expect(overlayPane.style.right).toBe('125px');
215+
});
216+
137217
describe('disableClose option', () => {
138218
it('should prevent closing via clicks on the backdrop', () => {
139219
dialog.open(PizzaMsg, {

src/lib/dialog/dialog.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ export {MdDialogRef} from './dialog-ref';
2222

2323
// TODO(jelbourn): add support for opening with a TemplateRef
2424
// TODO(jelbourn): add `closeAll` method
25-
// TODO(jelbourn): default dialog config
26-
// TODO(jelbourn): escape key closes dialog
2725
// TODO(jelbourn): dialog content directives (e.g., md-dialog-header)
2826
// TODO(jelbourn): animations
2927

@@ -119,12 +117,25 @@ export class MdDialog {
119117
*/
120118
private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState {
121119
let state = new OverlayState();
120+
let strategy = this._overlay.position().global();
121+
let position = dialogConfig.position;
122122

123123
state.hasBackdrop = true;
124-
state.positionStrategy = this._overlay.position()
125-
.global()
126-
.centerHorizontally()
127-
.centerVertically();
124+
state.positionStrategy = strategy;
125+
126+
if (position && (position.left || position.right)) {
127+
position.left ? strategy.left(position.left) : strategy.right(position.right);
128+
} else {
129+
strategy.centerHorizontally();
130+
}
131+
132+
if (position && (position.top || position.bottom)) {
133+
position.top ? strategy.top(position.top) : strategy.bottom(position.bottom);
134+
} else {
135+
strategy.centerVertically();
136+
}
137+
138+
strategy.width(dialogConfig.width).height(dialogConfig.height);
128139

129140
return state;
130141
}

0 commit comments

Comments
 (0)