From bc6cf6edee11aaacb5801ab5dfbe7741c1c8d544 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 30 Nov 2016 20:47:06 +0100 Subject: [PATCH] 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. --- src/demo-app/dialog/dialog-demo.html | 33 +++++++- src/demo-app/dialog/dialog-demo.scss | 5 ++ src/demo-app/dialog/dialog-demo.ts | 15 +++- .../position/global-position-strategy.spec.ts | 34 ++++++++ .../position/global-position-strategy.ts | 52 ++++++++++-- src/lib/dialog/README.md | 7 +- src/lib/dialog/dialog-config.ts | 20 ++++- src/lib/dialog/dialog-container.scss | 7 +- src/lib/dialog/dialog.spec.ts | 80 +++++++++++++++++++ src/lib/dialog/dialog.ts | 23 ++++-- 10 files changed, 253 insertions(+), 23 deletions(-) diff --git a/src/demo-app/dialog/dialog-demo.html b/src/demo-app/dialog/dialog-demo.html index c87a0c9956a8..fe71fed7f98f 100644 --- a/src/demo-app/dialog/dialog-demo.html +++ b/src/demo-app/dialog/dialog-demo.html @@ -1,7 +1,32 @@

Dialog demo

- + -

- Last close result: {{lastCloseResult}} -

+ + +

Dialog dimensions

+ +

+ + +

+ +

Dialog position

+ +

+ + +

+ +

+ + +

+ +

Other options

+ + Disable close +
+
+ +

Last close result: {{lastCloseResult}}

diff --git a/src/demo-app/dialog/dialog-demo.scss b/src/demo-app/dialog/dialog-demo.scss index 5ac00398d901..6f7f4cac254a 100644 --- a/src/demo-app/dialog/dialog-demo.scss +++ b/src/demo-app/dialog/dialog-demo.scss @@ -1,3 +1,8 @@ .demo-dialog { color: rebeccapurple; } + +.demo-dialog-card { + max-width: 350px; + margin: 20px 0; +} diff --git a/src/demo-app/dialog/dialog-demo.ts b/src/demo-app/dialog/dialog-demo.ts index 5935059be7c8..c2ffc0dad2ee 100644 --- a/src/demo-app/dialog/dialog-demo.ts +++ b/src/demo-app/dialog/dialog-demo.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {MdDialog, MdDialogRef} from '@angular/material'; +import {MdDialog, MdDialogRef, MdDialogConfig} from '@angular/material'; @Component({ moduleId: module.id, @@ -10,11 +10,22 @@ import {MdDialog, MdDialogRef} from '@angular/material'; export class DialogDemo { dialogRef: MdDialogRef; lastCloseResult: string; + config: MdDialogConfig = { + disableClose: false, + width: '', + height: '', + position: { + top: '', + bottom: '', + left: '', + right: '' + } + }; constructor(public dialog: MdDialog) { } open() { - this.dialogRef = this.dialog.open(JazzDialog); + this.dialogRef = this.dialog.open(JazzDialog, this.config); this.dialogRef.afterClosed().subscribe(result => { this.lastCloseResult = result; diff --git a/src/lib/core/overlay/position/global-position-strategy.spec.ts b/src/lib/core/overlay/position/global-position-strategy.spec.ts index 2f7f0924dffe..3a1f31117165 100644 --- a/src/lib/core/overlay/position/global-position-strategy.spec.ts +++ b/src/lib/core/overlay/position/global-position-strategy.spec.ts @@ -100,6 +100,40 @@ describe('GlobalPositonStrategy', () => { expect(element.style.position).toBe('fixed'); })); + + it('should set the element width', fakeAsync(() => { + strategy.width('100px').apply(element); + + flushMicrotasks(); + + expect(element.style.width).toBe('100px'); + })); + + it('should set the element height', fakeAsync(() => { + strategy.height('100px').apply(element); + + flushMicrotasks(); + + expect(element.style.height).toBe('100px'); + })); + + it('should reset the horizontal position and offset when the width is 100%', fakeAsync(() => { + strategy.centerHorizontally().width('100%').apply(element); + + flushMicrotasks(); + + expect(element.style.left).toBe('0px'); + expect(element.style.transform).toBe(''); + })); + + it('should reset the vertical position and offset when the height is 100%', fakeAsync(() => { + strategy.centerVertically().height('100%').apply(element); + + flushMicrotasks(); + + expect(element.style.top).toBe('0px'); + expect(element.style.transform).toBe(''); + })); }); function fakeAsyncTest(fn: () => void) { diff --git a/src/lib/core/overlay/position/global-position-strategy.ts b/src/lib/core/overlay/position/global-position-strategy.ts index bf0c377abde6..322b1aaa81fb 100644 --- a/src/lib/core/overlay/position/global-position-strategy.ts +++ b/src/lib/core/overlay/position/global-position-strategy.ts @@ -12,6 +12,8 @@ export class GlobalPositionStrategy implements PositionStrategy { private _bottom: string = ''; private _left: string = ''; private _right: string = ''; + private _width: string = ''; + private _height: string = ''; /** Array of individual applications of translateX(). Currently only for centering. */ private _translateX: string[] = []; @@ -63,14 +65,45 @@ export class GlobalPositionStrategy implements PositionStrategy { return this; } + /** Sets the overlay width and clears any previously set width. */ + width(value: string) { + this._width = value; + + // When the width is 100%, we should reset the `left` and the offset, + // in order to ensure that the element is flush against the viewport edge. + if (value === '100%') { + this.left('0px'); + } + + return this; + } + + /** Sets the overlay height and clears any previously set height. */ + height(value: string) { + this._height = value; + + // When the height is 100%, we should reset the `top` and the offset, + // in order to ensure that the element is flush against the viewport edge. + if (value === '100%') { + this.top('0px'); + } + + return this; + } + /** * Centers the overlay horizontally with an optional offset. * Clears any previously set horizontal position. */ - centerHorizontally(offset = '0px') { + centerHorizontally(offset = '') { this._left = '50%'; this._right = ''; - this._translateX = ['-50%', offset]; + this._translateX = ['-50%']; + + if (offset) { + this._translateX.push(offset); + } + return this; } @@ -78,10 +111,15 @@ export class GlobalPositionStrategy implements PositionStrategy { * Centers the overlay vertically with an optional offset. * Clears any previously set vertical position. */ - centerVertically(offset = '0px') { + centerVertically(offset = '') { this._top = '50%'; this._bottom = ''; - this._translateY = ['-50%', offset]; + this._translateY = ['-50%']; + + if (offset) { + this._translateY.push(offset); + } + return this; } @@ -95,13 +133,15 @@ export class GlobalPositionStrategy implements PositionStrategy { element.style.left = this._left; element.style.bottom = this._bottom; element.style.right = this._right; + element.style.width = this._width; + element.style.height = this._height; // TODO(jelbourn): we don't want to always overwrite the transform property here, // because it will need to be used for animations. - let tranlateX = this._reduceTranslateValues('translateX', this._translateX); + let translateX = this._reduceTranslateValues('translateX', this._translateX); let translateY = this._reduceTranslateValues('translateY', this._translateY); - applyCssTransform(element, `${tranlateX} ${translateY}`); + applyCssTransform(element, `${translateX} ${translateY}`); return Promise.resolve(null); } diff --git a/src/lib/dialog/README.md b/src/lib/dialog/README.md index 53ae1ec0bbcf..b9fb3c3c1506 100644 --- a/src/lib/dialog/README.md +++ b/src/lib/dialog/README.md @@ -12,8 +12,11 @@ MdDialog is a service, which opens dialogs components in the view. | Key | Description | | --- | --- | -| `role: DialogRole = 'dialog'` | The ARIA role of the dialog element. Possible values are `dialog` and `alertdialog`. Defaults to `dialog`. | -| `disableClose: boolean = false` | Whether to prevent the user from closing a dialog by clicking on the backdrop or pressing escape. Defaults to `false`. | +| `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. | +| `height: string = ''` | Height of the dialog. Takes any valid CSS value. Optional. | +| `position: { top?: string, bottom?: string, left?: string, right?: string }` | Position of the dialog that overrides the default centering in it's axis. Optional. | | `viewContainerRef: ViewContainerRef` | The view container ref to attach the dialog to. Optional. | ## MdDialogRef diff --git a/src/lib/dialog/dialog-config.ts b/src/lib/dialog/dialog-config.ts index a98f1b0a904e..070ed8272876 100644 --- a/src/lib/dialog/dialog-config.ts +++ b/src/lib/dialog/dialog-config.ts @@ -1,8 +1,15 @@ import {ViewContainerRef} from '@angular/core'; /** Valid ARIA roles for a dialog element. */ -export type DialogRole = 'dialog' | 'alertdialog' +export type DialogRole = 'dialog' | 'alertdialog'; +/** Possible overrides for a dialog's position. */ +export interface DialogPosition { + top?: string; + bottom?: string; + left?: string; + right?: string; +}; /** @@ -17,5 +24,14 @@ export class MdDialogConfig { /** Whether the user can use escape or clicking outside to close a modal. */ disableClose?: boolean = false; - // TODO(jelbourn): add configuration for size, lifecycle hooks, ARIA labelling. + /** Width of the dialog. */ + width?: string = ''; + + /** Height of the dialog. */ + height?: string = ''; + + /** Position overrides. */ + position?: DialogPosition; + + // TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling. } diff --git a/src/lib/dialog/dialog-container.scss b/src/lib/dialog/dialog-container.scss index e8031eaae7b5..2e47025c5344 100644 --- a/src/lib/dialog/dialog-container.scss +++ b/src/lib/dialog/dialog-container.scss @@ -8,7 +8,12 @@ md-dialog-container { @include md-elevation(24); display: block; - overflow: hidden; 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%; } diff --git a/src/lib/dialog/dialog.spec.ts b/src/lib/dialog/dialog.spec.ts index 26b5b7855fd6..6d2a43466ded 100644 --- a/src/lib/dialog/dialog.spec.ts +++ b/src/lib/dialog/dialog.spec.ts @@ -134,6 +134,86 @@ describe('MdDialog', () => { expect(overlayContainerElement.querySelector('md-dialog-container')).toBeFalsy(); }); + it('should should override the width of the overlay pane', () => { + dialog.open(PizzaMsg, { + width: '500px' + }); + + viewContainerFixture.detectChanges(); + + let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement; + + expect(overlayPane.style.width).toBe('500px'); + }); + + it('should should override the height of the overlay pane', () => { + dialog.open(PizzaMsg, { + height: '100px' + }); + + viewContainerFixture.detectChanges(); + + let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement; + + expect(overlayPane.style.height).toBe('100px'); + }); + + it('should should override the top offset of the overlay pane', () => { + dialog.open(PizzaMsg, { + position: { + top: '100px' + } + }); + + viewContainerFixture.detectChanges(); + + let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement; + + expect(overlayPane.style.top).toBe('100px'); + }); + + it('should should override the bottom offset of the overlay pane', () => { + dialog.open(PizzaMsg, { + position: { + bottom: '200px' + } + }); + + viewContainerFixture.detectChanges(); + + let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement; + + expect(overlayPane.style.bottom).toBe('200px'); + }); + + it('should should override the left offset of the overlay pane', () => { + dialog.open(PizzaMsg, { + position: { + left: '250px' + } + }); + + viewContainerFixture.detectChanges(); + + let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement; + + expect(overlayPane.style.left).toBe('250px'); + }); + + it('should should override the right offset of the overlay pane', () => { + dialog.open(PizzaMsg, { + position: { + right: '125px' + } + }); + + viewContainerFixture.detectChanges(); + + let overlayPane = overlayContainerElement.querySelector('.md-overlay-pane') as HTMLElement; + + expect(overlayPane.style.right).toBe('125px'); + }); + describe('disableClose option', () => { it('should prevent closing via clicks on the backdrop', () => { dialog.open(PizzaMsg, { diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index a71b260e021d..87a30befe8ae 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -22,8 +22,6 @@ export {MdDialogRef} from './dialog-ref'; // TODO(jelbourn): add support for opening with a TemplateRef // TODO(jelbourn): add `closeAll` method -// TODO(jelbourn): default dialog config -// TODO(jelbourn): escape key closes dialog // TODO(jelbourn): dialog content directives (e.g., md-dialog-header) // TODO(jelbourn): animations @@ -119,12 +117,25 @@ export class MdDialog { */ private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState { let state = new OverlayState(); + let strategy = this._overlay.position().global(); + let position = dialogConfig.position; state.hasBackdrop = true; - state.positionStrategy = this._overlay.position() - .global() - .centerHorizontally() - .centerVertically(); + state.positionStrategy = strategy; + + if (position && (position.left || position.right)) { + position.left ? strategy.left(position.left) : strategy.right(position.right); + } else { + strategy.centerHorizontally(); + } + + if (position && (position.top || position.bottom)) { + position.top ? strategy.top(position.top) : strategy.bottom(position.bottom); + } else { + strategy.centerVertically(); + } + + strategy.width(dialogConfig.width).height(dialogConfig.height); return state; }