From ff4c1a470532e9428fc1c6e95889e4e7b984029d Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Fri, 16 Jun 2017 11:12:09 -0700 Subject: [PATCH 01/28] # This is a combination of 9 commits. # This is the 1st commit message: # This is a combination of 11 commits. # This is the 1st commit message: add lib files for sticky-header add chose parent add support to 'optional 'cdkStickyRegion' input ' add app-demo for sticky-header fix bugs and deleted unused tag id in HTML files modify fix some code according to PR review comments change some format to pass TSlint check add '_' before private elements delete @Injectable for StickyHeaderDirective. Because we do not need @Injectable refine code encapsulate 'set style for element' change @Input() Delete 'Observable.fromEvent(this.upperScrollableContainer, 'scroll')' add const STICK_START_CLASS and STICK_END_CLASS Add doc for [cdkStickyRegion] and 'unstuckElement()'. Delete 'detach()' function, add its content into 'ngOnDestroy()'. change 'MdStickyHeaderModule' to 'CdkStickyHeaderModule'; encapsulate reset css style operation for sticky header. delete unnecessary gloable variables delete global variable '_width' Add doc for 'sticker()' function. explained how it works. add more doc for 'sticker()', explaining 'isStuck' flag 2 space for indent # This is the commit message #2: fix # This is the commit message #3: delete sticky-header demo part from this branch # This is the commit message #4: revert firebase file # This is the commit message #5: change code according to comments in PR # This is the commit message #6: revert firbaserc # This is the commit message #7: revert demo-app.ts # This is the commit message #8: revert routes.ts # This is the commit message #9: revert demo-app-module.ts # This is the commit message #10: change # This is the commit message #11: fix the problem of : 'this.stickyParent' might be 'null' # This is the commit message #2: change doc # This is the commit message #3: Change the constructor of 'cdkStickyRegion' to 'constructor(public readonly _elementRef: ElementRef) { }' # This is the commit message #4: Added prefix 'mat-' for CSS class # This is the commit message #5: Delete 'public' before variables # This is the commit message #6: Object.assign isn't supported in IE11; use extendObject from src/lib/core/util. # This is the commit message #7: IE11 will have trouble with `translate3d(0, 0, 0);', change to `translate3d(0px, 0px, 0px);' # This is the commit message #8: Added docs for all variables # This is the commit message #9: extract 'generate CSS style' --- src/demo-app/demo-app-module.ts | 3 + src/lib/sticky-header/sticky-header-dir.ts | 323 +++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 src/lib/sticky-header/sticky-header-dir.ts diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 98b2c3dc8b1e..27be478d37ea 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -80,6 +80,7 @@ import { } from '@angular/material'; import {CdkTableModule} from '@angular/cdk'; import {TableHeaderDemo} from './table/table-header-demo'; +import {StickyHeaderDemo} from './sticky-header/sticky-header-demo'; /** * NgModule that includes all Material modules that are required to serve the demo-app. @@ -119,6 +120,8 @@ import {TableHeaderDemo} from './table/table-header-demo'; MdNativeDateModule, CdkTableModule, StyleModule + CdkDataTableModule, + StyleModule ] }) export class DemoMaterialModule {} diff --git a/src/lib/sticky-header/sticky-header-dir.ts b/src/lib/sticky-header/sticky-header-dir.ts new file mode 100644 index 000000000000..5d3fc3ac53c9 --- /dev/null +++ b/src/lib/sticky-header/sticky-header-dir.ts @@ -0,0 +1,323 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component, Directive, Input, Output, + OnDestroy, AfterViewInit, ElementRef, Injectable, Optional} from '@angular/core'; +import {Scrollable} from '../core/overlay/scroll/scrollable'; +import {extendObject} from '../core/util/object-extend'; + + +/** + * Directive that marks an element as a "sticky region", meant to contain exactly one sticky-header + * along with the content associated with that header. The sticky-header inside of the region will + * "stick" to the top of the scrolling container as long as this region is within the scrolling + * viewport. + * + * If a user does not explicitly define a sticky-region for a sticky-header, the direct + * parent node of the sticky-header will be used as the sticky-region. + */ +@Directive({ + selector: '[cdkStickyRegion]', +}) +export class CdkStickyRegion { + constructor(public readonly _elementRef: ElementRef) { } +} + + +const STICK_START_CLASS = 'mat-stick-start'; +const STICK_END_CLASS = 'mat-stick-end'; +@Directive({ + selector: '[cdkStickyHeader]', +}) +/** + * 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 + * region (see cdkStickyRegion) is in view. + */ +export class CdkStickyHeader implements OnDestroy, AfterViewInit { + + /** + * Set the sticky-header's z-index as 10 in default. Make it as an input + * variable to make user be able to customize the zIndex when + * the sticky-header's zIndex is not the largest in current page. + * Because if the sticky-header's zIndex is not the largest in current page, + * it may be sheltered by other element when being stuck. + */ + @Input('cdkStickyHeaderZIndex') zIndex: number = 10; + @Input('cdkStickyParentRegion') parentRegion: HTMLElement; + @Input('cdkStickyScrollableRegion') scrollableRegion: HTMLElement; + + private _onScrollBind: EventListener = this.onScroll.bind(this); + private _onResizeBind: EventListener = this.onResize.bind(this); + private _onTouchMoveBind: EventListener = this.onTouchMove.bind(this); + isStuck: boolean = false; + /** + * The element with the 'cdkStickyHeader' tag + */ + element: HTMLElement; + /** + * The upper container element with the 'cdkStickyRegion' tag + */ + stickyParent: HTMLElement | null; + /** + * The upper scrollable container + */ + upperScrollableContainer: HTMLElement; + + /** + * The original css of the sticky element, used to reset the sticky element + * when it is being unstuck + */ + originalCss: any; + + /** + * 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. + * It is used with '_scrollFinish' to judge whether the current header + * need to be stuck. + */ + private _containerStart: number; + /** + * It is 'The bottom of CdkStickyRegion of current sticky header - the height + * of current header', which is used with '_containerStart' to judge whether the current header + * need to be stuck. + */ + private _scrollFinish: number; + /** + * The width of the sticky-header when it is stuck. + */ + private _scrollingWidth: number; + + constructor(_element: ElementRef, + scrollable: Scrollable, + @Optional() public parentReg: CdkStickyRegion) { + this.element = _element.nativeElement; + this.upperScrollableContainer = scrollable.getElementRef().nativeElement; + this.scrollableRegion = scrollable.getElementRef().nativeElement; + } + + ngAfterViewInit(): void { + this.stickyParent = this.parentReg != null ? + this.parentReg._elementRef.nativeElement : this.element.parentElement; + this.originalCss = { + zIndex: this.getCssValue(this.element, 'zIndex'), + position: this.getCssValue(this.element, 'position'), + top: this.getCssValue(this.element, 'top'), + right: this.getCssValue(this.element, 'right'), + left: this.getCssValue(this.element, 'left'), + bottom: this.getCssValue(this.element, 'bottom'), + width: this.getCssValue(this.element, 'width'), + }; + this.attach(); + this.defineRestrictionsAndStick(); + } + + ngOnDestroy(): void { + this.upperScrollableContainer.removeEventListener('scroll', this._onScrollBind); + this.upperScrollableContainer.removeEventListener('resize', this._onResizeBind); + this.upperScrollableContainer.removeEventListener('touchmove', this._onTouchMoveBind); + } + + attach() { + this.upperScrollableContainer.addEventListener('scroll', this._onScrollBind, false); + this.upperScrollableContainer.addEventListener('resize', this._onResizeBind, false); + + // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones + this.upperScrollableContainer.addEventListener('touchmove', this._onTouchMoveBind, false); + } + + onScroll(): void { + this.defineRestrictionsAndStick(); + } + + onTouchMove(): void { + this.defineRestrictionsAndStick(); + } + + onResize(): void { + this.defineRestrictionsAndStick(); + // If there's already a header being stick when the page is + // resized. The CSS style of the cdkStickyHeader element may be not fit + // the resized window. So we need to unstuck it then re-stick it. + // unstuck() can set 'isStuck' to FALSE. Then stickElement() can work. + if (this.isStuck) { + this.unstuckElement(); + this.stickElement(); + } + } + + /** + * define the restrictions of the sticky header(including stickyWidth, + * when to start, when to finish) + */ + defineRestrictions(): void { + if(this.stickyParent == null) { + return; + } + let containerTop: any = this.stickyParent.getBoundingClientRect(); + let elemHeight: number = this.element.offsetHeight; + let containerHeight: number = this.getCssNumber(this.stickyParent, 'height'); + this._containerStart = containerTop.top; + + // the padding of the element being sticked + let elementPadding: any = this.getCssValue(this.element, 'padding'); + + let paddingNumber: any = Number(elementPadding.slice(0, -2)); + this._scrollingWidth = this.upperScrollableContainer.clientWidth - + paddingNumber - paddingNumber; + + this._scrollFinish = this._containerStart + (containerHeight - elemHeight); + } + + /** + * Reset element to its original CSS + */ + resetElement(): void { + this.element.classList.remove(STICK_START_CLASS); + extendObject(this.element.style, this.originalCss); + } + + /** + * Stuck element, make the element stick to the top of the scrollable container. + */ + stickElement(): void { + this.isStuck = true; + + this.element.classList.remove(STICK_END_CLASS); + this.element.classList.add(STICK_START_CLASS); + + /** + * Have to add the translate3d function for the sticky element's css style. + * Because iPhone and iPad's browser is using its owning rendering engine. And + * even if you are using Chrome on an iPhone, you are just using Safari with + * a Chrome skin around it. + * + * Safari on iPad and Safari on iPhone do not have resizable windows. + * In Safari on iPhone and iPad, the window size is set to the size of + * the screen (minus Safari user interface controls), and cannot be changed + * by the user. To move around a webpage, the user changes the zoom level and position + * of the viewport as they double tap or pinch to zoom in or out, or by touching + * and dragging to pan the page. As a user changes the zoom level and position of the + * viewport they are doing so within a viewable content area of fixed size + * (that is, the window). This means that webpage elements that have their position + * "fixed" to the viewport can end up outside the viewable content area, offscreen. + * + * So the 'position: fixed' does not work on iPhone and iPad. To make it work, + * 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. + **/ + this.element.style.transform = 'translate3d(0px,0px,0px)'; + + let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right; + + let stickyCss:any = { + zIndex: this.zIndex, + position: 'fixed', + top: this.upperScrollableContainer.offsetTop + 'px', + right: stuckRight + 'px', + left: this.upperScrollableContainer.offsetLeft + 'px', + bottom: 'auto', + width: this._scrollingWidth + 'px', + }; + extendObject(this.element.style, stickyCss); + } + + /** + * Unstuck element: When an element reaches the bottom of its cdkStickyRegion, + * It should be unstuck. And its position will be set as 'relative', its bottom + * will be set as '0'. So it will be stick at the bottom of its cdkStickyRegion and + * will be scrolled up with its cdkStickyRegion element. In this way, the sticky header + * can be changed smoothly when two sticky header meet and the later one need to replace + * the former one. + */ + unstuckElement(): void { + this.isStuck = false; + + if(this.stickyParent == null) { + return; + } + + this.element.classList.add(STICK_END_CLASS); + this.stickyParent.style.position = 'relative'; + let unstuckCss: any = { + position: 'absolute', + top: 'auto', + right: '0', + left: 'auto', + bottom: '0', + width: this.originalCss.width, + }; + extendObject(this.element.style, unstuckCss); + } + + + /** + * 'sticker()' function contains the main logic of sticky-header. It decides when + * a header should be stick and when should it be unstuck. It will first get + * the offsetTop of the upper scrollable container. And then get the Start and End + * of the sticky-header's stickyRegion. + * The header will be stick if 'stickyRegion Start < container offsetTop < stickyRegion End'. + * And when 'stickyRegion End < container offsetTop', the header will be unstuck. It will be + * stick to the bottom of its stickyRegion container and being scrolled up with its stickyRegion + * container. + * When 'stickyRegion Start > container offsetTop', which means the header come back to the + * middle of the scrollable container, the header will be reset to its + * original CSS. + * A flag, isStuck. is used in this function. When a header is stick, isStuck = true. + * And when the 'isStuck' flag is TRUE, the sticky-header will not be repaint, which + * decreases the times on repainting sticky-header. + */ + sticker(): void { + let currentPosition: number = this.upperScrollableContainer.offsetTop; + + // unstuck when the element is scrolled out of the sticky region + if (this.isStuck && + (currentPosition < this._containerStart || currentPosition > this._scrollFinish) || + currentPosition >= this._scrollFinish) { + this.resetElement(); + if (currentPosition >= this._scrollFinish) { + this.unstuckElement(); + } + this.isStuck = false; // stick when the element is within the sticky region + } else if ( this.isStuck === false && + currentPosition > this._containerStart && currentPosition < this._scrollFinish) { + this.stickElement(); + } + } + + defineRestrictionsAndStick(): void { + this.defineRestrictions(); + this.sticker(); + } + + generateCssStyle(zIndex:any, position:any, top:any, right:any, + left:any, bottom:any, width:any): any { + let curCSS = { + zIndex: zIndex, + position: position, + top: top, + right: right, + left: left, + bottom: bottom, + width: width, + }; + return curCSS; +} + + + private getCssValue(element: any, property: string): any { + let result: any = ''; + if (typeof window.getComputedStyle !== 'undefined') { + result = window.getComputedStyle(element, '').getPropertyValue(property); + } else if (typeof element.currentStyle !== 'undefined') { + result = element.currentStyle.property; + } + return result; + } + + private getCssNumber(element: any, property: string): number { + return parseInt(this.getCssValue(element, property), 10) || 0; + } +} From 096b55323a09e7bb28a9777ad6d3642665936ce8 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 26 Jun 2017 17:09:19 -0700 Subject: [PATCH 02/28] # This is a combination of 5 commits. # This is the 1st commit message: add lib files for sticky-header add chose parent add support to 'optional 'cdkStickyRegion' input ' add app-demo for sticky-header fix bugs and deleted unused tag id in HTML files modify fix some code according to PR review comments change some format to pass TSlint check add '_' before private elements delete @Injectable for StickyHeaderDirective. Because we do not need @Injectable refine code encapsulate 'set style for element' change @Input() Delete 'Observable.fromEvent(this.upperScrollableContainer, 'scroll')' add const STICK_START_CLASS and STICK_END_CLASS Add doc for [cdkStickyRegion] and 'unstuckElement()'. Delete 'detach()' function, add its content into 'ngOnDestroy()'. change 'MdStickyHeaderModule' to 'CdkStickyHeaderModule'; encapsulate reset css style operation for sticky header. delete unnecessary gloable variables delete global variable '_width' Add doc for 'sticker()' function. explained how it works. add more doc for 'sticker()', explaining 'isStuck' flag 2 space for indent fix delete sticky-header demo part from this branch revert firebase file change code according to comments in PR revert firbaserc revert demo-app.ts revert routes.ts revert demo-app-module.ts change fix the problem of : 'this.stickyParent' might be 'null' change doc Change the constructor of 'cdkStickyRegion' to 'constructor(public readonly _elementRef: ElementRef) { }' Added prefix 'mat-' for CSS class Delete 'public' before variables Object.assign isn't supported in IE11; use extendObject from src/lib/core/util. IE11 will have trouble with `translate3d(0, 0, 0);', change to `translate3d(0px, 0px, 0px);' Added docs for all variables extract 'generate CSS style' created a generateStyleCSS() function, let it be responsible for generating all those CSS styles. reformat add debounce to solve 'getBoundingClientRect() cause slow down' problem. add position:sticky and check whether browser support it. If not , use the naive implementation removed unused import Removed unused 'scrollableRegion' and 'parentRegion' removed commented lines default public Add comments about why setting style top and position for iPhone and not IE. And extract detectBrowser() as a new function format consider all circumstances of browser. use "===" instead of '==' make 'navigator.userAgent.toLocaleLowerCase()' a local variable optimize Added comments on const 'STICK_START_CLASS' and 'STICK_END_CLASS'. change their content to cdk-sticky-header-start and cdk-sticky-header-end Added comments for STICK_START_CLASS and STICK_END_CLASS. Changed the format of one-line JsDoc unsubscribe sbscriptions onDestory Use what modernizr does on compatibility instead of get the browser version directly. add 'padding' and 'stickyRegionHeight' variables to avoid calling 'getComputedStyle()' too many times (which is expensive). move docs above @Directive removed the underscore in'_element: ElementRef', expand 'reg' to 'region' use 'if (this.isIE)' instead of 'if(this.isIE === true)' added more newlines between params in 'generateCssStyle()' function to make it easier to understand. Added reference link to Modernizer in docs of getSupportList() Deleted "_supportList" variable renamed 'isIE' to 'isStickyPositionSupported', and removed extra space before Observable Set debounce time as a const variable Added docs for 'const DEBOUNCE_TIME: number = 5;' Changed ' if(this.stickyParent == null)' to ' if(!this.stickyParent)' Removed the @param and @returns and make sure the types are correct in the function signature in 'generateCssStyle(...)' function Added docs for `isStickyPositionSupported` variable changed '+=' to '=' of 'stickyText' in getSupportList() function nit added " " between 'if' and '(' nit Added comments deleted unused import change comments optimize comments deleted unnecessary global variables(padding and stickyRegionHeight) Added check whether we are on browser Array to string[] test? try to reopen the old PR fix after rebase revert list.ts test test 222 revert demo revert list.ts second time Move code to 'src/cdk' revert 'move code to 'src/cdk'' , it should be done in a new PR revert avoid calling 'getComputedStyle()' too many times. rename as sticky-header.ts # This is the commit message #2: imported PlatformModule # This is the commit message #3: Add blank lines between these top-level symbol # This is the commit message #4: make '_isStickyPositionSupported' private # This is the commit message #5: Changed the originalCSS to private and use '{} as CSSStyleDeclaration' instead of ''any. --- src/demo-app/demo-app-module.ts | 3 - src/lib/sticky-header/sticky-header-dir.ts | 323 ------------------ src/lib/sticky-header/sticky-header.ts | 358 +++++++++++++------- src/lib/sticky-header/sticky-header.ts~HEAD | 289 ++++++++++++++++ 4 files changed, 524 insertions(+), 449 deletions(-) delete mode 100644 src/lib/sticky-header/sticky-header-dir.ts create mode 100644 src/lib/sticky-header/sticky-header.ts~HEAD diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 27be478d37ea..98b2c3dc8b1e 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -80,7 +80,6 @@ import { } from '@angular/material'; import {CdkTableModule} from '@angular/cdk'; import {TableHeaderDemo} from './table/table-header-demo'; -import {StickyHeaderDemo} from './sticky-header/sticky-header-demo'; /** * NgModule that includes all Material modules that are required to serve the demo-app. @@ -120,8 +119,6 @@ import {StickyHeaderDemo} from './sticky-header/sticky-header-demo'; MdNativeDateModule, CdkTableModule, StyleModule - CdkDataTableModule, - StyleModule ] }) export class DemoMaterialModule {} diff --git a/src/lib/sticky-header/sticky-header-dir.ts b/src/lib/sticky-header/sticky-header-dir.ts deleted file mode 100644 index 5d3fc3ac53c9..000000000000 --- a/src/lib/sticky-header/sticky-header-dir.ts +++ /dev/null @@ -1,323 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import {Component, Directive, Input, Output, - OnDestroy, AfterViewInit, ElementRef, Injectable, Optional} from '@angular/core'; -import {Scrollable} from '../core/overlay/scroll/scrollable'; -import {extendObject} from '../core/util/object-extend'; - - -/** - * Directive that marks an element as a "sticky region", meant to contain exactly one sticky-header - * along with the content associated with that header. The sticky-header inside of the region will - * "stick" to the top of the scrolling container as long as this region is within the scrolling - * viewport. - * - * If a user does not explicitly define a sticky-region for a sticky-header, the direct - * parent node of the sticky-header will be used as the sticky-region. - */ -@Directive({ - selector: '[cdkStickyRegion]', -}) -export class CdkStickyRegion { - constructor(public readonly _elementRef: ElementRef) { } -} - - -const STICK_START_CLASS = 'mat-stick-start'; -const STICK_END_CLASS = 'mat-stick-end'; -@Directive({ - selector: '[cdkStickyHeader]', -}) -/** - * 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 - * region (see cdkStickyRegion) is in view. - */ -export class CdkStickyHeader implements OnDestroy, AfterViewInit { - - /** - * Set the sticky-header's z-index as 10 in default. Make it as an input - * variable to make user be able to customize the zIndex when - * the sticky-header's zIndex is not the largest in current page. - * Because if the sticky-header's zIndex is not the largest in current page, - * it may be sheltered by other element when being stuck. - */ - @Input('cdkStickyHeaderZIndex') zIndex: number = 10; - @Input('cdkStickyParentRegion') parentRegion: HTMLElement; - @Input('cdkStickyScrollableRegion') scrollableRegion: HTMLElement; - - private _onScrollBind: EventListener = this.onScroll.bind(this); - private _onResizeBind: EventListener = this.onResize.bind(this); - private _onTouchMoveBind: EventListener = this.onTouchMove.bind(this); - isStuck: boolean = false; - /** - * The element with the 'cdkStickyHeader' tag - */ - element: HTMLElement; - /** - * The upper container element with the 'cdkStickyRegion' tag - */ - stickyParent: HTMLElement | null; - /** - * The upper scrollable container - */ - upperScrollableContainer: HTMLElement; - - /** - * The original css of the sticky element, used to reset the sticky element - * when it is being unstuck - */ - originalCss: any; - - /** - * 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. - * It is used with '_scrollFinish' to judge whether the current header - * need to be stuck. - */ - private _containerStart: number; - /** - * It is 'The bottom of CdkStickyRegion of current sticky header - the height - * of current header', which is used with '_containerStart' to judge whether the current header - * need to be stuck. - */ - private _scrollFinish: number; - /** - * The width of the sticky-header when it is stuck. - */ - private _scrollingWidth: number; - - constructor(_element: ElementRef, - scrollable: Scrollable, - @Optional() public parentReg: CdkStickyRegion) { - this.element = _element.nativeElement; - this.upperScrollableContainer = scrollable.getElementRef().nativeElement; - this.scrollableRegion = scrollable.getElementRef().nativeElement; - } - - ngAfterViewInit(): void { - this.stickyParent = this.parentReg != null ? - this.parentReg._elementRef.nativeElement : this.element.parentElement; - this.originalCss = { - zIndex: this.getCssValue(this.element, 'zIndex'), - position: this.getCssValue(this.element, 'position'), - top: this.getCssValue(this.element, 'top'), - right: this.getCssValue(this.element, 'right'), - left: this.getCssValue(this.element, 'left'), - bottom: this.getCssValue(this.element, 'bottom'), - width: this.getCssValue(this.element, 'width'), - }; - this.attach(); - this.defineRestrictionsAndStick(); - } - - ngOnDestroy(): void { - this.upperScrollableContainer.removeEventListener('scroll', this._onScrollBind); - this.upperScrollableContainer.removeEventListener('resize', this._onResizeBind); - this.upperScrollableContainer.removeEventListener('touchmove', this._onTouchMoveBind); - } - - attach() { - this.upperScrollableContainer.addEventListener('scroll', this._onScrollBind, false); - this.upperScrollableContainer.addEventListener('resize', this._onResizeBind, false); - - // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones - this.upperScrollableContainer.addEventListener('touchmove', this._onTouchMoveBind, false); - } - - onScroll(): void { - this.defineRestrictionsAndStick(); - } - - onTouchMove(): void { - this.defineRestrictionsAndStick(); - } - - onResize(): void { - this.defineRestrictionsAndStick(); - // If there's already a header being stick when the page is - // resized. The CSS style of the cdkStickyHeader element may be not fit - // the resized window. So we need to unstuck it then re-stick it. - // unstuck() can set 'isStuck' to FALSE. Then stickElement() can work. - if (this.isStuck) { - this.unstuckElement(); - this.stickElement(); - } - } - - /** - * define the restrictions of the sticky header(including stickyWidth, - * when to start, when to finish) - */ - defineRestrictions(): void { - if(this.stickyParent == null) { - return; - } - let containerTop: any = this.stickyParent.getBoundingClientRect(); - let elemHeight: number = this.element.offsetHeight; - let containerHeight: number = this.getCssNumber(this.stickyParent, 'height'); - this._containerStart = containerTop.top; - - // the padding of the element being sticked - let elementPadding: any = this.getCssValue(this.element, 'padding'); - - let paddingNumber: any = Number(elementPadding.slice(0, -2)); - this._scrollingWidth = this.upperScrollableContainer.clientWidth - - paddingNumber - paddingNumber; - - this._scrollFinish = this._containerStart + (containerHeight - elemHeight); - } - - /** - * Reset element to its original CSS - */ - resetElement(): void { - this.element.classList.remove(STICK_START_CLASS); - extendObject(this.element.style, this.originalCss); - } - - /** - * Stuck element, make the element stick to the top of the scrollable container. - */ - stickElement(): void { - this.isStuck = true; - - this.element.classList.remove(STICK_END_CLASS); - this.element.classList.add(STICK_START_CLASS); - - /** - * Have to add the translate3d function for the sticky element's css style. - * Because iPhone and iPad's browser is using its owning rendering engine. And - * even if you are using Chrome on an iPhone, you are just using Safari with - * a Chrome skin around it. - * - * Safari on iPad and Safari on iPhone do not have resizable windows. - * In Safari on iPhone and iPad, the window size is set to the size of - * the screen (minus Safari user interface controls), and cannot be changed - * by the user. To move around a webpage, the user changes the zoom level and position - * of the viewport as they double tap or pinch to zoom in or out, or by touching - * and dragging to pan the page. As a user changes the zoom level and position of the - * viewport they are doing so within a viewable content area of fixed size - * (that is, the window). This means that webpage elements that have their position - * "fixed" to the viewport can end up outside the viewable content area, offscreen. - * - * So the 'position: fixed' does not work on iPhone and iPad. To make it work, - * 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. - **/ - this.element.style.transform = 'translate3d(0px,0px,0px)'; - - let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right; - - let stickyCss:any = { - zIndex: this.zIndex, - position: 'fixed', - top: this.upperScrollableContainer.offsetTop + 'px', - right: stuckRight + 'px', - left: this.upperScrollableContainer.offsetLeft + 'px', - bottom: 'auto', - width: this._scrollingWidth + 'px', - }; - extendObject(this.element.style, stickyCss); - } - - /** - * Unstuck element: When an element reaches the bottom of its cdkStickyRegion, - * It should be unstuck. And its position will be set as 'relative', its bottom - * will be set as '0'. So it will be stick at the bottom of its cdkStickyRegion and - * will be scrolled up with its cdkStickyRegion element. In this way, the sticky header - * can be changed smoothly when two sticky header meet and the later one need to replace - * the former one. - */ - unstuckElement(): void { - this.isStuck = false; - - if(this.stickyParent == null) { - return; - } - - this.element.classList.add(STICK_END_CLASS); - this.stickyParent.style.position = 'relative'; - let unstuckCss: any = { - position: 'absolute', - top: 'auto', - right: '0', - left: 'auto', - bottom: '0', - width: this.originalCss.width, - }; - extendObject(this.element.style, unstuckCss); - } - - - /** - * 'sticker()' function contains the main logic of sticky-header. It decides when - * a header should be stick and when should it be unstuck. It will first get - * the offsetTop of the upper scrollable container. And then get the Start and End - * of the sticky-header's stickyRegion. - * The header will be stick if 'stickyRegion Start < container offsetTop < stickyRegion End'. - * And when 'stickyRegion End < container offsetTop', the header will be unstuck. It will be - * stick to the bottom of its stickyRegion container and being scrolled up with its stickyRegion - * container. - * When 'stickyRegion Start > container offsetTop', which means the header come back to the - * middle of the scrollable container, the header will be reset to its - * original CSS. - * A flag, isStuck. is used in this function. When a header is stick, isStuck = true. - * And when the 'isStuck' flag is TRUE, the sticky-header will not be repaint, which - * decreases the times on repainting sticky-header. - */ - sticker(): void { - let currentPosition: number = this.upperScrollableContainer.offsetTop; - - // unstuck when the element is scrolled out of the sticky region - if (this.isStuck && - (currentPosition < this._containerStart || currentPosition > this._scrollFinish) || - currentPosition >= this._scrollFinish) { - this.resetElement(); - if (currentPosition >= this._scrollFinish) { - this.unstuckElement(); - } - this.isStuck = false; // stick when the element is within the sticky region - } else if ( this.isStuck === false && - currentPosition > this._containerStart && currentPosition < this._scrollFinish) { - this.stickElement(); - } - } - - defineRestrictionsAndStick(): void { - this.defineRestrictions(); - this.sticker(); - } - - generateCssStyle(zIndex:any, position:any, top:any, right:any, - left:any, bottom:any, width:any): any { - let curCSS = { - zIndex: zIndex, - position: position, - top: top, - right: right, - left: left, - bottom: bottom, - width: width, - }; - return curCSS; -} - - - private getCssValue(element: any, property: string): any { - let result: any = ''; - if (typeof window.getComputedStyle !== 'undefined') { - result = window.getComputedStyle(element, '').getPropertyValue(property); - } else if (typeof element.currentStyle !== 'undefined') { - result = element.currentStyle.property; - } - return result; - } - - private getCssNumber(element: any, property: string): number { - return parseInt(this.getCssValue(element, property), 10) || 0; - } -} diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 1c520ed7c8df..cbeb63787379 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -10,10 +10,10 @@ import {Directive, Input, import {Platform} from '../core/platform'; import {Scrollable} from '../core/overlay/scroll/scrollable'; import {extendObject} from '../core/util/object-extend'; +import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; -import {fromEvent} from 'rxjs/observable/fromEvent'; -import {RxChain, debounceTime} from '../core/rxjs/index'; -import {isPositionStickySupported} from '@angular/cdk'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/debounceTime'; /** @@ -40,9 +40,9 @@ const STICK_START_CLASS = 'cdk-sticky-header-start'; const STICK_END_CLASS = 'cdk-sticky-header-end'; /** - * Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, - * touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the - * positioning adversely. + * Set a debounce time which is used in debounce() function when adding event listeners. + * Set is as 5. Because if the DEBOUNCE_TIME is set as a too large number. The sticky effect + * during scroll will become vary strange and can not be scrolled smoothly. */ const DEBOUNCE_TIME: number = 5; @@ -56,13 +56,19 @@ const DEBOUNCE_TIME: number = 5; }) export class CdkStickyHeader implements OnDestroy, AfterViewInit { - /** z-index to be applied to the sticky header (default is 10). */ + /** + * Set the sticky-header's z-index as 10 in default. Make it as an input + * variable to make user be able to customize the zIndex when + * the sticky-header's zIndex is not the largest in current page. + * Because if the sticky-header's zIndex is not the largest in current page, + * it may be sheltered by other element when being stuck. + */ @Input('cdkStickyHeaderZIndex') zIndex: number = 10; - /** boolean value to mark whether the current header is stuck*/ isStuck: boolean = false; /** Whether the browser support CSS sticky positioning. */ - private _isPositionStickySupported: boolean = false; + private _isStickyPositionSupported: boolean = true; + /** The element with the 'cdkStickyHeader' tag. */ element: HTMLElement; @@ -77,16 +83,14 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { private _originalStyles = {} as CSSStyleDeclaration; /** * 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. - * It is used with '_stickyRegionBottomThreshold' to judge whether the current header + * It is used with '_scrollFinish' to judge whether the current header * need to be stuck. */ - private _stickyRegionTop: number; + private _containerStart: number; /** - * Bottom of the sticky region offset by the height of the sticky header. - * Once the sticky header is scrolled to this position it will stay in place - * so that it will scroll naturally out of view with the rest of the sticky region. + * `_scrollFinish` is the place from where the stuck element should be unstuck */ - private _stickyRegionBottomThreshold: number; + private _scrollFinish: number; private _onScrollSubscription: Subscription; @@ -101,189 +105,297 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { if (platform.isBrowser) { this.element = element.nativeElement; this.upperScrollableContainer = scrollable.getElementRef().nativeElement; - this._setStrategyAccordingToCompatibility(); + this.setStrategyAccordingToCompatibility(); } } ngAfterViewInit(): void { - if (!this._isPositionStickySupported) { - + if (!this._isStickyPositionSupported) { this.stickyParent = this.parentRegion != null ? this.parentRegion._elementRef.nativeElement : this.element.parentElement; - - let headerStyles = window.getComputedStyle(this.element, ''); - this._originalStyles = { - position: headerStyles.position, - top: headerStyles.top, - right: headerStyles.right, - left: headerStyles.left, - bottom: headerStyles.bottom, - width: headerStyles.width, - zIndex: headerStyles.zIndex - } as CSSStyleDeclaration; - - this._attachEventListeners(); - this._updateStickyPositioning(); + let values = window.getComputedStyle(this.element, ''); + this._originalStyles = this.generateCssStyle( + values.getPropertyValue('position'), + values.getPropertyValue('top'), + values.getPropertyValue('right'), + values.getPropertyValue('left'), + values.getPropertyValue('bottom'), + values.getPropertyValue('width'), + values.getPropertyValue('zIndex')); + this.attach(); + this.defineRestrictionsAndStick(); } } ngOnDestroy(): void { - [this._onScrollSubscription, this._onScrollSubscription, this._onResizeSubscription] - .forEach(s => s && s.unsubscribe()); + if (this._onScrollSubscription) { + this._onScrollSubscription.unsubscribe(); + } + + if (this._onResizeSubscription) { + this._onResizeSubscription.unsubscribe(); + } + + if (this._onTouchSubscription) { + this._onTouchSubscription.unsubscribe(); + } } /** - * Check if current browser supports sticky positioning. If yes, apply - * sticky positioning. If not, use the original implementation. + * getSupportList() is used to get a list of string which can be set to + * sticky-header's style.position and make Sticky positioning work. + * It returns a list of string. + * + * According to the "Position:sticky Browser compatibility" in + * "https://developer.mozilla.org/en-US/docs/Web/CSS/position". + * + * For Desktop: Sticky positioning works well on Chrome, Edge, Firefox and Opera. And can + * also work well on Safari with a "-webkit-" prefix. It only does not work on IE. + * + * For Mobile: Sticky positioning works well on Android Webview, Chrome for Android, Edge, + * Firefox Mobile, Opera Mobile. And can also work well on Safari Mobile with a "-webkit-" prefix. + * It won't always work on IE phone. + * + * The implementation references the compatibility checking in Modernizer + * (https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/positionsticky.js). + */ + getSupportList(): string[] { + let prefixTestList = ['', '-webkit-', '-ms-', '-moz-', '-o-']; + let supportList: Array = new Array(); + let stickyText = ''; + for (let i = 0; i < prefixTestList.length; i++ ) { + stickyText = 'position:' + prefixTestList[i] + 'sticky;'; + // Create a DOM to check if the browser support current prefix for sticky-position. + let div = document.createElement('div'); + let body = document.body; + div.style.cssText = 'display:none;' + stickyText; + body.appendChild(div); + let isSupport = /sticky/i.test(this.getCssValue(div, 'position')); + body.removeChild(div); + if (isSupport == true) { + supportList.push(prefixTestList[i]); + } + } + return supportList; + } + + /** + * Get the first element from this._supportList. Set it as a prefix of + * sticky positioning. + * + * If the this._supportList is empty, which means the browser does not support + * sticky positioning. Set isStickyPositionSupported as 'true' and use the original + * implementation of sticky-header. */ - private _setStrategyAccordingToCompatibility(): void { - this._isPositionStickySupported = isPositionStickySupported(); - if (this._isPositionStickySupported) { - this.element.style.top = '0'; - this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; - // TODO(sllethe): add css class with both 'sticky' and '-webkit-sticky' on position - // when @Directory supports adding CSS class + setStrategyAccordingToCompatibility(): void { + let supportList = this.getSupportList(); + if (supportList.length === 0) { + this._isStickyPositionSupported = false; + } else { + // Only need supportList[0], Because supportList contains all the prefix + // that can make sticky positioning work in the current browser. + // We only need to get one prefix and make position: prefix + 'sticky', + // then sticky position will work. + let prefix: string = supportList[0]; + + this.element.style.top = '0px'; + this.element.style.position = prefix + 'sticky'; } } - /** Add listeners for events that affect sticky positioning. */ - private _attachEventListeners() { - this._onScrollSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'scroll')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); + attach() { + this._onScrollSubscription = Observable.fromEvent(this.upperScrollableContainer, 'scroll') + .debounceTime(DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones - this._onTouchSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'touchmove')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); + this._onTouchSubscription = Observable.fromEvent(this.upperScrollableContainer, 'touchmove') + .debounceTime(DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); - this._onResizeSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'resize')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.onResize()); + this._onResizeSubscription = Observable.fromEvent(this.upperScrollableContainer, 'resize') + .debounceTime(DEBOUNCE_TIME).subscribe(() => this.onResize()); + } + + onScroll(): void { + this.defineRestrictionsAndStick(); + } + + onTouchMove(): void { + this.defineRestrictionsAndStick(); } onResize(): void { - this._updateStickyPositioning(); - // If there's already a header being stick when the page is - // resized. The CSS style of the cdkStickyHeader element may be not fit - // the resized window. So we need to unstuck it then re-stick it. - // unstuck() can set 'isStuck' to FALSE. Then _stickElement() can work. + this.defineRestrictionsAndStick(); + /** + * If there's already a header being stick when the page is + * resized. The CSS style of the cdkStickyHeader element may be not fit + * the resized window. So we need to unstuck it then re-stick it. + * unstuck() can set 'isStuck' to FALSE. Then stickElement() can work. + */ if (this.isStuck) { - this._unstickElement(); - this._stickElement(); + this.unstuckElement(); + this.stickElement(); } } - /** Measures the boundaries of the sticky regions to be used in subsequent positioning. */ - private _measureStickyRegionBounds(): void { + /** + * define the restrictions of the sticky header(including stickyWidth, + * when to start, when to finish) + */ + defineRestrictions(): void { if (!this.stickyParent) { return; } - const boundingClientRect: any = this.stickyParent.getBoundingClientRect(); - this._stickyRegionTop = boundingClientRect.top; + let boundingClientRect: any = this.stickyParent.getBoundingClientRect(); + let elemHeight: number = this.element.offsetHeight; + this._containerStart = boundingClientRect.top; let stickRegionHeight = boundingClientRect.height; - this._stickyRegionBottomThreshold = this._stickyRegionTop + - (stickRegionHeight - this.element.offsetHeight); + this._scrollFinish = this._containerStart + (stickRegionHeight - elemHeight); } /** Reset element to its original CSS. */ - private _resetElementStyles(): void { + resetElement(): void { this.element.classList.remove(STICK_START_CLASS); extendObject(this.element.style, this._originalStyles); } /** Stuck element, make the element stick to the top of the scrollable container. */ - private _stickElement(): void { + stickElement(): void { this.isStuck = true; this.element.classList.remove(STICK_END_CLASS); this.element.classList.add(STICK_START_CLASS); - // Have to add the translate3d function for the sticky element's css style. - // Because iPhone and iPad's browser is using its owning rendering engine. And - // even if you are using Chrome on an iPhone, you are just using Safari with - // a Chrome skin around it. - // - // Safari on iPad and Safari on iPhone do not have resizable windows. - // In Safari on iPhone and iPad, the window size is set to the size of - // the screen (minus Safari user interface controls), and cannot be changed - // by the user. To move around a webpage, the user changes the zoom level and position - // of the viewport as they double tap or pinch to zoom in or out, or by touching - // and dragging to pan the page. As a user changes the zoom level and position of the - // viewport they are doing so within a viewable content area of fixed size - // (that is, the window). This means that webpage elements that have their position - // "fixed" to the viewport can end up outside the viewable content area, offscreen. - // - // So the 'position: fixed' does not work on iPhone and iPad. To make it work, - // 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. + /** + * Have to add the translate3d function for the sticky element's css style. + * Because iPhone and iPad's browser is using its owning rendering engine. And + * even if you are using Chrome on an iPhone, you are just using Safari with + * a Chrome skin around it. + * + * Safari on iPad and Safari on iPhone do not have resizable windows. + * In Safari on iPhone and iPad, the window size is set to the size of + * the screen (minus Safari user interface controls), and cannot be changed + * by the user. To move around a webpage, the user changes the zoom level and position + * of the viewport as they double tap or pinch to zoom in or out, or by touching + * and dragging to pan the page. As a user changes the zoom level and position of the + * viewport they are doing so within a viewable content area of fixed size + * (that is, the window). This means that webpage elements that have their position + * "fixed" to the viewport can end up outside the viewable content area, offscreen. + * + * So the 'position: fixed' does not work on iPhone and iPad. To make it work, + * 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. + **/ this.element.style.transform = 'translate3d(0px,0px,0px)'; - let stuckRight: number = this.upperScrollableContainer.getBoundingClientRect().right; + let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right; - let stickyCss = { - position: 'fixed', - top: this.upperScrollableContainer.offsetTop + 'px', - right: stuckRight + 'px', - left: this.upperScrollableContainer.offsetLeft + 'px', - bottom: 'auto', - width: this._originalStyles.width, - zIndex: this.zIndex + '' - }; + let stickyCss:any = this.generateCssStyle( + 'fixed', + this.upperScrollableContainer.offsetTop + 'px', + stuckRight + 'px', + this.upperScrollableContainer.offsetLeft + 'px', + 'auto', + this._originalStyles.width, + this.zIndex + '',); extendObject(this.element.style, stickyCss); } /** - * Unsticks the header so that it goes back to scrolling normally. - * - * This should be called when the element reaches the bottom of its cdkStickyRegion so that it - * smoothly scrolls out of view as the next sticky-header moves in. + * Unstuck element: When an element reaches the bottom of its cdkStickyRegion, + * It should be unstuck. And its position will be set as 'relative', its bottom + * will be set as '0'. So it will be stick at the bottom of its cdkStickyRegion and + * will be scrolled up with its cdkStickyRegion element. In this way, the sticky header + * can be changed smoothly when two sticky header meet and the later one need to replace + * the former one. */ - private _unstickElement(): void { + unstuckElement(): void { this.isStuck = false; - if (!this.stickyParent) { + if (this.stickyParent == null) { return; } this.element.classList.add(STICK_END_CLASS); this.stickyParent.style.position = 'relative'; - let unstuckCss = { - position: 'absolute', - top: 'auto', - right: '0', - left: 'auto', - bottom: '0', - width: this._originalStyles.width - }; + let unstuckCss: any = this.generateCssStyle( + 'absolute', + 'auto', + '0', + 'auto', + '0', + this._originalStyles.width); extendObject(this.element.style, unstuckCss); } /** - * '_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. + * 'sticker()' function contains the main logic of sticky-header. It decides when + * a header should be stick and when should it be unstuck. It will first get + * the offsetTop of the upper scrollable container. And then get the Start and End + * of the sticky-header's stickyRegion. + * The header will be stick if 'stickyRegion Start < container offsetTop < stickyRegion End'. + * And when 'stickyRegion End < container offsetTop', the header will be unstuck. It will be + * stick to the bottom of its stickyRegion container and being scrolled up with its stickyRegion + * container. + * When 'stickyRegion Start > container offsetTop', which means the header come back to the + * middle of the scrollable container, the header will be reset to its + * original CSS. + * A flag, isStuck. is used in this function. When a header is stick, isStuck = true. + * And when the 'isStuck' flag is TRUE, the sticky-header will not be repaint, which + * decreases the times on repainting sticky-header. */ - _applyStickyPositionStyles(): void { + sticker(): void { let currentPosition: number = this.upperScrollableContainer.offsetTop; // unstuck when the element is scrolled out of the sticky region if (this.isStuck && - (currentPosition < this._stickyRegionTop || - currentPosition > this._stickyRegionBottomThreshold) || - currentPosition >= this._stickyRegionBottomThreshold) { - this._resetElementStyles(); - if (currentPosition >= this._stickyRegionBottomThreshold) { - this._unstickElement(); + (currentPosition < this._containerStart || currentPosition > this._scrollFinish) || + currentPosition >= this._scrollFinish) { + this.resetElement(); + if (currentPosition >= this._scrollFinish) { + this.unstuckElement(); } this.isStuck = false; // stick when the element is within the sticky region } else if ( this.isStuck === false && - currentPosition > this._stickyRegionTop && - currentPosition < this._stickyRegionBottomThreshold) { - this._stickElement(); + currentPosition > this._containerStart && currentPosition < this._scrollFinish) { + this.stickElement(); + } + } + + defineRestrictionsAndStick(): void { + this.defineRestrictions(); + this.sticker(); + } + + /** + * This function is used to generate a variable which contains 7 css styles. + */ + generateCssStyle(position:string, top:string, right:string, + left:string, bottom:string, width:string | null, zIndex?:string, ): any { + let targetCSS = { + position: position, + top: top, + right: right, + left: left, + bottom: bottom, + width: width, + zIndex: zIndex + }; + return targetCSS; +} + + + private getCssValue(element: any, property: string): any { + let result: any = ''; + if (typeof window.getComputedStyle !== 'undefined') { + result = window.getComputedStyle(element, '').getPropertyValue(property); + } else if (typeof element.currentStyle !== 'undefined') { + result = element.currentStyle.property; } + return result; } - _updateStickyPositioning(): void { - this._measureStickyRegionBounds(); - this._applyStickyPositionStyles(); + private getCssNumber(element: any, property: string): number { + return parseInt(this.getCssValue(element, property), 10) || 0; } } diff --git a/src/lib/sticky-header/sticky-header.ts~HEAD b/src/lib/sticky-header/sticky-header.ts~HEAD new file mode 100644 index 000000000000..1c520ed7c8df --- /dev/null +++ b/src/lib/sticky-header/sticky-header.ts~HEAD @@ -0,0 +1,289 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * 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'; +import {Scrollable} from '../core/overlay/scroll/scrollable'; +import {extendObject} from '../core/util/object-extend'; +import {Subscription} from 'rxjs/Subscription'; +import {fromEvent} from 'rxjs/observable/fromEvent'; +import {RxChain, debounceTime} from '../core/rxjs/index'; +import {isPositionStickySupported} from '@angular/cdk'; + + +/** + * Directive that marks an element as a "sticky region", meant to contain exactly one sticky-header + * along with the content associated with that header. The sticky-header inside of the region will + * "stick" to the top of the scrolling container as long as this region is within the scrolling + * viewport. + * + * If a user does not explicitly define a sticky-region for a sticky-header, the direct + * parent node of the sticky-header will be used as the sticky-region. + */ +@Directive({ + selector: '[cdkStickyRegion]', +}) +export class CdkStickyRegion { + constructor(public readonly _elementRef: ElementRef) { } +} + + +/** Class applied when the header is "stuck" */ +const STICK_START_CLASS = 'cdk-sticky-header-start'; + +/** Class applied when the header is not "stuck" */ +const STICK_END_CLASS = 'cdk-sticky-header-end'; + +/** + * Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, + * touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the + * positioning adversely. + */ +const DEBOUNCE_TIME: number = 5; + +/** + * 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 + * region (see cdkStickyRegion) is in view. + */ +@Directive({ + selector: '[cdkStickyHeader]', +}) +export class CdkStickyHeader implements OnDestroy, AfterViewInit { + + /** z-index to be applied to the sticky header (default is 10). */ + @Input('cdkStickyHeaderZIndex') zIndex: number = 10; + + /** 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; + /** The upper container element with the 'cdkStickyRegion' tag. */ + stickyParent: HTMLElement | null; + /** The upper scrollable container. */ + upperScrollableContainer: HTMLElement; + /** + * The original css of the sticky element, used to reset the sticky element + * when it is being unstuck + */ + private _originalStyles = {} as CSSStyleDeclaration; + /** + * 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. + * It is used with '_stickyRegionBottomThreshold' to judge whether the current header + * need to be stuck. + */ + private _stickyRegionTop: number; + /** + * Bottom of the sticky region offset by the height of the sticky header. + * Once the sticky header is scrolled to this position it will stay in place + * so that it will scroll naturally out of view with the rest of the sticky region. + */ + private _stickyRegionBottomThreshold: number; + + private _onScrollSubscription: Subscription; + + private _onTouchSubscription: Subscription; + + private _onResizeSubscription: Subscription; + + constructor(element: ElementRef, + scrollable: Scrollable, + @Optional() public parentRegion: CdkStickyRegion, + platform: Platform) { + if (platform.isBrowser) { + this.element = element.nativeElement; + this.upperScrollableContainer = scrollable.getElementRef().nativeElement; + this._setStrategyAccordingToCompatibility(); + } + } + + ngAfterViewInit(): void { + if (!this._isPositionStickySupported) { + + this.stickyParent = this.parentRegion != null ? + this.parentRegion._elementRef.nativeElement : this.element.parentElement; + + let headerStyles = window.getComputedStyle(this.element, ''); + this._originalStyles = { + position: headerStyles.position, + top: headerStyles.top, + right: headerStyles.right, + left: headerStyles.left, + bottom: headerStyles.bottom, + width: headerStyles.width, + zIndex: headerStyles.zIndex + } as CSSStyleDeclaration; + + this._attachEventListeners(); + this._updateStickyPositioning(); + } + } + + ngOnDestroy(): void { + [this._onScrollSubscription, this._onScrollSubscription, this._onResizeSubscription] + .forEach(s => s && s.unsubscribe()); + } + + /** + * Check if current browser supports sticky positioning. If yes, apply + * 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; '; + // TODO(sllethe): add css class with both 'sticky' and '-webkit-sticky' on position + // when @Directory supports adding CSS class + } + } + + /** Add listeners for events that affect sticky positioning. */ + private _attachEventListeners() { + this._onScrollSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'scroll')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); + + // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones + this._onTouchSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'touchmove')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); + + this._onResizeSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'resize')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.onResize()); + } + + onResize(): void { + this._updateStickyPositioning(); + // If there's already a header being stick when the page is + // resized. The CSS style of the cdkStickyHeader element may be not fit + // the resized window. So we need to unstuck it then re-stick it. + // unstuck() can set 'isStuck' to FALSE. Then _stickElement() can work. + if (this.isStuck) { + this._unstickElement(); + this._stickElement(); + } + } + + /** Measures the boundaries of the sticky regions to be used in subsequent positioning. */ + private _measureStickyRegionBounds(): void { + if (!this.stickyParent) { + return; + } + const boundingClientRect: any = this.stickyParent.getBoundingClientRect(); + this._stickyRegionTop = boundingClientRect.top; + let stickRegionHeight = boundingClientRect.height; + + this._stickyRegionBottomThreshold = this._stickyRegionTop + + (stickRegionHeight - this.element.offsetHeight); + } + + /** Reset element to its original CSS. */ + private _resetElementStyles(): void { + this.element.classList.remove(STICK_START_CLASS); + extendObject(this.element.style, this._originalStyles); + } + + /** Stuck element, make the element stick to the top of the scrollable container. */ + private _stickElement(): void { + this.isStuck = true; + + this.element.classList.remove(STICK_END_CLASS); + this.element.classList.add(STICK_START_CLASS); + + // Have to add the translate3d function for the sticky element's css style. + // Because iPhone and iPad's browser is using its owning rendering engine. And + // even if you are using Chrome on an iPhone, you are just using Safari with + // a Chrome skin around it. + // + // Safari on iPad and Safari on iPhone do not have resizable windows. + // In Safari on iPhone and iPad, the window size is set to the size of + // the screen (minus Safari user interface controls), and cannot be changed + // by the user. To move around a webpage, the user changes the zoom level and position + // of the viewport as they double tap or pinch to zoom in or out, or by touching + // and dragging to pan the page. As a user changes the zoom level and position of the + // viewport they are doing so within a viewable content area of fixed size + // (that is, the window). This means that webpage elements that have their position + // "fixed" to the viewport can end up outside the viewable content area, offscreen. + // + // So the 'position: fixed' does not work on iPhone and iPad. To make it work, + // 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. + this.element.style.transform = 'translate3d(0px,0px,0px)'; + + let stuckRight: number = this.upperScrollableContainer.getBoundingClientRect().right; + + let stickyCss = { + position: 'fixed', + top: this.upperScrollableContainer.offsetTop + 'px', + right: stuckRight + 'px', + left: this.upperScrollableContainer.offsetLeft + 'px', + bottom: 'auto', + width: this._originalStyles.width, + zIndex: this.zIndex + '' + }; + extendObject(this.element.style, stickyCss); + } + + /** + * Unsticks the header so that it goes back to scrolling normally. + * + * This should be called when the element reaches the bottom of its cdkStickyRegion so that it + * smoothly scrolls out of view as the next sticky-header moves in. + */ + private _unstickElement(): void { + this.isStuck = false; + + if (!this.stickyParent) { + return; + } + + this.element.classList.add(STICK_END_CLASS); + this.stickyParent.style.position = 'relative'; + let unstuckCss = { + position: 'absolute', + top: 'auto', + right: '0', + left: 'auto', + bottom: '0', + width: this._originalStyles.width + }; + extendObject(this.element.style, unstuckCss); + } + + + /** + * '_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; + + // unstuck when the element is scrolled out of the sticky region + if (this.isStuck && + (currentPosition < this._stickyRegionTop || + currentPosition > this._stickyRegionBottomThreshold) || + currentPosition >= this._stickyRegionBottomThreshold) { + this._resetElementStyles(); + if (currentPosition >= this._stickyRegionBottomThreshold) { + this._unstickElement(); + } + this.isStuck = false; // stick when the element is within the sticky region + } else if ( this.isStuck === false && + currentPosition > this._stickyRegionTop && + currentPosition < this._stickyRegionBottomThreshold) { + this._stickElement(); + } + } + + _updateStickyPositioning(): void { + this._measureStickyRegionBounds(); + this._applyStickyPositionStyles(); + } +} From 2848f2c369fc3429fe7a19ff379ab8a2cc64fb6b Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Wed, 19 Jul 2017 13:39:36 -0700 Subject: [PATCH 03/28] # This is a combination of 16 commits. # This is the 1st commit message: # This is a combination of 10 commits. # This is the 1st commit message: add lib files for sticky-header add chose parent add support to 'optional 'cdkStickyRegion' input ' add app-demo for sticky-header fix bugs and deleted unused tag id in HTML files modify fix some code according to PR review comments change some format to pass TSlint check add '_' before private elements delete @Injectable for StickyHeaderDirective. Because we do not need @Injectable refine code encapsulate 'set style for element' change @Input() Delete 'Observable.fromEvent(this.upperScrollableContainer, 'scroll')' add const STICK_START_CLASS and STICK_END_CLASS Add doc for [cdkStickyRegion] and 'unstuckElement()'. Delete 'detach()' function, add its content into 'ngOnDestroy()'. change 'MdStickyHeaderModule' to 'CdkStickyHeaderModule'; encapsulate reset css style operation for sticky header. delete unnecessary gloable variables delete global variable '_width' Add doc for 'sticker()' function. explained how it works. add more doc for 'sticker()', explaining 'isStuck' flag 2 space for indent fix delete sticky-header demo part from this branch revert firebase file change code according to comments in PR revert firbaserc revert demo-app.ts revert routes.ts revert demo-app-module.ts change fix the problem of : 'this.stickyParent' might be 'null' change doc Change the constructor of 'cdkStickyRegion' to 'constructor(public readonly _elementRef: ElementRef) { }' Added prefix 'mat-' for CSS class Delete 'public' before variables Object.assign isn't supported in IE11; use extendObject from src/lib/core/util. IE11 will have trouble with `translate3d(0, 0, 0);', change to `translate3d(0px, 0px, 0px);' Added docs for all variables extract 'generate CSS style' created a generateStyleCSS() function, let it be responsible for generating all those CSS styles. reformat add debounce to solve 'getBoundingClientRect() cause slow down' problem. add position:sticky and check whether browser support it. If not , use the naive implementation removed unused import Removed unused 'scrollableRegion' and 'parentRegion' removed commented lines default public Add comments about why setting style top and position for iPhone and not IE. And extract detectBrowser() as a new function format consider all circumstances of browser. use "===" instead of '==' make 'navigator.userAgent.toLocaleLowerCase()' a local variable optimize Added comments on const 'STICK_START_CLASS' and 'STICK_END_CLASS'. change their content to cdk-sticky-header-start and cdk-sticky-header-end Added comments for STICK_START_CLASS and STICK_END_CLASS. Changed the format of one-line JsDoc unsubscribe sbscriptions onDestory Use what modernizr does on compatibility instead of get the browser version directly. add 'padding' and 'stickyRegionHeight' variables to avoid calling 'getComputedStyle()' too many times (which is expensive). move docs above @Directive removed the underscore in'_element: ElementRef', expand 'reg' to 'region' use 'if (this.isIE)' instead of 'if(this.isIE === true)' added more newlines between params in 'generateCssStyle()' function to make it easier to understand. Added reference link to Modernizer in docs of getSupportList() Deleted "_supportList" variable renamed 'isIE' to 'isStickyPositionSupported', and removed extra space before Observable Set debounce time as a const variable Added docs for 'const DEBOUNCE_TIME: number = 5;' Changed ' if(this.stickyParent == null)' to ' if(!this.stickyParent)' Removed the @param and @returns and make sure the types are correct in the function signature in 'generateCssStyle(...)' function Added docs for `isStickyPositionSupported` variable changed '+=' to '=' of 'stickyText' in getSupportList() function nit added " " between 'if' and '(' nit Added comments deleted unused import change comments optimize comments deleted unnecessary global variables(padding and stickyRegionHeight) Added check whether we are on browser Array to string[] test? try to reopen the old PR fix after rebase revert list.ts test test 222 revert demo revert list.ts second time Move code to 'src/cdk' revert 'move code to 'src/cdk'' , it should be done in a new PR revert avoid calling 'getComputedStyle()' too many times. rename as sticky-header.ts imported PlatformModule Add blank lines between these top-level symbol make '_isStickyPositionSupported' private Changed the originalCSS to private and use '{} as CSSStyleDeclaration' instead of ''any. Rename '_containerStart' to '_stickyRegionTop' # This is the commit message #2: rename # This is the commit message #3: optimize discription for '_stickyRegionBottomThreshold' # This is the commit message #4: private _originalStyles = { position: '', top: '', right: '', left: '', bottom: '', width: '', zIndex: ''}; # This is the commit message #5: Deleted 'generateCssStyle()' and 'getCssNumber()' function # This is the commit message #6: Deleted 'getCssValue()' function # This is the commit message #7: fix CSSStyleDeclaration # This is the commit message #8: change sticky width to 'this.upperScrollableContainer.clientWidth' # This is the commit message #9: fix # This is the commit message #10: nit # This is the commit message #2: Added the 'isPositionStickySupported() ' function to src/cdk/platform/features.ts. Consume that function in this component and just always use both the webkit and unprefixed styles. # This is the commit message #3: nit # This is the commit message #4: nit # This is the commit message #5: update doc 'Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the positioning adversely.' # This is the commit message #6: changed the doc to '/** z-index to be applied to the sticky header (default is 10). */' # This is the commit message #7: fix tslint error # This is the commit message #8: for comment 'Can you evaluate each method to make sure their accessor privacy is right? E.g. see which functions need to be public, private, static, etc' # This is the commit message #9: Deleted variable 'elemHeight' # This is the commit message #10: Chaned to 'if (!this.stickyParent)' # This is the commit message #11: Simplified Docs for 'sticker()'. # This is the commit message #12: set 'defineRestriction()' function to private # This is the commit message #13: use 'RxChain' # This is the commit message #14: deleted unused 'tableModule' in modules.ts # This is the commit message #15: rename to '_isPositionStickySupported' # This is the commit message #16: Use // for comments, /* */ for docs --- src/lib/sticky-header/sticky-header.ts | 300 ++++++++----------------- 1 file changed, 99 insertions(+), 201 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index cbeb63787379..a2d9c040724f 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -10,10 +10,10 @@ import {Directive, Input, import {Platform} from '../core/platform'; import {Scrollable} from '../core/overlay/scroll/scrollable'; import {extendObject} from '../core/util/object-extend'; -import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/operator/debounceTime'; +import {fromEvent} from 'rxjs/observable/fromEvent'; +import {RxChain, debounceTime} from '../core/rxjs/index'; +import {isPositionStickySupported} from '../../cdk/platform/features'; /** @@ -40,9 +40,9 @@ const STICK_START_CLASS = 'cdk-sticky-header-start'; const STICK_END_CLASS = 'cdk-sticky-header-end'; /** - * Set a debounce time which is used in debounce() function when adding event listeners. - * Set is as 5. Because if the DEBOUNCE_TIME is set as a too large number. The sticky effect - * during scroll will become vary strange and can not be scrolled smoothly. + * Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, + * touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the + * positioning adversely. */ const DEBOUNCE_TIME: number = 5; @@ -56,19 +56,13 @@ const DEBOUNCE_TIME: number = 5; }) export class CdkStickyHeader implements OnDestroy, AfterViewInit { - /** - * Set the sticky-header's z-index as 10 in default. Make it as an input - * variable to make user be able to customize the zIndex when - * the sticky-header's zIndex is not the largest in current page. - * Because if the sticky-header's zIndex is not the largest in current page, - * it may be sheltered by other element when being stuck. - */ + /** z-index to be applied to the sticky header (default is 10). */ @Input('cdkStickyHeaderZIndex') zIndex: number = 10; + /** boolean value to mark whether the current header is stuck*/ isStuck: boolean = false; /** Whether the browser support CSS sticky positioning. */ - private _isStickyPositionSupported: boolean = true; - + private _isPositionStickySupported: boolean = true; /** The element with the 'cdkStickyHeader' tag. */ element: HTMLElement; @@ -83,14 +77,16 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { private _originalStyles = {} as CSSStyleDeclaration; /** * 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. - * It is used with '_scrollFinish' to judge whether the current header + * It is used with '_stickyRegionBottomThreshold' to judge whether the current header * need to be stuck. */ - private _containerStart: number; + private _stickyRegionTop: number; /** - * `_scrollFinish` is the place from where the stuck element should be unstuck + * Bottom of the sticky region offset by the height of the sticky header. + * Once the sticky header is scrolled to this position it will stay in place + * so that it will scroll naturally out of view with the rest of the sticky region. */ - private _scrollFinish: number; + private _stickyRegionBottomThreshold: number; private _onScrollSubscription: Subscription; @@ -105,23 +101,26 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { if (platform.isBrowser) { this.element = element.nativeElement; this.upperScrollableContainer = scrollable.getElementRef().nativeElement; - this.setStrategyAccordingToCompatibility(); + this._setStrategyAccordingToCompatibility(); } } ngAfterViewInit(): void { - if (!this._isStickyPositionSupported) { + if (!this._isPositionStickySupported) { + this.stickyParent = this.parentRegion != null ? this.parentRegion._elementRef.nativeElement : this.element.parentElement; + let values = window.getComputedStyle(this.element, ''); - this._originalStyles = this.generateCssStyle( - values.getPropertyValue('position'), - values.getPropertyValue('top'), - values.getPropertyValue('right'), - values.getPropertyValue('left'), - values.getPropertyValue('bottom'), - values.getPropertyValue('width'), - values.getPropertyValue('zIndex')); + this._originalStyles = { + position: values.position, + top: values.top, + right: values.right, + left: values.left, + bottom: values.bottom, + width: values.width, + zIndex: values.zIndex} as CSSStyleDeclaration; + this.attach(); this.defineRestrictionsAndStick(); } @@ -142,98 +141,40 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { } /** - * getSupportList() is used to get a list of string which can be set to - * sticky-header's style.position and make Sticky positioning work. - * It returns a list of string. - * - * According to the "Position:sticky Browser compatibility" in - * "https://developer.mozilla.org/en-US/docs/Web/CSS/position". - * - * For Desktop: Sticky positioning works well on Chrome, Edge, Firefox and Opera. And can - * also work well on Safari with a "-webkit-" prefix. It only does not work on IE. - * - * For Mobile: Sticky positioning works well on Android Webview, Chrome for Android, Edge, - * Firefox Mobile, Opera Mobile. And can also work well on Safari Mobile with a "-webkit-" prefix. - * It won't always work on IE phone. - * - * The implementation references the compatibility checking in Modernizer - * (https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/positionsticky.js). + * Check if current browser supports sticky positioning. If yes, apply + * sticky positioning. If not, use the original implementation. */ - getSupportList(): string[] { - let prefixTestList = ['', '-webkit-', '-ms-', '-moz-', '-o-']; - let supportList: Array = new Array(); - let stickyText = ''; - for (let i = 0; i < prefixTestList.length; i++ ) { - stickyText = 'position:' + prefixTestList[i] + 'sticky;'; - // Create a DOM to check if the browser support current prefix for sticky-position. - let div = document.createElement('div'); - let body = document.body; - div.style.cssText = 'display:none;' + stickyText; - body.appendChild(div); - let isSupport = /sticky/i.test(this.getCssValue(div, 'position')); - body.removeChild(div); - if (isSupport == true) { - supportList.push(prefixTestList[i]); - } - } - return supportList; - } - - /** - * Get the first element from this._supportList. Set it as a prefix of - * sticky positioning. - * - * If the this._supportList is empty, which means the browser does not support - * sticky positioning. Set isStickyPositionSupported as 'true' and use the original - * implementation of sticky-header. - */ - setStrategyAccordingToCompatibility(): void { - let supportList = this.getSupportList(); - if (supportList.length === 0) { - this._isStickyPositionSupported = false; - } else { - // Only need supportList[0], Because supportList contains all the prefix - // that can make sticky positioning work in the current browser. - // We only need to get one prefix and make position: prefix + 'sticky', - // then sticky position will work. - let prefix: string = supportList[0]; - + private _setStrategyAccordingToCompatibility(): void { + this._isPositionStickySupported = isPositionStickySupported(); + if (this._isPositionStickySupported) { this.element.style.top = '0px'; - this.element.style.position = prefix + 'sticky'; + this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; + // TODO add css class with both 'sticky' and '-webkit-sticky' on position + // when @Directory supports adding CSS class } } attach() { - this._onScrollSubscription = Observable.fromEvent(this.upperScrollableContainer, 'scroll') - .debounceTime(DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); + this._onScrollSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'scroll')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones - this._onTouchSubscription = Observable.fromEvent(this.upperScrollableContainer, 'touchmove') - .debounceTime(DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); - - this._onResizeSubscription = Observable.fromEvent(this.upperScrollableContainer, 'resize') - .debounceTime(DEBOUNCE_TIME).subscribe(() => this.onResize()); - } + this._onTouchSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'touchmove')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); - onScroll(): void { - this.defineRestrictionsAndStick(); - } - - onTouchMove(): void { - this.defineRestrictionsAndStick(); + this._onResizeSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'resize')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.onResize()); } onResize(): void { this.defineRestrictionsAndStick(); - /** - * If there's already a header being stick when the page is - * resized. The CSS style of the cdkStickyHeader element may be not fit - * the resized window. So we need to unstuck it then re-stick it. - * unstuck() can set 'isStuck' to FALSE. Then stickElement() can work. - */ + // If there's already a header being stick when the page is + // resized. The CSS style of the cdkStickyHeader element may be not fit + // the resized window. So we need to unstuck it then re-stick it. + // unstuck() can set 'isStuck' to FALSE. Then _stickElement() can work. if (this.isStuck) { - this.unstuckElement(); - this.stickElement(); + this._unstuckElement(); + this._stickElement(); } } @@ -241,16 +182,16 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * define the restrictions of the sticky header(including stickyWidth, * when to start, when to finish) */ - defineRestrictions(): void { + private _defineRestrictions(): void { if (!this.stickyParent) { return; } - let boundingClientRect: any = this.stickyParent.getBoundingClientRect(); - let elemHeight: number = this.element.offsetHeight; - this._containerStart = boundingClientRect.top; + const boundingClientRect: any = this.stickyParent.getBoundingClientRect(); + this._stickyRegionTop = boundingClientRect.top; let stickRegionHeight = boundingClientRect.height; - this._scrollFinish = this._containerStart + (stickRegionHeight - elemHeight); + this._stickyRegionBottomThreshold = this._stickyRegionTop + + (stickRegionHeight - this.element.offsetHeight); } /** Reset element to its original CSS. */ @@ -260,43 +201,41 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { } /** Stuck element, make the element stick to the top of the scrollable container. */ - stickElement(): void { + private _stickElement(): void { this.isStuck = true; this.element.classList.remove(STICK_END_CLASS); this.element.classList.add(STICK_START_CLASS); - /** - * Have to add the translate3d function for the sticky element's css style. - * Because iPhone and iPad's browser is using its owning rendering engine. And - * even if you are using Chrome on an iPhone, you are just using Safari with - * a Chrome skin around it. - * - * Safari on iPad and Safari on iPhone do not have resizable windows. - * In Safari on iPhone and iPad, the window size is set to the size of - * the screen (minus Safari user interface controls), and cannot be changed - * by the user. To move around a webpage, the user changes the zoom level and position - * of the viewport as they double tap or pinch to zoom in or out, or by touching - * and dragging to pan the page. As a user changes the zoom level and position of the - * viewport they are doing so within a viewable content area of fixed size - * (that is, the window). This means that webpage elements that have their position - * "fixed" to the viewport can end up outside the viewable content area, offscreen. - * - * So the 'position: fixed' does not work on iPhone and iPad. To make it work, - * 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. - **/ + // Have to add the translate3d function for the sticky element's css style. + // Because iPhone and iPad's browser is using its owning rendering engine. And + // even if you are using Chrome on an iPhone, you are just using Safari with + // a Chrome skin around it. + // + // Safari on iPad and Safari on iPhone do not have resizable windows. + // In Safari on iPhone and iPad, the window size is set to the size of + // the screen (minus Safari user interface controls), and cannot be changed + // by the user. To move around a webpage, the user changes the zoom level and position + // of the viewport as they double tap or pinch to zoom in or out, or by touching + // and dragging to pan the page. As a user changes the zoom level and position of the + // viewport they are doing so within a viewable content area of fixed size + // (that is, the window). This means that webpage elements that have their position + // "fixed" to the viewport can end up outside the viewable content area, offscreen. + // + // So the 'position: fixed' does not work on iPhone and iPad. To make it work, + // 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. this.element.style.transform = 'translate3d(0px,0px,0px)'; let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right; - let stickyCss:any = this.generateCssStyle( - 'fixed', - this.upperScrollableContainer.offsetTop + 'px', - stuckRight + 'px', - this.upperScrollableContainer.offsetLeft + 'px', - 'auto', - this._originalStyles.width, - this.zIndex + '',); + let stickyCss = { + position: 'fixed', + top: this.upperScrollableContainer.offsetTop + 'px', + right: stuckRight + 'px', + left: this.upperScrollableContainer.offsetLeft + 'px', + bottom: 'auto', + width: this._originalStyles.width, + zIndex: this.zIndex + '',}; extendObject(this.element.style, stickyCss); } @@ -308,94 +247,53 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * can be changed smoothly when two sticky header meet and the later one need to replace * the former one. */ - unstuckElement(): void { + private _unstuckElement(): void { this.isStuck = false; - if (this.stickyParent == null) { + if (!this.stickyParent) { return; } this.element.classList.add(STICK_END_CLASS); this.stickyParent.style.position = 'relative'; - let unstuckCss: any = this.generateCssStyle( - 'absolute', - 'auto', - '0', - 'auto', - '0', - this._originalStyles.width); + let unstuckCss = { + position: 'absolute', + top: 'auto', + right: '0', + left: 'auto', + bottom: '0', + width: this._originalStyles.width}; extendObject(this.element.style, unstuckCss); } /** * 'sticker()' function contains the main logic of sticky-header. It decides when - * a header should be stick and when should it be unstuck. It will first get - * the offsetTop of the upper scrollable container. And then get the Start and End - * of the sticky-header's stickyRegion. - * The header will be stick if 'stickyRegion Start < container offsetTop < stickyRegion End'. - * And when 'stickyRegion End < container offsetTop', the header will be unstuck. It will be - * stick to the bottom of its stickyRegion container and being scrolled up with its stickyRegion - * container. - * When 'stickyRegion Start > container offsetTop', which means the header come back to the - * middle of the scrollable container, the header will be reset to its - * original CSS. - * A flag, isStuck. is used in this function. When a header is stick, isStuck = true. - * And when the 'isStuck' flag is TRUE, the sticky-header will not be repaint, which - * decreases the times on repainting sticky-header. + * 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. */ sticker(): void { let currentPosition: number = this.upperScrollableContainer.offsetTop; // unstuck when the element is scrolled out of the sticky region if (this.isStuck && - (currentPosition < this._containerStart || currentPosition > this._scrollFinish) || - currentPosition >= this._scrollFinish) { + (currentPosition < this._stickyRegionTop || + currentPosition > this._stickyRegionBottomThreshold) + || currentPosition >= this._stickyRegionBottomThreshold) { this.resetElement(); - if (currentPosition >= this._scrollFinish) { - this.unstuckElement(); + if (currentPosition >= this._stickyRegionBottomThreshold) { + this._unstuckElement(); } this.isStuck = false; // stick when the element is within the sticky region } else if ( this.isStuck === false && - currentPosition > this._containerStart && currentPosition < this._scrollFinish) { - this.stickElement(); + currentPosition > this._stickyRegionTop && + currentPosition < this._stickyRegionBottomThreshold) { + this._stickElement(); } } defineRestrictionsAndStick(): void { - this.defineRestrictions(); + this._defineRestrictions(); this.sticker(); } - - /** - * This function is used to generate a variable which contains 7 css styles. - */ - generateCssStyle(position:string, top:string, right:string, - left:string, bottom:string, width:string | null, zIndex?:string, ): any { - let targetCSS = { - position: position, - top: top, - right: right, - left: left, - bottom: bottom, - width: width, - zIndex: zIndex - }; - return targetCSS; -} - - - private getCssValue(element: any, property: string): any { - let result: any = ''; - if (typeof window.getComputedStyle !== 'undefined') { - result = window.getComputedStyle(element, '').getPropertyValue(property); - } else if (typeof element.currentStyle !== 'undefined') { - result = element.currentStyle.property; - } - return result; - } - - private getCssNumber(element: any, property: string): number { - return parseInt(this.getCssValue(element, property), 10) || 0; - } } From fbf6350c8fad034137fa59cfd68a648ce5f4a735 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 24 Jul 2017 11:10:24 -0700 Subject: [PATCH 04/28] add lib files for sticky-header add chose parent add support to 'optional 'cdkStickyRegion' input ' add app-demo for sticky-header fix bugs and deleted unused tag id in HTML files modify fix some code according to PR review comments change some format to pass TSlint check add '_' before private elements delete @Injectable for StickyHeaderDirective. Because we do not need @Injectable refine code encapsulate 'set style for element' change @Input() Delete 'Observable.fromEvent(this.upperScrollableContainer, 'scroll')' add const STICK_START_CLASS and STICK_END_CLASS Add doc for [cdkStickyRegion] and 'unstuckElement()'. Delete 'detach()' function, add its content into 'ngOnDestroy()'. change 'MdStickyHeaderModule' to 'CdkStickyHeaderModule'; encapsulate reset css style operation for sticky header. delete unnecessary gloable variables delete global variable '_width' Add doc for 'sticker()' function. explained how it works. add more doc for 'sticker()', explaining 'isStuck' flag 2 space for indent fix delete sticky-header demo part from this branch revert firebase file change code according to comments in PR revert firbaserc revert demo-app.ts revert routes.ts revert demo-app-module.ts change fix the problem of : 'this.stickyParent' might be 'null' change doc Change the constructor of 'cdkStickyRegion' to 'constructor(public readonly _elementRef: ElementRef) { }' Added prefix 'mat-' for CSS class Delete 'public' before variables Object.assign isn't supported in IE11; use extendObject from src/lib/core/util. IE11 will have trouble with `translate3d(0, 0, 0);', change to `translate3d(0px, 0px, 0px);' Added docs for all variables extract 'generate CSS style' created a generateStyleCSS() function, let it be responsible for generating all those CSS styles. reformat add debounce to solve 'getBoundingClientRect() cause slow down' problem. add position:sticky and check whether browser support it. If not , use the naive implementation removed unused import Removed unused 'scrollableRegion' and 'parentRegion' removed commented lines default public Add comments about why setting style top and position for iPhone and not IE. And extract detectBrowser() as a new function format consider all circumstances of browser. use "===" instead of '==' make 'navigator.userAgent.toLocaleLowerCase()' a local variable optimize Added comments on const 'STICK_START_CLASS' and 'STICK_END_CLASS'. change their content to cdk-sticky-header-start and cdk-sticky-header-end Added comments for STICK_START_CLASS and STICK_END_CLASS. Changed the format of one-line JsDoc unsubscribe sbscriptions onDestory Use what modernizr does on compatibility instead of get the browser version directly. add 'padding' and 'stickyRegionHeight' variables to avoid calling 'getComputedStyle()' too many times (which is expensive). move docs above @Directive removed the underscore in'_element: ElementRef', expand 'reg' to 'region' use 'if (this.isIE)' instead of 'if(this.isIE === true)' added more newlines between params in 'generateCssStyle()' function to make it easier to understand. Added reference link to Modernizer in docs of getSupportList() Deleted "_supportList" variable renamed 'isIE' to 'isStickyPositionSupported', and removed extra space before Observable Set debounce time as a const variable Added docs for 'const DEBOUNCE_TIME: number = 5;' Changed ' if(this.stickyParent == null)' to ' if(!this.stickyParent)' Removed the @param and @returns and make sure the types are correct in the function signature in 'generateCssStyle(...)' function Added docs for `isStickyPositionSupported` variable changed '+=' to '=' of 'stickyText' in getSupportList() function nit added " " between 'if' and '(' nit Added comments deleted unused import change comments optimize comments deleted unnecessary global variables(padding and stickyRegionHeight) Added check whether we are on browser Array to string[] test? try to reopen the old PR fix after rebase revert list.ts test test 222 revert demo revert list.ts second time Move code to 'src/cdk' revert 'move code to 'src/cdk'' , it should be done in a new PR revert avoid calling 'getComputedStyle()' too many times. rename as sticky-header.ts imported PlatformModule Add blank lines between these top-level symbol make '_isStickyPositionSupported' private Changed the originalCSS to private and use '{} as CSSStyleDeclaration' instead of ''any. Rename '_containerStart' to '_stickyRegionTop' rename optimize discription for '_stickyRegionBottomThreshold' private _originalStyles = { position: '', top: '', right: '', left: '', bottom: '', width: '', zIndex: ''}; Deleted 'generateCssStyle()' and 'getCssNumber()' function Deleted 'getCssValue()' function fix CSSStyleDeclaration change sticky width to 'this.upperScrollableContainer.clientWidth' fix nit Added the 'isPositionStickySupported() ' function to src/cdk/platform/features.ts. Consume that function in this component and just always use both the webkit and unprefixed styles. nit nit update doc 'Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the positioning adversely.' changed the doc to '/** z-index to be applied to the sticky header (default is 10). */' fix tslint error for comment 'Can you evaluate each method to make sure their accessor privacy is right? E.g. see which functions need to be public, private, static, etc' Deleted variable 'elemHeight' Chaned to 'if (!this.stickyParent)' Simplified Docs for 'sticker()'. set 'defineRestriction()' function to private use 'RxChain' deleted unused 'tableModule' in modules.ts rename to '_isPositionStickySupported' Use // for comments, /* */ for docs @angular/cdk rename : values -> headerStyles Move closing brace to the next line optimized: [this._onScrollSubscription, this._onScrollSubscription, this._onResizeSubscription] .forEach(s => s && s.unsubscribe()); You should be able to do just '0' instead of '0px' Format like TODO(sllethe): ... private _attachEventListeners? Add a description like "Add listeners for events that affect sticky positioning." optimize doc Rename 'defineRestrictions' to '_measureStickyRegionBounds' rename: private _resetElementStyles let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right; chaned 'any' to 'number' nit change doc '/** * Unsticks the header so that it goes back to scrolling normally. * * This should be called when the element reaches the bottom of its cdkStickyRegion so that it * smoothly scrolls out of view as the next sticky-header moves in. */' _unstuckElement -> _unstickElement rename 'sticker()' to '_applyStickyPositionStyles()' rename 'defineRestrictionsAndStick()' to '_updateStickyPositioning()' --- src/lib/sticky-header/sticky-header.ts | 102 +++++++++++-------------- 1 file changed, 46 insertions(+), 56 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index a2d9c040724f..1c520ed7c8df 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -13,7 +13,7 @@ import {extendObject} from '../core/util/object-extend'; import {Subscription} from 'rxjs/Subscription'; import {fromEvent} from 'rxjs/observable/fromEvent'; import {RxChain, debounceTime} from '../core/rxjs/index'; -import {isPositionStickySupported} from '../../cdk/platform/features'; +import {isPositionStickySupported} from '@angular/cdk'; /** @@ -62,7 +62,7 @@ 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 = true; + private _isPositionStickySupported: boolean = false; /** The element with the 'cdkStickyHeader' tag. */ element: HTMLElement; @@ -111,33 +111,25 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { this.stickyParent = this.parentRegion != null ? this.parentRegion._elementRef.nativeElement : this.element.parentElement; - let values = window.getComputedStyle(this.element, ''); + let headerStyles = window.getComputedStyle(this.element, ''); this._originalStyles = { - position: values.position, - top: values.top, - right: values.right, - left: values.left, - bottom: values.bottom, - width: values.width, - zIndex: values.zIndex} as CSSStyleDeclaration; - - this.attach(); - this.defineRestrictionsAndStick(); + position: headerStyles.position, + top: headerStyles.top, + right: headerStyles.right, + left: headerStyles.left, + bottom: headerStyles.bottom, + width: headerStyles.width, + zIndex: headerStyles.zIndex + } as CSSStyleDeclaration; + + this._attachEventListeners(); + this._updateStickyPositioning(); } } ngOnDestroy(): void { - if (this._onScrollSubscription) { - this._onScrollSubscription.unsubscribe(); - } - - if (this._onResizeSubscription) { - this._onResizeSubscription.unsubscribe(); - } - - if (this._onTouchSubscription) { - this._onTouchSubscription.unsubscribe(); - } + [this._onScrollSubscription, this._onScrollSubscription, this._onResizeSubscription] + .forEach(s => s && s.unsubscribe()); } /** @@ -147,42 +139,40 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { private _setStrategyAccordingToCompatibility(): void { this._isPositionStickySupported = isPositionStickySupported(); if (this._isPositionStickySupported) { - this.element.style.top = '0px'; + this.element.style.top = '0'; this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; - // TODO add css class with both 'sticky' and '-webkit-sticky' on position + // TODO(sllethe): add css class with both 'sticky' and '-webkit-sticky' on position // when @Directory supports adding CSS class } } - attach() { + /** Add listeners for events that affect sticky positioning. */ + private _attachEventListeners() { this._onScrollSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'scroll')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones this._onTouchSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'touchmove')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); this._onResizeSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'resize')) .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.onResize()); } onResize(): void { - this.defineRestrictionsAndStick(); + this._updateStickyPositioning(); // If there's already a header being stick when the page is // resized. The CSS style of the cdkStickyHeader element may be not fit // the resized window. So we need to unstuck it then re-stick it. // unstuck() can set 'isStuck' to FALSE. Then _stickElement() can work. if (this.isStuck) { - this._unstuckElement(); + this._unstickElement(); this._stickElement(); } } - /** - * define the restrictions of the sticky header(including stickyWidth, - * when to start, when to finish) - */ - private _defineRestrictions(): void { + /** Measures the boundaries of the sticky regions to be used in subsequent positioning. */ + private _measureStickyRegionBounds(): void { if (!this.stickyParent) { return; } @@ -195,7 +185,7 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { } /** Reset element to its original CSS. */ - resetElement(): void { + private _resetElementStyles(): void { this.element.classList.remove(STICK_START_CLASS); extendObject(this.element.style, this._originalStyles); } @@ -226,7 +216,7 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { // 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. this.element.style.transform = 'translate3d(0px,0px,0px)'; - let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right; + let stuckRight: number = this.upperScrollableContainer.getBoundingClientRect().right; let stickyCss = { position: 'fixed', @@ -235,19 +225,18 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { left: this.upperScrollableContainer.offsetLeft + 'px', bottom: 'auto', width: this._originalStyles.width, - zIndex: this.zIndex + '',}; + zIndex: this.zIndex + '' + }; extendObject(this.element.style, stickyCss); } /** - * Unstuck element: When an element reaches the bottom of its cdkStickyRegion, - * It should be unstuck. And its position will be set as 'relative', its bottom - * will be set as '0'. So it will be stick at the bottom of its cdkStickyRegion and - * will be scrolled up with its cdkStickyRegion element. In this way, the sticky header - * can be changed smoothly when two sticky header meet and the later one need to replace - * the former one. + * Unsticks the header so that it goes back to scrolling normally. + * + * This should be called when the element reaches the bottom of its cdkStickyRegion so that it + * smoothly scrolls out of view as the next sticky-header moves in. */ - private _unstuckElement(): void { + private _unstickElement(): void { this.isStuck = false; if (!this.stickyParent) { @@ -262,27 +251,28 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { right: '0', left: 'auto', bottom: '0', - width: this._originalStyles.width}; + width: this._originalStyles.width + }; extendObject(this.element.style, unstuckCss); } /** - * 'sticker()' function contains the main logic of sticky-header. It decides when + * '_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. */ - sticker(): void { + _applyStickyPositionStyles(): void { let currentPosition: number = this.upperScrollableContainer.offsetTop; // unstuck when the element is scrolled out of the sticky region if (this.isStuck && (currentPosition < this._stickyRegionTop || - currentPosition > this._stickyRegionBottomThreshold) - || currentPosition >= this._stickyRegionBottomThreshold) { - this.resetElement(); + currentPosition > this._stickyRegionBottomThreshold) || + currentPosition >= this._stickyRegionBottomThreshold) { + this._resetElementStyles(); if (currentPosition >= this._stickyRegionBottomThreshold) { - this._unstuckElement(); + this._unstickElement(); } this.isStuck = false; // stick when the element is within the sticky region } else if ( this.isStuck === false && @@ -292,8 +282,8 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { } } - defineRestrictionsAndStick(): void { - this._defineRestrictions(); - this.sticker(); + _updateStickyPositioning(): void { + this._measureStickyRegionBounds(); + this._applyStickyPositionStyles(); } } From b0172d1fddf0dca0d6061f65044f24c83dde5c18 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Wed, 26 Jul 2017 15:27:51 -0700 Subject: [PATCH 05/28] added tests for sticky-header --- src/lib/sticky-header/index.ts | 4 +- src/lib/sticky-header/sticky-header.spec.ts | 191 ++++++++++++++++++++ src/lib/sticky-header/sticky-header.ts | 23 ++- 3 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 src/lib/sticky-header/sticky-header.spec.ts diff --git a/src/lib/sticky-header/index.ts b/src/lib/sticky-header/index.ts index dce3a728cf46..239413d8351e 100644 --- a/src/lib/sticky-header/index.ts +++ b/src/lib/sticky-header/index.ts @@ -8,7 +8,8 @@ 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'; @@ -16,6 +17,7 @@ import {CdkStickyRegion, CdkStickyHeader} from './sticky-header'; 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..79703e49ce03 --- /dev/null +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -0,0 +1,191 @@ +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_PROVIDER} 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'; + + + +describe('sticky-header with positioning not supported', () => { + let fixture: ComponentFixture; + let testComponent: StickyHeaderTest; + let stickyElement: DebugElement; + let stickyParentElement: DebugElement; + let scrollableElement: DebugElement; + let stickyHeaderDir: CdkStickyHeader; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ OverlayModule, PlatformModule, StickyHeaderModule ], + declarations: [StickyHeaderTest], + providers: [ + {provide: STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER, useFactory: mockStickyHeaderCheckFail()}, + ], + }); + 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)); + stickyHeaderDir = stickyElement.injector.get(CdkStickyHeader); + scrollableElement = fixture.debugElement.query(By.directive(Scrollable)); + }); + + it('should be able to find stickyParent when sticky positioning is not supported', () => { + fixture.detectChanges(); + expect(stickyElement.nativeElement.stickyParent).not.toBe(null); + }); + + it('should be able to find scrollableContainer when sticky positioning is not supported', () => { + fixture.detectChanges(); + expect(stickyElement.nativeElement.upperScrollableContainer).not.toBe(null); + }); + + it('should stick in the right place when scrolled to the top of the container', fakeAsync(() => { + fixture.detectChanges(); + let scrollableContainerTop = stickyHeaderDir.upperScrollableContainer + .getBoundingClientRect().top; + expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + tick(0); + + // Scroll the scrollableContainer up to stick + fixture.componentInstance.scrollDown(); + tick(100); + + expect(stickyHeaderDir.element.getBoundingClientRect().top).toBe(scrollableContainerTop); + })); + + it('should unstuck when scrolled off the top of the container', fakeAsync(() => { + fixture.detectChanges(); + let scrollableContainerTop = stickyHeaderDir.upperScrollableContainer + .getBoundingClientRect().top; + expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + tick(0); + + // Scroll the scrollableContainer up to stick + fixture.componentInstance.scrollDown(); + tick(100); + + expect(stickyHeaderDir.element.getBoundingClientRect().top).toBe(scrollableContainerTop); + + // Scroll the scrollableContainer down to unstuck + fixture.componentInstance.scrollBack(); + tick(100); + + expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + + })); + + function mockStickyHeaderCheckFail() { + return false; + } +}); + +describe('sticky-header with positioning supported', () => { + let fixture: ComponentFixture; + let testComponent: StickyHeaderTest; + let stickyElement: DebugElement; + let stickyParentElement: DebugElement; + let scrollableElement: HTMLElement; + let stickyHeaderDir: CdkStickyHeader; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ OverlayModule, PlatformModule, StickyHeaderModule ], + declarations: [StickyHeaderTest], + providers: [ + {provide: STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER, + useFactory: mockStickyHeaderCheckSuccess()}, + ], + }); + 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)); + stickyHeaderDir = stickyElement.injector.get(CdkStickyHeader); + }); + + it('should find sticky positioning is applied', () => { + fixture.detectChanges(); + let position = window.getComputedStyle(stickyHeaderDir.element).position; + expect(position).not.toBe(null); + if (position != null) { + expect(/sticky/i.test(position)).toBe(true); + } + }); + + function mockStickyHeaderCheckSuccess() { + return true; + } + +}); + +@Component({ + template: ` +
+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+
+
+

Heading 1

+
+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+

test test test

+
+
+ `}) +class StickyHeaderTest { + @ViewChild(Scrollable) scrollingContainer: Scrollable; + + 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'); + } +} diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 1c520ed7c8df..f35c3f2c8800 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} 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'; @@ -46,6 +47,19 @@ 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'); + +export function STICKY_HEADER_SUPPORT_STRATEGY_FACTORY() { + return isPositionStickySupported(); +} + +/** Create a factory for sticky-positioning check to make code more testable */ +export const STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER = { + provide: STICKY_HEADER_SUPPORT_STRATEGY, + deps: [], + useFactory: STICKY_HEADER_SUPPORT_STRATEGY_FACTORY +}; + /** * 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 @@ -97,7 +111,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 _stickyPositionSupportCheck) { if (platform.isBrowser) { this.element = element.nativeElement; this.upperScrollableContainer = scrollable.getElementRef().nativeElement; @@ -137,7 +152,7 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * sticky positioning. If not, use the original implementation. */ private _setStrategyAccordingToCompatibility(): void { - this._isPositionStickySupported = isPositionStickySupported(); + this._isPositionStickySupported = this._stickyPositionSupportCheck; if (this._isPositionStickySupported) { this.element.style.top = '0'; this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; From d354ef0f254afc0dfbeba7bff7a498b2e9c15f39 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Wed, 26 Jul 2017 15:34:03 -0700 Subject: [PATCH 06/28] deleted --- src/lib/sticky-header/sticky-header.ts~HEAD | 289 -------------------- 1 file changed, 289 deletions(-) delete mode 100644 src/lib/sticky-header/sticky-header.ts~HEAD diff --git a/src/lib/sticky-header/sticky-header.ts~HEAD b/src/lib/sticky-header/sticky-header.ts~HEAD deleted file mode 100644 index 1c520ed7c8df..000000000000 --- a/src/lib/sticky-header/sticky-header.ts~HEAD +++ /dev/null @@ -1,289 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * 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'; -import {Scrollable} from '../core/overlay/scroll/scrollable'; -import {extendObject} from '../core/util/object-extend'; -import {Subscription} from 'rxjs/Subscription'; -import {fromEvent} from 'rxjs/observable/fromEvent'; -import {RxChain, debounceTime} from '../core/rxjs/index'; -import {isPositionStickySupported} from '@angular/cdk'; - - -/** - * Directive that marks an element as a "sticky region", meant to contain exactly one sticky-header - * along with the content associated with that header. The sticky-header inside of the region will - * "stick" to the top of the scrolling container as long as this region is within the scrolling - * viewport. - * - * If a user does not explicitly define a sticky-region for a sticky-header, the direct - * parent node of the sticky-header will be used as the sticky-region. - */ -@Directive({ - selector: '[cdkStickyRegion]', -}) -export class CdkStickyRegion { - constructor(public readonly _elementRef: ElementRef) { } -} - - -/** Class applied when the header is "stuck" */ -const STICK_START_CLASS = 'cdk-sticky-header-start'; - -/** Class applied when the header is not "stuck" */ -const STICK_END_CLASS = 'cdk-sticky-header-end'; - -/** - * Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, - * touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the - * positioning adversely. - */ -const DEBOUNCE_TIME: number = 5; - -/** - * 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 - * region (see cdkStickyRegion) is in view. - */ -@Directive({ - selector: '[cdkStickyHeader]', -}) -export class CdkStickyHeader implements OnDestroy, AfterViewInit { - - /** z-index to be applied to the sticky header (default is 10). */ - @Input('cdkStickyHeaderZIndex') zIndex: number = 10; - - /** 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; - /** The upper container element with the 'cdkStickyRegion' tag. */ - stickyParent: HTMLElement | null; - /** The upper scrollable container. */ - upperScrollableContainer: HTMLElement; - /** - * The original css of the sticky element, used to reset the sticky element - * when it is being unstuck - */ - private _originalStyles = {} as CSSStyleDeclaration; - /** - * 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. - * It is used with '_stickyRegionBottomThreshold' to judge whether the current header - * need to be stuck. - */ - private _stickyRegionTop: number; - /** - * Bottom of the sticky region offset by the height of the sticky header. - * Once the sticky header is scrolled to this position it will stay in place - * so that it will scroll naturally out of view with the rest of the sticky region. - */ - private _stickyRegionBottomThreshold: number; - - private _onScrollSubscription: Subscription; - - private _onTouchSubscription: Subscription; - - private _onResizeSubscription: Subscription; - - constructor(element: ElementRef, - scrollable: Scrollable, - @Optional() public parentRegion: CdkStickyRegion, - platform: Platform) { - if (platform.isBrowser) { - this.element = element.nativeElement; - this.upperScrollableContainer = scrollable.getElementRef().nativeElement; - this._setStrategyAccordingToCompatibility(); - } - } - - ngAfterViewInit(): void { - if (!this._isPositionStickySupported) { - - this.stickyParent = this.parentRegion != null ? - this.parentRegion._elementRef.nativeElement : this.element.parentElement; - - let headerStyles = window.getComputedStyle(this.element, ''); - this._originalStyles = { - position: headerStyles.position, - top: headerStyles.top, - right: headerStyles.right, - left: headerStyles.left, - bottom: headerStyles.bottom, - width: headerStyles.width, - zIndex: headerStyles.zIndex - } as CSSStyleDeclaration; - - this._attachEventListeners(); - this._updateStickyPositioning(); - } - } - - ngOnDestroy(): void { - [this._onScrollSubscription, this._onScrollSubscription, this._onResizeSubscription] - .forEach(s => s && s.unsubscribe()); - } - - /** - * Check if current browser supports sticky positioning. If yes, apply - * 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; '; - // TODO(sllethe): add css class with both 'sticky' and '-webkit-sticky' on position - // when @Directory supports adding CSS class - } - } - - /** Add listeners for events that affect sticky positioning. */ - private _attachEventListeners() { - this._onScrollSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'scroll')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); - - // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones - this._onTouchSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'touchmove')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning()); - - this._onResizeSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'resize')) - .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.onResize()); - } - - onResize(): void { - this._updateStickyPositioning(); - // If there's already a header being stick when the page is - // resized. The CSS style of the cdkStickyHeader element may be not fit - // the resized window. So we need to unstuck it then re-stick it. - // unstuck() can set 'isStuck' to FALSE. Then _stickElement() can work. - if (this.isStuck) { - this._unstickElement(); - this._stickElement(); - } - } - - /** Measures the boundaries of the sticky regions to be used in subsequent positioning. */ - private _measureStickyRegionBounds(): void { - if (!this.stickyParent) { - return; - } - const boundingClientRect: any = this.stickyParent.getBoundingClientRect(); - this._stickyRegionTop = boundingClientRect.top; - let stickRegionHeight = boundingClientRect.height; - - this._stickyRegionBottomThreshold = this._stickyRegionTop + - (stickRegionHeight - this.element.offsetHeight); - } - - /** Reset element to its original CSS. */ - private _resetElementStyles(): void { - this.element.classList.remove(STICK_START_CLASS); - extendObject(this.element.style, this._originalStyles); - } - - /** Stuck element, make the element stick to the top of the scrollable container. */ - private _stickElement(): void { - this.isStuck = true; - - this.element.classList.remove(STICK_END_CLASS); - this.element.classList.add(STICK_START_CLASS); - - // Have to add the translate3d function for the sticky element's css style. - // Because iPhone and iPad's browser is using its owning rendering engine. And - // even if you are using Chrome on an iPhone, you are just using Safari with - // a Chrome skin around it. - // - // Safari on iPad and Safari on iPhone do not have resizable windows. - // In Safari on iPhone and iPad, the window size is set to the size of - // the screen (minus Safari user interface controls), and cannot be changed - // by the user. To move around a webpage, the user changes the zoom level and position - // of the viewport as they double tap or pinch to zoom in or out, or by touching - // and dragging to pan the page. As a user changes the zoom level and position of the - // viewport they are doing so within a viewable content area of fixed size - // (that is, the window). This means that webpage elements that have their position - // "fixed" to the viewport can end up outside the viewable content area, offscreen. - // - // So the 'position: fixed' does not work on iPhone and iPad. To make it work, - // 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. - this.element.style.transform = 'translate3d(0px,0px,0px)'; - - let stuckRight: number = this.upperScrollableContainer.getBoundingClientRect().right; - - let stickyCss = { - position: 'fixed', - top: this.upperScrollableContainer.offsetTop + 'px', - right: stuckRight + 'px', - left: this.upperScrollableContainer.offsetLeft + 'px', - bottom: 'auto', - width: this._originalStyles.width, - zIndex: this.zIndex + '' - }; - extendObject(this.element.style, stickyCss); - } - - /** - * Unsticks the header so that it goes back to scrolling normally. - * - * This should be called when the element reaches the bottom of its cdkStickyRegion so that it - * smoothly scrolls out of view as the next sticky-header moves in. - */ - private _unstickElement(): void { - this.isStuck = false; - - if (!this.stickyParent) { - return; - } - - this.element.classList.add(STICK_END_CLASS); - this.stickyParent.style.position = 'relative'; - let unstuckCss = { - position: 'absolute', - top: 'auto', - right: '0', - left: 'auto', - bottom: '0', - width: this._originalStyles.width - }; - extendObject(this.element.style, unstuckCss); - } - - - /** - * '_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; - - // unstuck when the element is scrolled out of the sticky region - if (this.isStuck && - (currentPosition < this._stickyRegionTop || - currentPosition > this._stickyRegionBottomThreshold) || - currentPosition >= this._stickyRegionBottomThreshold) { - this._resetElementStyles(); - if (currentPosition >= this._stickyRegionBottomThreshold) { - this._unstickElement(); - } - this.isStuck = false; // stick when the element is within the sticky region - } else if ( this.isStuck === false && - currentPosition > this._stickyRegionTop && - currentPosition < this._stickyRegionBottomThreshold) { - this._stickElement(); - } - } - - _updateStickyPositioning(): void { - this._measureStickyRegionBounds(); - this._applyStickyPositionStyles(); - } -} From 6e58b23d33570fc14421d3727c123b85af16c251 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Wed, 26 Jul 2017 17:04:21 -0700 Subject: [PATCH 07/28] removed 'deps' in providers --- src/lib/sticky-header/sticky-header.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index f35c3f2c8800..6c917c8e05e4 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -56,7 +56,6 @@ export function STICKY_HEADER_SUPPORT_STRATEGY_FACTORY() { /** Create a factory for sticky-positioning check to make code more testable */ export const STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER = { provide: STICKY_HEADER_SUPPORT_STRATEGY, - deps: [], useFactory: STICKY_HEADER_SUPPORT_STRATEGY_FACTORY }; From 32b1b93f9eb0d51c3579ad42ef8c0635ae838a6f Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Wed, 26 Jul 2017 17:12:04 -0700 Subject: [PATCH 08/28] put provider in index --- src/lib/sticky-header/index.ts | 5 +++-- src/lib/sticky-header/sticky-header.ts | 18 +++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/lib/sticky-header/index.ts b/src/lib/sticky-header/index.ts index 239413d8351e..08e4a278f636 100644 --- a/src/lib/sticky-header/index.ts +++ b/src/lib/sticky-header/index.ts @@ -9,7 +9,8 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {OverlayModule, MdCommonModule, PlatformModule} from '../core'; import {CdkStickyRegion, CdkStickyHeader, - STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER} from './sticky-header'; + STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER, STICKY_HEADER_SUPPORT_STRATEGY} from './sticky-header'; +import {isPositionStickySupported} from '@angular/cdk'; @@ -17,7 +18,7 @@ import {CdkStickyRegion, CdkStickyHeader, imports: [OverlayModule, MdCommonModule, CommonModule, PlatformModule], declarations: [CdkStickyRegion, CdkStickyHeader], exports: [CdkStickyRegion, CdkStickyHeader, MdCommonModule], - providers: [STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER] + providers: [{provide: STICKY_HEADER_SUPPORT_STRATEGY, useFactory: isPositionStickySupported}] }) export class StickyHeaderModule {} diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 6c917c8e05e4..582ff4db9083 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -49,15 +49,11 @@ const DEBOUNCE_TIME: number = 5; export const STICKY_HEADER_SUPPORT_STRATEGY = new InjectionToken('sticky-header-support-strategy'); -export function STICKY_HEADER_SUPPORT_STRATEGY_FACTORY() { - return isPositionStickySupported(); -} - -/** Create a factory for sticky-positioning check to make code more testable */ -export const STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER = { - provide: STICKY_HEADER_SUPPORT_STRATEGY, - useFactory: STICKY_HEADER_SUPPORT_STRATEGY_FACTORY -}; +// /** Create a factory for sticky-positioning check to make code more testable */ +// export const STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER = { +// provide: STICKY_HEADER_SUPPORT_STRATEGY, +// useFactory: isPositionStickySupported() +// }; /** * Directive that marks an element as a sticky-header. Inside of a scrolling container (marked with @@ -111,7 +107,7 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { scrollable: Scrollable, @Optional() public parentRegion: CdkStickyRegion, platform: Platform, - @Inject(STICKY_HEADER_SUPPORT_STRATEGY) public _stickyPositionSupportCheck) { + @Inject(STICKY_HEADER_SUPPORT_STRATEGY) public _isPositionStickySupported) { if (platform.isBrowser) { this.element = element.nativeElement; this.upperScrollableContainer = scrollable.getElementRef().nativeElement; @@ -151,7 +147,7 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * sticky positioning. If not, use the original implementation. */ private _setStrategyAccordingToCompatibility(): void { - this._isPositionStickySupported = this._stickyPositionSupportCheck; + //this._isPositionStickySupported = this._isPositionStickySupported; if (this._isPositionStickySupported) { this.element.style.top = '0'; this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; From a555a23cc983f6608cce82a505c7ef498b3faedd Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Thu, 27 Jul 2017 09:51:09 -0700 Subject: [PATCH 09/28] optimize provider --- src/lib/sticky-header/sticky-header.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 582ff4db9083..8fe169ec8e82 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -7,7 +7,7 @@ */ import {Directive, Input, OnDestroy, AfterViewInit, ElementRef, Optional, - InjectionToken, Injectable, Inject} from '@angular/core'; + 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'; @@ -49,6 +49,12 @@ const DEBOUNCE_TIME: number = 5; export const STICKY_HEADER_SUPPORT_STRATEGY = new InjectionToken('sticky-header-support-strategy'); +/** 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 +}; + // /** Create a factory for sticky-positioning check to make code more testable */ // export const STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER = { // provide: STICKY_HEADER_SUPPORT_STRATEGY, @@ -70,8 +76,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; @@ -147,7 +151,6 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * sticky positioning. If not, use the original implementation. */ private _setStrategyAccordingToCompatibility(): void { - //this._isPositionStickySupported = this._isPositionStickySupported; if (this._isPositionStickySupported) { this.element.style.top = '0'; this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; @@ -268,9 +271,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; From 469036d54cadb366df5708e4372e71584f8e2ee6 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Thu, 27 Jul 2017 10:11:27 -0700 Subject: [PATCH 10/28] Removed unused 'fixture.detectChanges()' in test --- src/lib/sticky-header/index.ts | 2 +- src/lib/sticky-header/sticky-header.spec.ts | 22 +++------------------ 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/lib/sticky-header/index.ts b/src/lib/sticky-header/index.ts index 08e4a278f636..1c6d1b5766d2 100644 --- a/src/lib/sticky-header/index.ts +++ b/src/lib/sticky-header/index.ts @@ -9,7 +9,7 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {OverlayModule, MdCommonModule, PlatformModule} from '../core'; import {CdkStickyRegion, CdkStickyHeader, - STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER, STICKY_HEADER_SUPPORT_STRATEGY} from './sticky-header'; + STICKY_HEADER_SUPPORT_STRATEGY} from './sticky-header'; import {isPositionStickySupported} from '@angular/cdk'; diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 79703e49ce03..d6a09037b0ef 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -1,7 +1,7 @@ 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_PROVIDER} from './index'; + 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'; @@ -22,7 +22,7 @@ describe('sticky-header with positioning not supported', () => { imports: [ OverlayModule, PlatformModule, StickyHeaderModule ], declarations: [StickyHeaderTest], providers: [ - {provide: STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER, useFactory: mockStickyHeaderCheckFail()}, + {provide: STICKY_HEADER_SUPPORT_STRATEGY, useValue: false}, ], }); TestBed.compileComponents(); @@ -39,17 +39,14 @@ describe('sticky-header with positioning not supported', () => { }); it('should be able to find stickyParent when sticky positioning is not supported', () => { - fixture.detectChanges(); expect(stickyElement.nativeElement.stickyParent).not.toBe(null); }); it('should be able to find scrollableContainer when sticky positioning is not supported', () => { - fixture.detectChanges(); expect(stickyElement.nativeElement.upperScrollableContainer).not.toBe(null); }); it('should stick in the right place when scrolled to the top of the container', fakeAsync(() => { - fixture.detectChanges(); let scrollableContainerTop = stickyHeaderDir.upperScrollableContainer .getBoundingClientRect().top; expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); @@ -63,7 +60,6 @@ describe('sticky-header with positioning not supported', () => { })); it('should unstuck when scrolled off the top of the container', fakeAsync(() => { - fixture.detectChanges(); let scrollableContainerTop = stickyHeaderDir.upperScrollableContainer .getBoundingClientRect().top; expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); @@ -82,10 +78,6 @@ describe('sticky-header with positioning not supported', () => { expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); })); - - function mockStickyHeaderCheckFail() { - return false; - } }); describe('sticky-header with positioning supported', () => { @@ -93,7 +85,6 @@ describe('sticky-header with positioning supported', () => { let testComponent: StickyHeaderTest; let stickyElement: DebugElement; let stickyParentElement: DebugElement; - let scrollableElement: HTMLElement; let stickyHeaderDir: CdkStickyHeader; beforeEach(async(() => { @@ -101,8 +92,7 @@ describe('sticky-header with positioning supported', () => { imports: [ OverlayModule, PlatformModule, StickyHeaderModule ], declarations: [StickyHeaderTest], providers: [ - {provide: STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER, - useFactory: mockStickyHeaderCheckSuccess()}, + {provide: STICKY_HEADER_SUPPORT_STRATEGY, useValue: true}, ], }); TestBed.compileComponents(); @@ -118,18 +108,12 @@ describe('sticky-header with positioning supported', () => { }); it('should find sticky positioning is applied', () => { - fixture.detectChanges(); let position = window.getComputedStyle(stickyHeaderDir.element).position; expect(position).not.toBe(null); if (position != null) { expect(/sticky/i.test(position)).toBe(true); } }); - - function mockStickyHeaderCheckSuccess() { - return true; - } - }); @Component({ From 41003110b8ac13dc1fec71a6d2e21dbc200ccc0f Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Thu, 27 Jul 2017 10:15:12 -0700 Subject: [PATCH 11/28] fix --- src/lib/sticky-header/index.ts | 6 ++---- src/lib/sticky-header/sticky-header.ts | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/lib/sticky-header/index.ts b/src/lib/sticky-header/index.ts index 1c6d1b5766d2..42f669327ac4 100644 --- a/src/lib/sticky-header/index.ts +++ b/src/lib/sticky-header/index.ts @@ -9,16 +9,14 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {OverlayModule, MdCommonModule, PlatformModule} from '../core'; import {CdkStickyRegion, CdkStickyHeader, - STICKY_HEADER_SUPPORT_STRATEGY} from './sticky-header'; -import {isPositionStickySupported} from '@angular/cdk'; - + STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER} from './sticky-header'; @NgModule({ imports: [OverlayModule, MdCommonModule, CommonModule, PlatformModule], declarations: [CdkStickyRegion, CdkStickyHeader], exports: [CdkStickyRegion, CdkStickyHeader, MdCommonModule], - providers: [{provide: STICKY_HEADER_SUPPORT_STRATEGY, useFactory: isPositionStickySupported}] + providers: [STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER] }) export class StickyHeaderModule {} diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 8fe169ec8e82..2ce7f745b956 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -33,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'; From 44201f05f1f7a3c2cc9d00846a31502b95ec4cf0 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Thu, 27 Jul 2017 11:55:53 -0700 Subject: [PATCH 12/28] You can remove when sticky positioning is not supported since it's in describe --- src/lib/sticky-header/sticky-header.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index d6a09037b0ef..8a237371271f 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -38,11 +38,11 @@ describe('sticky-header with positioning not supported', () => { scrollableElement = fixture.debugElement.query(By.directive(Scrollable)); }); - it('should be able to find stickyParent when sticky positioning is not supported', () => { + it('should be able to find stickyParent', () => { expect(stickyElement.nativeElement.stickyParent).not.toBe(null); }); - it('should be able to find scrollableContainer when sticky positioning is not supported', () => { + it('should be able to find scrollableContainer', () => { expect(stickyElement.nativeElement.upperScrollableContainer).not.toBe(null); }); From 4aa498add855876865bfc885e53c8e2321087c65 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Thu, 27 Jul 2017 11:57:51 -0700 Subject: [PATCH 13/28] stickyHeaderDir.stickyParent --- src/lib/sticky-header/sticky-header.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 8a237371271f..9616e3323425 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -39,11 +39,11 @@ describe('sticky-header with positioning not supported', () => { }); it('should be able to find stickyParent', () => { - expect(stickyElement.nativeElement.stickyParent).not.toBe(null); + expect(stickyHeaderDir.stickyParent).not.toBe(null); }); it('should be able to find scrollableContainer', () => { - expect(stickyElement.nativeElement.upperScrollableContainer).not.toBe(null); + expect(stickyHeaderDir.upperScrollableContainer).not.toBe(null); }); it('should stick in the right place when scrolled to the top of the container', fakeAsync(() => { From 4adcd526b47ef5b241957228c3acd6193c48b025 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Thu, 27 Jul 2017 11:59:11 -0700 Subject: [PATCH 14/28] remove commented code --- src/lib/sticky-header/sticky-header.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 2ce7f745b956..91c7b83fb2a4 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -54,12 +54,6 @@ export const STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER: Provider = { useFactory: isPositionStickySupported }; -// /** Create a factory for sticky-positioning check to make code more testable */ -// export const STICKY_HEADER_SUPPORT_STRATEGY_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 From 783274c1f2d0e4be4179734e01a83edac394134c Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 10:59:38 -0700 Subject: [PATCH 15/28] rename stickyHeaderDir to stickyHeader --- src/lib/sticky-header/sticky-header.spec.ts | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 9616e3323425..e5593a5b05d4 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -15,7 +15,7 @@ describe('sticky-header with positioning not supported', () => { let stickyElement: DebugElement; let stickyParentElement: DebugElement; let scrollableElement: DebugElement; - let stickyHeaderDir: CdkStickyHeader; + let stickyHeader: CdkStickyHeader; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -34,48 +34,48 @@ describe('sticky-header with positioning not supported', () => { testComponent = fixture.debugElement.componentInstance; stickyElement = fixture.debugElement.query(By.directive(CdkStickyHeader)); stickyParentElement = fixture.debugElement.query(By.directive(CdkStickyRegion)); - stickyHeaderDir = stickyElement.injector.get(CdkStickyHeader); + stickyHeader = stickyElement.injector.get(CdkStickyHeader); scrollableElement = fixture.debugElement.query(By.directive(Scrollable)); }); it('should be able to find stickyParent', () => { - expect(stickyHeaderDir.stickyParent).not.toBe(null); + expect(stickyHeader.stickyParent).not.toBe(null); }); it('should be able to find scrollableContainer', () => { - expect(stickyHeaderDir.upperScrollableContainer).not.toBe(null); + expect(stickyHeader.upperScrollableContainer).not.toBe(null); }); it('should stick in the right place when scrolled to the top of the container', fakeAsync(() => { - let scrollableContainerTop = stickyHeaderDir.upperScrollableContainer + let scrollableContainerTop = stickyHeader.upperScrollableContainer .getBoundingClientRect().top; - expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); tick(0); // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); tick(100); - expect(stickyHeaderDir.element.getBoundingClientRect().top).toBe(scrollableContainerTop); + expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); })); it('should unstuck when scrolled off the top of the container', fakeAsync(() => { - let scrollableContainerTop = stickyHeaderDir.upperScrollableContainer + let scrollableContainerTop = stickyHeader.upperScrollableContainer .getBoundingClientRect().top; - expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); tick(0); // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); tick(100); - expect(stickyHeaderDir.element.getBoundingClientRect().top).toBe(scrollableContainerTop); + expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); // Scroll the scrollableContainer down to unstuck fixture.componentInstance.scrollBack(); tick(100); - expect(stickyHeaderDir.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); + expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); })); }); @@ -85,7 +85,7 @@ describe('sticky-header with positioning supported', () => { let testComponent: StickyHeaderTest; let stickyElement: DebugElement; let stickyParentElement: DebugElement; - let stickyHeaderDir: CdkStickyHeader; + let stickyHeader: CdkStickyHeader; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -104,11 +104,11 @@ describe('sticky-header with positioning supported', () => { testComponent = fixture.debugElement.componentInstance; stickyElement = fixture.debugElement.query(By.directive(CdkStickyHeader)); stickyParentElement = fixture.debugElement.query(By.directive(CdkStickyRegion)); - stickyHeaderDir = stickyElement.injector.get(CdkStickyHeader); + stickyHeader = stickyElement.injector.get(CdkStickyHeader); }); it('should find sticky positioning is applied', () => { - let position = window.getComputedStyle(stickyHeaderDir.element).position; + let position = window.getComputedStyle(stickyHeader.element).position; expect(position).not.toBe(null); if (position != null) { expect(/sticky/i.test(position)).toBe(true); From 9c28f3408cfa3175a1aaf76b53203fa93beb3df9 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 11:35:11 -0700 Subject: [PATCH 16/28] Changed to use 'ngFor' to expand content instead of typing tons of '

' --- src/lib/sticky-header/sticky-header.spec.ts | 40 ++++++++------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index e5593a5b05d4..3a1f1fd41a87 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -123,40 +123,30 @@ describe('sticky-header with positioning supported', () => { -moz-appearance: none; height: 300px; overflow: auto;"> -

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-
+

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

+

Heading 1

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

-

test test test

+

{{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; From b037d83aff999cf2e2be65dc6966318078b7a7cb Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 11:42:44 -0700 Subject: [PATCH 17/28] Add a comment that explains what these inline styles are for? Any reason you can't set them via the styles configuration for the component? --- src/lib/sticky-header/sticky-header.spec.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 3a1f1fd41a87..20f5cd5675a2 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -117,15 +117,24 @@ describe('sticky-header with positioning supported', () => { }); @Component({ - template: ` -
+ overflow: auto; + } + .heading-style { + background: whitesmoke; + padding: 5px; + } + `], + template: ` +

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

-
+

Heading 1

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

From 13d7aca02eff554a2463339c3968cf763ce4fb50 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 11:45:00 -0700 Subject: [PATCH 18/28] Added explainations on styles --- src/lib/sticky-header/sticky-header.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 20f5cd5675a2..338ae654429b 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -117,6 +117,8 @@ describe('sticky-header with positioning supported', () => { }); @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; From 824b305e067570c4bcbc36e6e8aa99755f8c1fe1 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 11:48:41 -0700 Subject: [PATCH 19/28] Added /** @docs-private */ --- src/lib/sticky-header/sticky-header.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index 91c7b83fb2a4..6185b4f5c783 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -48,7 +48,9 @@ const DEBOUNCE_TIME: number = 5; export const STICKY_HEADER_SUPPORT_STRATEGY = new InjectionToken('sticky-header-support-strategy'); -/** Create a factory for sticky-positioning check to make code more testable */ +/** @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 From 72706d8477a2f84429b3a31bbbbbc82073c27f6b Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 12:06:39 -0700 Subject: [PATCH 20/28] Added comments to explaining why tick(100) is needed. Changed 'tick(0)' to 'tick()' --- src/lib/sticky-header/sticky-header.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 338ae654429b..a8b5c2aa25a4 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -50,10 +50,12 @@ describe('sticky-header with positioning not supported', () => { let scrollableContainerTop = stickyHeader.upperScrollableContainer .getBoundingClientRect().top; expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); - tick(0); + tick(); // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); + // Use tick(100) to tick forward to let stickyHeader's _applyStickyPositionStyles() + // function finished tick(100); expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); @@ -63,10 +65,12 @@ describe('sticky-header with positioning not supported', () => { let scrollableContainerTop = stickyHeader.upperScrollableContainer .getBoundingClientRect().top; expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop); - tick(0); + tick(); // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); + // Use tick(100) to tick forward to let stickyHeader's _applyStickyPositionStyles() + // function finished tick(100); expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); From 886b6221892613e0e0775316d49c81a91a944ee6 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 15:52:49 -0700 Subject: [PATCH 21/28] nit --- src/lib/sticky-header/sticky-header.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index a8b5c2aa25a4..9d0fe830949d 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -1,7 +1,7 @@ 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 {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'; From 089a0ca5b0c34e4dd9711acbd74fb2ec373656f6 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 16:07:40 -0700 Subject: [PATCH 22/28] explained why need 'tick(100)' --- src/lib/sticky-header/sticky-header.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 9d0fe830949d..3d0b349627f4 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -1,7 +1,11 @@ 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 { + 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'; @@ -54,8 +58,7 @@ describe('sticky-header with positioning not supported', () => { // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); - // Use tick(100) to tick forward to let stickyHeader's _applyStickyPositionStyles() - // function finished + // wait till animation has finished tick(100); expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); @@ -69,8 +72,7 @@ describe('sticky-header with positioning not supported', () => { // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); - // Use tick(100) to tick forward to let stickyHeader's _applyStickyPositionStyles() - // function finished + // wait till animation has finished tick(100); expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); From 3391650569ca84aa936a3d2a876595a348b03299 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 16:13:21 -0700 Subject: [PATCH 23/28] expect(/sticky/i.test(position!)).toBe(true); --- src/lib/sticky-header/sticky-header.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 3d0b349627f4..1b67e62178fd 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -116,9 +116,7 @@ describe('sticky-header with positioning supported', () => { it('should find sticky positioning is applied', () => { let position = window.getComputedStyle(stickyHeader.element).position; expect(position).not.toBe(null); - if (position != null) { - expect(/sticky/i.test(position)).toBe(true); - } + expect(/sticky/i.test(position!)).toBe(true); }); }); From 54dccf6c24bffe826c44b69b7a13a1f1d64de007 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 16:15:53 -0700 Subject: [PATCH 24/28] change CSS indent to +2 --- src/lib/sticky-header/sticky-header.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 1b67e62178fd..df9f31b7335a 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -125,15 +125,15 @@ describe('sticky-header with positioning supported', () => { // 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; + text-align: center; + -webkit-appearance: none; + -moz-appearance: none; + height: 300px; + overflow: auto; } .heading-style { - background: whitesmoke; - padding: 5px; + background: whitesmoke; + padding: 5px; } `], template: ` From cc53d2862a66f77f3e33ac68cee903a15bb248ab Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Mon, 31 Jul 2017 16:18:16 -0700 Subject: [PATCH 25/28] nit --- src/lib/sticky-header/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/sticky-header/index.ts b/src/lib/sticky-header/index.ts index 42f669327ac4..b515617bef3d 100644 --- a/src/lib/sticky-header/index.ts +++ b/src/lib/sticky-header/index.ts @@ -8,8 +8,11 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {OverlayModule, MdCommonModule, PlatformModule} from '../core'; -import {CdkStickyRegion, CdkStickyHeader, - STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER} from './sticky-header'; +import { + CdkStickyRegion, + CdkStickyHeader, + STICKY_HEADER_SUPPORT_STRATEGY_PROVIDER +} from './sticky-header'; @NgModule({ From 0abc5ee2eec35034a7fa6ef65a3964c37b4cf650 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Tue, 1 Aug 2017 11:21:47 -0700 Subject: [PATCH 26/28] Added test for sticky-header without StickyRegion --- src/lib/sticky-header/sticky-header.spec.ts | 73 +++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index df9f31b7335a..55acc3fa2cca 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -120,6 +120,38 @@ describe('sticky-header with positioning supported', () => { }); }); +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.toBe(null); + 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. @@ -178,3 +210,44 @@ class StickyHeaderTest { 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?'}, + ]; +} From b943a623ac68360e45237f97b9d5fe79042f6472 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Wed, 2 Aug 2017 10:31:12 -0700 Subject: [PATCH 27/28] change 'toBe(null)' to 'toBeNull()' --- src/lib/sticky-header/sticky-header.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 55acc3fa2cca..15a179c359ed 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -43,11 +43,11 @@ describe('sticky-header with positioning not supported', () => { }); it('should be able to find stickyParent', () => { - expect(stickyHeader.stickyParent).not.toBe(null); + expect(stickyHeader.stickyParent).not.toBeNull(); }); it('should be able to find scrollableContainer', () => { - expect(stickyHeader.upperScrollableContainer).not.toBe(null); + expect(stickyHeader.upperScrollableContainer).not.toBeNull(); }); it('should stick in the right place when scrolled to the top of the container', fakeAsync(() => { @@ -115,7 +115,7 @@ describe('sticky-header with positioning supported', () => { it('should find sticky positioning is applied', () => { let position = window.getComputedStyle(stickyHeader.element).position; - expect(position).not.toBe(null); + expect(position).not.toBeNull(); expect(/sticky/i.test(position!)).toBe(true); }); }); @@ -147,7 +147,7 @@ describe('test sticky-header without StickyRegion', () => { it('should be able to find stickyParent', () => { let p = stickyHeader.stickyParent; - expect(p).not.toBe(null); + expect(p).not.toBeNull(); expect(p!.id).toBe('default-region'); }); }); From 94e647406059d30b38357ac28db8ef4c066e8d41 Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Fri, 4 Aug 2017 11:00:37 -0700 Subject: [PATCH 28/28] Cool ! tick(debounce time) --- src/lib/sticky-header/sticky-header.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.spec.ts b/src/lib/sticky-header/sticky-header.spec.ts index 15a179c359ed..c4b395ae2757 100644 --- a/src/lib/sticky-header/sticky-header.spec.ts +++ b/src/lib/sticky-header/sticky-header.spec.ts @@ -11,7 +11,7 @@ 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; @@ -58,8 +58,8 @@ describe('sticky-header with positioning not supported', () => { // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); - // wait till animation has finished - tick(100); + // wait for the DEBOUNCE_TIME + tick(DEBOUNCE_TIME); expect(stickyHeader.element.getBoundingClientRect().top).toBe(scrollableContainerTop); })); @@ -72,14 +72,14 @@ describe('sticky-header with positioning not supported', () => { // Scroll the scrollableContainer up to stick fixture.componentInstance.scrollDown(); - // wait till animation has finished - tick(100); + // 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(100); + tick(DEBOUNCE_TIME); expect(stickyHeader.element.getBoundingClientRect().top).not.toBe(scrollableContainerTop);