Skip to content

Commit 4d31485

Browse files
sllethetinayuangao
authored andcommitted
feat(sticky-header): Initial version of sticky header new PRNew Sticky header (#5858)
* 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 'CdkStickyHeaderModule' to 'StickyHeaderModule' * 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<string> 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 * rename 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 isPositionStickySupported() to 'src/cdk/platform/featrues.ts' * 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()'
1 parent b018f26 commit 4d31485

File tree

5 files changed

+335
-1
lines changed

5 files changed

+335
-1
lines changed

src/cdk/platform/features.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,22 @@ export function getSupportedInputTypes(): Set<string> {
6262

6363
return supportedInputTypes;
6464
}
65+
66+
let computedPositionStickySupported: boolean | null = null;
67+
68+
/**
69+
* Whether the browser support css `position: sticky`.
70+
* Based on the check from modernizr:
71+
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/positionsticky.js
72+
*/
73+
export function isPositionStickySupported() {
74+
if (computedPositionStickySupported != null) {
75+
return computedPositionStickySupported;
76+
}
77+
78+
const elementStyle = document.createElement('div').style;
79+
elementStyle.cssText = ['', '-webkit-'].map(p => `position: ${p}sticky`).join(';');
80+
computedPositionStickySupported = elementStyle.cssText.indexOf('sticky') !== -1;
81+
return computedPositionStickySupported;
82+
}
83+

src/lib/module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {MdExpansionModule} from './expansion/index';
4848
import {MdTableModule} from './table/index';
4949
import {MdSortModule} from './sort/index';
5050
import {MdPaginatorModule} from './paginator/index';
51+
import {StickyHeaderModule} from './sticky-header/index';
5152

5253
const MATERIAL_MODULES = [
5354
MdAutocompleteModule,
@@ -86,7 +87,8 @@ const MATERIAL_MODULES = [
8687
A11yModule,
8788
PlatformModule,
8889
MdCommonModule,
89-
ObserveContentModule
90+
ObserveContentModule,
91+
StickyHeaderModule,
9092
];
9193

9294
/** @deprecated */

src/lib/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ export * from './tabs/index';
4444
export * from './tabs/tab-nav-bar/index';
4545
export * from './toolbar/index';
4646
export * from './tooltip/index';
47+
export * from './sticky-header/index';

src/lib/sticky-header/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {NgModule} from '@angular/core';
9+
import {CommonModule} from '@angular/common';
10+
import {OverlayModule, MdCommonModule, PlatformModule} from '../core';
11+
import {CdkStickyRegion, CdkStickyHeader} from './sticky-header';
12+
13+
14+
15+
@NgModule({
16+
imports: [OverlayModule, MdCommonModule, CommonModule, PlatformModule],
17+
declarations: [CdkStickyRegion, CdkStickyHeader],
18+
exports: [CdkStickyRegion, CdkStickyHeader, MdCommonModule],
19+
})
20+
export class StickyHeaderModule {}
21+
22+
23+
export * from './sticky-header';
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {Directive, Input,
9+
OnDestroy, AfterViewInit, ElementRef, Optional} from '@angular/core';
10+
import {Platform} from '../core/platform';
11+
import {Scrollable} from '../core/overlay/scroll/scrollable';
12+
import {extendObject} from '../core/util/object-extend';
13+
import {Subscription} from 'rxjs/Subscription';
14+
import {fromEvent} from 'rxjs/observable/fromEvent';
15+
import {RxChain, debounceTime} from '../core/rxjs/index';
16+
import {isPositionStickySupported} from '@angular/cdk';
17+
18+
19+
/**
20+
* Directive that marks an element as a "sticky region", meant to contain exactly one sticky-header
21+
* along with the content associated with that header. The sticky-header inside of the region will
22+
* "stick" to the top of the scrolling container as long as this region is within the scrolling
23+
* viewport.
24+
*
25+
* If a user does not explicitly define a sticky-region for a sticky-header, the direct
26+
* parent node of the sticky-header will be used as the sticky-region.
27+
*/
28+
@Directive({
29+
selector: '[cdkStickyRegion]',
30+
})
31+
export class CdkStickyRegion {
32+
constructor(public readonly _elementRef: ElementRef) { }
33+
}
34+
35+
36+
/** Class applied when the header is "stuck" */
37+
const STICK_START_CLASS = 'cdk-sticky-header-start';
38+
39+
/** Class applied when the header is not "stuck" */
40+
const STICK_END_CLASS = 'cdk-sticky-header-end';
41+
42+
/**
43+
* Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize,
44+
* touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the
45+
* positioning adversely.
46+
*/
47+
const DEBOUNCE_TIME: number = 5;
48+
49+
/**
50+
* Directive that marks an element as a sticky-header. Inside of a scrolling container (marked with
51+
* cdkScrollable), this header will "stick" to the top of the scrolling viewport while its sticky
52+
* region (see cdkStickyRegion) is in view.
53+
*/
54+
@Directive({
55+
selector: '[cdkStickyHeader]',
56+
})
57+
export class CdkStickyHeader implements OnDestroy, AfterViewInit {
58+
59+
/** z-index to be applied to the sticky header (default is 10). */
60+
@Input('cdkStickyHeaderZIndex') zIndex: number = 10;
61+
62+
/** boolean value to mark whether the current header is stuck*/
63+
isStuck: boolean = false;
64+
/** Whether the browser support CSS sticky positioning. */
65+
private _isPositionStickySupported: boolean = false;
66+
67+
/** The element with the 'cdkStickyHeader' tag. */
68+
element: HTMLElement;
69+
/** The upper container element with the 'cdkStickyRegion' tag. */
70+
stickyParent: HTMLElement | null;
71+
/** The upper scrollable container. */
72+
upperScrollableContainer: HTMLElement;
73+
/**
74+
* The original css of the sticky element, used to reset the sticky element
75+
* when it is being unstuck
76+
*/
77+
private _originalStyles = {} as CSSStyleDeclaration;
78+
/**
79+
* 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header.
80+
* It is used with '_stickyRegionBottomThreshold' to judge whether the current header
81+
* need to be stuck.
82+
*/
83+
private _stickyRegionTop: number;
84+
/**
85+
* Bottom of the sticky region offset by the height of the sticky header.
86+
* Once the sticky header is scrolled to this position it will stay in place
87+
* so that it will scroll naturally out of view with the rest of the sticky region.
88+
*/
89+
private _stickyRegionBottomThreshold: number;
90+
91+
private _onScrollSubscription: Subscription;
92+
93+
private _onTouchSubscription: Subscription;
94+
95+
private _onResizeSubscription: Subscription;
96+
97+
constructor(element: ElementRef,
98+
scrollable: Scrollable,
99+
@Optional() public parentRegion: CdkStickyRegion,
100+
platform: Platform) {
101+
if (platform.isBrowser) {
102+
this.element = element.nativeElement;
103+
this.upperScrollableContainer = scrollable.getElementRef().nativeElement;
104+
this._setStrategyAccordingToCompatibility();
105+
}
106+
}
107+
108+
ngAfterViewInit(): void {
109+
if (!this._isPositionStickySupported) {
110+
111+
this.stickyParent = this.parentRegion != null ?
112+
this.parentRegion._elementRef.nativeElement : this.element.parentElement;
113+
114+
let headerStyles = window.getComputedStyle(this.element, '');
115+
this._originalStyles = {
116+
position: headerStyles.position,
117+
top: headerStyles.top,
118+
right: headerStyles.right,
119+
left: headerStyles.left,
120+
bottom: headerStyles.bottom,
121+
width: headerStyles.width,
122+
zIndex: headerStyles.zIndex
123+
} as CSSStyleDeclaration;
124+
125+
this._attachEventListeners();
126+
this._updateStickyPositioning();
127+
}
128+
}
129+
130+
ngOnDestroy(): void {
131+
[this._onScrollSubscription, this._onScrollSubscription, this._onResizeSubscription]
132+
.forEach(s => s && s.unsubscribe());
133+
}
134+
135+
/**
136+
* Check if current browser supports sticky positioning. If yes, apply
137+
* sticky positioning. If not, use the original implementation.
138+
*/
139+
private _setStrategyAccordingToCompatibility(): void {
140+
this._isPositionStickySupported = isPositionStickySupported();
141+
if (this._isPositionStickySupported) {
142+
this.element.style.top = '0';
143+
this.element.style.cssText += 'position: -webkit-sticky; position: sticky; ';
144+
// TODO(sllethe): add css class with both 'sticky' and '-webkit-sticky' on position
145+
// when @Directory supports adding CSS class
146+
}
147+
}
148+
149+
/** Add listeners for events that affect sticky positioning. */
150+
private _attachEventListeners() {
151+
this._onScrollSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'scroll'))
152+
.call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning());
153+
154+
// Have to add a 'onTouchMove' listener to make sticky header work on mobile phones
155+
this._onTouchSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'touchmove'))
156+
.call(debounceTime, DEBOUNCE_TIME).subscribe(() => this._updateStickyPositioning());
157+
158+
this._onResizeSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'resize'))
159+
.call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.onResize());
160+
}
161+
162+
onResize(): void {
163+
this._updateStickyPositioning();
164+
// If there's already a header being stick when the page is
165+
// resized. The CSS style of the cdkStickyHeader element may be not fit
166+
// the resized window. So we need to unstuck it then re-stick it.
167+
// unstuck() can set 'isStuck' to FALSE. Then _stickElement() can work.
168+
if (this.isStuck) {
169+
this._unstickElement();
170+
this._stickElement();
171+
}
172+
}
173+
174+
/** Measures the boundaries of the sticky regions to be used in subsequent positioning. */
175+
private _measureStickyRegionBounds(): void {
176+
if (!this.stickyParent) {
177+
return;
178+
}
179+
const boundingClientRect: any = this.stickyParent.getBoundingClientRect();
180+
this._stickyRegionTop = boundingClientRect.top;
181+
let stickRegionHeight = boundingClientRect.height;
182+
183+
this._stickyRegionBottomThreshold = this._stickyRegionTop +
184+
(stickRegionHeight - this.element.offsetHeight);
185+
}
186+
187+
/** Reset element to its original CSS. */
188+
private _resetElementStyles(): void {
189+
this.element.classList.remove(STICK_START_CLASS);
190+
extendObject(this.element.style, this._originalStyles);
191+
}
192+
193+
/** Stuck element, make the element stick to the top of the scrollable container. */
194+
private _stickElement(): void {
195+
this.isStuck = true;
196+
197+
this.element.classList.remove(STICK_END_CLASS);
198+
this.element.classList.add(STICK_START_CLASS);
199+
200+
// Have to add the translate3d function for the sticky element's css style.
201+
// Because iPhone and iPad's browser is using its owning rendering engine. And
202+
// even if you are using Chrome on an iPhone, you are just using Safari with
203+
// a Chrome skin around it.
204+
//
205+
// Safari on iPad and Safari on iPhone do not have resizable windows.
206+
// In Safari on iPhone and iPad, the window size is set to the size of
207+
// the screen (minus Safari user interface controls), and cannot be changed
208+
// by the user. To move around a webpage, the user changes the zoom level and position
209+
// of the viewport as they double tap or pinch to zoom in or out, or by touching
210+
// and dragging to pan the page. As a user changes the zoom level and position of the
211+
// viewport they are doing so within a viewable content area of fixed size
212+
// (that is, the window). This means that webpage elements that have their position
213+
// "fixed" to the viewport can end up outside the viewable content area, offscreen.
214+
//
215+
// So the 'position: fixed' does not work on iPhone and iPad. To make it work,
216+
// 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element.
217+
this.element.style.transform = 'translate3d(0px,0px,0px)';
218+
219+
let stuckRight: number = this.upperScrollableContainer.getBoundingClientRect().right;
220+
221+
let stickyCss = {
222+
position: 'fixed',
223+
top: this.upperScrollableContainer.offsetTop + 'px',
224+
right: stuckRight + 'px',
225+
left: this.upperScrollableContainer.offsetLeft + 'px',
226+
bottom: 'auto',
227+
width: this._originalStyles.width,
228+
zIndex: this.zIndex + ''
229+
};
230+
extendObject(this.element.style, stickyCss);
231+
}
232+
233+
/**
234+
* Unsticks the header so that it goes back to scrolling normally.
235+
*
236+
* This should be called when the element reaches the bottom of its cdkStickyRegion so that it
237+
* smoothly scrolls out of view as the next sticky-header moves in.
238+
*/
239+
private _unstickElement(): void {
240+
this.isStuck = false;
241+
242+
if (!this.stickyParent) {
243+
return;
244+
}
245+
246+
this.element.classList.add(STICK_END_CLASS);
247+
this.stickyParent.style.position = 'relative';
248+
let unstuckCss = {
249+
position: 'absolute',
250+
top: 'auto',
251+
right: '0',
252+
left: 'auto',
253+
bottom: '0',
254+
width: this._originalStyles.width
255+
};
256+
extendObject(this.element.style, unstuckCss);
257+
}
258+
259+
260+
/**
261+
* '_applyStickyPositionStyles()' function contains the main logic of sticky-header. It decides when
262+
* a header should be stick and when should it be unstuck by comparing the offsetTop
263+
* of scrollable container with the top and bottom of the sticky region.
264+
*/
265+
_applyStickyPositionStyles(): void {
266+
let currentPosition: number = this.upperScrollableContainer.offsetTop;
267+
268+
// unstuck when the element is scrolled out of the sticky region
269+
if (this.isStuck &&
270+
(currentPosition < this._stickyRegionTop ||
271+
currentPosition > this._stickyRegionBottomThreshold) ||
272+
currentPosition >= this._stickyRegionBottomThreshold) {
273+
this._resetElementStyles();
274+
if (currentPosition >= this._stickyRegionBottomThreshold) {
275+
this._unstickElement();
276+
}
277+
this.isStuck = false; // stick when the element is within the sticky region
278+
} else if ( this.isStuck === false &&
279+
currentPosition > this._stickyRegionTop &&
280+
currentPosition < this._stickyRegionBottomThreshold) {
281+
this._stickElement();
282+
}
283+
}
284+
285+
_updateStickyPositioning(): void {
286+
this._measureStickyRegionBounds();
287+
this._applyStickyPositionStyles();
288+
}
289+
}

0 commit comments

Comments
 (0)