-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(sticky-header): Initial version of sticky header new PRNew Sticky header #5858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
140 commits
Select commit
Hold shift + click to select a range
cfc5c9d
add lib files for sticky-header
sllethe d370455
fix some code according to PR review comments
sllethe 89f8bde
change some format to pass TSlint check
sllethe 97b9af1
add '_' before private elements
sllethe 8d632a9
delete @Injectable for StickyHeaderDirective. Because we do not need …
sllethe bb1fc6e
refine code
sllethe e7a51d0
encapsulate 'set style for element'
sllethe fbd2f97
change @Input()
sllethe f73ff9a
Delete 'Observable.fromEvent(this.upperScrollableContainer, 'scroll')'
sllethe 1f13a51
add const STICK_START_CLASS and STICK_END_CLASS
sllethe 7a140ec
Add doc for [cdkStickyRegion] and 'unstuckElement()'. Delete 'detach(…
sllethe 80abe92
change 'MdStickyHeaderModule' to 'CdkStickyHeaderModule';
sllethe e20697e
encapsulate reset css style operation for sticky header.
sllethe c45688d
delete unnecessary gloable variables
sllethe bea44a0
delete global variable '_width'
sllethe 461e771
Add doc for 'sticker()' function. explained how it works.
sllethe a69958f
add more doc for 'sticker()', explaining 'isStuck' flag
sllethe 711455f
2 space for indent
sllethe f7d6f36
fix
sllethe feddf5e
delete sticky-header demo part from this branch
sllethe 95a128e
revert firebase file
sllethe 6e8e9d2
change code according to comments in PR
sllethe 3dbe181
revert firbaserc
sllethe 52d1381
revert demo-app.ts
sllethe d0b1055
revert routes.ts
sllethe 9bf23da
revert demo-app-module.ts
sllethe 5bbae16
change
sllethe afed8b2
fix the problem of : 'this.stickyParent' might be 'null'
sllethe 8f52999
change 'CdkStickyHeaderModule' to 'StickyHeaderModule'
sllethe c6973a2
change doc
sllethe fc8e9d9
Change the constructor of 'cdkStickyRegion' to 'constructor(public re…
sllethe 7eb2cff
Added prefix 'mat-' for CSS class
sllethe d8fef5e
Delete 'public' before variables
sllethe e4b5bf0
Object.assign isn't supported in IE11; use extendObject from src/lib/…
sllethe 019e852
IE11 will have trouble with `translate3d(0, 0, 0);', change to `tra…
sllethe 2561fdd
Added docs for all variables
sllethe 0b2746b
extract 'generate CSS style'
sllethe a3cd649
created a generateStyleCSS() function, let it be responsible for gene…
sllethe a996d4c
reformat
sllethe 4139062
add debounce to solve 'getBoundingClientRect() cause slow down' problem.
sllethe 030753c
add position:sticky and check whether browser support it. If not , us…
sllethe 6cd819e
removed unused import
sllethe 1903487
Removed unused 'scrollableRegion' and 'parentRegion'
sllethe 2f44367
removed commented lines
sllethe b284e50
default public
sllethe 54cec3f
Add comments about why setting style top and position for iPhone and …
sllethe 14a0373
format
sllethe 6f4a671
consider all circumstances of browser.
sllethe a9e5f3f
use "===" instead of '=='
sllethe 1c92794
make 'navigator.userAgent.toLocaleLowerCase()' a local variable
sllethe bc6c4ea
optimize
sllethe a8b3a2d
Added comments on const 'STICK_START_CLASS' and 'STICK_END_CLASS'.
sllethe 45f78e7
Added comments for STICK_START_CLASS and STICK_END_CLASS.
sllethe e6400c2
Changed the format of one-line JsDoc
sllethe 0721716
unsubscribe sbscriptions onDestory
sllethe b6248d4
Use what modernizr does on compatibility instead of get the browser v…
sllethe d0eaa43
add 'padding' and 'stickyRegionHeight' variables to avoid calling 'ge…
sllethe 7101052
move docs above @Directive
sllethe 7cb26e2
removed the underscore in'_element: ElementRef',
sllethe f607048
expand 'reg' to 'region'
sllethe c191c3f
use 'if (this.isIE)' instead of 'if(this.isIE === true)'
sllethe b8aae78
added more newlines between params in 'generateCssStyle()' function t…
sllethe 9e6b714
Added reference link to Modernizer in docs of getSupportList()
sllethe a69a0c7
Deleted "_supportList" variable
sllethe ce92cc5
renamed 'isIE' to 'isStickyPositionSupported', and removed extra spac…
sllethe 4cb13fe
Set debounce time as a const variable
sllethe 46b8555
Added docs for 'const DEBOUNCE_TIME: number = 5;'
sllethe 0d875f8
Changed ' if(this.stickyParent == null)' to ' if(!this.stickyParent)'
sllethe b1b1d1c
Removed the @param and @returns and make sure the types are correct …
sllethe 62f1a29
Added docs for `isStickyPositionSupported` variable
sllethe 005fb8a
changed '+=' to '=' of 'stickyText' in getSupportList() function
sllethe 07307f4
nit added " " between 'if' and '('
sllethe 9eb5bda
nit
sllethe aa87066
Added comments
sllethe c7d72d7
deleted unused import
sllethe b9a3b7c
change comments
sllethe 1bf0e07
optimize comments
sllethe 192ef2d
deleted unnecessary global variables(padding and stickyRegionHeight)
sllethe 9546b18
Added check whether we are on browser
sllethe 127f18b
Array<string> to string[]
sllethe bf64a10
test?
sllethe 02c7a82
try to reopen the old PR
sllethe 2981a72
2524fe5
revert list.ts
sllethe d3e3b67
test
sllethe b22ce2e
test 222
sllethe 568cd46
revert demo
sllethe b614bd7
547ae64
6ef102c
9ff3b7d
9e5bba3
7898370
65e84cc
19bf26c
328f39d
30ec433
6c93866
3e881e8
e5e888f
0f680f4
f55cd9b
bcfde65
cd14090
3f3b5d8
fix CSSStyleDeclaration
sllethe 5191f10
de93a04
1cedcfa
b99a76c
c98c6a7
e4a2b05
nit
sllethe deae9cc
eb68ad9
49636eb
490a4d9
1ee0b1f
for comment 'Can you evaluate each method to make sure their accessor…
sllethe 11b3818
9dd41da
9ddc4a8
4684e8e
86d35c4
2a7496b
c5e2913
938cb94
688210f
be465ec
36b0450
46eaaf0
b1bbee3
7123975
Format like TODO(sllethe): ...
sllethe 7b4bf1f
5ff2394
ea47c55
94669e9
135c8b6
8f2cff3
2791cca
ac07aef
663564b
rename 'sticker()' to '_applyStickyPositionStyles()'
sllethe 8230411
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/** | ||
* @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 {NgModule} from '@angular/core'; | ||
import {CommonModule} from '@angular/common'; | ||
import {OverlayModule, MdCommonModule, PlatformModule} from '../core'; | ||
import {CdkStickyRegion, CdkStickyHeader} from './sticky-header'; | ||
|
||
|
||
|
||
@NgModule({ | ||
imports: [OverlayModule, MdCommonModule, CommonModule, PlatformModule], | ||
declarations: [CdkStickyRegion, CdkStickyHeader], | ||
exports: [CdkStickyRegion, CdkStickyHeader, MdCommonModule], | ||
}) | ||
export class StickyHeaderModule {} | ||
|
||
|
||
export * from './sticky-header'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename
p
asprefix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually prefer single-character variables like this in map, filter, etc.