Skip to content

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 140 commits into from
Jul 25, 2017
Merged
Show file tree
Hide file tree
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 Jun 16, 2017
d370455
fix some code according to PR review comments
sllethe Jun 21, 2017
89f8bde
change some format to pass TSlint check
sllethe Jun 21, 2017
97b9af1
add '_' before private elements
sllethe Jun 21, 2017
8d632a9
delete @Injectable for StickyHeaderDirective. Because we do not need …
sllethe Jun 21, 2017
bb1fc6e
refine code
sllethe Jun 21, 2017
e7a51d0
encapsulate 'set style for element'
sllethe Jun 21, 2017
fbd2f97
change @Input()
sllethe Jun 22, 2017
f73ff9a
Delete 'Observable.fromEvent(this.upperScrollableContainer, 'scroll')'
sllethe Jun 22, 2017
1f13a51
add const STICK_START_CLASS and STICK_END_CLASS
sllethe Jun 22, 2017
7a140ec
Add doc for [cdkStickyRegion] and 'unstuckElement()'. Delete 'detach(…
sllethe Jun 22, 2017
80abe92
change 'MdStickyHeaderModule' to 'CdkStickyHeaderModule';
sllethe Jun 22, 2017
e20697e
encapsulate reset css style operation for sticky header.
sllethe Jun 22, 2017
c45688d
delete unnecessary gloable variables
sllethe Jun 22, 2017
bea44a0
delete global variable '_width'
sllethe Jun 22, 2017
461e771
Add doc for 'sticker()' function. explained how it works.
sllethe Jun 22, 2017
a69958f
add more doc for 'sticker()', explaining 'isStuck' flag
sllethe Jun 22, 2017
711455f
2 space for indent
sllethe Jun 22, 2017
f7d6f36
fix
sllethe Jun 22, 2017
feddf5e
delete sticky-header demo part from this branch
sllethe Jun 22, 2017
95a128e
revert firebase file
sllethe Jun 22, 2017
6e8e9d2
change code according to comments in PR
sllethe Jun 22, 2017
3dbe181
revert firbaserc
sllethe Jun 23, 2017
52d1381
revert demo-app.ts
sllethe Jun 23, 2017
d0b1055
revert routes.ts
sllethe Jun 23, 2017
9bf23da
revert demo-app-module.ts
sllethe Jun 23, 2017
5bbae16
change
sllethe Jun 23, 2017
afed8b2
fix the problem of : 'this.stickyParent' might be 'null'
sllethe Jun 26, 2017
8f52999
change 'CdkStickyHeaderModule' to 'StickyHeaderModule'
sllethe Jun 26, 2017
c6973a2
change doc
sllethe Jun 26, 2017
fc8e9d9
Change the constructor of 'cdkStickyRegion' to 'constructor(public re…
sllethe Jun 26, 2017
7eb2cff
Added prefix 'mat-' for CSS class
sllethe Jun 26, 2017
d8fef5e
Delete 'public' before variables
sllethe Jun 26, 2017
e4b5bf0
Object.assign isn't supported in IE11; use extendObject from src/lib/…
sllethe Jun 26, 2017
019e852
IE11 will have trouble with `translate3d(0, 0, 0);', change to `tra…
sllethe Jun 26, 2017
2561fdd
Added docs for all variables
sllethe Jun 26, 2017
0b2746b
extract 'generate CSS style'
sllethe Jun 26, 2017
a3cd649
created a generateStyleCSS() function, let it be responsible for gene…
sllethe Jun 27, 2017
a996d4c
reformat
sllethe Jun 27, 2017
4139062
add debounce to solve 'getBoundingClientRect() cause slow down' problem.
sllethe Jul 7, 2017
030753c
add position:sticky and check whether browser support it. If not , us…
sllethe Jul 12, 2017
6cd819e
removed unused import
sllethe Jul 12, 2017
1903487
Removed unused 'scrollableRegion' and 'parentRegion'
sllethe Jul 12, 2017
2f44367
removed commented lines
sllethe Jul 12, 2017
b284e50
default public
sllethe Jul 12, 2017
54cec3f
Add comments about why setting style top and position for iPhone and …
sllethe Jul 13, 2017
14a0373
format
sllethe Jul 13, 2017
6f4a671
consider all circumstances of browser.
sllethe Jul 13, 2017
a9e5f3f
use "===" instead of '=='
sllethe Jul 13, 2017
1c92794
make 'navigator.userAgent.toLocaleLowerCase()' a local variable
sllethe Jul 13, 2017
bc6c4ea
optimize
sllethe Jul 14, 2017
a8b3a2d
Added comments on const 'STICK_START_CLASS' and 'STICK_END_CLASS'.
sllethe Jul 14, 2017
45f78e7
Added comments for STICK_START_CLASS and STICK_END_CLASS.
sllethe Jul 14, 2017
e6400c2
Changed the format of one-line JsDoc
sllethe Jul 14, 2017
0721716
unsubscribe sbscriptions onDestory
sllethe Jul 14, 2017
b6248d4
Use what modernizr does on compatibility instead of get the browser v…
sllethe Jul 14, 2017
d0eaa43
add 'padding' and 'stickyRegionHeight' variables to avoid calling 'ge…
sllethe Jul 14, 2017
7101052
move docs above @Directive
sllethe Jul 15, 2017
7cb26e2
removed the underscore in'_element: ElementRef',
sllethe Jul 15, 2017
f607048
expand 'reg' to 'region'
sllethe Jul 15, 2017
c191c3f
use 'if (this.isIE)' instead of 'if(this.isIE === true)'
sllethe Jul 15, 2017
b8aae78
added more newlines between params in 'generateCssStyle()' function t…
sllethe Jul 15, 2017
9e6b714
Added reference link to Modernizer in docs of getSupportList()
sllethe Jul 17, 2017
a69a0c7
Deleted "_supportList" variable
sllethe Jul 17, 2017
ce92cc5
renamed 'isIE' to 'isStickyPositionSupported', and removed extra spac…
sllethe Jul 17, 2017
4cb13fe
Set debounce time as a const variable
sllethe Jul 17, 2017
46b8555
Added docs for 'const DEBOUNCE_TIME: number = 5;'
sllethe Jul 17, 2017
0d875f8
Changed ' if(this.stickyParent == null)' to ' if(!this.stickyParent)'
sllethe Jul 17, 2017
b1b1d1c
Removed the @param and @returns and make sure the types are correct …
sllethe Jul 17, 2017
62f1a29
Added docs for `isStickyPositionSupported` variable
sllethe Jul 17, 2017
005fb8a
changed '+=' to '=' of 'stickyText' in getSupportList() function
sllethe Jul 17, 2017
07307f4
nit added " " between 'if' and '('
sllethe Jul 17, 2017
9eb5bda
nit
sllethe Jul 17, 2017
aa87066
Added comments
sllethe Jul 17, 2017
c7d72d7
deleted unused import
sllethe Jul 17, 2017
b9a3b7c
change comments
sllethe Jul 17, 2017
1bf0e07
optimize comments
sllethe Jul 17, 2017
192ef2d
deleted unnecessary global variables(padding and stickyRegionHeight)
sllethe Jul 18, 2017
9546b18
Added check whether we are on browser
sllethe Jul 18, 2017
127f18b
Array<string> to string[]
sllethe Jul 18, 2017
bf64a10
test?
sllethe Jul 18, 2017
02c7a82
try to reopen the old PR
sllethe Jul 18, 2017
2981a72
sllethe Jul 18, 2017
2524fe5
revert list.ts
sllethe Jul 18, 2017
d3e3b67
test
sllethe Jul 18, 2017
b22ce2e
test 222
sllethe Jul 18, 2017
568cd46
revert demo
sllethe Jul 18, 2017
b614bd7
sllethe Jul 18, 2017
547ae64
sllethe Jul 18, 2017
6ef102c
sllethe Jul 18, 2017
9ff3b7d
sllethe Jul 18, 2017
9e5bba3
sllethe Jul 19, 2017
7898370
sllethe Jul 19, 2017
65e84cc
sllethe Jul 19, 2017
19bf26c
sllethe Jul 19, 2017
328f39d
sllethe Jul 19, 2017
30ec433
sllethe Jul 19, 2017
6c93866
sllethe Jul 19, 2017
3e881e8
sllethe Jul 19, 2017
e5e888f
sllethe Jul 19, 2017
0f680f4
sllethe Jul 19, 2017
f55cd9b
sllethe Jul 20, 2017
bcfde65
sllethe Jul 20, 2017
cd14090
sllethe Jul 20, 2017
3f3b5d8
fix CSSStyleDeclaration
sllethe Jul 20, 2017
5191f10
sllethe Jul 20, 2017
de93a04
sllethe Jul 20, 2017
1cedcfa
sllethe Jul 20, 2017
b99a76c
sllethe Jul 20, 2017
c98c6a7
sllethe Jul 20, 2017
e4a2b05
nit
sllethe Jul 20, 2017
deae9cc
sllethe Jul 20, 2017
eb68ad9
sllethe Jul 21, 2017
49636eb
sllethe Jul 21, 2017
490a4d9
sllethe Jul 21, 2017
1ee0b1f
for comment 'Can you evaluate each method to make sure their accessor…
sllethe Jul 21, 2017
11b3818
sllethe Jul 21, 2017
9dd41da
sllethe Jul 21, 2017
9ddc4a8
sllethe Jul 21, 2017
4684e8e
sllethe Jul 21, 2017
86d35c4
sllethe Jul 21, 2017
2a7496b
sllethe Jul 21, 2017
c5e2913
sllethe Jul 21, 2017
938cb94
sllethe Jul 21, 2017
688210f
sllethe Jul 24, 2017
be465ec
sllethe Jul 24, 2017
36b0450
sllethe Jul 24, 2017
46eaaf0
sllethe Jul 24, 2017
b1bbee3
sllethe Jul 24, 2017
7123975
Format like TODO(sllethe): ...
sllethe Jul 24, 2017
7b4bf1f
sllethe Jul 24, 2017
5ff2394
sllethe Jul 24, 2017
ea47c55
sllethe Jul 24, 2017
94669e9
sllethe Jul 24, 2017
135c8b6
sllethe Jul 24, 2017
8f2cff3
sllethe Jul 24, 2017
2791cca
sllethe Jul 24, 2017
ac07aef
sllethe Jul 24, 2017
663564b
rename 'sticker()' to '_applyStickyPositionStyles()'
sllethe Jul 24, 2017
8230411
sllethe Jul 24, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/cdk/platform/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,22 @@ export function getSupportedInputTypes(): Set<string> {

return supportedInputTypes;
}

let computedPositionStickySupported: boolean | null = null;

/**
* Whether the browser support css `position: sticky`.
* Based on the check from modernizr:
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/positionsticky.js
*/
export function isPositionStickySupported() {
if (computedPositionStickySupported != null) {
return computedPositionStickySupported;
}

const elementStyle = document.createElement('div').style;
elementStyle.cssText = ['', '-webkit-'].map(p => `position: ${p}sticky`).join(';');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename p as prefix

Copy link
Member

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.

computedPositionStickySupported = elementStyle.cssText.indexOf('sticky') !== -1;
return computedPositionStickySupported;
}

4 changes: 3 additions & 1 deletion src/lib/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {MdExpansionModule} from './expansion/index';
import {MdTableModule} from './table/index';
import {MdSortModule} from './sort/index';
import {MdPaginatorModule} from './paginator/index';
import {StickyHeaderModule} from './sticky-header/index';

const MATERIAL_MODULES = [
MdAutocompleteModule,
Expand Down Expand Up @@ -86,7 +87,8 @@ const MATERIAL_MODULES = [
A11yModule,
PlatformModule,
MdCommonModule,
ObserveContentModule
ObserveContentModule,
StickyHeaderModule,
];

/** @deprecated */
Expand Down
1 change: 1 addition & 0 deletions src/lib/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ export * from './tabs/index';
export * from './tabs/tab-nav-bar/index';
export * from './toolbar/index';
export * from './tooltip/index';
export * from './sticky-header/index';
23 changes: 23 additions & 0 deletions src/lib/sticky-header/index.ts
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';
289 changes: 289 additions & 0 deletions src/lib/sticky-header/sticky-header.ts
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();
}
}