, viewportTopOffset: number ) {
+ const scrollableAncestorsRects = scrollableAncestors.map( ancestor => {
+ // The document (window) is yet another scrollable ancestor, but cropped by the top offset.
+ if ( ancestor instanceof Document ) {
+ const windowRect = new Rect( global.window );
+
+ windowRect.top += viewportTopOffset;
+ windowRect.height -= viewportTopOffset;
+
+ return windowRect;
+ } else {
+ return new Rect( ancestor );
}
- // Detach the panel from the top edge of the viewport.
- else {
- this._isStickyToTheLimiter = false;
- this._hasViewportTopOffset = false;
- this._marginLeft = null;
+ } );
+
+ let scrollableAncestorsIntersectionRect: Rect | null = scrollableAncestorsRects[ 0 ];
+
+ // @if CK_DEBUG_STICKYPANEL // for ( const scrollableAncestorRect of scrollableAncestorsRects ) {
+ // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( scrollableAncestorRect, {
+ // @if CK_DEBUG_STICKYPANEL // outlineWidth: '1px', opacity: '.7', outlineStyle: 'dashed'
+ // @if CK_DEBUG_STICKYPANEL // }, 'Scrollable ancestor' );
+ // @if CK_DEBUG_STICKYPANEL // }
+
+ for ( const scrollableAncestorRect of scrollableAncestorsRects.slice( 1 ) ) {
+ if ( scrollableAncestorsIntersectionRect ) {
+ scrollableAncestorsIntersectionRect = scrollableAncestorsIntersectionRect.getIntersection( scrollableAncestorRect );
}
}
+
+ return scrollableAncestorsIntersectionRect;
}
diff --git a/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.html b/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.html
index a8cf3d6c35e..9c15684b9f6 100644
--- a/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.html
+++ b/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.html
@@ -4,15 +4,15 @@ Sticky to the top of the viewport
-
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
+
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
@@ -22,20 +22,41 @@ Sticky to the green box
-
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
- An editable content mock–up.
+
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
+ Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter. Panel limiter.
- The panel should stick to me instead of the viewport.
+ The panel should stick to me instead of the viewport.
+
+
+
+
In scrollable ancestors
+
+
+
+
+
The panel should stick to me instead of the viewport.
@@ -46,13 +67,22 @@ Sticky to the green box
padding-top: 150px;
}
+ .ck-reset {
+ font-size: 10px;
+ }
+
+ h2 {
+ font-size: 18px;
+ }
+
#columns {
overflow: hidden;
+ margin-top: 50px;
}
.column {
float: left;
- width: 350px;
+ width: 300px;
padding: 1em;
}
@@ -68,7 +98,10 @@ Sticky to the green box
}
.ck-sticky-panel__content:after {
- content: "A sticky panel mock–up.";
+ content: "Sticky panel.";
+ text-align: center;
+ width: 100%;
+ display: block;
}
.offset-visualizer {
@@ -78,15 +111,16 @@ Sticky to the green box
bottom: 0;
background: red;
color: #fff;
+ opacity: .8;
}
.offset-visualizer:after {
content: "An offset mock–up. Toolbar should *never* cover the red area.";
}
- #fixed {
+ .fixed {
height: 100px;
- width: 350px;
+ width: 300px;
position: fixed;
top: 0;
z-index: 9999;
@@ -96,4 +130,25 @@ Sticky to the green box
box-sizing: border-box;
padding: 30px;
}
+
+ .scrollable {
+ border: 1px solid #aaa;
+ padding: 30px;
+ padding-bottom: 200px;
+ box-sizing: border-box;
+ height: 150px;
+ overflow: auto;
+ }
+
+ .scrollable > .scrollable {
+ margin-bottom: 200px;
+ }
+
+ .scrollable > .ck-reset {
+ margin-bottom: 100px;
+ }
+
+ .limiter {
+ overflow: hidden;
+ }
diff --git a/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.js b/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.js
index 6d206bb807a..b0bf8dffdd6 100644
--- a/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.js
+++ b/packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.js
@@ -8,13 +8,13 @@ import StickyPanelView from '../../../../src/panel/sticky/stickypanelview';
const ui = testUtils.createTestUIView( {
stickyToTheTop: '.ck-sticky_to-the-top .ck-editor__top',
- stickyToTheBox: '.ck-sticky_to-the-box .ck-editor__top'
+ stickyToTheBox: '.ck-sticky_to-the-box .ck-editor__top',
+ stickyWithScrollableAncestors: '.ck-sticky_with-scrollable-ancestors .ck-editor__top'
} );
createStickyPanel( ui.stickyToTheTop );
-const stickyToTheBoxTPanel = createStickyPanel( ui.stickyToTheBox );
-
-stickyToTheBoxTPanel.viewportTopOffset = 100;
+createStickyPanel( ui.stickyToTheBox ).viewportTopOffset = 100;
+createStickyPanel( ui.stickyWithScrollableAncestors ).viewportTopOffset = 100;
function createStickyPanel( collection ) {
const panel = new StickyPanelView();
diff --git a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js
index c600dc4f348..3ead2c3bd56 100644
--- a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js
+++ b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
-/* globals document */
+/* globals document, Event */
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import StickyPanelView from '../../../src/panel/sticky/stickypanelview';
@@ -11,10 +11,9 @@ import View from '../../../src/view';
import LabelView from '../../../src/label/labelview';
import ViewCollection from '../../../src/viewcollection';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
describe( 'StickyPanelView', () => {
- let view, element, contentElement, placeholderElement, limiterElement, locale, windowStub;
+ let view, element, contentElement, placeholderElement, limiterElement, locale;
testUtils.createSinonSandbox();
@@ -29,18 +28,8 @@ describe( 'StickyPanelView', () => {
contentElement = view.element.lastChild;
placeholderElement = view.element.firstChild;
- // Dummy values just to let non–geometrical tests pass without reference errors.
- view._toolbarRect = { top: 10, right: 20, bottom: 30, left: 40, width: 50, height: 60 };
- view._limiterRect = { top: 5, right: 10, bottom: 15, left: 20, width: 25, height: 30 };
-
- windowStub = Object.create( DomEmitterMixin );
-
- Object.assign( windowStub, {
- scrollX: 0,
- scrollY: 0
- } );
-
- testUtils.sinon.stub( global, 'window' ).value( windowStub );
+ sinon.stub( global.window, 'innerWidth' ).value( 1000 );
+ sinon.stub( global.window, 'innerHeight' ).value( 500 );
document.body.appendChild( element );
} );
@@ -69,14 +58,16 @@ describe( 'StickyPanelView', () => {
} );
it( 'sets view attributes', () => {
+ expect( view.isActive ).to.be.false;
expect( view.isSticky ).to.be.false;
expect( view.limiterElement ).to.be.null;
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;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
+ expect( view._stickyTopOffset ).to.be.null;
+ expect( view._stickyBottomOffset ).to.be.null;
} );
it( 'accepts the locale', () => {
@@ -101,25 +92,25 @@ describe( 'StickyPanelView', () => {
expect( contentElement.classList.contains( 'ck-sticky-panel__content_sticky' ) ).to.be.true;
} );
- it( 'update the class on view#_isStickyToTheLimiter change', () => {
- view._isStickyToTheLimiter = false;
+ it( 'update the class on view#_isStickyToTheBottomOfLimiter change', () => {
+ view._isStickyToTheBottomOfLimiter = false;
expect( contentElement.classList.contains( 'ck-sticky-panel__content_sticky_bottom-limit' ) ).to.be.false;
- view._isStickyToTheLimiter = true;
+ view._isStickyToTheBottomOfLimiter = true;
expect( contentElement.classList.contains( 'ck-sticky-panel__content_sticky_bottom-limit' ) ).to.be.true;
} );
- it( 'update the styles.top on view#_hasViewportTopOffset change', () => {
+ it( 'update the style.top on view#_stickyTopOffset change', () => {
view.viewportTopOffset = 100;
- view._hasViewportTopOffset = false;
- expect( contentElement.style.top ).to.equal( '' );
+ view._stickyTopOffset = 0;
+ expect( contentElement.style.top ).to.equal( '0px' );
- view._hasViewportTopOffset = true;
+ view._stickyTopOffset = 100;
expect( contentElement.style.top ).to.equal( '100px' );
} );
- it( 'update the styles.width on view#isSticky change', () => {
+ it( 'update the style.width on view#isSticky change', () => {
testUtils.sinon.stub( view._contentPanelPlaceholder, 'getBoundingClientRect' ).returns( { width: 100 } );
view.isSticky = false;
@@ -129,15 +120,15 @@ describe( 'StickyPanelView', () => {
expect( contentElement.style.width ).to.equal( '100px' );
} );
- it( 'update the styles.bottom on view#_isStickyToTheLimiter change', () => {
- view._isStickyToTheLimiter = false;
- expect( contentElement.style.bottom ).to.equal( '' );
+ it( 'update the style.bottom on view#_stickyBottomOffset change', () => {
+ view._stickyBottomOffset = 0;
+ expect( contentElement.style.bottom ).to.equal( '0px' );
- view._isStickyToTheLimiter = true;
+ view._stickyBottomOffset = 50;
expect( contentElement.style.bottom ).to.equal( '50px' );
} );
- it( 'update the styles.marginLeft on view#marginLeft change', () => {
+ it( 'update the style.marginLeft on view#marginLeft change', () => {
view._marginLeft = '30px';
expect( contentElement.style.marginLeft ).to.equal( '30px' );
@@ -151,7 +142,7 @@ describe( 'StickyPanelView', () => {
view.limiterElement = limiterElement;
} );
- it( 'update the styles.display on view#isSticky change', () => {
+ it( 'update the style.display on view#isSticky change', () => {
view.isSticky = false;
expect( placeholderElement.style.display ).to.equal( 'none' );
@@ -159,7 +150,7 @@ describe( 'StickyPanelView', () => {
expect( placeholderElement.style.display ).to.equal( 'block' );
} );
- it( 'update the styles.height on view#isSticky change', () => {
+ it( 'update the style.height on view#isSticky change', () => {
view._panelRect = { height: 50 };
view.isSticky = false;
@@ -201,28 +192,31 @@ describe( 'StickyPanelView', () => {
} );
it( 'checks if the panel should be sticky', () => {
- const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
+ const spy = testUtils.sinon.spy( view, 'checkIfShouldBeSticky' );
expect( spy.notCalled ).to.be.true;
view.render();
expect( spy.calledOnce ).to.be.true;
} );
- it( 'listens to window#scroll event and calls view._checkIfShouldBeSticky', () => {
- const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
+ it( 'listens to document#scroll event and calls view.checkIfShouldBeSticky()', () => {
+ const spy = testUtils.sinon.spy( view, 'checkIfShouldBeSticky' );
expect( spy.notCalled ).to.be.true;
view.render();
+ expect( spy.calledOnce ).to.be.true;
- global.window.fire( 'scroll' );
+ global.document.dispatchEvent( new Event( 'scroll' ) );
expect( spy.calledTwice ).to.be.true;
} );
- it( 'listens to view.isActive and calls view._checkIfShouldBeSticky', () => {
- const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
+ it( 'listens to view.isActive and calls view.checkIfShouldBeSticky()', () => {
+ const spy = testUtils.sinon.spy( view, 'checkIfShouldBeSticky' );
expect( spy.notCalled ).to.be.true;
view.render();
+ expect( spy.calledOnce ).to.be.true;
+
view.isActive = true;
expect( spy.calledTwice ).to.be.true;
@@ -247,11 +241,40 @@ describe( 'StickyPanelView', () => {
} );
} );
- describe( '_checkIfShouldBeSticky', () => {
+ describe( 'checkIfShouldBeSticky()', () => {
beforeEach( () => {
view.limiterElement = limiterElement;
} );
+ it( 'should unstick the panel if limiter element is not set', () => {
+ view.limiterElement = null;
+
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
+
+ it( 'should unstick the panel if it is not active', () => {
+ view.isActive = true;
+
+ const unstickSpy = testUtils.sinon.spy( view, '_unstick' );
+
+ view.isActive = false;
+
+ sinon.assert.calledOnce( unstickSpy );
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
+
describe( 'view.isSticky', () => {
beforeEach( () => {
testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
@@ -302,12 +325,12 @@ describe( 'StickyPanelView', () => {
} );
} );
- describe( 'view._isStickyToTheLimiter', () => {
+ describe( 'view._isStickyToTheBottomOfLimiter', () => {
it( 'is true if view.isSticky is true and reached the bottom edge of view.limiterElement', () => {
testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
- top: -10,
- bottom: 10,
- height: 100
+ top: -80,
+ bottom: 60,
+ height: 140
} );
testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
@@ -315,12 +338,12 @@ describe( 'StickyPanelView', () => {
} );
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
view.isActive = true;
expect( view.isSticky ).to.be.true;
- expect( view._isStickyToTheLimiter ).to.be.true;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.true;
} );
it( 'is false if view.isSticky is true and not reached the bottom edge of view.limiterElement', () => {
@@ -335,12 +358,12 @@ describe( 'StickyPanelView', () => {
} );
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
view.isActive = true;
expect( view.isSticky ).to.be.true;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
} );
it( 'is false if view.isSticky is false', () => {
@@ -349,82 +372,387 @@ describe( 'StickyPanelView', () => {
} );
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
view.isActive = true;
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
} );
} );
- describe( 'view._hasViewportTopOffset', () => {
- it( 'is true if view._isStickyToTheLimiter is false and view.viewportTopOffset has been specified', () => {
- view.viewportTopOffset = 100;
-
- testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
- top: 90,
- bottom: 190,
- height: 100
- } );
+ describe( 'after scrolling', () => {
+ it( 'should do nothing if scrolled element does not contain the panel', () => {
+ view.isActive = true;
- testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
- height: 20
- } );
+ const separateElement = document.createElement( 'div' );
- view.isActive = true;
+ view.checkIfShouldBeSticky( separateElement );
- expect( view.isSticky ).to.be.true;
- expect( view._isStickyToTheLimiter ).to.be.false;
- expect( view._hasViewportTopOffset ).to.be.true;
+ expect( view.isSticky ).to.be.false;
} );
- it( 'is false if view._isStickyToTheLimiter is true and view.viewportTopOffset has been specified', () => {
- view.viewportTopOffset = 100;
+ describe( 'if there is one scrollable non-window parent', () => {
+ let scrollableContainer;
- testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
- top: 10,
- bottom: 110,
- height: 100
- } );
+ beforeEach( () => {
+ scrollableContainer = document.createElement( 'div' );
+ scrollableContainer.className = 'scrollable';
+ scrollableContainer.style.overflow = 'scroll';
+ scrollableContainer.appendChild( limiterElement );
+ global.document.body.appendChild( scrollableContainer );
- testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
- height: 20
+ view.isActive = true;
} );
- view.isActive = true;
+ afterEach( () => {
+ scrollableContainer.remove();
+ } );
- expect( view.isSticky ).to.be.true;
- expect( view._isStickyToTheLimiter ).to.be.true;
- expect( view._hasViewportTopOffset ).to.be.false;
+ describe( 'scrolled the container', () => {
+ it( 'should make panel sticky to the top if the limiter top is not visible', () => {
+ const stickToTopSpy = testUtils.sinon.spy( view, '_stickToTopOfAncestors' );
+
+ testUtils.sinon.stub( scrollableContainer, 'getBoundingClientRect' ).returns( {
+ top: 40,
+ bottom: 140,
+ height: 100
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: 20,
+ bottom: 200,
+ height: 180
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableContainer );
+
+ sinon.assert.calledOnce( stickToTopSpy );
+ assureStickiness( {
+ isSticky: true,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: 40,
+ _stickyBottomOffset: null,
+ _marginLeft: '0px'
+ } );
+ } );
+
+ it( 'should make panel sticky to the bottom if there is enough space left', () => {
+ const stickToBottomSpy = testUtils.sinon.spy( view, '_stickToBottomOfLimiter' );
+
+ testUtils.sinon.stub( scrollableContainer, 'getBoundingClientRect' ).returns( {
+ top: 40,
+ bottom: 140,
+ height: 100
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: -80,
+ bottom: 60,
+ height: 140
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableContainer );
+
+ expect( view.isSticky ).to.be.true;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.true;
+
+ sinon.assert.calledOnce( stickToBottomSpy );
+ assureStickiness( {
+ isSticky: true,
+ _isStickyToTheBottomOfLimiter: true,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: 50,
+ _marginLeft: '0px'
+ } );
+ } );
+
+ it( 'should unstick the panel if the limiter top is still visible', () => {
+ const stickToBottomSpy = testUtils.sinon.spy( view, '_stickToBottomOfLimiter' );
+ const stickToTopSpy = testUtils.sinon.spy( view, '_stickToTopOfAncestors' );
+ const unstickSpy = testUtils.sinon.spy( view, '_unstick' );
+
+ testUtils.sinon.stub( scrollableContainer, 'getBoundingClientRect' ).returns( {
+ top: 20,
+ bottom: 140,
+ height: 120
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: 20,
+ bottom: 200,
+ height: 180
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableContainer );
+
+ sinon.assert.notCalled( stickToBottomSpy );
+ sinon.assert.notCalled( stickToTopSpy );
+ sinon.assert.calledOnce( unstickSpy );
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
+
+ it( 'should unstick the panel if there is not enough space left in the limiter', () => {
+ const spy = sinon.spy( view, '_unstick' );
+
+ testUtils.sinon.stub( scrollableContainer, 'getBoundingClientRect' ).returns( {
+ top: 40,
+ bottom: 140,
+ height: 100
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: -80,
+ bottom: 60,
+ height: 140
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 100
+ } );
+
+ view.checkIfShouldBeSticky( scrollableContainer );
+
+ sinon.assert.calledOnce( spy );
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
+
+ it( 'should unstick the panel if panel limiter is not visible in the viewport', () => {
+ const spy = sinon.spy( view, '_unstick' );
+
+ testUtils.sinon.stub( scrollableContainer, 'getBoundingClientRect' ).returns( {
+ top: 120,
+ bottom: 140,
+ height: 100
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: -80,
+ bottom: 60,
+ height: 140
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableContainer );
+
+ sinon.assert.calledOnce( spy );
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
+ } );
} );
- it( 'is false if view._isStickyToTheLimiter is false and view.viewportTopOffset is 0', () => {
- view.viewportTopOffset = 100;
+ describe( 'if there are multiple scrollable non-window parents', () => {
+ let scrollableOuterParent, scrollableInnerParent;
- testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
- top: 90,
- bottom: 190,
- height: 100
+ beforeEach( () => {
+ scrollableOuterParent = document.createElement( 'div' );
+ scrollableOuterParent.className = 'scrollable-outer';
+ scrollableOuterParent.style.overflow = 'scroll';
+
+ scrollableInnerParent = document.createElement( 'div' );
+ scrollableInnerParent.className = 'scrollable-inner';
+ scrollableInnerParent.style.overflow = 'scroll';
+
+ scrollableInnerParent.appendChild( limiterElement );
+ scrollableOuterParent.appendChild( scrollableInnerParent );
+ global.document.body.appendChild( scrollableOuterParent );
+
+ view.isActive = true;
} );
- testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
- height: 20
+ afterEach( () => {
+ scrollableInnerParent.remove();
+ scrollableOuterParent.remove();
} );
- view.isActive = true;
+ it( 'should unstick the panel if the limiter is still visible', () => {
+ const unstickSpy = testUtils.sinon.spy( view, '_unstick' );
+
+ testUtils.sinon.stub( scrollableOuterParent, 'getBoundingClientRect' ).returns( {
+ top: 10,
+ bottom: 160,
+ height: 150
+ } );
+
+ testUtils.sinon.stub( scrollableInnerParent, 'getBoundingClientRect' ).returns( {
+ top: 20,
+ bottom: 140,
+ height: 120
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: 40,
+ bottom: 100,
+ height: 60
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableOuterParent );
+
+ sinon.assert.calledOnce( unstickSpy );
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
- expect( view.isSticky ).to.be.true;
- expect( view._isStickyToTheLimiter ).to.be.false;
- expect( view._hasViewportTopOffset ).to.be.true;
+ it( 'should stick the panel to the top if the outer container was scrolled over the limiter top', () => {
+ const stickToTopSpy = testUtils.sinon.spy( view, '_stickToTopOfAncestors' );
+
+ testUtils.sinon.stub( scrollableOuterParent, 'getBoundingClientRect' ).returns( {
+ top: 50,
+ bottom: 160,
+ height: 150
+ } );
+
+ testUtils.sinon.stub( scrollableInnerParent, 'getBoundingClientRect' ).returns( {
+ top: 20,
+ bottom: 140,
+ height: 120
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: 40,
+ bottom: 140,
+ height: 100
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableOuterParent );
+
+ sinon.assert.calledOnce( stickToTopSpy );
+ assureStickiness( {
+ isSticky: true,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: 50,
+ _stickyBottomOffset: null,
+ _marginLeft: '0px'
+ } );
+ } );
+
+ it( 'should unstick the panel if the outer container was scrolled but there is no space below', () => {
+ const unstickSpy = testUtils.sinon.spy( view, '_unstick' );
+
+ testUtils.sinon.stub( scrollableOuterParent, 'getBoundingClientRect' ).returns( {
+ top: 50,
+ bottom: 160,
+ height: 150
+ } );
+
+ testUtils.sinon.stub( scrollableInnerParent, 'getBoundingClientRect' ).returns( {
+ top: 20,
+ bottom: 140,
+ height: 120
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: 40,
+ bottom: 110,
+ height: 60
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableOuterParent );
+
+ sinon.assert.calledOnce( unstickSpy );
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
+
+ it( 'should unstick the panel if the outer container was scrolled over the inner container top', () => {
+ const unstickSpy = testUtils.sinon.spy( view, '_unstick' );
+
+ testUtils.sinon.stub( scrollableOuterParent, 'getBoundingClientRect' ).returns( {
+ top: 50,
+ bottom: 160,
+ height: 150
+ } );
+
+ testUtils.sinon.stub( scrollableInnerParent, 'getBoundingClientRect' ).returns( {
+ top: -20,
+ bottom: 50,
+ height: 70
+ } );
+
+ testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
+ top: 0,
+ bottom: 40,
+ height: 40
+ } );
+
+ testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
+ height: 20
+ } );
+
+ view.checkIfShouldBeSticky( scrollableOuterParent );
+
+ sinon.assert.calledOnce( unstickSpy );
+ assureStickiness( {
+ isSticky: false,
+ _isStickyToTheBottomOfLimiter: false,
+ _stickyTopOffset: null,
+ _stickyBottomOffset: null,
+ _marginLeft: null
+ } );
+ } );
} );
} );
describe( 'view._marginLeft', () => {
- it( 'is set if view.isSticky is true view._isStickyToTheLimiter is false', () => {
+ it( 'is set if view.isSticky is true view._stickyTopOffset is set', () => {
testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
top: -10,
- bottom: 80,
+ bottom: 70,
height: 100
} );
@@ -432,28 +760,26 @@ describe( 'StickyPanelView', () => {
height: 20
} );
- Object.assign( windowStub, {
- scrollX: 10,
- scrollY: 0
- } );
+ sinon.stub( global.window, 'scrollX' ).value( 10 );
+ sinon.stub( global.window, 'scrollY' ).value( 0 );
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
expect( view._marginLeft ).to.equal( null );
view.isActive = true;
expect( view.isSticky ).to.be.true;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._stickyTopOffset ).to.not.equal( null );
expect( view._marginLeft ).to.equal( '-10px' );
} );
- it( 'is not set if view._isStickyToTheLimiter is true', () => {
+ it( 'is set if view._isStickyToTheBottomOfLimiter is true', () => {
testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
- top: -10,
- bottom: 10,
+ top: -30,
+ bottom: 50,
left: 60,
- height: 100
+ height: 80
} );
testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
@@ -464,32 +790,43 @@ describe( 'StickyPanelView', () => {
left: 40
} );
+ sinon.stub( global.window, 'innerHeight' ).value( 100 );
+
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
expect( view._marginLeft ).to.equal( null );
view.isActive = true;
expect( view.isSticky ).to.be.true;
- expect( view._isStickyToTheLimiter ).to.be.true;
- expect( view._marginLeft ).to.equal( null );
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.true;
+ expect( view._marginLeft ).to.equal( '0px' );
} );
- it( 'is not set if view.isSticky is false', () => {
+ it( 'is null if view.isSticky is false', () => {
testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
top: 10
} );
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
expect( view._marginLeft ).to.equal( null );
view.isActive = true;
expect( view.isSticky ).to.be.false;
- expect( view._isStickyToTheLimiter ).to.be.false;
+ expect( view._isStickyToTheBottomOfLimiter ).to.be.false;
expect( view._marginLeft ).to.equal( null );
} );
} );
} );
+
+ function assureStickiness( options ) {
+ expect( view.isSticky, 'isSticky is incorrect' ).to.equal( options.isSticky );
+ expect( view._isStickyToTheBottomOfLimiter, '_isStickyToTheBottomOfLimiter is incorrect' )
+ .to.equal( options._isStickyToTheBottomOfLimiter );
+ expect( view._stickyTopOffset, '_stickyTopOffset is incorrect' ).to.equal( options._stickyTopOffset );
+ expect( view._stickyBottomOffset, '_stickyBottomOffset is incorrect' ).to.equal( options._stickyBottomOffset );
+ expect( view._marginLeft, '_marginLeft is incorrect' ).to.equal( options._marginLeft );
+ }
} );