diff --git a/src/lib/sticky-header/index.ts b/src/lib/sticky-header/index.ts index dce3a728cf46..b515617bef3d 100644 --- a/src/lib/sticky-header/index.ts +++ b/src/lib/sticky-header/index.ts @@ -8,14 +8,18 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {OverlayModule, MdCommonModule, PlatformModule} from '../core'; -import {CdkStickyRegion, CdkStickyHeader} from './sticky-header'; - +import { + CdkStickyRegion, + CdkStickyHeader, + STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER +} from './sticky-header'; @NgModule({ imports: [OverlayModule, MdCommonModule, CommonModule, PlatformModule], declarations: [CdkStickyRegion, CdkStickyHeader], exports: [CdkStickyRegion, CdkStickyHeader, MdCommonModule], + providers: [STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER] }) export class StickyHeaderModule {} diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts new file mode 100644 index 000000000000..c4b395ae2757 --- /dev/null +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -0,0 +1,253 @@ +import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {Component, DebugElement, ViewChild} from '@angular/core'; +import { + StickyHeaderModule, + CdkStickyRegion, + CdkStickyHeader, + STICKY_HEADER_SUPPORT_STRATEGY +} from './index'; +import {OverlayModule, Scrollable} from '../core/overlay/index'; +import {PlatformModule} from '../core/platform/index'; +import {By} from '@angular/platform-browser'; +import {dispatchFakeEvent} from '@angular/cdk/testing'; + +const DEBOUNCE_TIME: number = 5; + +describe('sticky-header with positioning not supported', () => { + let fixture: ComponentFixture; + let testComponent: StickyHeaderTest; + let stickyElement: DebugElement; + let stickyParentElement: DebugElement; + let scrollableElement: DebugElement; + let stickyHeader: CdkStickyHeader; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ OverlayModule, PlatformModule, StickyHeaderModule ], + declarations: [StickyHeaderTest], + providers: [ + {provide: STICKY_HEADER_SUPPORT_STRATEGY, useValue: false}, + ], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StickyHeaderTest); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + stickyElement = fixture.debugElement.query(By.directive(CdkStickyHeader)); + stickyParentElement = fixture.debugElement.query(By.directive(CdkStickyRegion)); + stickyHeader = stickyElement.injector.get(CdkStickyHeader); + scrollableElement = fixture.debugElement.query(By.directive(Scrollable)); + }); + + it('should be able to find stickyParent', () => { + expect(stickyHeader.stickyParent).not.toBeNull(); + }); + + it('should be able to find scrollableContainer', () => { + expect(stickyHeader.upperScrollableContainer).not.toBeNull(); + }); + + it('should stick in the right place when scrolled to the top of the container', fakeAsync(() => { + let scrollableContainerTop = stickyHeader.upperScrollableContainer + .getBoundingClientRect().top; + expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + tick(); + + // Scroll the scrollableContainer up to stick + fixture.componentInstance.scrollDown(); + // wait for the DEBOUNCE_TIME + tick(DEBOUNCE_TIME); + + expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); + })); + + it('should unstuck when scrolled off the top of the container', fakeAsync(() => { + let scrollableContainerTop = stickyHeader.upperScrollableContainer + .getBoundingClientRect().top; + expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + tick(); + + // Scroll the scrollableContainer up to stick + fixture.componentInstance.scrollDown(); + // wait for the DEBOUNCE_TIME + tick(DEBOUNCE_TIME); + + expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); + + // Scroll the scrollableContainer down to unstuck + fixture.componentInstance.scrollBack(); + tick(DEBOUNCE_TIME); + + expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + + })); +}); + +describe('sticky-header with positioning supported', () => { + let fixture: ComponentFixture; + let testComponent: StickyHeaderTest; + let stickyElement: DebugElement; + let stickyParentElement: DebugElement; + let stickyHeader: CdkStickyHeader; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ OverlayModule, PlatformModule, StickyHeaderModule ], + declarations: [StickyHeaderTest], + providers: [ + {provide: STICKY_HEADER_SUPPORT_STRATEGY, useValue: true}, + ], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StickyHeaderTest); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + stickyElement = fixture.debugElement.query(By.directive(CdkStickyHeader)); + stickyParentElement = fixture.debugElement.query(By.directive(CdkStickyRegion)); + stickyHeader = stickyElement.injector.get(CdkStickyHeader); + }); + + it('should find sticky positioning is applied', () => { + let position = window.getComputedStyle(stickyHeader.element).position; + expect(position).not.toBeNull(); + expect(/sticky/i.test(position!)).toBe(true); + }); +}); + +describe('test sticky-header without StickyRegion', () => { + let fixture: ComponentFixture; + let testComponent: StickyHeaderTestNoStickyRegion; + let stickyElement: DebugElement; + let stickyHeader: CdkStickyHeader; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ OverlayModule, PlatformModule, StickyHeaderModule ], + declarations: [StickyHeaderTestNoStickyRegion], + providers: [ + {provide: STICKY_HEADER_SUPPORT_STRATEGY, useValue: false}, + ], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StickyHeaderTestNoStickyRegion); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + stickyElement = fixture.debugElement.query(By.directive(CdkStickyHeader)); + stickyHeader = stickyElement.injector.get(CdkStickyHeader); + }); + + it('should be able to find stickyParent', () => { + let p = stickyHeader.stickyParent; + expect(p).not.toBeNull(); + expect(p!.id).toBe('default-region'); + }); +}); + +@Component({ + // Use styles to define the style of scrollable container and header, + // which help test to make sure whether the header is stuck at the right position. + styles:[` + .scrollable-style { + text-align: center; + -webkit-appearance: none; + -moz-appearance: none; + height: 300px; + overflow: auto; + } + .heading-style { + background: whitesmoke; + padding: 5px; + } + `], + template: ` +
+

{{item.name}} : {{item.message}}

+
+
+

Heading 1

+
+

{{item.name}} : {{item.message}}

+

{{item.name}} : {{item.message}}

+

{{item.name}} : {{item.message}}

+
+
+ `}) +class StickyHeaderTest { + @ViewChild(Scrollable) scrollingContainer: Scrollable; + + items: any[] = [ + {'name': 'Forrest', 'message': 'Life was like a box of chocolates'}, + {'name': 'Gump', 'message': 'you never know what you are gonna get'}, + {'name': 'Lion King', 'message': 'Everything you see exists together'}, + {'name': 'Jack', 'message': 'in a delicate balance'}, + {'name': 'Garfield', 'message': 'Save Water'}, + {'name': 'Shawshank', 'message': 'There is something inside'}, + {'name': 'Jone', 'message': 'Enough movies?'}, + ]; + + scrollDown() { + const scrollingContainerEl = this.scrollingContainer.getElementRef().nativeElement; + scrollingContainerEl.scrollTop = 300; + + // Emit a scroll event from the scrolling element in our component. + dispatchFakeEvent(scrollingContainerEl, 'scroll'); + } + + scrollBack() { + const scrollingContainerEl = this.scrollingContainer.getElementRef().nativeElement; + scrollingContainerEl.scrollTop = 0; + + // Emit a scroll event from the scrolling element in our component. + dispatchFakeEvent(scrollingContainerEl, 'scroll'); + } +} + +@Component({ + // Use styles to define the style of scrollable container and header, + // which help test to make sure whether the header is stuck at the right position. + styles:[` + .scrollable-style { + text-align: center; + -webkit-appearance: none; + -moz-appearance: none; + height: 300px; + overflow: auto; + } + .heading-style { + background: whitesmoke; + padding: 5px; + } + `], + template: ` +
+

{{item.name}} : {{item.message}}

+
+
+

Heading 1

+
+

{{item.name}} : {{item.message}}

+

{{item.name}} : {{item.message}}

+

{{item.name}} : {{item.message}}

+
+
+ `}) +class StickyHeaderTestNoStickyRegion { + items: any[] = [ + {'name': 'Forrest', 'message': 'Life was like a box of chocolates'}, + {'name': 'Gump', 'message': 'you never know what you are gonna get'}, + {'name': 'Lion King', 'message': 'Everything you see exists together'}, + {'name': 'Jack', 'message': 'in a delicate balance'}, + {'name': 'Garfield', 'message': 'Save Water'}, + {'name': 'Shawshank', 'message': 'There is something inside'}, + {'name': 'Jone', 'message': 'Enough movies?'}, + ]; +} diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 1c520ed7c8df..6185b4f5c783 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ import {Directive, Input, - OnDestroy, AfterViewInit, ElementRef, Optional} from '@angular/core'; -import {Platform} from '../core/platform'; + OnDestroy, AfterViewInit, ElementRef, Optional, + InjectionToken, Injectable, Inject, Provider} from '@angular/core'; +import {Platform} from '../core/platform/index'; import {Scrollable} from '../core/overlay/scroll/scrollable'; import {extendObject} from '../core/util/object-extend'; import {Subscription} from 'rxjs/Subscription'; @@ -32,7 +33,6 @@ export class CdkStickyRegion { constructor(public readonly _elementRef: ElementRef) { } } - /** Class applied when the header is "stuck" */ const STICK_START_CLASS = 'cdk-sticky-header-start'; @@ -46,6 +46,16 @@ const STICK_END_CLASS = 'cdk-sticky-header-end'; */ const DEBOUNCE_TIME: number = 5; +export const STICKY_HEADER_SUPPORT_STRATEGY = new InjectionToken('sticky-header-support-strategy'); + +/** @docs-private + * Create a factory for sticky-positioning check to make code more testable + */ +export const STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER: Provider = { + provide: STICKY_HEADER_SUPPORT_STRATEGY, + useFactory: isPositionStickySupported +}; + /** * Directive that marks an element as a sticky-header. Inside of a scrolling container (marked with * cdkScrollable), this header will "stick" to the top of the scrolling viewport while its sticky @@ -61,8 +71,6 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { /** boolean value to mark whether the current header is stuck*/ isStuck: boolean = false; - /** Whether the browser support CSS sticky positioning. */ - private _isPositionStickySupported: boolean = false; /** The element with the 'cdkStickyHeader' tag. */ element: HTMLElement; @@ -97,7 +105,8 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { constructor(element: ElementRef, scrollable: Scrollable, @Optional() public parentRegion: CdkStickyRegion, - platform: Platform) { + platform: Platform, + @Inject(STICKY_HEADER_SUPPORT_STRATEGY) public _isPositionStickySupported) { if (platform.isBrowser) { this.element = element.nativeElement; this.upperScrollableContainer = scrollable.getElementRef().nativeElement; @@ -137,7 +146,6 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * sticky positioning. If not, use the original implementation. */ private _setStrategyAccordingToCompatibility(): void { - this._isPositionStickySupported = isPositionStickySupported(); if (this._isPositionStickySupported) { this.element.style.top = '0'; this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; @@ -258,9 +266,9 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { /** - * '_applyStickyPositionStyles()' function contains the main logic of sticky-header. It decides when - * a header should be stick and when should it be unstuck by comparing the offsetTop - * of scrollable container with the top and bottom of the sticky region. + * '_applyStickyPositionStyles()' function contains the main logic of sticky-header. + * It decides when a header should be stick and when should it be unstuck by comparing + * the offsetTop of scrollable container with the top and bottom of the sticky region. */ _applyStickyPositionStyles(): void { let currentPosition: number = this.upperScrollableContainer.offsetTop;