diff --git a/demos/app/app.component.css b/demos/app/app.component.css index d1d289a7cf4..949759164ee 100644 --- a/demos/app/app.component.css +++ b/demos/app/app.component.css @@ -1,9 +1,13 @@ -.aside ::-webkit-scrollbar { +.ig-drawer-content { + background: #fff; +} + +.ig-drawer-content ::-webkit-scrollbar { width: 3px; background: #e4e4e4; } -.aside ::-webkit-scrollbar-thumb { +.ig-drawer-content ::-webkit-scrollbar-thumb { background: #ec6f74; border-radius: 0; } @@ -25,15 +29,6 @@ overflow-y: scroll; } -.aside { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: #fff; -} - .nav { position: absolute; width: 100%; diff --git a/demos/app/app.component.html b/demos/app/app.component.html index 8ee557b6637..a1de8fd0f75 100644 --- a/demos/app/app.component.html +++ b/demos/app/app.component.html @@ -1,153 +1,151 @@
- +
- +
-
+
diff --git a/demos/app/app.component.ts b/demos/app/app.component.ts index 6c70751272c..8954397fb73 100644 --- a/demos/app/app.component.ts +++ b/demos/app/app.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Output, ViewChild } from "@angular/core"; +import { NavigationStart, Router } from "@angular/router"; import { NavigationDrawer, NavigationDrawerModule } from "../../src/main"; @Component({ @@ -12,9 +13,21 @@ export class AppComponent { public drawerState = { enableGestures: true, open: true, - pin: true, - pinThreshold: "768px", + pin: false, + pinThreshold: 768, position: "left", width: "242px" }; + + constructor(private router: Router) {} + public ngOnInit(): void { + this.router.events + .filter((x) => x instanceof NavigationStart) + .subscribe((event: NavigationStart) => { + if (event.url !== "/" && !this.drawerState.pin) { + // Close drawer when a sample is selected + this.navdrawer.close(); + } + }); + } } diff --git a/demos/app/app.module.ts b/demos/app/app.module.ts index 68673b45648..a4804a98875 100644 --- a/demos/app/app.module.ts +++ b/demos/app/app.module.ts @@ -3,7 +3,7 @@ import { FormsModule } from "@angular/forms"; import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { IgxComponentsModule, IgxDirectivesModule, NavigationService } from "../../src/main"; +import { IgxComponentsModule, IgxDirectivesModule, IgxNavigationModule } from "../../src/main"; import { AppComponent } from "./app.component"; import { routing } from "./app.routing"; @@ -44,6 +44,7 @@ import { IgxToastSampleModule } from "./toast/sample.module"; routing, IgxComponentsModule, IgxDirectivesModule, + IgxNavigationModule, InputSampleModule, CarouselSampleModule, TabBarSampleModule, @@ -70,7 +71,6 @@ import { IgxToastSampleModule } from "./toast/sample.module"; IgxCalendarSampleModule, IgxDatePickerSampleModule, LayoutSampleModule - ], - providers: [NavigationService] + ] }) export class AppModule { } diff --git a/demos/app/pageHeading/pageHeading.module.ts b/demos/app/pageHeading/pageHeading.module.ts index 1b49be3d9a9..63555877f1a 100644 --- a/demos/app/pageHeading/pageHeading.module.ts +++ b/demos/app/pageHeading/pageHeading.module.ts @@ -1,10 +1,10 @@ import { NgModule } from "@angular/core"; -import { IgxDirectivesModule, IgxIconModule, NavigationToggle } from "../../../src/main"; +import { IgxDirectivesModule, IgxIconModule, IgxNavigationModule } from "../../../src/main"; import { PageHeaderComponent } from "./pageHeading.component"; @NgModule({ - declarations: [PageHeaderComponent, NavigationToggle], + declarations: [PageHeaderComponent], exports: [PageHeaderComponent], - imports: [IgxDirectivesModule, IgxIconModule] + imports: [IgxDirectivesModule, IgxIconModule, IgxNavigationModule] }) export class PageHeaderModule { } diff --git a/src/card/card.component.ts b/src/card/card.component.ts index f52468f0d00..c8739d2038d 100644 --- a/src/card/card.component.ts +++ b/src/card/card.component.ts @@ -1,6 +1,5 @@ import { Component, Directive, NgModule, ViewEncapsulation } from "@angular/core"; import { IgxButtonModule } from "../button/button.directive"; -import { HammerGesturesManager } from "../core/touch"; /** * IgxCardHeader is container for the card header @@ -43,7 +42,6 @@ export class IgxCardFooter { } @Component({ encapsulation: ViewEncapsulation.None, moduleId: module.id, - providers: [HammerGesturesManager], selector: "igx-card", styleUrls: ["./card.component.css"], templateUrl: "card.component.html" diff --git a/src/carousel/carousel.component.spec.ts b/src/carousel/carousel.component.spec.ts index d7b06acd390..67c712a0093 100644 --- a/src/carousel/carousel.component.spec.ts +++ b/src/carousel/carousel.component.spec.ts @@ -4,7 +4,6 @@ import { TestBed } from "@angular/core/testing"; import {By} from "@angular/platform-browser"; -import { HammerGesturesManager } from "../core/touch"; import {IgxCarousel, IgxCarouselModule, IgxSlide} from "./carousel.component"; function dispatchEv(element: HTMLElement, eventType: string) { diff --git a/src/carousel/carousel.component.ts b/src/carousel/carousel.component.ts index acc03db5141..661a3d9bcc8 100644 --- a/src/carousel/carousel.component.ts +++ b/src/carousel/carousel.component.ts @@ -10,7 +10,6 @@ import { Output, ViewEncapsulation } from "@angular/core"; -import { HammerGesturesManager } from "../core/touch"; import { IgxIconModule } from "../icon/icon.component"; export enum Direction { NONE, NEXT, PREV } @@ -36,7 +35,6 @@ export enum Direction { NONE, NEXT, PREV } role: "region" }, moduleId: module.id, - providers: [HammerGesturesManager], selector: "igx-carousel", styleUrls: ["./carousel.component.css"], templateUrl: "carousel.component.html" diff --git a/src/core/navigation/directives.ts b/src/core/navigation/directives.ts index f955dc1bcb7..05f51ade7d0 100644 --- a/src/core/navigation/directives.ts +++ b/src/core/navigation/directives.ts @@ -1,7 +1,15 @@ import {Directive, HostListener, Input} from "@angular/core"; import {NavigationService} from "./nav-service"; -// TODO: (style) NavToggleDirective, igNavToggle selector ? +/** + * Directive that can toggle targets through provided NavigationService. + * + * Usage: + * ``` + * + * ``` + * Where the `ID` matches the ID of compatible `IToggleView` component. + */ @Directive({ selector: "[igxNavToggle]" }) export class NavigationToggle { public state: NavigationService; @@ -18,7 +26,15 @@ export class NavigationToggle { } } -// TODO: (style) NavCloseDirective, igNavClose selector ? +/** + * Directive that can close targets through provided NavigationService. + * + * Usage: + * ``` + * + * ``` + * Where the `ID` matches the ID of compatible `IToggleView` component. + */ @Directive({ selector: "[igxNavClose]" }) export class NavigationClose { public state: NavigationService; diff --git a/src/core/touch.ts b/src/core/touch.ts index b61f780c07f..b1fac7c8561 100644 --- a/src/core/touch.ts +++ b/src/core/touch.ts @@ -46,9 +46,12 @@ export class HammerGesturesManager { // Creating the manager bind events, must be done outside of angular return this._zone.runOutsideAngular(() => { - // new Hammer is a shortcut for Manager with defaults - const mc = new Hammer(element, this.hammerOptions); - this.addManagerForElement(element, mc); + let mc: HammerManager = this.getManagerForElement(element); + if (mc === null) { + // new Hammer is a shortcut for Manager with defaults + mc = new Hammer(element, this.hammerOptions); + this.addManagerForElement(element, mc); + } const handler = (eventObj) => { this._zone.run(() => { eventHandler(eventObj); }); }; mc.on(eventName, handler); return () => { mc.off(eventName, handler); }; @@ -62,12 +65,21 @@ export class HammerGesturesManager { * @param target Can be one of either window, body or document(fallback default). */ public addGlobalEventListener(target: string, eventName: string, eventHandler: (eventObj) => void): () => void { - const element = getDOM().getGlobalEventTarget(this.doc, target); + const element = this.getGlobalEventTarget(target); // Creating the manager bind events, must be done outside of angular return this.addEventListener(element as HTMLElement, eventName, eventHandler); } + /** + * Exposes [Dom]Adapter.getGlobalEventTarget to get global event targets. + * Supported: window, document, body. Defaults to document for invalid args. + * @param target Target name + */ + public getGlobalEventTarget(target: string): EventTarget { + return getDOM().getGlobalEventTarget(this.doc, target); + } + /** * Set HammerManager options. * diff --git a/src/date-picker/date-picker.component.ts b/src/date-picker/date-picker.component.ts index 4025fcbd070..9c1d35388c4 100644 --- a/src/date-picker/date-picker.component.ts +++ b/src/date-picker/date-picker.component.ts @@ -13,7 +13,6 @@ import { import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { WEEKDAYS } from "../calendar/calendar"; import { IgxCalendarComponent, IgxCalendarModule } from "../calendar/calendar.component"; -import { HammerGesturesManager } from "../core/touch"; import { IgxDialog, IgxDialogModule } from "../dialog/dialog.component"; import { IgxInput } from "../input/input.directive"; @@ -21,8 +20,7 @@ import { IgxInput } from "../input/input.directive"; encapsulation: ViewEncapsulation.None, moduleId: module.id, providers: - [HammerGesturesManager, - { provide: NG_VALUE_ACCESSOR, useExisting: IgxDatePickerComponent, multi: true }], + [{ provide: NG_VALUE_ACCESSOR, useExisting: IgxDatePickerComponent, multi: true }], selector: "igx-datePicker", styleUrls: ["date-picker.component.css"], templateUrl: "date-picker.component.html" diff --git a/src/directives/filter.directive.spec.ts b/src/directives/filter.directive.spec.ts index d19e3df7584..c1b00ee2cae 100644 --- a/src/directives/filter.directive.spec.ts +++ b/src/directives/filter.directive.spec.ts @@ -1,7 +1,6 @@ import { Component, ContentChildren, ViewChild } from "@angular/core"; import { async, ComponentFixtureAutoDetect, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; -import { HammerGesturesManager } from "../core/touch"; import { IgxList, IgxListItem, IgxListModule } from "../list/list.component"; import { IgxFilterDirective, IgxFilterModule, IgxFilterOptions, IgxFilterPipe } from "./filter.directive"; diff --git a/src/list/list.component.spec.ts b/src/list/list.component.spec.ts index dcc43217a0b..bb7feeaddc7 100644 --- a/src/list/list.component.spec.ts +++ b/src/list/list.component.spec.ts @@ -2,7 +2,6 @@ import { Component, ContentChildren, QueryList, ViewChild } from "@angular/core" import { async, ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { Observable } from "rxjs/Rx"; -import { HammerGesturesManager } from "../core/touch"; import { IgxList, IgxListItem, IgxListModule, IgxListPanState } from "./list.component"; declare var Simulator: any; diff --git a/src/list/list.component.ts b/src/list/list.component.ts index fbddc1eac7b..467b4ea0cff 100644 --- a/src/list/list.component.ts +++ b/src/list/list.component.ts @@ -4,7 +4,6 @@ import { Inject, Input, NgModule, OnDestroy, OnInit, Output, QueryList, Renderer2, ViewChild } from "@angular/core"; import { IgxButtonModule } from "../button/button.directive"; -import { HammerGesturesManager } from "../core/touch"; import { IgxRippleModule } from "../directives/ripple.directive"; export interface IListChild { diff --git a/src/modules.ts b/src/modules.ts index 65b71b9c5e8..58a82999299 100644 --- a/src/modules.ts +++ b/src/modules.ts @@ -24,6 +24,8 @@ import { IgxSwitchModule} from "./switch/switch.component"; import { IgxTabBarModule } from "./tabbar/tabbar.component"; import { IgxToastModule } from "./toast/toast.component"; +import { NavigationClose, NavigationService, NavigationToggle } from "./core/navigation"; + @NgModule({ exports: [ IgxAvatarModule, @@ -103,3 +105,16 @@ import { IgxLayout } from "./layout/layout.directive"; ] }) export class IgxDirectivesModule {} + +@NgModule({ + declarations: [ + NavigationToggle, + NavigationClose + ], + exports: [ + NavigationToggle, + NavigationClose + ], + providers: [ NavigationService ] +}) +export class IgxNavigationModule {} diff --git a/src/navbar/navbar.component.ts b/src/navbar/navbar.component.ts index 1d3fa1c3f38..49f732004a2 100644 --- a/src/navbar/navbar.component.ts +++ b/src/navbar/navbar.component.ts @@ -10,13 +10,11 @@ import { ViewEncapsulation } from "@angular/core"; import { IgxButtonModule } from "../button/button.directive"; -import { HammerGesturesManager } from "../core/touch"; import { IgxIconModule } from "../icon/icon.component"; @Component({ encapsulation: ViewEncapsulation.None, moduleId: module.id, - providers: [HammerGesturesManager], selector: "igx-navbar", styleUrls: ["./navbar.component.css"], templateUrl: "navbar.component.html" diff --git a/src/navigation-drawer/navigation-drawer.component.spec.ts b/src/navigation-drawer/navigation-drawer.component.spec.ts index aeb6e7a7c79..6227c0b1f7f 100644 --- a/src/navigation-drawer/navigation-drawer.component.spec.ts +++ b/src/navigation-drawer/navigation-drawer.component.spec.ts @@ -69,7 +69,7 @@ describe("Navigation Drawer", () => { })); it("should attach events and register to nav service and detach on destroy", async(() => { - const template = ''; + const template = ''; TestBed.overrideComponent(TestComponentDI, { set: { template @@ -169,10 +169,10 @@ describe("Navigation Drawer", () => { })); it("should properly initialize with min template", async(() => { - const template = ` + const template = `
-
`; + `; TestBed.overrideComponent(TestComponentDI, { set: { template @@ -191,7 +191,8 @@ describe("Navigation Drawer", () => { })); it("should set pin, gestures options", async(() => { - const template = ''; + const template = ` + `; TestBed.overrideComponent(TestComponentPin, { set: { template @@ -288,9 +289,9 @@ describe("Navigation Drawer", () => { }, 10000); it("should update edge zone with mini width", async(() => { - const template = ` + const template = `
-
`; + `; let fixture: ComponentFixture; TestBed.overrideComponent(TestComponentDI, { set: { @@ -317,10 +318,10 @@ describe("Navigation Drawer", () => { })); it("should update width from css or property", (done) => { - const template = ` + const template = `
-
`; + `; let fixture: ComponentFixture; TestBed.overrideComponent(TestComponentDI, { set: { @@ -361,6 +362,57 @@ describe("Navigation Drawer", () => { }); }); + it("should update pin based on window width (pinThreshold)", (done) => { + const template = `''`; + const originalWidth = window.innerWidth; + let fixture: ComponentFixture; + let widthSpyOverride: jasmine.Spy; + + TestBed.overrideComponent(TestComponentPin, { + set: { + template + }}); + + // compile after overrides, not in before each: https://github.com/angular/angular/issues/10712 + TestBed.compileComponents().then(() => { + fixture = TestBed.createComponent(TestComponentPin); + fixture.detectChanges(); + + // defaults: + expect(fixture.componentInstance.viewChild.pin) + .toBe(originalWidth >= fixture.componentInstance.pinThreshold); + + // Using Window through DI causes AOT error (https://github.com/angular/angular/issues/15640) + // so for tests just force override the the `getWindowWidth` + widthSpyOverride = spyOn(fixture.componentInstance.viewChild as any, "getWindowWidth") + .and.returnValue(fixture.componentInstance.pinThreshold); + window.dispatchEvent(new Event("resize")); + // wait for debounce + return new Promise((resolve) => { + setTimeout(() => { resolve(); }, 200); + }); + }) + .then(() => { + expect(fixture.componentInstance.viewChild.pin).toBe(true); + + widthSpyOverride.and.returnValue(768); + window.dispatchEvent(new Event("resize")); + // wait for debounce + return new Promise((resolve) => { + setTimeout(() => { resolve(); }, 200); + }); + }) + .then(() => { + expect(fixture.componentInstance.viewChild.pin).toBe(false); + fixture.componentInstance.pinThreshold = 500; + fixture.detectChanges(); + expect(fixture.componentInstance.viewChild.pin).toBe(true); + done(); + }).catch ((reason) => { + return Promise.reject(reason); + }); + }); + function swipe(element, posX, posY, duration, deltaX, deltaY) { const swipeOptions = { deltaX, @@ -398,7 +450,7 @@ describe("Navigation Drawer", () => { @Component({ selector: "test-cmp", - template: "" + template: "" }) class TestComponent { @ViewChild(Infragistics.NavigationDrawer) public viewChild: Infragistics.NavigationDrawer; @@ -407,7 +459,7 @@ class TestComponent { @Component({ providers: [Infragistics.NavigationService], selector: "test-cmp", - template: "" + template: "" }) class TestComponentDI { public drawerMiniWidth: string | number; @@ -418,4 +470,5 @@ class TestComponentDI { class TestComponentPin extends TestComponentDI { public pin: boolean = true; public enableGestures: string = ""; + public pinThreshold: number = 1024; } diff --git a/src/navigation-drawer/navigation-drawer.component.ts b/src/navigation-drawer/navigation-drawer.component.ts index 375011495e8..46f1ed5539d 100644 --- a/src/navigation-drawer/navigation-drawer.component.ts +++ b/src/navigation-drawer/navigation-drawer.component.ts @@ -15,6 +15,7 @@ import { SimpleChange } from "@angular/core"; // import {AnimationBuilder} from 'angular2/src/animate/animation_builder'; TODO +import { Observable, Subscription } from "rxjs/Rx"; import { BaseComponent } from "../core/base"; import { IToggleView, NavigationService } from "../core/navigation"; import { HammerGesturesManager } from "../core/touch"; @@ -27,11 +28,11 @@ declare var module: any; * Navigation Drawer component supports collapsible side navigation container. * Usage: * ``` - * + * *
* *
- *
+ * * ``` * Can also include an optional `
`. * ID required to register with NavigationService allow directives to target the control. @@ -43,7 +44,7 @@ declare var module: any; */ moduleId: module.id, // commonJS standard providers: [HammerGesturesManager], - selector: "ig-nav-drawer", + selector: "igx-nav-drawer", styleUrls: ["./navigation-drawer.component.css"], templateUrl: "navigation-drawer.component.html" }) @@ -90,6 +91,8 @@ export class NavigationDrawer extends BaseComponent implements IToggleView, */ @Input() public miniWidth: string; + /** Pinned state change output for two-way binding */ + @Output() public pinChange = new EventEmitter(); /** Event fired as the Navigation Drawer is about to open. */ @Output() public opening = new EventEmitter(); /** Event fired when the Navigation Drawer has opened. */ @@ -102,6 +105,7 @@ export class NavigationDrawer extends BaseComponent implements IToggleView, private _hasMimiTempl: boolean = false; private _gesturesAttached: boolean = false; private _widthCache: { width: number, miniWidth: number } = { width: null, miniWidth: null }; + private _resizeObserver: Subscription; private css: { [name: string]: string; } = { drawer: "ig-nav-drawer", mini: "mini", @@ -203,13 +207,12 @@ export class NavigationDrawer extends BaseComponent implements IToggleView, // wait for template and ng-content to be ready this._hasMimiTempl = this.getChild(this.css.miniProjection) !== null; this.updateEdgeZone(); - if (this.pinThreshold && this.getWindowWidth() >= this.pinThreshold) { - this.pin = true; - } + this.checkPinThreshold(); // need to set height without absolute positioning this.ensureDrawerHeight(); this.ensureEvents(); + // TODO: apply platform-safe Ruler from http://plnkr.co/edit/81nWDyreYMzkunihfRgX?p=preview // (https://github.com/angular/angular/issues/6515), blocked by https://github.com/angular/angular/issues/6904 } @@ -219,6 +222,7 @@ export class NavigationDrawer extends BaseComponent implements IToggleView, if (this._state) { this._state.remove(this.id); } + this._resizeObserver.unsubscribe(); } public ngOnChanges(changes: { [propName: string]: SimpleChange }) { @@ -238,6 +242,16 @@ export class NavigationDrawer extends BaseComponent implements IToggleView, } } + if (changes.pinThreshold) { + if (this.pinThreshold) { + this.ensureEvents(); + this.checkPinThreshold(); + } else { + this._resizeObserver.unsubscribe(); + this._resizeObserver = null; + } + } + if (changes.width && this.isOpen) { this.setDrawerWidth(changes.width.currentValue); } @@ -421,6 +435,10 @@ export class NavigationDrawer extends BaseComponent implements IToggleView, this._touchManager.addGlobalEventListener("document", "panmove", this.pan); this._touchManager.addGlobalEventListener("document", "panend", this.panEnd); } + if (this.pinThreshold && !this._resizeObserver) { + this._resizeObserver = Observable.fromEvent(window, "resize").debounce(() => Observable.interval(150)) + .subscribe((value) => { this.checkPinThreshold(); }); + } } private updateEdgeZone() { @@ -432,9 +450,21 @@ export class NavigationDrawer extends BaseComponent implements IToggleView, } } + private checkPinThreshold = (evt?: Event) => { + let windowWidth; + if (this.pinThreshold) { + windowWidth = this.getWindowWidth(); + if (!this.pin && windowWidth >= this.pinThreshold) { + this.pin = true; + this.pinChange.emit(true); + } else if (this.pin && windowWidth < this.pinThreshold) { + this.pin = false; + this.pinChange.emit(false); + } + } + } + private swipe = (evt: HammerInput) => { - // use lambda to keep class scope - // http://stackoverflow.com/questions/18423410/typescript-retain-scope-in-event-listener // TODO: Could also force input type: http://stackoverflow.com/a/27108052 if (!this.enableGestures || evt.pointerType !== "touch") { return; diff --git a/src/slider/slider.component.ts b/src/slider/slider.component.ts index fdd2e869f1f..acfd0cd2064 100644 --- a/src/slider/slider.component.ts +++ b/src/slider/slider.component.ts @@ -4,7 +4,6 @@ import { ViewChild } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { HammerGesturesManager } from "../core/touch"; export enum SliderType { /** @@ -40,7 +39,7 @@ function MakeProvider(type: any) { @Component({ moduleId: module.id, - providers: [HammerGesturesManager, MakeProvider(IgxSlider)], + providers: [MakeProvider(IgxSlider)], selector: "igx-slider", styleUrls: ["./slider.component.css"], templateUrl: "slider.component.html" diff --git a/src/snackbar/snackbar.component.ts b/src/snackbar/snackbar.component.ts index bea6ab7a9b8..0d030cee181 100644 --- a/src/snackbar/snackbar.component.ts +++ b/src/snackbar/snackbar.component.ts @@ -16,7 +16,6 @@ import { Output } from "@angular/core"; import { fadeIn, fadeOut, slideInBottom, slideOutBottom } from "../animations/main"; -import { HammerGesturesManager } from "../core/touch"; /** * IgxSnackbar provides feedback about an operation by showing brief message at the bottom of the screen on mobile * and lower left on larger devices. IgxSnackbar will appear above all @@ -71,7 +70,6 @@ import { HammerGesturesManager } from "../core/touch"; ]) ], moduleId: module.id, - providers: [HammerGesturesManager], selector: "igx-snackbar", styleUrls: ["./snackbar.component.css"], templateUrl: "snackbar.component.html"