diff --git a/docs/articles/install-into-existing.md b/docs/articles/install-into-existing.md index 1d63b126b8..dea9afe9a7 100644 --- a/docs/articles/install-into-existing.md +++ b/docs/articles/install-into-existing.md @@ -54,7 +54,7 @@ Same way you can enable Auth Module (more details under [Auth Module Concepts & ## Install Styles Now, let's import Nebular styles -Include Bootstrap and default Nebular theme CSS files into your `.angular-cli.json` file: +Include Bootstrap and default Nebular theme CSS files into your `angular.json` file (or `.angular-cli.json` for Angular < 6.0): ```scss "styles": [ diff --git a/docs/assets/images/components/modal.svg b/docs/assets/images/components/dialog.svg similarity index 100% rename from docs/assets/images/components/modal.svg rename to docs/assets/images/components/dialog.svg diff --git a/docs/structure.ts b/docs/structure.ts index 4ce54b0614..54853b516d 100644 --- a/docs/structure.ts +++ b/docs/structure.ts @@ -302,6 +302,16 @@ export const structure = [ 'NbContextMenuDirective', ], }, + { + type: 'tabs', + name: 'Dialog', + icon: 'dialog.svg', + source: [ + 'NbDialogService', + 'NbDialogRef', + 'NbDialogConfig', + ], + }, { type: 'tabs', name: 'Toastr', diff --git a/e2e/popover.e2e-spec.ts b/e2e/popover.e2e-spec.ts index f6ae058e9a..773764c3d8 100644 --- a/e2e/popover.e2e-spec.ts +++ b/e2e/popover.e2e-spec.ts @@ -49,28 +49,28 @@ describe('nb-popover', () => { element(placementRight).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('right'); + expect(container.getAttribute('class')).toEqual('nb-overlay-right'); }); it('render container in the bottom', () => { element(placementBottom).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('bottom'); + expect(container.getAttribute('class')).toEqual('nb-overlay-bottom'); }); it('render container in the top', () => { element(placementTop).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('top'); + expect(container.getAttribute('class')).toEqual('nb-overlay-top'); }); it('render container in the left', () => { element(placementLeft).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('left'); + expect(container.getAttribute('class')).toEqual('nb-overlay-left'); }); it('open popover by host click', () => { diff --git a/package-lock.json b/package-lock.json index 27dfa7da96..3e65eeba01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,6 +232,15 @@ "tslib": "1.9.1" } }, + "@angular/cdk": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-6.0.0.tgz", + "integrity": "sha512-GVWUwmKWJPkK4gJTi0tgaLDs5QlRvkozIs6KnrsozkPUNDIsZyQCyEUB+llHiUB9AeDGcCDbpQyGIDLdya5khQ==", + "dev": true, + "requires": { + "tslib": "1.9.1" + } + }, "@angular/cli": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-6.0.3.tgz", diff --git a/scripts/gulp/tasks/bundle/rollup-config.ts b/scripts/gulp/tasks/bundle/rollup-config.ts index 7e68160d9a..3815eba4d4 100644 --- a/scripts/gulp/tasks/bundle/rollup-config.ts +++ b/scripts/gulp/tasks/bundle/rollup-config.ts @@ -18,8 +18,9 @@ const ROLLUP_GLOBALS = { '@angular/common/testing': 'ng.common.testing', '@angular/common/http/testing': 'ng.common.http.testing', '@angular/cdk/overlay': 'ng.cdk.overlay', - '@angular/cdk/portal': 'ng.cdk.overlay', - '@angular/cdk/platform': 'ng.cdk.overlay', + '@angular/cdk/platform': 'ng.cdk.platform', + '@angular/cdk/portal': 'ng.cdk.portal', + '@angular/cdk/a11y': 'ng.cdk.a11y', // RxJS dependencies diff --git a/src/framework/theme/components/cdk/a11y/a11y.module.ts b/src/framework/theme/components/cdk/a11y/a11y.module.ts new file mode 100644 index 0000000000..005ed21819 --- /dev/null +++ b/src/framework/theme/components/cdk/a11y/a11y.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; + +import { NbFocusTrapFactoryService } from './focus-trap'; + + +@NgModule({ + providers: [ + NbFocusTrapFactoryService, + ], +}) +export class NbA11yModule { +} diff --git a/src/framework/theme/components/cdk/a11y/focus-trap.ts b/src/framework/theme/components/cdk/a11y/focus-trap.ts new file mode 100644 index 0000000000..b5b541e12d --- /dev/null +++ b/src/framework/theme/components/cdk/a11y/focus-trap.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable, NgZone } from '@angular/core'; +import { FocusTrap, FocusTrapFactory, InteractivityChecker } from '@angular/cdk/a11y'; + +import { NB_DOCUMENT } from '../../../theme.options'; + + +/** + * Overrides angular cdk focus trap to keep restore functionality inside trap. + * */ +export class NbFocusTrap extends FocusTrap { + protected previouslyFocusedElement: HTMLElement; + + constructor( + protected element: HTMLElement, + protected checker: InteractivityChecker, + protected ngZone: NgZone, + protected document: Document, + deferAnchors) { + super(element, checker, ngZone, document, deferAnchors); + this.savePreviouslyFocusedElement(); + } + + restoreFocus() { + this.previouslyFocusedElement.focus(); + this.destroy(); + } + + blurPreviouslyFocusedElement() { + this.previouslyFocusedElement.blur(); + } + + protected savePreviouslyFocusedElement() { + this.previouslyFocusedElement = this.document.activeElement as HTMLElement; + } +} + +@Injectable() +export class NbFocusTrapFactoryService extends FocusTrapFactory { + constructor( + protected checker: InteractivityChecker, + protected ngZone: NgZone, + @Inject(NB_DOCUMENT) private document) { + super(checker, ngZone, document); + } + + create(element: HTMLElement, deferCaptureElements?: boolean): NbFocusTrap { + return new NbFocusTrap(element, this.checker, this.ngZone, this.document, deferCaptureElements); + } +} diff --git a/src/framework/theme/components/cdk/adapter/adapter.module.ts b/src/framework/theme/components/cdk/adapter/adapter.module.ts index a1514881de..6a28a37339 100644 --- a/src/framework/theme/components/cdk/adapter/adapter.module.ts +++ b/src/framework/theme/components/cdk/adapter/adapter.module.ts @@ -14,5 +14,5 @@ import { NbViewportRulerAdapter } from './viewport-ruler-adapter'; { provide: ScrollDispatcher, useClass: NbScrollDispatcherAdapter }, ], }) -export class NbAdapterModule { +export class NbCdkAdapterModule { } diff --git a/src/framework/theme/components/cdk/adapter/scroll-dispatcher-adapter.ts b/src/framework/theme/components/cdk/adapter/scroll-dispatcher-adapter.ts index 3c48253a88..6a0a3cc655 100644 --- a/src/framework/theme/components/cdk/adapter/scroll-dispatcher-adapter.ts +++ b/src/framework/theme/components/cdk/adapter/scroll-dispatcher-adapter.ts @@ -7,8 +7,8 @@ import { NbLayoutScrollService } from '../../../services/scroll.service'; @Injectable() export class NbScrollDispatcherAdapter extends ScrollDispatcher { - constructor(_ngZone: NgZone, _platform: NbPlatform, protected scrollService: NbLayoutScrollService) { - super(_ngZone, _platform); + constructor(ngZone: NgZone, platform: NbPlatform, protected scrollService: NbLayoutScrollService) { + super(ngZone, platform); } scrolled(auditTimeInMs?: number): Observable { diff --git a/src/framework/theme/components/cdk/adapter/viewport-ruler-adapter.ts b/src/framework/theme/components/cdk/adapter/viewport-ruler-adapter.ts index 317d1f2168..4298dda350 100644 --- a/src/framework/theme/components/cdk/adapter/viewport-ruler-adapter.ts +++ b/src/framework/theme/components/cdk/adapter/viewport-ruler-adapter.ts @@ -9,14 +9,18 @@ import { NbLayoutScrollService, NbScrollPosition } from '../../../services/scrol @Injectable() export class NbViewportRulerAdapter extends ViewportRuler { - constructor(_platform: NbPlatform, ngZone: NgZone, + constructor(platform: NbPlatform, ngZone: NgZone, protected ruler: NbLayoutRulerService, protected scroll: NbLayoutScrollService) { - super(_platform, ngZone); + super(platform, ngZone); } getViewportSize(): Readonly<{ width: number; height: number; }> { let res; + /* + * getDimensions call is really synchronous operation. + * And we have to conform with the interface of the original service. + * */ this.ruler.getDimensions() .pipe(map(dimensions => ({ width: dimensions.clientWidth, height: dimensions.clientHeight }))) .subscribe(rect => res = rect); @@ -25,6 +29,10 @@ export class NbViewportRulerAdapter extends ViewportRuler { getViewportScrollPosition(): { left: number; top: number } { let res; + /* + * getPosition call is really synchronous operation. + * And we have to conform with the interface of the original service. + * */ this.scroll.getPosition() .pipe(map((position: NbScrollPosition) => ({ top: position.y, left: position.x }))) .subscribe(position => res = position); diff --git a/src/framework/theme/components/cdk/index.ts b/src/framework/theme/components/cdk/index.ts index 40adbf0f47..3eebe8a106 100644 --- a/src/framework/theme/components/cdk/index.ts +++ b/src/framework/theme/components/cdk/index.ts @@ -1 +1,6 @@ export * from './overlay'; +export * from './a11y/a11y.module'; +export * from './a11y/focus-trap'; +export * from './adapter/overlay-container-adapter'; +export * from './adapter/scroll-dispatcher-adapter'; +export * from './adapter/viewport-ruler-adapter'; diff --git a/src/framework/theme/components/cdk/overlay/_overlay.theme.scss b/src/framework/theme/components/cdk/overlay/_overlay.theme.scss index 431b1974b5..5a487f936f 100644 --- a/src/framework/theme/components/cdk/overlay/_overlay.theme.scss +++ b/src/framework/theme/components/cdk/overlay/_overlay.theme.scss @@ -1,5 +1,7 @@ +@import '~@angular/cdk/overlay-prebuilt.css'; + @mixin nb-overlay-theme { .overlay-backdrop { - background: rgba(0, 0, 0, 0.288); + background: nb-theme(overlay-backdrop-bg); } } diff --git a/src/framework/theme/components/cdk/overlay/mapping.ts b/src/framework/theme/components/cdk/overlay/mapping.ts index 9f89e051cd..c1e51adf0f 100644 --- a/src/framework/theme/components/cdk/overlay/mapping.ts +++ b/src/framework/theme/components/cdk/overlay/mapping.ts @@ -1,5 +1,13 @@ import { Directive, Injectable, NgModule, TemplateRef, ViewContainerRef } from '@angular/core'; -import { CdkPortal, ComponentPortal, Portal, PortalModule, TemplatePortal } from '@angular/cdk/portal'; +import { + CdkPortal, + CdkPortalOutlet, + ComponentPortal, + Portal, + PortalInjector, + PortalModule, + TemplatePortal, +} from '@angular/cdk/portal'; import { ComponentType, ConnectedOverlayPositionChange, @@ -13,6 +21,7 @@ import { OverlayPositionBuilder, OverlayRef, PositionStrategy, + ScrollStrategy, ScrollStrategyOptions, } from '@angular/cdk/overlay'; import { Platform } from '@angular/cdk/platform'; @@ -22,6 +31,10 @@ import { Platform } from '@angular/cdk/platform'; export class NbPortalDirective extends CdkPortal { } +@Directive({ selector: '[nbPortalOutlet]' }) +export class NbPortalOutletDirective extends CdkPortalOutlet { +} + @Injectable() export class NbOverlay extends Overlay { } @@ -49,6 +62,9 @@ export class NbOverlayContainer extends OverlayContainer { export class NbFlexibleConnectedPositionStrategy extends FlexibleConnectedPositionStrategy { } +export class NbPortalInjector extends PortalInjector { +} + export type NbPortal = Portal; export type NbOverlayRef = OverlayRef; export type NbComponentType = ComponentType; @@ -58,6 +74,7 @@ export type NbConnectedOverlayPositionChange = ConnectedOverlayPositionChange; export type NbConnectionPositionPair = ConnectionPositionPair; export type NbOverlayConfig = OverlayConfig; export type NbScrollStrategyOptions = ScrollStrategyOptions; +export type NbScrollStrategy = ScrollStrategy; const CDK_MODULES = [OverlayModule, PortalModule]; @@ -76,8 +93,9 @@ const CDK_PROVIDERS = [ exports: [ ...CDK_MODULES, NbPortalDirective, + NbPortalOutletDirective, ], - declarations: [NbPortalDirective], + declarations: [NbPortalDirective, NbPortalOutletDirective], providers: [...CDK_PROVIDERS], }) export class NbCdkMappingModule { diff --git a/src/framework/theme/components/cdk/overlay/overlay-container.ts b/src/framework/theme/components/cdk/overlay/overlay-container.ts index 2f8ee68e51..c0eef1b250 100644 --- a/src/framework/theme/components/cdk/overlay/overlay-container.ts +++ b/src/framework/theme/components/cdk/overlay/overlay-container.ts @@ -6,22 +6,22 @@ import { NbPosition } from './overlay-position'; export abstract class NbPositionedContainer { @Input() position: NbPosition; - @HostBinding('class.top') + @HostBinding('class.nb-overlay-top') get top(): boolean { return this.position === NbPosition.TOP } - @HostBinding('class.right') + @HostBinding('class.nb-overlay-right') get right(): boolean { return this.position === NbPosition.RIGHT } - @HostBinding('class.bottom') + @HostBinding('class.nb-overlay-bottom') get bottom(): boolean { return this.position === NbPosition.BOTTOM } - @HostBinding('class.left') + @HostBinding('class.nb-overlay-left') get left(): boolean { return this.position === NbPosition.LEFT } @@ -54,7 +54,7 @@ export class NbOverlayContainerComponent { if (this.isComponent) { Object.assign(el._componentRef.instance, this.context); /** - * Change detection have to performed here, because another way applied context + * Change detection has to be performed here, because another way applied context * will be rendered on the next change detection loop and * we'll have incorrect positioning. Because rendered component may change its size * based on the context. diff --git a/src/framework/theme/components/cdk/overlay/overlay.module.ts b/src/framework/theme/components/cdk/overlay/overlay.module.ts index 38ebf9de52..0d88b8a496 100644 --- a/src/framework/theme/components/cdk/overlay/overlay.module.ts +++ b/src/framework/theme/components/cdk/overlay/overlay.module.ts @@ -2,16 +2,16 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { NbSharedModule } from '../../shared/shared.module'; import { NbCdkMappingModule } from './mapping'; -import { NbAdapterModule } from '../adapter/adapter.module'; import { NbPositionBuilderService } from './overlay-position'; import { NbOverlayContainerComponent } from './overlay-container'; import { NbOverlayService } from './overlay'; +import { NbCdkAdapterModule } from '../adapter/adapter.module'; @NgModule({ imports: [ NbCdkMappingModule, - NbAdapterModule, + NbCdkAdapterModule, NbSharedModule, ], declarations: [NbOverlayContainerComponent], diff --git a/src/framework/theme/components/context-menu/_context-menu.component.theme.scss b/src/framework/theme/components/context-menu/_context-menu.component.theme.scss index 13bbae0b04..c0d4eab0ad 100644 --- a/src/framework/theme/components/context-menu/_context-menu.component.theme.scss +++ b/src/framework/theme/components/context-menu/_context-menu.component.theme.scss @@ -37,24 +37,24 @@ } } - &.bottom .arrow { + &.nb-overlay-bottom .arrow { top: calc(-#{$arrow-size} + 1px); left: calc(50% - #{$arrow-size}); } - &.left .arrow { + &.nb-overlay-left .arrow { right: round(-$arrow-size - $arrow-size / 2 + 2px); top: calc(50% - #{$arrow-size / 2}); transform: rotate(90deg); } - &.top .arrow { + &.nb-overlay-top .arrow { bottom: calc(-#{$arrow-size} + 1px); left: calc(50% - #{$arrow-size}); transform: rotate(180deg); } - &.right .arrow { + &.nb-overlay-right .arrow { left: round(-$arrow-size - $arrow-size / 2 + 2px); top: calc(50% - #{$arrow-size / 2}); transform: rotate(270deg); diff --git a/src/framework/theme/components/context-menu/context-menu.directive.ts b/src/framework/theme/components/context-menu/context-menu.directive.ts index ba285bfb7e..286119cd0a 100644 --- a/src/framework/theme/components/context-menu/context-menu.directive.ts +++ b/src/framework/theme/components/context-menu/context-menu.directive.ts @@ -95,7 +95,7 @@ export class NbContextMenuDirective implements AfterViewInit, OnDestroy { * Basic menu items, will be passed to the internal NbMenuComponent. * */ @Input('nbContextMenu') - set _items(items: NbMenuItem[]) { + set setItems(items: NbMenuItem[]) { this.validateItems(items); this.items = items; }; diff --git a/src/framework/theme/components/dialog/dialog-config.ts b/src/framework/theme/components/dialog/dialog-config.ts new file mode 100644 index 0000000000..34ad86ccc6 --- /dev/null +++ b/src/framework/theme/components/dialog/dialog-config.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { InjectionToken, ViewContainerRef } from '@angular/core'; + + +export const NB_DIALOG_CONFIG = new InjectionToken('Default dialog options'); + +/** + * Describes all available options that may be passed to the NbDialogService. + * */ +export class NbDialogConfig { + /** + * If true than overlay will render backdrop under a dialog. + * */ + hasBackdrop: boolean = true; + + /** + * Class that'll be assigned to the backdrop element. + * */ + backdropClass: string = 'overlay-backdrop'; + + /** + * If true then mouse clicks by backdrop will close a dialog. + * */ + closeOnBackdropClick: boolean = true; + + /** + * If true then escape press will close a dialog. + * */ + closeOnEsc: boolean = true; + + /** + * Disables scroll on content under dialog if true and does nothing otherwise. + * */ + hasScroll: boolean = false; + + /** + * Focuses dialog automatically after open if true. + * */ + autoFocus: boolean = true; + + /** + * 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; + + context: D; + + constructor(config: Partial) { + Object.assign(this, config); + } +} diff --git a/src/framework/theme/components/dialog/dialog-container.ts b/src/framework/theme/components/dialog/dialog-container.ts new file mode 100644 index 0000000000..cb945b73a9 --- /dev/null +++ b/src/framework/theme/components/dialog/dialog-container.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component, ComponentRef, ElementRef, EmbeddedViewRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; + +import { + NbComponentPortal, + NbFocusTrap, + NbFocusTrapFactoryService, + NbPortalOutletDirective, + NbTemplatePortal, +} from '../cdk'; +import { NbDialogConfig } from './dialog-config'; + + +/** + * Container component for each dialog. + * All the dialogs will be attached to it. + * // TODO add animations + * */ +@Component({ + selector: 'nb-dialog-container', + template: '', +}) +export class NbDialogContainerComponent implements OnInit, OnDestroy { + @ViewChild(NbPortalOutletDirective) portalOutlet: NbPortalOutletDirective; + + protected focusTrap: NbFocusTrap; + + constructor(protected config: NbDialogConfig, + protected elementRef: ElementRef, + protected focusTrapFactory: NbFocusTrapFactoryService) { + } + + ngOnInit() { + if (this.config.autoFocus) { + this.focusTrap = this.focusTrapFactory.create(this.elementRef.nativeElement); + this.focusTrap.blurPreviouslyFocusedElement(); + this.focusTrap.focusInitialElement(); + } + } + + ngOnDestroy() { + if (this.config.autoFocus && this.focusTrap) { + this.focusTrap.restoreFocus(); + } + } + + attachComponentPortal(portal: NbComponentPortal): ComponentRef { + return this.portalOutlet.attachComponentPortal(portal); + } + + attachTemplatePortal(portal: NbTemplatePortal): EmbeddedViewRef { + return this.portalOutlet.attachTemplatePortal(portal); + } +} diff --git a/src/framework/theme/components/dialog/dialog-ref.ts b/src/framework/theme/components/dialog/dialog-ref.ts new file mode 100644 index 0000000000..4e06e74896 --- /dev/null +++ b/src/framework/theme/components/dialog/dialog-ref.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ComponentRef } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; + +import { NbOverlayRef } from '../cdk'; + + +/** + * The `NbDialogRef` helps to manipulate dialog after it was created. + * The dialog can be dismissed by using `close` method of the dialogRef. + * You can access rendered component as `content` property of the dialogRef. + * `onBackdropClick` streams click events on the backdrop of the dialog. + * */ +export class NbDialogRef { + + componentRef: ComponentRef; + + /** + * Stream of backdrop click events. + * */ + readonly onBackdropClick: Observable; + protected onClose$: Subject = new Subject(); + readonly onClose: Observable = this.onClose$.asObservable(); + + constructor(protected overlayRef: NbOverlayRef) { + this.onBackdropClick = this.overlayRef.backdropClick(); + } + + /** + * Hides dialog. + * */ + close(res?: any) { + this.overlayRef.detach(); + this.overlayRef.dispose(); + this.onClose$.next(res); + this.onClose$.complete(); + } +} diff --git a/src/framework/theme/components/dialog/dialog.module.ts b/src/framework/theme/components/dialog/dialog.module.ts new file mode 100644 index 0000000000..8bbdb157eb --- /dev/null +++ b/src/framework/theme/components/dialog/dialog.module.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ModuleWithProviders, NgModule } from '@angular/core'; + +import { NbSharedModule } from '../shared/shared.module'; +import { NbA11yModule, NbOverlayModule } from '../cdk'; +import { NbDialogService } from './dialog.service'; +import { NbDialogContainerComponent } from './dialog-container'; +import { NB_DIALOG_CONFIG, NbDialogConfig } from './dialog-config'; + + +@NgModule({ + imports: [NbSharedModule, NbA11yModule, NbOverlayModule], + declarations: [NbDialogContainerComponent], + entryComponents: [NbDialogContainerComponent], +}) +export class NbDialogModule { + static forRoot(dialogConfig: Partial = {}): ModuleWithProviders { + return { + ngModule: NbDialogModule, + providers: [ + NbDialogService, + { provide: NB_DIALOG_CONFIG, useValue: dialogConfig }, + ], + } + } +} diff --git a/src/framework/theme/components/dialog/dialog.service.spec.ts b/src/framework/theme/components/dialog/dialog.service.spec.ts new file mode 100644 index 0000000000..c60c7a7fc1 --- /dev/null +++ b/src/framework/theme/components/dialog/dialog.service.spec.ts @@ -0,0 +1,147 @@ +import { Component, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { NbOverlayContainerAdapter, NbOverlayModule, NbOverlayService } from '../cdk'; +import { NbDialogService } from './dialog.service'; +import { NbDialogModule } from './dialog.module'; +import { NbThemeModule } from '../../theme.module'; +import { NB_DOCUMENT } from '../../theme.options'; + + +@Component({ selector: 'nb-test-dialog', template: '' }) +class NbTestDialogComponent { +} + +@NgModule({ + declarations: [NbTestDialogComponent], + entryComponents: [NbTestDialogComponent], +}) +class NbTestDialogModule { +} + + +describe('dialog-service', () => { + let dialog: NbDialogService; + let overlayContainerService: NbOverlayContainerAdapter; + let overlayContainer: HTMLElement; + let overlayService: NbOverlayService; + let document: Document; + + const queryBackdrop = () => overlayContainer.querySelector('.overlay-backdrop'); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NbTestDialogModule, + NbThemeModule.forRoot({ name: 'default' }), + NbDialogModule.forRoot(), + NbOverlayModule.forRoot(), + ], + }); + + dialog = TestBed.get(NbDialogService); + overlayContainerService = TestBed.get(NbOverlayContainerAdapter); + document = TestBed.get(NB_DOCUMENT); + overlayService = TestBed.get(NbOverlayService); + }); + + beforeEach(() => { + overlayContainer = document.createElement('div'); + overlayContainerService.setContainer(overlayContainer); + }); + + afterAll(() => { + overlayContainerService.clearContainer(); + }); + + it('should render dialog', () => { + const ref = dialog.open(NbTestDialogComponent); + expect(ref.componentRef).toBeTruthy(); + expect(ref.componentRef.instance instanceof NbTestDialogComponent).toBeTruthy(); + }); + + it('should assign default backdropClass', () => { + dialog.open(NbTestDialogComponent); + expect(queryBackdrop()).toBeTruthy(); + }); + + it('should assign backdropClass if provided', () => { + dialog.open(NbTestDialogComponent, { backdropClass: 'nb-overlay-test-backdrop-class' }); + expect(overlayContainer.querySelector('.nb-overlay-test-backdrop-class')).toBeTruthy(); + }); + + + it('should render with backdrop if hasBackdrop is true', () => { + dialog.open(NbTestDialogComponent, { hasBackdrop: true }); + expect(queryBackdrop()).toBeTruthy(); + }); + + it('should render without backdrop if hasBackdrop is false', () => { + dialog.open(NbTestDialogComponent, { hasBackdrop: false }); + expect(queryBackdrop()).toBeFalsy(); + }); + + it('should leave capability scroll content under dialog if hasScroll is true', () => { + const noopSpy = spyOn(overlayService.scrollStrategies, 'noop'); + const blockSpy = spyOn(overlayService.scrollStrategies, 'block'); + + dialog.open(NbTestDialogComponent, { hasScroll: true }); + + expect(noopSpy).toHaveBeenCalledTimes(1); + expect(blockSpy).toHaveBeenCalledTimes(0); + }); + + it('should disable scroll under dialog if hasScroll is false', () => { + const noopSpy = spyOn(overlayService.scrollStrategies, 'noop'); + const blockSpy = spyOn(overlayService.scrollStrategies, 'block'); + + dialog.open(NbTestDialogComponent, { hasScroll: false }); + + expect(noopSpy).toHaveBeenCalledTimes(0); + expect(blockSpy).toHaveBeenCalledTimes(1); + }); + + it('should fire onBackdropClick if backdrop was clicked', done => { + const ref = dialog.open(NbTestDialogComponent, { closeOnBackdropClick: false }); + const backdrop = queryBackdrop(); + + ref.onBackdropClick.subscribe(e => { + expect(e.target).toBe(backdrop); + done(); + }); + + backdrop.dispatchEvent(new Event('click')); + }); + + it('should not fire onBackdropClick if backdrop wasn\'t clicked', () => { + const ref = dialog.open(NbTestDialogComponent, { closeOnBackdropClick: false }); + const spy = jasmine.createSpy(); + ref.onBackdropClick.subscribe(spy); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('should close on backdrop click if closeOnBackdropClick is true', () => { + dialog.open(NbTestDialogComponent, { closeOnBackdropClick: true, autoFocus: false }); + queryBackdrop().dispatchEvent(new Event('click')); + expect(queryBackdrop()).toBeFalsy(); + }); + + it('should not close on backdrop click if closeOnBackdropClick is false', () => { + dialog.open(NbTestDialogComponent, { closeOnBackdropClick: false }); + queryBackdrop().dispatchEvent(new Event('click')); + expect(queryBackdrop()).toBeTruthy(); + }); + + it('should close on escape press if closeOnEsc is true', () => { + dialog.open(NbTestDialogComponent, { closeOnEsc: true }); + document.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 27 })); + expect(queryBackdrop()).toBeFalsy(); + }); + + it('should not close on escape press if closeOnEsc is false', () => { + dialog.open(NbTestDialogComponent, { closeOnEsc: false }); + document.dispatchEvent(new KeyboardEvent('keyup', { keyCode: 27 })); + expect(queryBackdrop()).toBeTruthy(); + }); +}); + diff --git a/src/framework/theme/components/dialog/dialog.service.ts b/src/framework/theme/components/dialog/dialog.service.ts new file mode 100644 index 0000000000..5a28190e44 --- /dev/null +++ b/src/framework/theme/components/dialog/dialog.service.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Inject, Injectable, Injector, TemplateRef, Type } from '@angular/core'; +import { fromEvent as observableFromEvent } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { + NbComponentPortal, + NbGlobalPositionStrategy, + NbOverlayRef, + NbOverlayService, + NbPortalInjector, + NbPositionBuilderService, + NbScrollStrategy, + NbTemplatePortal, +} from '../cdk'; +import { NB_DOCUMENT } from '../../theme.options'; +import { NB_DIALOG_CONFIG, NbDialogConfig } from './dialog-config'; +import { NbDialogRef } from './dialog-ref'; +import { NbDialogContainerComponent } from './dialog-container'; + + +/** + * The `NbDialogService` helps to open dialogs. + * + * @stacked-example(Showcase, dialog/dialog-showcase.component) + * + * A new dialog is opened by calling the `open` method with a component to be loaded and an optional configuration. + * `open` method will return `NbDialogRef` that can be used for the further manipulations. + * + * ```ts + * const dialogRef = this.dialogService.open(MyDialogComponent, { ... }); + * ``` + * + * `NbDialogRef` gives capability access reference to the rendered dialog component, + * destroy dialog and some other options described below. + * + * Also, you can inject `NbDialogRef` in dialog component. + * + * ```ts + * this.dialogService.open(MyDialogComponent, { ... }); + * + * // my-dialog.component.ts + * constructor(protected dialogRef: NbDialogRef) { + * } + * + * close() { + * this.dialogRef.close(); + * } + * ``` + * + * Instead of component you can create dialog from TemplateRef: + * + * @stacked-example(Template ref, dialog/dialog-template.component) + * + * The dialog may return result through `NbDialogRef`. Calling component can receive this result with `onClose` + * stream of `NbDialogRef`. + * + * @stacked-example(Result, dialog/dialog-result.component) + * + * ### Configuration + * + * As we mentioned above, `open` method of the `NbDialogService` may receive optional configuration options. + * Also, you can provide global dialogs configuration through `NbDialogModule.forRoot({ ... })`. + * + * This config may contain the following: + * + * `context` - both, template and component may receive data through `config.context` property. + * For components, this data will be assigned through inputs. + * For templates, you can access it inside template as $implicit. + * + * ```ts + * this.dialogService.open(template, { context: 'pass data in template' }); + * ``` + * + * ```html + * + * {{ some-additional-data }} + * + * ``` + * + * `hasBackdrop` - determines is service have to render backdrop under the dialog. + * Default is true. + * @stacked-example(Backdrop, dialog/dialog-has-backdrop.component) + * + * `closeOnBackdropClick` - close dialog on backdrop click if true. + * Default is true. + * @stacked-example(Backdrop click, dialog/dialog-backdrop-click.component) + * + * `closeOnEsc` - close dialog on escape button on the keyboard. + * Default is true. + * @stacked-example(Escape hit, dialog/dialog-esc.component) + * + * `hasScroll` - Disables scroll on content under dialog if true and does nothing otherwise. + * Default is false. + * Please, open dialogs in the separate window and try to scroll. + * @stacked-example(Scroll, dialog/dialog-scroll.component) + * + * `autoFocus` - Focuses dialog automatically after open if true. It's useful to prevent misclicks on + * trigger elements and opening multiple dialogs. + * Default is true. + * + * As you can see, if you open dialog with auto focus dialog will focus first focusable element + * or just blur previously focused automatically. + * Otherwise, without auto focus, the focus will stay on the previously focused element. + * Please, open dialogs in the separate window and try to click on the button without focus + * and then hit space any times. Multiple same dialogs will be opened. + * @stacked-example(Auto focus, dialog/dialog-auto-focus.component) + * */ +@Injectable() +export class NbDialogService { + constructor(@Inject(NB_DOCUMENT) protected document, + @Inject(NB_DIALOG_CONFIG) protected globalConfig, + protected positionBuilder: NbPositionBuilderService, + protected overlay: NbOverlayService, + protected injector: Injector) { + } + + /** + * Opens new instance of the dialog, may receive optional config. + * */ + open(content: Type | TemplateRef, userConfig: Partial> = {}): NbDialogRef { + const config = new NbDialogConfig({ ...this.globalConfig, ...userConfig }); + const overlayRef = this.createOverlay(config); + const dialogRef = new NbDialogRef(overlayRef); + const container = this.createContainer(config, overlayRef); + this.createContent(config, content, container, dialogRef); + + this.registerCloseListeners(config, overlayRef, dialogRef); + + return dialogRef; + } + + protected createOverlay(config: NbDialogConfig): NbOverlayRef { + const positionStrategy = this.createPositionStrategy(); + const scrollStrategy = this.createScrollStrategy(config.hasScroll); + + return this.overlay.create({ + positionStrategy, + scrollStrategy, + hasBackdrop: config.hasBackdrop, + backdropClass: config.backdropClass, + }); + } + + protected createPositionStrategy(): NbGlobalPositionStrategy { + return this.positionBuilder + .global() + .centerVertically() + .centerHorizontally(); + } + + protected createScrollStrategy(hasScroll: boolean): NbScrollStrategy { + if (hasScroll) { + return this.overlay.scrollStrategies.noop(); + } else { + return this.overlay.scrollStrategies.block(); + } + } + + protected createContainer(config: NbDialogConfig, overlayRef: NbOverlayRef): NbDialogContainerComponent { + const injector = new NbPortalInjector(this.createInjector(config), new WeakMap([[NbDialogConfig, config]])); + const containerPortal = new NbComponentPortal(NbDialogContainerComponent, null, injector); + const containerRef = overlayRef.attach(containerPortal); + return containerRef.instance; + } + + protected createContent(config: NbDialogConfig, + content: Type | TemplateRef, + container: NbDialogContainerComponent, + dialogRef: NbDialogRef) { + if (content instanceof TemplateRef) { + const portal = this.createTemplatePortal(config, content, dialogRef); + container.attachTemplatePortal(portal); + } else { + const portal = this.createComponentPortal(config, content, dialogRef); + dialogRef.componentRef = container.attachComponentPortal(portal); + + if (config.context) { + Object.assign(dialogRef.componentRef.instance, { ...config.context }) + } + } + } + + protected createTemplatePortal(config: NbDialogConfig, + content: TemplateRef, + dialogRef: NbDialogRef): NbTemplatePortal { + return new NbTemplatePortal(content, null, { $implicit: config.context, dialogRef }); + } + + /** + * We're creating portal with custom injector provided through config or using global injector. + * This approach provides us capability inject `NbDialogRef` in dialog component. + * */ + protected createComponentPortal(config: NbDialogConfig, + content: Type, + dialogRef: NbDialogRef): NbComponentPortal { + const injector = this.createInjector(config); + const portalInjector = new NbPortalInjector(injector, new WeakMap([[NbDialogRef, dialogRef]])); + return new NbComponentPortal(content, config.viewContainerRef, portalInjector); + } + + protected createInjector(config: NbDialogConfig): Injector { + return config.viewContainerRef && config.viewContainerRef.injector || this.injector; + } + + protected registerCloseListeners(config: NbDialogConfig, overlayRef: NbOverlayRef, dialogRef: NbDialogRef) { + if (config.closeOnBackdropClick) { + overlayRef.backdropClick().subscribe(() => dialogRef.close()); + } + + if (config.closeOnEsc) { + observableFromEvent(this.document, 'keyup') + .pipe(filter((event: KeyboardEvent) => event.keyCode === 27)) + .subscribe(() => dialogRef.close()); + } + } +} diff --git a/src/framework/theme/components/dialog/index.ts b/src/framework/theme/components/dialog/index.ts new file mode 100644 index 0000000000..70a4c20a75 --- /dev/null +++ b/src/framework/theme/components/dialog/index.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +export * from './dialog-config'; +export * from './dialog-ref'; +export * from './dialog.service'; +export * from './dialog.module'; diff --git a/src/framework/theme/components/popover/_popover.component.theme.scss b/src/framework/theme/components/popover/_popover.component.theme.scss index fc048ef1a5..e3caed4d5f 100644 --- a/src/framework/theme/components/popover/_popover.component.theme.scss +++ b/src/framework/theme/components/popover/_popover.component.theme.scss @@ -37,24 +37,24 @@ } } - &.bottom .arrow { + &.nb-overlay-bottom .arrow { top: calc(-#{$arrow-size} + 1px); left: calc(50% - #{$arrow-size}); } - &.left .arrow { + &.nb-overlay-left .arrow { right: round(-$arrow-size - $arrow-size / 2 + 2px); top: calc(50% - #{$arrow-size / 2}); transform: rotate(90deg); } - &.top .arrow { + &.nb-overlay-top .arrow { bottom: calc(-#{$arrow-size} + 1px); left: calc(50% - #{$arrow-size}); transform: rotate(180deg); } - &.right .arrow { + &.nb-overlay-right .arrow { left: round(-$arrow-size - $arrow-size / 2 + 2px); top: calc(50% - #{$arrow-size / 2}); transform: rotate(270deg); diff --git a/src/framework/theme/index.ts b/src/framework/theme/index.ts index 596b951794..938c6b4a1d 100644 --- a/src/framework/theme/index.ts +++ b/src/framework/theme/index.ts @@ -71,5 +71,6 @@ export * from './components/list/infinite-list.directive'; export * from './components/input/input.directive'; export * from './components/input/input.module'; export * from './components/cdk/overlay'; +export * from './components/dialog'; export * from './components/toastr/toastr.module'; export * from './components/toastr/toastr.service'; diff --git a/src/framework/theme/styles/global/_components.scss b/src/framework/theme/styles/global/_components.scss index 639647ab5c..8a166b2356 100644 --- a/src/framework/theme/styles/global/_components.scss +++ b/src/framework/theme/styles/global/_components.scss @@ -4,7 +4,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -@import '~@angular/cdk/overlay-prebuilt.css'; @import '../../components/layout/layout.component.theme'; @import '../../components/sidebar/sidebar.component.theme'; @import '../../components/calendar-kit/calendar-kit.theme'; diff --git a/src/framework/theme/styles/themes/_default.scss b/src/framework/theme/styles/themes/_default.scss index c4f495e3e5..b4c81843ad 100644 --- a/src/framework/theme/styles/themes/_default.scss +++ b/src/framework/theme/styles/themes/_default.scss @@ -644,6 +644,8 @@ $theme: ( calendar-month-cell-large-height: 2.375rem, calendar-year-cell-large-width: calendar-month-cell-width, calendar-year-cell-large-height: calendar-month-cell-height, + + overlay-backdrop-bg: rgba(0, 0, 0, 0.288), ); // register the theme diff --git a/src/playground/dialog/dialog-auto-focus.component.ts b/src/playground/dialog/dialog-auto-focus.component.ts new file mode 100644 index 0000000000..7bdf32e3a1 --- /dev/null +++ b/src/playground/dialog/dialog-auto-focus.component.ts @@ -0,0 +1,69 @@ +import { Component, Input } from '@angular/core'; + +import { NbDialogRef, NbDialogService } from '@nebular/theme'; + + +@Component({ + selector: 'nb-dialog', + template: ` + + {{ title }} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis tincidunt tincidunt. + Vestibulum vulputate maximus massa vel tristique. Suspendisse potenti. Duis aliquet purus sed dictum dictum. + Donec fringilla, purus at fermentum imperdiet, velit enim malesuada turpis, quis luctus arcu arcu nec orci. + Duis eu mattis felis. Quisque sollicitudin elementum nunc vel tincidunt. Vestibulum egestas mi nec + iaculis varius. Morbi in risus sed sapien ultricies feugiat. Quisque pulvinar mattis purus, + in aliquet massa aliquet et. + + + + + + `, +}) +export class NbAutoFocusDialogComponent { + @Input() title: string; + + constructor(protected ref: NbDialogRef) { + } + + dismiss() { + this.ref.close(); + } +} + +@Component({ + selector: 'nb-dialog-auto-focus', + template: ` +
+ + +
+ `, + styles: [` + /deep/ nb-layout-column { + height: 80vw; + } + + button { + margin: 1rem; + } + `], +}) +export class NbDialogAutoFocusComponent { + constructor(private dialogService: NbDialogService) { + } + + openWithAutoFocus() { + this.open(true); + } + + openWithoutAutoFocus() { + this.open(false); + } + + protected open(autoFocus: boolean) { + this.dialogService.open(NbAutoFocusDialogComponent, { autoFocus }); + } +} diff --git a/src/playground/dialog/dialog-backdrop-click.component.ts b/src/playground/dialog/dialog-backdrop-click.component.ts new file mode 100644 index 0000000000..a19a161543 --- /dev/null +++ b/src/playground/dialog/dialog-backdrop-click.component.ts @@ -0,0 +1,68 @@ +import { Component, Input } from '@angular/core'; +import { NbDialogRef, NbDialogService } from '@nebular/theme'; + + +@Component({ + selector: 'nb-dialog', + template: ` + + {{ title }} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis tincidunt tincidunt. + Vestibulum vulputate maximus massa vel tristique. Suspendisse potenti. Duis aliquet purus sed dictum dictum. + Donec fringilla, purus at fermentum imperdiet, velit enim malesuada turpis, quis luctus arcu arcu nec orci. + Duis eu mattis felis. Quisque sollicitudin elementum nunc vel tincidunt. Vestibulum egestas mi nec + iaculis varius. Morbi in risus sed sapien ultricies feugiat. Quisque pulvinar mattis purus, + in aliquet massa aliquet et. + + + + + + `, +}) +export class NbBackdropClickDialogComponent { + @Input() title: string; + + constructor(protected ref: NbDialogRef) { + } + + dismiss() { + this.ref.close(); + } +} + +@Component({ + selector: 'nb-dialog-backdrop-click', + template: ` +
+ + +
+ `, + styles: [` + /deep/ nb-layout-column { + height: 80vw; + } + + button { + margin: 1rem; + } + `], +}) +export class NbDialogBackdropClickComponent { + constructor(private dialogService: NbDialogService) { + } + + openWithBackdropClick() { + this.open(true); + } + + openWithoutBackdropClick() { + this.open(false); + } + + protected open(closeOnBackdropClick: boolean) { + this.dialogService.open(NbBackdropClickDialogComponent, { closeOnBackdropClick }); + } +} diff --git a/src/playground/dialog/dialog-esc.component.ts b/src/playground/dialog/dialog-esc.component.ts new file mode 100644 index 0000000000..00891adf06 --- /dev/null +++ b/src/playground/dialog/dialog-esc.component.ts @@ -0,0 +1,68 @@ +import { Component, Input } from '@angular/core'; +import { NbDialogRef, NbDialogService } from '@nebular/theme'; + + +@Component({ + selector: 'nb-dialog', + template: ` + + {{ title }} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis tincidunt tincidunt. + Vestibulum vulputate maximus massa vel tristique. Suspendisse potenti. Duis aliquet purus sed dictum dictum. + Donec fringilla, purus at fermentum imperdiet, velit enim malesuada turpis, quis luctus arcu arcu nec orci. + Duis eu mattis felis. Quisque sollicitudin elementum nunc vel tincidunt. Vestibulum egestas mi nec + iaculis varius. Morbi in risus sed sapien ultricies feugiat. Quisque pulvinar mattis purus, + in aliquet massa aliquet et. + + + + + + `, +}) +export class NbEscDialogComponent { + @Input() title: string; + + constructor(protected ref: NbDialogRef) { + } + + dismiss() { + this.ref.close(); + } +} + +@Component({ + selector: 'nb-dialog-esc', + template: ` +
+ + +
+ `, + styles: [` + /deep/ nb-layout-column { + height: 80vw; + } + + button { + margin: 1rem; + } + `], +}) +export class NbDialogEscComponent { + constructor(private dialogService: NbDialogService) { + } + + openWithEscClose() { + this.open(true); + } + + openWithoutEscClose() { + this.open(false); + } + + protected open(closeOnEsc: boolean) { + this.dialogService.open(NbEscDialogComponent, { closeOnEsc }); + } +} diff --git a/src/playground/dialog/dialog-has-backdrop.component.ts b/src/playground/dialog/dialog-has-backdrop.component.ts new file mode 100644 index 0000000000..6297649f8a --- /dev/null +++ b/src/playground/dialog/dialog-has-backdrop.component.ts @@ -0,0 +1,68 @@ +import { Component, Input } from '@angular/core'; +import { NbDialogRef, NbDialogService } from '@nebular/theme'; + + +@Component({ + selector: 'nb-dialog', + template: ` + + {{ title }} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis tincidunt tincidunt. + Vestibulum vulputate maximus massa vel tristique. Suspendisse potenti. Duis aliquet purus sed dictum dictum. + Donec fringilla, purus at fermentum imperdiet, velit enim malesuada turpis, quis luctus arcu arcu nec orci. + Duis eu mattis felis. Quisque sollicitudin elementum nunc vel tincidunt. Vestibulum egestas mi nec + iaculis varius. Morbi in risus sed sapien ultricies feugiat. Quisque pulvinar mattis purus, + in aliquet massa aliquet et. + + + + + + `, +}) +export class NbHasBackdropDialogComponent { + @Input() title: string; + + constructor(protected ref: NbDialogRef) { + } + + dismiss() { + this.ref.close(); + } +} + +@Component({ + selector: 'nb-dialog-has-backdrop', + template: ` +
+ + +
+ `, + styles: [` + /deep/ nb-layout-column { + height: 80vw; + } + + button { + margin: 1rem; + } + `], +}) +export class NbDialogHasBackdropComponent { + constructor(private dialogService: NbDialogService) { + } + + openWithBackdrop() { + this.open(true); + } + + openWithoutBackdrop() { + this.open(false); + } + + protected open(hasBackdrop: boolean) { + this.dialogService.open(NbHasBackdropDialogComponent, { hasBackdrop }); + } +} diff --git a/src/playground/dialog/dialog-result.component.ts b/src/playground/dialog/dialog-result.component.ts new file mode 100644 index 0000000000..111209642a --- /dev/null +++ b/src/playground/dialog/dialog-result.component.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; +import { NbDialogRef, NbDialogService } from '@nebular/theme'; + +@Component({ + selector: 'nb-name-prompt', + template: ` + + Enter your name + + + + + + + + + `, + styles: [` + button { + margin: 1rem; + } + `], +}) +export class NbDialogNamePromptComponent { + constructor(protected dialogRef: NbDialogRef) { + } + + cancel() { + this.dialogRef.close(); + } + + submit(name) { + this.dialogRef.close(name); + } +} + +@Component({ + selector: 'nb-dialog-result', + template: ` + +
+

Names:

+
    +
  • {{ name }}
  • +
+ `, + styles: [` + /deep/ nb-layout-column { + height: 80vw; + } + `], +}) +export class NbDialogResultComponent { + names: string[] = []; + + constructor(private dialogService: NbDialogService) { + } + + open() { + this.dialogService.open(NbDialogNamePromptComponent) + .onClose.subscribe(name => name && this.names.push(name)); + } +} diff --git a/src/playground/dialog/dialog-scroll.component.ts b/src/playground/dialog/dialog-scroll.component.ts new file mode 100644 index 0000000000..c4176cbd5f --- /dev/null +++ b/src/playground/dialog/dialog-scroll.component.ts @@ -0,0 +1,68 @@ +import { Component, Input } from '@angular/core'; +import { NbDialogRef, NbDialogService } from '@nebular/theme'; + + +@Component({ + selector: 'nb-dialog', + template: ` + + {{ title }} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis tincidunt tincidunt. + Vestibulum vulputate maximus massa vel tristique. Suspendisse potenti. Duis aliquet purus sed dictum dictum. + Donec fringilla, purus at fermentum imperdiet, velit enim malesuada turpis, quis luctus arcu arcu nec orci. + Duis eu mattis felis. Quisque sollicitudin elementum nunc vel tincidunt. Vestibulum egestas mi nec + iaculis varius. Morbi in risus sed sapien ultricies feugiat. Quisque pulvinar mattis purus, + in aliquet massa aliquet et. + + + + + + `, +}) +export class NbScrollDialogComponent { + @Input() title: string; + + constructor(protected ref: NbDialogRef) { + } + + dismiss() { + this.ref.close(); + } +} + +@Component({ + selector: 'nb-dialog-scroll', + template: ` +
+ + +
+ `, + styles: [` + /deep/ nb-layout-column { + height: 80vw; + } + + button { + margin: 1rem; + } + `], +}) +export class NbDialogScrollComponent { + constructor(private dialogService: NbDialogService) { + } + + openWithScroll() { + this.open(true); + } + + openWithoutScroll() { + this.open(false); + } + + protected open(hasScroll: boolean) { + this.dialogService.open(NbScrollDialogComponent, { hasScroll }); + } +} diff --git a/src/playground/dialog/dialog-showcase.component.ts b/src/playground/dialog/dialog-showcase.component.ts new file mode 100644 index 0000000000..4ea34b9500 --- /dev/null +++ b/src/playground/dialog/dialog-showcase.component.ts @@ -0,0 +1,53 @@ +import { Component, Input } from '@angular/core'; +import { NbDialogRef, NbDialogService } from '@nebular/theme'; + + +@Component({ + selector: 'nb-dialog', + template: ` + + {{ title }} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis tincidunt tincidunt. + Vestibulum vulputate maximus massa vel tristique. Suspendisse potenti. Duis aliquet purus sed dictum dictum. + Donec fringilla, purus at fermentum imperdiet, velit enim malesuada turpis, quis luctus arcu arcu nec orci. + Duis eu mattis felis. Quisque sollicitudin elementum nunc vel tincidunt. Vestibulum egestas mi nec + iaculis varius. Morbi in risus sed sapien ultricies feugiat. Quisque pulvinar mattis purus, + in aliquet massa aliquet et. + + + + + + `, +}) +export class NbShowcaseDialogComponent { + @Input() title: string; + + constructor(protected ref: NbDialogRef) { + } + + dismiss() { + this.ref.close(); + } +} + +@Component({ + selector: 'nb-dialog-showcase', + template: '', + styles: [` /deep/ nb-layout-column { + height: 80vw; + } `], +}) +export class NbDialogShowcaseComponent { + constructor(private dialogService: NbDialogService) { + } + + open() { + this.dialogService.open(NbShowcaseDialogComponent, { + context: { + title: 'This is a title passed to the dialog component', + }, + }); + } +} diff --git a/src/playground/dialog/dialog-template.component.ts b/src/playground/dialog/dialog-template.component.ts new file mode 100644 index 0000000000..c96d6a9a4e --- /dev/null +++ b/src/playground/dialog/dialog-template.component.ts @@ -0,0 +1,32 @@ +import { Component, TemplateRef } from '@angular/core'; +import { NbDialogService } from '@nebular/theme'; + + +@Component({ + selector: 'nb-dialog-template', + template: ` + + + Template Dialog + {{ data }} + + + + + + + `, + styles: [` + /deep/ nb-layout-column { + height: 80vw; + } + `], +}) +export class NbDialogTemplateComponent { + constructor(private dialogService: NbDialogService) { + } + + open(dialog: TemplateRef) { + this.dialogService.open(dialog, { context: 'this is some additional data passed to dialog' }); + } +} diff --git a/src/playground/playground-routing.module.ts b/src/playground/playground-routing.module.ts index 180ff09c38..700f853591 100644 --- a/src/playground/playground-routing.module.ts +++ b/src/playground/playground-routing.module.ts @@ -82,7 +82,8 @@ import { } from './menu/menu-test.component'; import { NbPopoverTestComponent } from './popover/popover-test.component'; import { - NbRouteTabsetShowcaseChild1Component, NbRouteTabsetShowcaseChild2Component, + NbRouteTabsetShowcaseChild1Component, + NbRouteTabsetShowcaseChild2Component, NbRouteTabsetShowcaseComponent, } from './tabset/route-tabset-showcase.component'; import { NbSearchTestComponent } from './search/search-test.component'; @@ -159,6 +160,14 @@ import { NbCalendarMinMaxComponent } from './calendar/calendar-min-max.component import { NbCalendarSizeComponent } from './calendar/calendar-size.component'; import { NbCalendarKitFullCalendarShowcaseComponent } from './calendar-kit/calendar-kit-full-calendar.component'; import { NbOverlayShowcaseComponent } from './overlay/overlay-showcase.component'; +import { NbDialogShowcaseComponent } from './dialog/dialog-showcase.component'; +import { NbDialogHasBackdropComponent } from './dialog/dialog-has-backdrop.component'; +import { NbDialogBackdropClickComponent } from './dialog/dialog-backdrop-click.component'; +import { NbDialogEscComponent } from './dialog/dialog-esc.component'; +import { NbDialogScrollComponent } from './dialog/dialog-scroll.component'; +import { NbDialogAutoFocusComponent } from './dialog/dialog-auto-focus.component'; +import { NbDialogResultComponent } from './dialog/dialog-result.component'; +import { NbDialogTemplateComponent } from './dialog/dialog-template.component'; import { NbToastrShowcaseComponent } from './toastr/toastr-showcase.component'; import { NbToastrPositionsComponent } from './toastr/toastr-positions.component'; import { NbToastrStatusesComponent } from './toastr/toastr-statuses.component'; @@ -727,6 +736,43 @@ export const routes: Routes = [ }, ], }, + { + path: 'dialog', + children: [ + { + path: 'dialog-showcase.component', + component: NbDialogShowcaseComponent, + }, + { + path: 'dialog-has-backdrop.component', + component: NbDialogHasBackdropComponent, + }, + { + path: 'dialog-backdrop-click.component', + component: NbDialogBackdropClickComponent, + }, + { + path: 'dialog-esc.component', + component: NbDialogEscComponent, + }, + { + path: 'dialog-scroll.component', + component: NbDialogScrollComponent, + }, + { + path: 'dialog-auto-focus.component', + component: NbDialogAutoFocusComponent, + }, + { + path: 'dialog-result.component', + component: NbDialogResultComponent, + }, + { + path: 'dialog-template.component', + component: NbDialogTemplateComponent, + }, + ], + }, { path: 'toastr', children: [ diff --git a/src/playground/playground.module.ts b/src/playground/playground.module.ts index c7c0572909..e5bd7d60f5 100644 --- a/src/playground/playground.module.ts +++ b/src/playground/playground.module.ts @@ -25,7 +25,6 @@ import { NbLayoutModule, NbListModule, NbMenuModule, - NbOverlayModule, NbPopoverModule, NbProgressBarModule, NbRouteTabsetModule, @@ -37,6 +36,8 @@ import { NbThemeModule, NbToastrModule, NbUserModule, + NbOverlayModule, + NbDialogModule, } from '@nebular/theme'; import { NbPlaygroundRoutingModule } from './playground-routing.module'; @@ -208,6 +209,17 @@ import { NbToastrDurationComponent } from './toastr/toastr-duration.component'; import { NbToastrDestroyByClickComponent } from './toastr/toastr-destroy-by-click.component'; import { NbToastrPreventDuplicatesComponent } from './toastr/toastr-prevent-duplicates.component'; import { NbToastrIconComponent } from './toastr/toastr-icon.component'; +import { NbDialogShowcaseComponent, NbShowcaseDialogComponent } from './dialog/dialog-showcase.component'; +import { NbDialogHasBackdropComponent, NbHasBackdropDialogComponent } from './dialog/dialog-has-backdrop.component'; +import { + NbBackdropClickDialogComponent, + NbDialogBackdropClickComponent, +} from './dialog/dialog-backdrop-click.component'; +import { NbDialogEscComponent, NbEscDialogComponent } from './dialog/dialog-esc.component'; +import { NbDialogScrollComponent, NbScrollDialogComponent } from './dialog/dialog-scroll.component'; +import { NbAutoFocusDialogComponent, NbDialogAutoFocusComponent } from './dialog/dialog-auto-focus.component'; +import { NbDialogNamePromptComponent, NbDialogResultComponent } from './dialog/dialog-result.component'; +import { NbDialogTemplateComponent } from './dialog/dialog-template.component'; export const NB_MODULES = [ NbCardModule, @@ -243,6 +255,7 @@ export const NB_MODULES = [ NbCalendarKitModule, NbOverlayModule.forRoot(), NbToastrModule.forRoot(), + NbDialogModule.forRoot(), ]; export const NB_EXAMPLE_COMPONENTS = [ @@ -403,6 +416,21 @@ export const NB_EXAMPLE_COMPONENTS = [ NbToastrDestroyByClickComponent, NbToastrPreventDuplicatesComponent, NbToastrIconComponent, + NbAutoFocusDialogComponent, + NbBackdropClickDialogComponent, + NbEscDialogComponent, + NbHasBackdropDialogComponent, + NbScrollDialogComponent, + NbShowcaseDialogComponent, + NbDialogShowcaseComponent, + NbDialogHasBackdropComponent, + NbDialogBackdropClickComponent, + NbDialogEscComponent, + NbDialogScrollComponent, + NbDialogAutoFocusComponent, + NbDialogResultComponent, + NbDialogNamePromptComponent, + NbDialogTemplateComponent, ]; @NgModule({ @@ -418,6 +446,15 @@ export const NB_EXAMPLE_COMPONENTS = [ NbPlaygroundBaseComponent, ...NB_EXAMPLE_COMPONENTS, ], + entryComponents: [ + NbAutoFocusDialogComponent, + NbDialogNamePromptComponent, + NbEscDialogComponent, + NbHasBackdropDialogComponent, + NbScrollDialogComponent, + NbShowcaseDialogComponent, + NbBackdropClickDialogComponent, + ], }) export class NbPlaygroundModule { } diff --git a/src/playground/popover/popover-custom-component.component.ts b/src/playground/popover/popover-custom-component.component.ts index c0e80c0091..2c55f604ac 100644 --- a/src/playground/popover/popover-custom-component.component.ts +++ b/src/playground/popover/popover-custom-component.component.ts @@ -10,9 +10,11 @@ import { NbDynamicToAddComponent } from '../shared/dynamic.component'; @Component({ selector: 'nb-popover-custom-component', templateUrl: './popover-custom-component.component.html', - styles: [`nb-layout-column { - height: 50vw; - } `], + styles: [` + nb-layout-column { + height: 50vw; + } + `], }) export class NbPopoverCustomComponentComponent { diff --git a/src/playground/popover/popover-showcase.component.ts b/src/playground/popover/popover-showcase.component.ts index 6159ed8c06..ff5fb7fae7 100644 --- a/src/playground/popover/popover-showcase.component.ts +++ b/src/playground/popover/popover-showcase.component.ts @@ -10,10 +10,12 @@ import { Component } from '@angular/core'; @Component({ selector: 'nb-popover-showcase', templateUrl: './popover-showcase.component.html', - styles: [`:host { - display: block; - margin: 5rem; - } `], + styles: [` + :host { + display: block; + margin: 5rem; + } + `], }) export class NbPopoverShowcaseComponent { }