Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Other: StickyToolbarView should support a configurable vertical offse…
Browse files Browse the repository at this point in the history
…t from the top of the page. Closes #277.
  • Loading branch information
oleq committed Jul 19, 2017
1 parent d83d07d commit 0e3800b
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 13 deletions.
54 changes: 44 additions & 10 deletions src/toolbar/sticky/stickytoolbarview.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,25 @@ export default class StickyToolbarView extends ToolbarView {
* @readonly
* @observable
* @default 50
* @member {Number} #limiterOffset
* @member {Number} #limiterBottomOffset
*/
this.set( 'limiterOffset', 50 );
this.set( 'limiterBottomOffset', 50 );

/**
* The offset from the top edge of the web browser's viewport which makes the
* toolbar become sticky. The default value is `0`, which means the toolbar becomes
* sticky when it's upper edge touches the top of the page viewport.
*
* This attribute is useful when the web page has UI elements positioned to the top
* either using `position: fixed` or `position: sticky`, which would cover the
* sticky toolbar or vice–versa (depending on the `z-index` hierarchy).
*
* @readonly
* @observable
* @default 0
* @member {Number} #viewportTopOffset
*/
this.set( 'viewportTopOffset', 0 );

/**
* Controls the `margin-left` CSS style of the toolbar.
Expand All @@ -93,6 +109,18 @@ export default class StickyToolbarView extends ToolbarView {
*/
this.set( '_isStickyToTheLimiter', false );

/**
* Set `true` if the sticky toolbar uses the {@link #viewportTopOffset},
* i.e. not {@link #_isStickyToTheLimiter} and the {@link #viewportTopOffset}
* is not `0`.
*
* @protected
* @readonly
* @observable
* @member {Boolean} #_hasViewportTopOffset
*/
this.set( '_hasViewportTopOffset', false );

/**
* The DOM bounding client rect of the {@link module:ui/view~View#element} of the toolbar.
*
Expand Down Expand Up @@ -121,10 +149,14 @@ export default class StickyToolbarView extends ToolbarView {
} ),

bottom: bind.to( '_isStickyToTheLimiter', _isStickyToTheLimiter => {
return _isStickyToTheLimiter ? toPx( this.limiterOffset ) : null;
return _isStickyToTheLimiter ? toPx( this.limiterBottomOffset ) : null;
} ),

marginLeft: bind.to( '_marginLeft' )
marginLeft: bind.to( '_marginLeft' ),

top: bind.to( '_hasViewportTopOffset', _hasViewportTopOffset => {
return _hasViewportTopOffset ? toPx( this.viewportTopOffset ) : null;
} )
}
}
} );
Expand Down Expand Up @@ -191,23 +223,25 @@ export default class StickyToolbarView extends ToolbarView {

// The toolbar must be active to become sticky.
this.isSticky = this.isActive &&
// The limiter's top edge must be beyond the upper edge of the visible viewport.
limiterRect.top < 0 &&
// The model#limiterElement's height mustn't be smaller than the toolbar's height and model#limiterOffset.
// The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset).
limiterRect.top < this.viewportTopOffset &&
// The model#limiterElement's height mustn't be smaller than the toolbar's height and model#limiterBottomOffset.
// There's no point in entering the sticky mode if the model#limiterElement is very, very small, because
// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterOffset, the toolbar
// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the toolbar
// would be positioned before the model#limiterElement.
this._toolbarRect.height + this.limiterOffset < limiterRect.height;
this._toolbarRect.height + this.limiterBottomOffset < limiterRect.height;

// Stick the toolbar to the top edge of the viewport simulating CSS position:sticky.
// TODO: Possibly replaced by CSS in the future http://caniuse.com/#feat=css-sticky
if ( this.isSticky ) {
this._isStickyToTheLimiter = limiterRect.bottom < toolbarRect.height + this.limiterOffset;
this._isStickyToTheLimiter = limiterRect.bottom < toolbarRect.height + this.limiterBottomOffset + this.viewportTopOffset;
this._hasViewportTopOffset = !this._isStickyToTheLimiter && !!this.viewportTopOffset;
this._marginLeft = this._isStickyToTheLimiter ? null : toPx( -global.window.scrollX );
}
// Detach the toolbar from the top edge of the viewport.
else {
this._isStickyToTheLimiter = false;
this._hasViewportTopOffset = false;
this._marginLeft = null;
}
}
Expand Down
83 changes: 80 additions & 3 deletions tests/toolbar/sticky/stickytoolbarview.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ describe( 'StickyToolbarView', () => {
it( 'sets view attributes', () => {
expect( view.isSticky ).to.be.false;
expect( view.limiterElement ).to.be.null;
expect( view.limiterOffset ).to.equal( 50 );
expect( view.limiterBottomOffset ).to.equal( 50 );
expect( view.viewportTopOffset ).to.equal( 0 );

expect( view._isStickyToTheLimiter ).to.be.false;
expect( view._hasViewportTopOffset ).to.be.false;
expect( view._marginLeft ).to.be.null;
} );

Expand Down Expand Up @@ -83,6 +85,16 @@ describe( 'StickyToolbarView', () => {
expect( element.classList.contains( 'ck-toolbar_sticky_bottom-limit' ) ).to.be.true;
} );

it( 'update the styles.top on view#_hasViewportTopOffset change', () => {
view.viewportTopOffset = 100;

view._hasViewportTopOffset = false;
expect( element.style.top ).to.equal( '' );

view._hasViewportTopOffset = true;
expect( element.style.top ).to.equal( '100px' );
} );

it( 'update the styles.width on view#isSticky change', () => {
testUtils.sinon.stub( view._elementPlaceholder, 'getBoundingClientRect' ).returns( { width: 100 } );

Expand Down Expand Up @@ -236,10 +248,10 @@ describe( 'StickyToolbarView', () => {
expect( view.isSticky ).to.be.false;
} );

it( 'is false if view.limiterElement is smaller than the toolbar and view.limiterOffset (toolbar is active)', () => {
it( 'is false if view.limiterElement is smaller than the toolbar and view.limiterBottomOffset (toolbar is active)', () => {
testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 60 } );
view.isActive = true;
view.limiterOffset = 50;
view.limiterBottomOffset = 50;

expect( view.isSticky ).to.be.false;

Expand Down Expand Up @@ -307,6 +319,71 @@ describe( 'StickyToolbarView', () => {
} );
} );

describe( 'view._hasViewportTopOffset', () => {
it( 'is true if view._isStickyToTheLimiter is false and view.viewportTopOffset has been specified', () => {
view.viewportTopOffset = 100;

testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
top: 90,
bottom: 190,
height: 100
} );

testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
height: 20
} );

view.isActive = true;

view._checkIfShouldBeSticky();
expect( view.isSticky ).to.be.true;
expect( view._isStickyToTheLimiter ).to.be.false;
expect( view._hasViewportTopOffset ).to.be.true;
} );

it( 'is false if view._isStickyToTheLimiter is true and view.viewportTopOffset has been specified', () => {
view.viewportTopOffset = 100;

testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
top: 10,
bottom: 110,
height: 100
} );

testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
height: 20
} );

view.isActive = true;

view._checkIfShouldBeSticky();
expect( view.isSticky ).to.be.true;
expect( view._isStickyToTheLimiter ).to.be.true;
expect( view._hasViewportTopOffset ).to.be.false;
} );

it( 'is false if view._isStickyToTheLimiter is false and view.viewportTopOffset is 0', () => {
view.viewportTopOffset = 100;

testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
top: 90,
bottom: 190,
height: 100
} );

testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
height: 20
} );

view.isActive = true;

view._checkIfShouldBeSticky();
expect( view.isSticky ).to.be.true;
expect( view._isStickyToTheLimiter ).to.be.false;
expect( view._hasViewportTopOffset ).to.be.true;
} );
} );

describe( 'view._marginLeft', () => {
it( 'is set if view.isSticky is true view._isStickyToTheLimiter is false', () => {
testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
Expand Down

0 comments on commit 0e3800b

Please sign in to comment.