From f5801f203fe6dfbf5b78c964841160f6b84f1b27 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 5 Jul 2023 17:31:39 +0200 Subject: [PATCH 01/15] WIP --- .../src/panel/sticky/stickypanelview.ts | 238 +++++++++++++----- 1 file changed, 182 insertions(+), 56 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 5e131e69fdc..9c7541b5c95 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -16,9 +16,13 @@ import { global, toUnit, type Locale, - type ObservableChangeEvent + type ObservableChangeEvent, + findClosestScrollableAncestor, + Rect } from '@ckeditor/ckeditor5-utils'; +import RectDrawer from '@ckeditor/ckeditor5-utils/tests/_utils/rectdrawer'; + import '../../../theme/components/panel/stickypanel.css'; const toPx = toUnit( 'px' ); @@ -107,29 +111,22 @@ export default class StickyPanelView extends View { * @readonly * @observable */ - declare public _isStickyToTheLimiter: boolean; + declare public _isStickyToTheBottomOfLimiter: boolean; /** - * Set `true` if the sticky panel uses the {@link #viewportTopOffset}, - * i.e. not {@link #_isStickyToTheLimiter} and the {@link #viewportTopOffset} - * is not `0`. - * - * @private - * @readonly - * @observable + * The DOM bounding client rect of the {@link module:ui/view~View#element} of the panel. */ - declare public _hasViewportTopOffset: boolean; + private _panelRect?: Rect; /** - * The DOM bounding client rect of the {@link module:ui/view~View#element} of the panel. + * TODO */ - private _panelRect?: DOMRect; + declare public _stickyTopOffset: number | null; /** - * The DOM bounding client rect of the {@link #limiterElement} - * of the panel. + * TODO */ - private _limiterRect?: DOMRect; + declare public _stickyBottomOffset: number | null; /** * A dummy element which visually fills the space as long as the @@ -158,8 +155,10 @@ export default class StickyPanelView extends View { this.set( 'viewportTopOffset', 0 ); this.set( '_marginLeft', null ); - this.set( '_isStickyToTheLimiter', false ); - this.set( '_hasViewportTopOffset', false ); + this.set( '_isStickyToTheBottomOfLimiter', false ); + + this.set( '_stickyTopOffset', null ); + this.set( '_stickyBottomOffset', null ); this.content = this.createCollection(); @@ -188,20 +187,15 @@ export default class StickyPanelView extends View { 'ck-sticky-panel__content', // Toggle class of the panel when "sticky" state changes in the view. bind.if( 'isSticky', 'ck-sticky-panel__content_sticky' ), - bind.if( '_isStickyToTheLimiter', 'ck-sticky-panel__content_sticky_bottom-limit' ) + bind.if( '_isStickyToTheBottomOfLimiter', 'ck-sticky-panel__content_sticky_bottom-limit' ) ], style: { width: bind.to( 'isSticky', isSticky => { return isSticky ? toPx( this._contentPanelPlaceholder.getBoundingClientRect().width ) : null; } ), - top: bind.to( '_hasViewportTopOffset', _hasViewportTopOffset => { - return _hasViewportTopOffset ? toPx( this.viewportTopOffset ) : null; - } ), - - bottom: bind.to( '_isStickyToTheLimiter', _isStickyToTheLimiter => { - return _isStickyToTheLimiter ? toPx( this.limiterBottomOffset ) : null; - } ), + top: bind.to( '_stickyTopOffset', value => value ? toPx( value ) : value ), + bottom: bind.to( '_stickyBottomOffset', value => value ? toPx( value ) : value ), marginLeft: bind.to( '_marginLeft' ) } @@ -234,10 +228,10 @@ export default class StickyPanelView extends View { // Check if the panel should go into the sticky state immediately. this._checkIfShouldBeSticky(); - // Update sticky state of the panel as the window is being scrolled. - this.listenTo( global.window, 'scroll', () => { - this._checkIfShouldBeSticky(); - } ); + // Update sticky state of the panel as the window and ancestors are being scrolled. + this.listenTo( global.document, 'scroll', ( evt, data ) => { + this._checkIfShouldBeSticky( data.target as HTMLElement | Document ); + }, { useCapture: true } ); // Synchronize with `model.isActive` because sticking an inactive panel is pointless. this.listenTo( this, 'change:isActive', () => { @@ -249,39 +243,171 @@ export default class StickyPanelView extends View { * Analyzes the environment to decide whether the panel should * be sticky or not. */ - private _checkIfShouldBeSticky(): void { - const panelRect = this._panelRect = this._contentPanel.getBoundingClientRect(); - let limiterRect: DOMRect; + private _checkIfShouldBeSticky( scrollTarget?: HTMLElement | Document ): void { + RectDrawer.clear(); if ( !this.limiterElement ) { - this.isSticky = false; + this._unstick(); + this._log(); + + return; + } + + const scrollableAncestors = getScrollableAncestors( this.limiterElement ); + + if ( scrollTarget && !scrollableAncestors.includes( scrollTarget ) ) { + console.log( 'Something else was scrolled' ); + + return; + } + + const visibleAncestorsRect = getVisibleAncestorsRect( scrollableAncestors, this.viewportTopOffset ); + const limiterRect = new Rect( this.limiterElement ); + + if ( visibleAncestorsRect ) { + RectDrawer.draw( visibleAncestorsRect, + { outlineWidth: '3px', opacity: '.8', outlineColor: 'red', outlineOffset: '-3px' }, + 'Visible anc' + ); + } + + RectDrawer.draw( limiterRect, + { outlineWidth: '3px', opacity: '.8', outlineColor: 'green', outlineOffset: '-3px' }, + 'Limiter' + ); + + if ( visibleAncestorsRect && limiterRect.top < visibleAncestorsRect.top ) { + const visibleLimiterRect = limiterRect.getIntersection( visibleAncestorsRect ); + + if ( visibleLimiterRect ) { + RectDrawer.draw( visibleLimiterRect, + { outlineWidth: '3px', opacity: '.8', outlineColor: 'fuchsia', outlineOffset: '-3px' }, + 'Visible limiter' + ); + + const visibleAncestorsTop = visibleAncestorsRect.top; + + this._panelRect = new Rect( this._contentPanel ); + + if ( visibleAncestorsTop + this._panelRect.height + this.limiterBottomOffset > visibleLimiterRect.bottom ) { + this._stickToBottomOfLimiter( limiterRect, visibleAncestorsRect ); + } else { + this._stickToTopOfAncestors( visibleAncestorsTop ); + } + } else { + console.log( 'No visible limiter' ); + this._unstick(); + } } else { - limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect(); - - // The panel 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 (+the viewportTopOffset). - limiterRect.top < this.viewportTopOffset && - // The model#limiterElement's height mustn't be smaller than the panel'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#limiterBottomOffset, the panel - // would be positioned before the model#limiterElement. - this._panelRect.height + this.limiterBottomOffset < limiterRect.height; + console.log( 'No visible ancestors or limiter not touching yet' ); + this._unstick(); } - // Stick the panel 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 < panelRect.height + this.limiterBottomOffset + this.viewportTopOffset; - this._hasViewportTopOffset = !this._isStickyToTheLimiter && !!this.viewportTopOffset; - this._marginLeft = this._isStickyToTheLimiter ? null : toPx( -global.window.scrollX ); + this._log(); + } + + /** + * + * @param limiterRect + * @param visibleAncestorsRect + */ + private _stickToBottomOfLimiter( limiterRect: Rect, visibleAncestorsRect: Rect ) { + this.isSticky = true; + this._isStickyToTheBottomOfLimiter = true; + this._stickyTopOffset = null; + this._stickyBottomOffset = + Math.max( limiterRect.bottom - visibleAncestorsRect.bottom, 0 ) + this.limiterBottomOffset; + this._marginLeft = toPx( -global.window.scrollX ); + } + + /** + * + * @param topOffset + */ + private _stickToTopOfAncestors( topOffset: number ) { + this.isSticky = true; + this._isStickyToTheBottomOfLimiter = false; + this._stickyTopOffset = topOffset; + this._stickyBottomOffset = null; + this._marginLeft = null; + } + + /** + * TODO + */ + private _unstick() { + this.isSticky = false; + this._isStickyToTheBottomOfLimiter = false; + this._stickyTopOffset = null; + this._stickyBottomOffset = null; + this._marginLeft = null; + } + + /** + * TO BE REMOVED + */ + private _log() { + console.clear(); + console.log( 'isSticky', this.isSticky ); + console.log( '_stickyTopOffset', this._stickyTopOffset ); + console.log( '_stickyBottomOffset', this._stickyBottomOffset ); + console.log( '_isStickyToTheBottomOfLimiter', this._isStickyToTheBottomOfLimiter ); + console.log( '_marginLeft', this._marginLeft ); + } +} + +/** + * TODO + * + * @param element + * @returns + */ +function getScrollableAncestors( element: HTMLElement ) { + const scrollableAncestors = []; + let scrollableAncestor = findClosestScrollableAncestor( element ); + + while ( scrollableAncestor && scrollableAncestor !== global.document.body ) { + scrollableAncestors.push( scrollableAncestor ); + scrollableAncestor = findClosestScrollableAncestor( scrollableAncestor! ); + } + + scrollableAncestors.push( global.document ); + + return scrollableAncestors; +} + +/** + * TODO + * + * @param scrollableAncestors + * @param viewportTopOffset + * @returns + */ +function getVisibleAncestorsRect( scrollableAncestors: Array, viewportTopOffset: number ) { + const scrollableAncestorsRects = scrollableAncestors.map( ancestor => { + 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 ]; + + for ( const scrollableAncestorRect of scrollableAncestorsRects ) { + RectDrawer.draw( scrollableAncestorRect, { outlineWidth: '1px', opacity: '.7' }, 'Anc' ); + } + + for ( const scrollableAncestorRect of scrollableAncestorsRects.slice( 1 ) ) { + if ( scrollableAncestorsIntersectionRect ) { + scrollableAncestorsIntersectionRect = scrollableAncestorsIntersectionRect.getIntersection( scrollableAncestorRect ); } } + + return scrollableAncestorsIntersectionRect; } From 93ffbc8de64f4fe01be150b0773fe11188637e0e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 5 Jul 2023 17:32:14 +0200 Subject: [PATCH 02/15] Temp: Used article manual test to test sticky panel. --- tests/manual/article.html | 69 +++++++++++++++++++++++++-------------- tests/manual/article.js | 5 +++ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/tests/manual/article.html b/tests/manual/article.html index 4faffd0a25b..7529a1f2588 100644 --- a/tests/manual/article.html +++ b/tests/manual/article.html @@ -3,31 +3,52 @@ body { max-width: 800px; margin: 20px auto; + padding: 1000px 50px; + min-height: 5000px; + } + + .scrollable { + padding: 80px; + border: 1px solid #ccc; + overflow: scroll; + height: 250px; + margin-top: 30px; + margin-bottom: 200px; + box-sizing: border-box; + } + + .ck.ck-editor { + margin-bottom: 150px !important; } -
-

Heading 1

-

Paragraph

-

Bold Italic Link

-
    -
  • UL List item 1
  • -
  • UL List item 2
  • -
-
    -
  1. OL List item 1
  2. -
  3. OL List item 2
  4. -
-
- bar -
Caption
-
-
-

Quote

-
    -
  • Quoted UL List item 1
  • -
  • Quoted UL List item 2
  • -
-

Quote

-
+ +
+
+
+

Heading 1

+

Paragraph

+

Bold Italic Link

+
    +
  • UL List item 1
  • +
  • UL List item 2
  • +
+
    +
  1. OL List item 1
  2. +
  3. OL List item 2
  4. +
+
+ bar +
Caption
+
+
+

Quote

+
    +
  • Quoted UL List item 1
  • +
  • Quoted UL List item 2
  • +
+

Quote

+
+
+
diff --git a/tests/manual/article.js b/tests/manual/article.js index c734a603125..638e5e4eaac 100644 --- a/tests/manual/article.js +++ b/tests/manual/article.js @@ -39,6 +39,11 @@ ClassicEditor 'tableRow', 'mergeTableCells' ] + }, + ui: { + viewportOffset: { + top: 50 + } } } ) .then( editor => { From 4be8e08843a5fa11028233dc78aa1022bf9bf1ab Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 6 Jul 2023 10:37:30 +0200 Subject: [PATCH 03/15] Put sticky panel debug logs behind CK_DEBUG. --- .../src/panel/sticky/stickypanelview.ts | 60 +++++++------------ 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 9c7541b5c95..7e626d4ada4 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -21,7 +21,7 @@ import { Rect } from '@ckeditor/ckeditor5-utils'; -import RectDrawer from '@ckeditor/ckeditor5-utils/tests/_utils/rectdrawer'; +// @if CK_DEBUG_STICKYPANEL // const RectDrawer = require( '@ckeditor/ckeditor5-utils/tests/_utils/rectdrawer' ).default import '../../../theme/components/panel/stickypanel.css'; @@ -244,11 +244,10 @@ export default class StickyPanelView extends View { * be sticky or not. */ private _checkIfShouldBeSticky( scrollTarget?: HTMLElement | Document ): void { - RectDrawer.clear(); + // @if CK_DEBUG_STICKYPANEL // RectDrawer.clear(); if ( !this.limiterElement ) { this._unstick(); - this._log(); return; } @@ -264,26 +263,27 @@ export default class StickyPanelView extends View { const visibleAncestorsRect = getVisibleAncestorsRect( scrollableAncestors, this.viewportTopOffset ); const limiterRect = new Rect( this.limiterElement ); - if ( visibleAncestorsRect ) { - RectDrawer.draw( visibleAncestorsRect, - { outlineWidth: '3px', opacity: '.8', outlineColor: 'red', outlineOffset: '-3px' }, - 'Visible anc' - ); - } - - RectDrawer.draw( limiterRect, - { outlineWidth: '3px', opacity: '.8', outlineColor: 'green', outlineOffset: '-3px' }, - 'Limiter' - ); + // @if CK_DEBUG_STICKYPANEL // if ( visibleAncestorsRect ) { + // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleAncestorsRect, + // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'red', outlineOffset: '-3px' }, + // @if CK_DEBUG_STICKYPANEL // 'Visible anc' + // @if CK_DEBUG_STICKYPANEL // ); + // @if CK_DEBUG_STICKYPANEL // } + // @if CK_DEBUG_STICKYPANEL // + // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( limiterRect, + // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'green', outlineOffset: '-3px' }, + // @if CK_DEBUG_STICKYPANEL // 'Limiter' + // @if CK_DEBUG_STICKYPANEL // ); if ( visibleAncestorsRect && limiterRect.top < visibleAncestorsRect.top ) { const visibleLimiterRect = limiterRect.getIntersection( visibleAncestorsRect ); if ( visibleLimiterRect ) { - RectDrawer.draw( visibleLimiterRect, - { outlineWidth: '3px', opacity: '.8', outlineColor: 'fuchsia', outlineOffset: '-3px' }, - 'Visible limiter' - ); + // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleLimiterRect, + // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'fuchsia', outlineOffset: '-3px', + // @if CK_DEBUG_STICKYPANEL // backgroundColor: 'rgba(255, 0, 255, .3)' }, + // @if CK_DEBUG_STICKYPANEL // 'Visible limiter' + // @if CK_DEBUG_STICKYPANEL // ); const visibleAncestorsTop = visibleAncestorsRect.top; @@ -295,15 +295,11 @@ export default class StickyPanelView extends View { this._stickToTopOfAncestors( visibleAncestorsTop ); } } else { - console.log( 'No visible limiter' ); this._unstick(); } } else { - console.log( 'No visible ancestors or limiter not touching yet' ); this._unstick(); } - - this._log(); } /** @@ -342,18 +338,6 @@ export default class StickyPanelView extends View { this._stickyBottomOffset = null; this._marginLeft = null; } - - /** - * TO BE REMOVED - */ - private _log() { - console.clear(); - console.log( 'isSticky', this.isSticky ); - console.log( '_stickyTopOffset', this._stickyTopOffset ); - console.log( '_stickyBottomOffset', this._stickyBottomOffset ); - console.log( '_isStickyToTheBottomOfLimiter', this._isStickyToTheBottomOfLimiter ); - console.log( '_marginLeft', this._marginLeft ); - } } /** @@ -399,9 +383,11 @@ function getVisibleAncestorsRect( scrollableAncestors: Array Date: Thu, 6 Jul 2023 10:38:23 +0200 Subject: [PATCH 04/15] Added integration test for sticky panel to ckeditor5-editor-classic. --- .../tests/manual/stickypanel.html | 69 +++++++++++++++++++ .../tests/manual/stickypanel.js | 54 +++++++++++++++ .../tests/manual/stickypanel.md | 1 + 3 files changed, 124 insertions(+) create mode 100644 packages/ckeditor5-editor-classic/tests/manual/stickypanel.html create mode 100644 packages/ckeditor5-editor-classic/tests/manual/stickypanel.js create mode 100644 packages/ckeditor5-editor-classic/tests/manual/stickypanel.md diff --git a/packages/ckeditor5-editor-classic/tests/manual/stickypanel.html b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.html new file mode 100644 index 00000000000..1cbfa8e473c --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.html @@ -0,0 +1,69 @@ + + + + +
Viewport offset
+ +
+
+
+

Heading 1

+

Paragraph

+

Bold Italic Link

+
    +
  • UL List item 1
  • +
  • UL List item 2
  • +
+
    +
  1. OL List item 1
  2. +
  3. OL List item 2
  4. +
+
+ bar +
Caption
+
+
+

Quote

+
    +
  • Quoted UL List item 1
  • +
  • Quoted UL List item 2
  • +
+

Quote

+
+
+
+
diff --git a/packages/ckeditor5-editor-classic/tests/manual/stickypanel.js b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.js new file mode 100644 index 00000000000..8077ed0bec7 --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.js @@ -0,0 +1,54 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import ClassicEditor from '../../src/classiceditor'; + +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet ], + toolbar: [ + 'heading', + '|', + 'bold', + 'italic', + 'link', + 'bulletedList', + 'numberedList', + '|', + 'outdent', + 'indent', + '|', + 'blockQuote', + 'insertTable', + 'mediaEmbed', + 'undo', + 'redo' + ], + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'imageTextAlternative' ] + }, + table: { + contentToolbar: [ + 'tableColumn', + 'tableRow', + 'mergeTableCells' + ] + }, + ui: { + viewportOffset: { + top: 50 + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-classic/tests/manual/stickypanel.md b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.md new file mode 100644 index 00000000000..464090415c4 --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.md @@ -0,0 +1 @@ +# TODO From 47fde95ec04f40e5537db3f8bc30cd801fee61a4 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 6 Jul 2023 10:38:40 +0200 Subject: [PATCH 05/15] Updated StickyPanelView test in ckeditor5-ui. --- .../manual/panel/sticky/stickypanelview.html | 101 ++++++++++++++---- .../manual/panel/sticky/stickypanelview.js | 8 +- 2 files changed, 82 insertions(+), 27 deletions(-) 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

+ +
+
+
+
+

+ 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.
@@ -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(); From 92be7d93fb70bcf9c5f4be3bceaa53a6589aea15 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 6 Jul 2023 10:40:39 +0200 Subject: [PATCH 06/15] Code refactoring. --- packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 7e626d4ada4..a343a03f6ca 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -255,8 +255,6 @@ export default class StickyPanelView extends View { const scrollableAncestors = getScrollableAncestors( this.limiterElement ); if ( scrollTarget && !scrollableAncestors.includes( scrollTarget ) ) { - console.log( 'Something else was scrolled' ); - return; } From d555b40fe1b150acccc9c1d6f55848880d257593 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 6 Jul 2023 10:41:27 +0200 Subject: [PATCH 07/15] Tests: Reverted changes in the article manual test. --- tests/manual/article.html | 69 ++++++++++++++------------------------- tests/manual/article.js | 5 --- 2 files changed, 24 insertions(+), 50 deletions(-) diff --git a/tests/manual/article.html b/tests/manual/article.html index 7529a1f2588..4faffd0a25b 100644 --- a/tests/manual/article.html +++ b/tests/manual/article.html @@ -3,52 +3,31 @@ body { max-width: 800px; margin: 20px auto; - padding: 1000px 50px; - min-height: 5000px; - } - - .scrollable { - padding: 80px; - border: 1px solid #ccc; - overflow: scroll; - height: 250px; - margin-top: 30px; - margin-bottom: 200px; - box-sizing: border-box; - } - - .ck.ck-editor { - margin-bottom: 150px !important; } - -
-
-
-

Heading 1

-

Paragraph

-

Bold Italic Link

-
    -
  • UL List item 1
  • -
  • UL List item 2
  • -
-
    -
  1. OL List item 1
  2. -
  3. OL List item 2
  4. -
-
- bar -
Caption
-
-
-

Quote

-
    -
  • Quoted UL List item 1
  • -
  • Quoted UL List item 2
  • -
-

Quote

-
-
-
+
+

Heading 1

+

Paragraph

+

Bold Italic Link

+
    +
  • UL List item 1
  • +
  • UL List item 2
  • +
+
    +
  1. OL List item 1
  2. +
  3. OL List item 2
  4. +
+
+ bar +
Caption
+
+
+

Quote

+
    +
  • Quoted UL List item 1
  • +
  • Quoted UL List item 2
  • +
+

Quote

+
diff --git a/tests/manual/article.js b/tests/manual/article.js index 638e5e4eaac..c734a603125 100644 --- a/tests/manual/article.js +++ b/tests/manual/article.js @@ -39,11 +39,6 @@ ClassicEditor 'tableRow', 'mergeTableCells' ] - }, - ui: { - viewportOffset: { - top: 50 - } } } ) .then( editor => { From 726bc2e6ca56f35ea4bbb50c0751af11eb60308a Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 6 Jul 2023 11:53:17 +0200 Subject: [PATCH 08/15] Improvements to the sticky panel logic. --- .../src/panel/sticky/stickypanelview.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index a343a03f6ca..7ec6f6f2099 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -246,7 +246,7 @@ export default class StickyPanelView extends View { private _checkIfShouldBeSticky( scrollTarget?: HTMLElement | Document ): void { // @if CK_DEBUG_STICKYPANEL // RectDrawer.clear(); - if ( !this.limiterElement ) { + if ( !this.limiterElement || !this.isActive ) { this._unstick(); return; @@ -288,7 +288,22 @@ export default class StickyPanelView extends View { this._panelRect = new Rect( this._contentPanel ); if ( visibleAncestorsTop + this._panelRect.height + this.limiterBottomOffset > visibleLimiterRect.bottom ) { - this._stickToBottomOfLimiter( limiterRect, visibleAncestorsRect ); + const stickyBottomOffset = Math.max( limiterRect.bottom - visibleAncestorsRect.bottom, 0 ) + this.limiterBottomOffset; + + // @if CK_DEBUG_STICKYPANEL // const stickyBottomOffsetRect = new Rect( { + // @if CK_DEBUG_STICKYPANEL // top: limiterRect.bottom - stickyBottomOffset, left: 0, right: 0, + // @if CK_DEBUG_STICKYPANEL // bottom: limiterRect.bottom - stickyBottomOffset, width: 1000, height: 1 + // @if CK_DEBUG_STICKYPANEL // } ); + // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( stickyBottomOffsetRect, + // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '1px', opacity: '.8', outlineColor: 'black' }, + // @if CK_DEBUG_STICKYPANEL // 'Sticky bottom offset' + // @if CK_DEBUG_STICKYPANEL // ); + + if ( limiterRect.bottom - stickyBottomOffset > limiterRect.top + this._panelRect.height ) { + this._stickToBottomOfLimiter( limiterRect, visibleAncestorsRect, stickyBottomOffset ); + } else { + this._unstick(); + } } else { this._stickToTopOfAncestors( visibleAncestorsTop ); } @@ -298,6 +313,12 @@ export default class StickyPanelView extends View { } else { this._unstick(); } + + // @if CK_DEBUG_STICKYPANEL // console.clear(); + // @if CK_DEBUG_STICKYPANEL // console.log( 'isSticky', this.isSticky ); + // @if CK_DEBUG_STICKYPANEL // console.log( '_isStickyToTheBottomOfLimiter', this._isStickyToTheBottomOfLimiter ); + // @if CK_DEBUG_STICKYPANEL // console.log( '_stickyTopOffset', this._stickyTopOffset ); + // @if CK_DEBUG_STICKYPANEL // console.log( '_stickyBottomOffset', this._stickyBottomOffset ); } /** @@ -305,12 +326,11 @@ export default class StickyPanelView extends View { * @param limiterRect * @param visibleAncestorsRect */ - private _stickToBottomOfLimiter( limiterRect: Rect, visibleAncestorsRect: Rect ) { + private _stickToBottomOfLimiter( limiterRect: Rect, visibleAncestorsRect: Rect, stickyBottomOffset: number ) { this.isSticky = true; this._isStickyToTheBottomOfLimiter = true; this._stickyTopOffset = null; - this._stickyBottomOffset = - Math.max( limiterRect.bottom - visibleAncestorsRect.bottom, 0 ) + this.limiterBottomOffset; + this._stickyBottomOffset = stickyBottomOffset; this._marginLeft = toPx( -global.window.scrollX ); } From f72fdf4f406c2f5a9164f3f224002793944dd2e2 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 6 Jul 2023 12:18:04 +0200 Subject: [PATCH 09/15] Started updating StickyPanelView tests. --- .../src/panel/sticky/stickypanelview.ts | 9 +++ .../tests/panel/sticky/stickypanelview.js | 78 +++++++++---------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 7ec6f6f2099..066536890e1 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -273,10 +273,13 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // 'Limiter' // @if CK_DEBUG_STICKYPANEL // ); + console.log( visibleAncestorsRect, limiterRect.top < visibleAncestorsRect.top ); if ( visibleAncestorsRect && limiterRect.top < visibleAncestorsRect.top ) { const visibleLimiterRect = limiterRect.getIntersection( visibleAncestorsRect ); + console.log( 'a' ); if ( visibleLimiterRect ) { + console.log( 'b' ); // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleLimiterRect, // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'fuchsia', outlineOffset: '-3px', // @if CK_DEBUG_STICKYPANEL // backgroundColor: 'rgba(255, 0, 255, .3)' }, @@ -288,6 +291,7 @@ export default class StickyPanelView extends View { this._panelRect = new Rect( this._contentPanel ); if ( visibleAncestorsTop + this._panelRect.height + this.limiterBottomOffset > visibleLimiterRect.bottom ) { + console.log( 'c' ); const stickyBottomOffset = Math.max( limiterRect.bottom - visibleAncestorsRect.bottom, 0 ) + this.limiterBottomOffset; // @if CK_DEBUG_STICKYPANEL // const stickyBottomOffsetRect = new Rect( { @@ -300,17 +304,22 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // ); if ( limiterRect.bottom - stickyBottomOffset > limiterRect.top + this._panelRect.height ) { + console.log( 'd' ); this._stickToBottomOfLimiter( limiterRect, visibleAncestorsRect, stickyBottomOffset ); } else { + console.log( 'e' ); this._unstick(); } } else { + console.log( 'f' ); this._stickToTopOfAncestors( visibleAncestorsTop ); } } else { + console.log( 'g' ); this._unstick(); } } else { + console.log( 'h' ); this._unstick(); } diff --git a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js index c600dc4f348..2d082bebb11 100644 --- a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js +++ b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js @@ -12,9 +12,10 @@ 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'; +import { Rect } from '@ckeditor/ckeditor5-utils'; describe( 'StickyPanelView', () => { - let view, element, contentElement, placeholderElement, limiterElement, locale, windowStub; + let view, element, contentElement, placeholderElement, limiterElement, locale; testUtils.createSinonSandbox(); @@ -33,14 +34,8 @@ describe( 'StickyPanelView', () => { 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 ); } ); @@ -74,7 +69,7 @@ describe( 'StickyPanelView', () => { expect( view.limiterBottomOffset ).to.equal( 50 ); expect( view.viewportTopOffset ).to.equal( 0 ); - expect( view._isStickyToTheLimiter ).to.be.false; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; expect( view._hasViewportTopOffset ).to.be.false; expect( view._marginLeft ).to.be.null; } ); @@ -101,11 +96,11 @@ 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; } ); @@ -129,11 +124,11 @@ describe( 'StickyPanelView', () => { expect( contentElement.style.width ).to.equal( '100px' ); } ); - it( 'update the styles.bottom on view#_isStickyToTheLimiter change', () => { - view._isStickyToTheLimiter = false; + it( 'update the styles.bottom on view#_isStickyToTheBottomOfLimiter change', () => { + view._isStickyToTheBottomOfLimiter = false; expect( contentElement.style.bottom ).to.equal( '' ); - view._isStickyToTheLimiter = true; + view._isStickyToTheBottomOfLimiter = true; expect( contentElement.style.bottom ).to.equal( '50px' ); } ); @@ -302,7 +297,7 @@ 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, @@ -315,12 +310,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 +330,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,17 +344,17 @@ 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', () => { + it.only( 'is true if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset has been specified', () => { view.viewportTopOffset = 100; testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { @@ -375,11 +370,12 @@ describe( 'StickyPanelView', () => { view.isActive = true; expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheLimiter ).to.be.false; - expect( view._hasViewportTopOffset ).to.be.true; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; + expect( view._stickyTopOffset ).to.equal( 100 ); + expect( view._stickyBottomOffset ).to.be.null; } ); - it( 'is false if view._isStickyToTheLimiter is true and view.viewportTopOffset has been specified', () => { + it( 'is false if view._isStickyToTheBottomOfLimiter is true and view.viewportTopOffset has been specified', () => { view.viewportTopOffset = 100; testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { @@ -395,11 +391,11 @@ describe( 'StickyPanelView', () => { view.isActive = true; expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheLimiter ).to.be.true; + expect( view._isStickyToTheBottomOfLimiter ).to.be.true; expect( view._hasViewportTopOffset ).to.be.false; } ); - it( 'is false if view._isStickyToTheLimiter is false and view.viewportTopOffset is 0', () => { + it( 'is false if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset is 0', () => { view.viewportTopOffset = 100; testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { @@ -415,13 +411,13 @@ describe( 'StickyPanelView', () => { view.isActive = true; expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheLimiter ).to.be.false; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; expect( view._hasViewportTopOffset ).to.be.true; } ); } ); describe( 'view._marginLeft', () => { - it( 'is set if view.isSticky is true view._isStickyToTheLimiter is false', () => { + it( 'is set if view.isSticky is true view._isStickyToTheBottomOfLimiter is false', () => { testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, bottom: 80, @@ -432,23 +428,21 @@ 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._isStickyToTheBottomOfLimiter ).to.be.false; expect( view._marginLeft ).to.equal( '-10px' ); } ); - it( 'is not set if view._isStickyToTheLimiter is true', () => { + it( 'is not set if view._isStickyToTheBottomOfLimiter is true', () => { testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, bottom: 10, @@ -465,13 +459,13 @@ describe( 'StickyPanelView', () => { } ); 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._isStickyToTheBottomOfLimiter ).to.be.true; expect( view._marginLeft ).to.equal( null ); } ); @@ -481,13 +475,13 @@ describe( 'StickyPanelView', () => { } ); 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 ); } ); } ); From 2ab0c2e9014a5858b5bc6ffcecb933f9b0024d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Jarz=C4=99bski?= Date: Fri, 7 Jul 2023 15:25:54 +0200 Subject: [PATCH 10/15] Fix existing unit tests. --- .../src/panel/sticky/stickypanelview.ts | 34 ++++++-- .../tests/panel/sticky/stickypanelview.js | 77 ++++++++++--------- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 066536890e1..3fcb63880db 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -245,6 +245,7 @@ export default class StickyPanelView extends View { */ private _checkIfShouldBeSticky( scrollTarget?: HTMLElement | Document ): void { // @if CK_DEBUG_STICKYPANEL // RectDrawer.clear(); + this._panelRect = new Rect( this._contentPanel ); if ( !this.limiterElement || !this.isActive ) { this._unstick(); @@ -274,6 +275,13 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // ); console.log( visibleAncestorsRect, limiterRect.top < visibleAncestorsRect.top ); + + console.log( 'visibleAncestorsRect.top', visibleAncestorsRect.top ); + // console.log( 'this._panelRect.height', this._panelRect.height ); + console.log( 'this.limiterBottomOffset', this.limiterBottomOffset ); + // console.log( 'visibleLimiterRect.bottom', visibleLimiterRect.bottom ); + console.log( 'limiterRect.bottom', limiterRect.bottom ); + console.log( 'limiterRect.top', limiterRect.top ); if ( visibleAncestorsRect && limiterRect.top < visibleAncestorsRect.top ) { const visibleLimiterRect = limiterRect.getIntersection( visibleAncestorsRect ); console.log( 'a' ); @@ -287,13 +295,18 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // ); const visibleAncestorsTop = visibleAncestorsRect.top; - - this._panelRect = new Rect( this._contentPanel ); + console.log( 'visibleAncestorsTop', visibleAncestorsTop ); + console.log( 'this._panelRect.height', this._panelRect.height ); + console.log( 'this.limiterBottomOffset', this.limiterBottomOffset ); + console.log( 'visibleLimiterRect.bottom', visibleLimiterRect.bottom ); + console.log( 'limiterRect.bottom', limiterRect.bottom ); + console.log( 'limiterRect.top', limiterRect.top ); if ( visibleAncestorsTop + this._panelRect.height + this.limiterBottomOffset > visibleLimiterRect.bottom ) { console.log( 'c' ); + console.log( visibleAncestorsRect.bottom ); const stickyBottomOffset = Math.max( limiterRect.bottom - visibleAncestorsRect.bottom, 0 ) + this.limiterBottomOffset; - + console.log( 'stickyBottomOffset', stickyBottomOffset ); // @if CK_DEBUG_STICKYPANEL // const stickyBottomOffsetRect = new Rect( { // @if CK_DEBUG_STICKYPANEL // top: limiterRect.bottom - stickyBottomOffset, left: 0, right: 0, // @if CK_DEBUG_STICKYPANEL // bottom: limiterRect.bottom - stickyBottomOffset, width: 1000, height: 1 @@ -303,6 +316,7 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // 'Sticky bottom offset' // @if CK_DEBUG_STICKYPANEL // ); + console.log( 'result', limiterRect.bottom - stickyBottomOffset, limiterRect.top + this._panelRect.height ); if ( limiterRect.bottom - stickyBottomOffset > limiterRect.top + this._panelRect.height ) { console.log( 'd' ); this._stickToBottomOfLimiter( limiterRect, visibleAncestorsRect, stickyBottomOffset ); @@ -312,14 +326,20 @@ export default class StickyPanelView extends View { } } else { console.log( 'f' ); - this._stickToTopOfAncestors( visibleAncestorsTop ); + if ( this._panelRect.height + this.limiterBottomOffset < limiterRect.height ) { + console.log( 'g' ); + this._stickToTopOfAncestors( visibleAncestorsTop ); + } else { + console.log( 'h' ); + this._unstick(); + } } } else { - console.log( 'g' ); + console.log( 'i' ); this._unstick(); } } else { - console.log( 'h' ); + console.log( 'j' ); this._unstick(); } @@ -352,7 +372,7 @@ export default class StickyPanelView extends View { this._isStickyToTheBottomOfLimiter = false; this._stickyTopOffset = topOffset; this._stickyBottomOffset = null; - this._marginLeft = null; + this._marginLeft = toPx( -global.window.scrollX ); } /** diff --git a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js index 2d082bebb11..cc164d01a82 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,8 +11,6 @@ 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'; -import { Rect } from '@ckeditor/ckeditor5-utils'; describe( 'StickyPanelView', () => { let view, element, contentElement, placeholderElement, limiterElement, locale; @@ -30,10 +28,6 @@ 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 }; - sinon.stub( global.window, 'innerWidth' ).value( 1000 ); sinon.stub( global.window, 'innerHeight' ).value( 500 ); @@ -64,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._isStickyToTheBottomOfLimiter ).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', () => { @@ -104,17 +100,17 @@ describe( 'StickyPanelView', () => { 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; @@ -124,15 +120,15 @@ describe( 'StickyPanelView', () => { expect( contentElement.style.width ).to.equal( '100px' ); } ); - it( 'update the styles.bottom on view#_isStickyToTheBottomOfLimiter change', () => { - view._isStickyToTheBottomOfLimiter = 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._isStickyToTheBottomOfLimiter = 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' ); @@ -146,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' ); @@ -154,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; @@ -203,13 +199,14 @@ describe( 'StickyPanelView', () => { expect( spy.calledOnce ).to.be.true; } ); - it( 'listens to window#scroll event and calls 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; } ); @@ -218,6 +215,8 @@ describe( 'StickyPanelView', () => { expect( spy.notCalled ).to.be.true; view.render(); + expect( spy.calledOnce ).to.be.true; + view.isActive = true; expect( spy.calledTwice ).to.be.true; @@ -300,9 +299,9 @@ describe( 'StickyPanelView', () => { 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( { @@ -353,8 +352,8 @@ describe( 'StickyPanelView', () => { } ); } ); - describe( 'view._hasViewportTopOffset', () => { - it.only( 'is true if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset has been specified', () => { + describe( 'view._stickyTopOffset', () => { + it( 'is not null if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset has been specified', () => { view.viewportTopOffset = 100; testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { @@ -375,7 +374,7 @@ describe( 'StickyPanelView', () => { expect( view._stickyBottomOffset ).to.be.null; } ); - it( 'is false if view._isStickyToTheBottomOfLimiter is true and view.viewportTopOffset has been specified', () => { + it( 'is null if view._isStickyToTheBottomOfLimiter is true and view.viewportTopOffset has been specified', () => { view.viewportTopOffset = 100; testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { @@ -392,11 +391,11 @@ describe( 'StickyPanelView', () => { expect( view.isSticky ).to.be.true; expect( view._isStickyToTheBottomOfLimiter ).to.be.true; - expect( view._hasViewportTopOffset ).to.be.false; + expect( view._stickyTopOffset ).to.equal( null ); } ); - it( 'is false if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset is 0', () => { - view.viewportTopOffset = 100; + it( 'is null if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset is 0', () => { + view.viewportTopOffset = 0; testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: 90, @@ -410,9 +409,9 @@ describe( 'StickyPanelView', () => { view.isActive = true; - expect( view.isSticky ).to.be.true; + expect( view.isSticky ).to.be.false; expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - expect( view._hasViewportTopOffset ).to.be.true; + expect( view._stickyTopOffset ).to.equal( null ); } ); } ); @@ -444,10 +443,10 @@ describe( 'StickyPanelView', () => { it( 'is not 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( { @@ -458,6 +457,8 @@ describe( 'StickyPanelView', () => { left: 40 } ); + sinon.stub( global.window, 'innerHeight' ).value( 100 ); + expect( view.isSticky ).to.be.false; expect( view._isStickyToTheBottomOfLimiter ).to.be.false; expect( view._marginLeft ).to.equal( null ); @@ -466,7 +467,7 @@ describe( 'StickyPanelView', () => { expect( view.isSticky ).to.be.true; expect( view._isStickyToTheBottomOfLimiter ).to.be.true; - expect( view._marginLeft ).to.equal( null ); + expect( view._marginLeft ).to.equal( '0px' ); } ); it( 'is not set if view.isSticky is false', () => { From 5bdf5628dc92639b2e1099bb5832ec637e930658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Jarz=C4=99bski?= Date: Mon, 10 Jul 2023 14:34:24 +0200 Subject: [PATCH 11/15] Add missing test coverage. --- .../src/panel/sticky/stickypanelview.ts | 17 +- .../tests/panel/sticky/stickypanelview.js | 354 +++++++++++++++++- 2 files changed, 363 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 3fcb63880db..78b5d4856e2 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -255,6 +255,8 @@ export default class StickyPanelView extends View { const scrollableAncestors = getScrollableAncestors( this.limiterElement ); + // console.log( scrollTarget ); + // console.log( scrollableAncestors ); if ( scrollTarget && !scrollableAncestors.includes( scrollTarget ) ) { return; } @@ -274,14 +276,15 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // 'Limiter' // @if CK_DEBUG_STICKYPANEL // ); - console.log( visibleAncestorsRect, limiterRect.top < visibleAncestorsRect.top ); + // console.log( visibleAncestorsRect, limiterRect.top < visibleAncestorsRect.top ); + console.log( 'visibleAncestorsRect', visibleAncestorsRect ); console.log( 'visibleAncestorsRect.top', visibleAncestorsRect.top ); - // console.log( 'this._panelRect.height', this._panelRect.height ); - console.log( 'this.limiterBottomOffset', this.limiterBottomOffset ); - // console.log( 'visibleLimiterRect.bottom', visibleLimiterRect.bottom ); - console.log( 'limiterRect.bottom', limiterRect.bottom ); - console.log( 'limiterRect.top', limiterRect.top ); + // // console.log( 'this._panelRect.height', this._panelRect.height ); + // console.log( 'this.limiterBottomOffset', this.limiterBottomOffset ); + // // console.log( 'visibleLimiterRect.bottom', visibleLimiterRect.bottom ); + // console.log( 'limiterRect.bottom', limiterRect.bottom ); + // console.log( 'limiterRect.top', limiterRect.top ); if ( visibleAncestorsRect && limiterRect.top < visibleAncestorsRect.top ) { const visibleLimiterRect = limiterRect.getIntersection( visibleAncestorsRect ); console.log( 'a' ); @@ -404,6 +407,8 @@ function getScrollableAncestors( element: HTMLElement ) { scrollableAncestors.push( global.document ); + console.log( scrollableAncestors.length ); + return scrollableAncestors; } diff --git a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js index cc164d01a82..cbd2ae7e6e9 100644 --- a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js +++ b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js @@ -246,6 +246,352 @@ describe( 'StickyPanelView', () => { view.limiterElement = limiterElement; } ); + it( 'should unstick the panel if limiter element is not set', () => { + view.limiterElement = null; + + expect( view.isSticky ).to.be.false; + } ); + + it( 'should unstick the panel if it is not active', () => { + const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' ); + + view.isActive = true; + view.isActive = false; + + expect( view.isSticky ).to.be.false; + expect( spy.calledTwice ).to.be.true; + } ); + + describe( 'called after scrolling', () => { + it( 'should do nothing if scrolled element does not contain the panel', () => { + view.isActive = true; + + const separateElement = document.createElement( 'div' ); + + view._checkIfShouldBeSticky( separateElement ); + + expect( view.isSticky ).to.be.false; + } ); + + describe( 'with/without #limiterBottomOffset ', () => { + + } ); + + describe( 'with/without #viewportTopOffset ', () => { + + } ); + + describe( 'isActive/ is not active ', () => { + + } ); + + describe( 'if there is one scrollable non-window parent', () => { + let scrollableContainer; + + beforeEach( () => { + scrollableContainer = document.createElement( 'div' ); + scrollableContainer.className = 'scrollable'; + scrollableContainer.style.overflow = 'scroll'; + scrollableContainer.appendChild( limiterElement ); + global.document.body.appendChild( scrollableContainer ); + } ); + + afterEach( () => { + scrollableContainer.remove(); + } ); + + describe( 'scrolled the container', () => { + it( 'should not make panel sticky if the limiter top is still visible', () => { + view.isActive = true; + + 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 ); + + expect( view.isSticky ).to.be.false; + } ); + + it( 'should make panel sticky if the limiter top is not visible', () => { + view.isActive = true; + + 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 ); + + expect( view.isSticky ).to.be.true; + } ); + + it( 'should make panel sticky to the bottom if there is enough space left', () => { + view.isActive = true; + + 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; + } ); + + it( 'should unstick the panel if there is not enough space left in the limiter', () => { + view.isActive = true; + + 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 ); + + expect( view.isSticky ).to.be.false; + } ); + + it( 'should unstick the panel if panel limiter is not visible in the viewport', () => { + view.isActive = true; + + 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 ); + + expect( view.isSticky ).to.be.false; + } ); + } ); + } ); + + describe( 'if there are multiple scrollable non-window parents', () => { + let scrollableOuterParent, scrollableInnerParent; + + 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 ); + } ); + + afterEach( () => { + scrollableInnerParent.remove(); + scrollableOuterParent.remove(); + } ); + + it( 'should do something if scrolled element contains the panel', () => { + view.isActive = true; + + testUtils.sinon.stub( scrollableInnerParent, '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( scrollableOuterParent ); + + expect( view.isSticky ).to.be.false; + } ); + + it( 'should not stick the panel if the limiter is still visible', () => { + view.isActive = true; + + 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 ); + + expect( view.isSticky ).to.be.false; + } ); + + it( 'should stick the panel if the outer container was scrolled over the limiter top', () => { + view.isActive = true; + + 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: 115, + height: 60 + } ); + + testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { + height: 20 + } ); + + view._checkIfShouldBeSticky( scrollableOuterParent ); + + expect( view.isSticky ).to.be.true; + } ); + + it( 'should not stick the panel to the bottom if the outer container was scrolled there is no space below', () => { + view.isActive = true; + + 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 ); + + expect( view.isSticky ).to.be.false; + } ); + + it( 'should not stick the panel if the outer container was scrolled over the inner container top', () => { + view.isActive = true; + + 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 ); + + expect( view.isSticky ).to.be.false; + } ); + } ); + } ); + describe( 'view.isSticky', () => { beforeEach( () => { testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { @@ -415,6 +761,10 @@ describe( 'StickyPanelView', () => { } ); } ); + describe( 'view._stickyBottomOffset?', () => { + + } ); + describe( 'view._marginLeft', () => { it( 'is set if view.isSticky is true view._isStickyToTheBottomOfLimiter is false', () => { testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { @@ -441,7 +791,7 @@ describe( 'StickyPanelView', () => { expect( view._marginLeft ).to.equal( '-10px' ); } ); - it( 'is not set if view._isStickyToTheBottomOfLimiter is true', () => { + it( 'is null if view._isStickyToTheBottomOfLimiter is true', () => { testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -30, bottom: 50, @@ -470,7 +820,7 @@ describe( 'StickyPanelView', () => { 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 } ); From 343c3e49df5a811cf0f892592d988f204f47b6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Jarz=C4=99bski?= Date: Mon, 10 Jul 2023 16:49:59 +0200 Subject: [PATCH 12/15] Refactor tests. --- .../tests/panel/sticky/stickypanelview.js | 498 +++++++++--------- 1 file changed, 245 insertions(+), 253 deletions(-) diff --git a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js index cbd2ae7e6e9..c0662c2de6a 100644 --- a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js +++ b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js @@ -199,7 +199,7 @@ describe( 'StickyPanelView', () => { expect( spy.calledOnce ).to.be.true; } ); - it( 'listens to document#scroll event and calls 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; @@ -210,7 +210,7 @@ describe( 'StickyPanelView', () => { expect( spy.calledTwice ).to.be.true; } ); - it( 'listens to view.isActive and calls view._checkIfShouldBeSticky', () => { + it( 'listens to view.isActive and calls view._checkIfShouldBeSticky()', () => { const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' ); expect( spy.notCalled ).to.be.true; @@ -241,7 +241,7 @@ describe( 'StickyPanelView', () => { } ); } ); - describe( '_checkIfShouldBeSticky', () => { + describe( '_checkIfShouldBeSticky()', () => { beforeEach( () => { view.limiterElement = limiterElement; } ); @@ -249,40 +249,147 @@ describe( 'StickyPanelView', () => { it( 'should unstick the panel if limiter element is not set', () => { view.limiterElement = null; - expect( view.isSticky ).to.be.false; + assureStickiness( { + isSticky: false, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: null, + _stickyBottomOffset: null, + _marginLeft: null + } ); } ); it( 'should unstick the panel if it is not active', () => { - const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' ); - view.isActive = true; + + const unstickSpy = testUtils.sinon.spy( view, '_unstick' ); + view.isActive = false; - expect( view.isSticky ).to.be.false; - expect( spy.calledTwice ).to.be.true; + sinon.assert.calledOnce( unstickSpy ); + assureStickiness( { + isSticky: false, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: null, + _stickyBottomOffset: null, + _marginLeft: null + } ); } ); - describe( 'called after scrolling', () => { - it( 'should do nothing if scrolled element does not contain the panel', () => { + describe( 'view.isSticky', () => { + beforeEach( () => { + testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { + height: 20 + } ); + } ); + + it( 'is true if beyond the top of the viewport (panel is active)', () => { + testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } ); + + expect( view.isSticky ).to.be.false; + view.isActive = true; - const separateElement = document.createElement( 'div' ); + expect( view.isSticky ).to.be.true; + } ); - view._checkIfShouldBeSticky( separateElement ); + it( 'is false if beyond the top of the viewport (panel is inactive)', () => { + testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } ); + + expect( view.isSticky ).to.be.false; + + view.isActive = false; expect( view.isSticky ).to.be.false; } ); - describe( 'with/without #limiterBottomOffset ', () => { + it( 'is false if in the viewport (panel is active)', () => { + testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: 10, height: 100 } ); + + expect( view.isSticky ).to.be.false; + + view.isActive = true; + expect( view.isSticky ).to.be.false; } ); - describe( 'with/without #viewportTopOffset ', () => { + it( 'is false if view.limiterElement is smaller than the panel and view.limiterBottomOffset (panel is active)', () => { + testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 60 } ); + + view.limiterBottomOffset = 50; + + expect( view.isSticky ).to.be.false; + view.isActive = true; + + expect( view.isSticky ).to.be.false; } ); + } ); + + 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: -80, + bottom: 60, + height: 140 + } ); + + testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { + height: 20 + } ); + + expect( view.isSticky ).to.be.false; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - describe( 'isActive/ is not active ', () => { + view.isActive = true; + expect( view.isSticky ).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', () => { + testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { + top: -10, + bottom: 90, + height: 100 + } ); + + testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { + height: 20 + } ); + + expect( view.isSticky ).to.be.false; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; + + view.isActive = true; + + expect( view.isSticky ).to.be.true; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; + } ); + + it( 'is false if view.isSticky is false', () => { + testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { + top: 10 + } ); + + expect( view.isSticky ).to.be.false; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; + + view.isActive = true; + + expect( view.isSticky ).to.be.false; + expect( view._isStickyToTheBottomOfLimiter ).to.be.false; + } ); + } ); + + describe( 'after scrolling', () => { + it( 'should do nothing if scrolled element does not contain the panel', () => { + view.isActive = true; + + const separateElement = document.createElement( 'div' ); + + view._checkIfShouldBeSticky( separateElement ); + + expect( view.isSticky ).to.be.false; } ); describe( 'if there is one scrollable non-window parent', () => { @@ -294,6 +401,8 @@ describe( 'StickyPanelView', () => { scrollableContainer.style.overflow = 'scroll'; scrollableContainer.appendChild( limiterElement ); global.document.body.appendChild( scrollableContainer ); + + view.isActive = true; } ); afterEach( () => { @@ -301,13 +410,13 @@ describe( 'StickyPanelView', () => { } ); describe( 'scrolled the container', () => { - it( 'should not make panel sticky if the limiter top is still visible', () => { - view.isActive = true; + 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: 20, + top: 40, bottom: 140, - height: 120 + height: 100 } ); testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { @@ -322,11 +431,18 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableContainer ); - expect( view.isSticky ).to.be.false; + sinon.assert.calledOnce( stickToTopSpy ); + assureStickiness( { + isSticky: true, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: 40, + _stickyBottomOffset: null, + _marginLeft: '0px' + } ); } ); - it( 'should make panel sticky if the limiter top is not visible', () => { - view.isActive = true; + 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, @@ -335,9 +451,9 @@ describe( 'StickyPanelView', () => { } ); testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { - top: 20, - bottom: 200, - height: 180 + top: -80, + bottom: 60, + height: 140 } ); testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { @@ -347,21 +463,33 @@ describe( 'StickyPanelView', () => { 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 make panel sticky to the bottom if there is enough space left', () => { - view.isActive = true; + 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: 40, + top: 20, bottom: 140, - height: 100 + height: 120 } ); testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { - top: -80, - bottom: 60, - height: 140 + top: 20, + bottom: 200, + height: 180 } ); testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { @@ -370,12 +498,20 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableContainer ); - expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheBottomOfLimiter ).to.be.true; + 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', () => { - view.isActive = true; + const spy = sinon.spy( view, '_unstick' ); testUtils.sinon.stub( scrollableContainer, 'getBoundingClientRect' ).returns( { top: 40, @@ -395,11 +531,18 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableContainer ); - expect( view.isSticky ).to.be.false; + 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', () => { - view.isActive = true; + const spy = sinon.spy( view, '_unstick' ); testUtils.sinon.stub( scrollableContainer, 'getBoundingClientRect' ).returns( { top: 120, @@ -419,7 +562,14 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableContainer ); - expect( view.isSticky ).to.be.false; + sinon.assert.calledOnce( spy ); + assureStickiness( { + isSticky: false, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: null, + _stickyBottomOffset: null, + _marginLeft: null + } ); } ); } ); } ); @@ -439,6 +589,8 @@ describe( 'StickyPanelView', () => { scrollableInnerParent.appendChild( limiterElement ); scrollableOuterParent.appendChild( scrollableInnerParent ); global.document.body.appendChild( scrollableOuterParent ); + + view.isActive = true; } ); afterEach( () => { @@ -446,32 +598,8 @@ describe( 'StickyPanelView', () => { scrollableOuterParent.remove(); } ); - it( 'should do something if scrolled element contains the panel', () => { - view.isActive = true; - - testUtils.sinon.stub( scrollableInnerParent, '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( scrollableOuterParent ); - - expect( view.isSticky ).to.be.false; - } ); - - it( 'should not stick the panel if the limiter is still visible', () => { - 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, @@ -497,11 +625,18 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableOuterParent ); - expect( view.isSticky ).to.be.false; + sinon.assert.calledOnce( unstickSpy ); + assureStickiness( { + isSticky: false, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: null, + _stickyBottomOffset: null, + _marginLeft: null + } ); } ); - it( 'should stick the panel if the outer container was scrolled over the limiter top', () => { - view.isActive = 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, @@ -517,8 +652,8 @@ describe( 'StickyPanelView', () => { testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: 40, - bottom: 115, - height: 60 + bottom: 140, + height: 100 } ); testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { @@ -527,11 +662,18 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableOuterParent ); - expect( view.isSticky ).to.be.true; + sinon.assert.calledOnce( stickToTopSpy ); + assureStickiness( { + isSticky: true, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: 50, + _stickyBottomOffset: null, + _marginLeft: '0px' + } ); } ); - it( 'should not stick the panel to the bottom if the outer container was scrolled there is no space below', () => { - view.isActive = true; + 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, @@ -557,11 +699,18 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableOuterParent ); - expect( view.isSticky ).to.be.false; + sinon.assert.calledOnce( unstickSpy ); + assureStickiness( { + isSticky: false, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: null, + _stickyBottomOffset: null, + _marginLeft: null + } ); } ); - it( 'should not stick the panel if the outer container was scrolled over the inner container top', () => { - view.isActive = true; + 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, @@ -587,189 +736,23 @@ describe( 'StickyPanelView', () => { view._checkIfShouldBeSticky( scrollableOuterParent ); - expect( view.isSticky ).to.be.false; - } ); - } ); - } ); - - describe( 'view.isSticky', () => { - beforeEach( () => { - testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { - height: 20 - } ); - } ); - - it( 'is true if beyond the top of the viewport (panel is active)', () => { - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } ); - - expect( view.isSticky ).to.be.false; - - view.isActive = true; - - expect( view.isSticky ).to.be.true; - } ); - - it( 'is false if beyond the top of the viewport (panel is inactive)', () => { - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } ); - - expect( view.isSticky ).to.be.false; - - view.isActive = false; - - expect( view.isSticky ).to.be.false; - } ); - - it( 'is false if in the viewport (panel is active)', () => { - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: 10, height: 100 } ); - - expect( view.isSticky ).to.be.false; - - view.isActive = true; - - expect( view.isSticky ).to.be.false; - } ); - - it( 'is false if view.limiterElement is smaller than the panel and view.limiterBottomOffset (panel is active)', () => { - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 60 } ); - - view.limiterBottomOffset = 50; - - expect( view.isSticky ).to.be.false; - - view.isActive = true; - - expect( view.isSticky ).to.be.false; - } ); - } ); - - 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: -80, - bottom: 60, - height: 140 - } ); - - testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { - height: 20 - } ); - - expect( view.isSticky ).to.be.false; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - - view.isActive = true; - - expect( view.isSticky ).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', () => { - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { - top: -10, - bottom: 90, - height: 100 - } ); - - testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { - height: 20 - } ); - - expect( view.isSticky ).to.be.false; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - - view.isActive = true; - - expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - } ); - - it( 'is false if view.isSticky is false', () => { - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { - top: 10 - } ); - - expect( view.isSticky ).to.be.false; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - - view.isActive = true; - - expect( view.isSticky ).to.be.false; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - } ); - } ); - - describe( 'view._stickyTopOffset', () => { - it( 'is not null if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset has been specified', () => { - view.viewportTopOffset = 100; - - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { - top: 90, - bottom: 190, - height: 100 - } ); - - testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { - height: 20 - } ); - - view.isActive = true; - - expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - expect( view._stickyTopOffset ).to.equal( 100 ); - expect( view._stickyBottomOffset ).to.be.null; - } ); - - it( 'is null if view._isStickyToTheBottomOfLimiter is true and view.viewportTopOffset has been specified', () => { - view.viewportTopOffset = 100; - - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { - top: 10, - bottom: 110, - height: 100 - } ); - - testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { - height: 20 - } ); - - view.isActive = true; - - expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheBottomOfLimiter ).to.be.true; - expect( view._stickyTopOffset ).to.equal( null ); - } ); - - it( 'is null if view._isStickyToTheBottomOfLimiter is false and view.viewportTopOffset is 0', () => { - view.viewportTopOffset = 0; - - testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { - top: 90, - bottom: 190, - height: 100 - } ); - - testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( { - height: 20 + sinon.assert.calledOnce( unstickSpy ); + assureStickiness( { + isSticky: false, + _isStickyToTheBottomOfLimiter: false, + _stickyTopOffset: null, + _stickyBottomOffset: null, + _marginLeft: null + } ); } ); - - view.isActive = true; - - expect( view.isSticky ).to.be.false; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; - expect( view._stickyTopOffset ).to.equal( null ); } ); } ); - describe( 'view._stickyBottomOffset?', () => { - - } ); - describe( 'view._marginLeft', () => { - it( 'is set if view.isSticky is true view._isStickyToTheBottomOfLimiter 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 } ); @@ -787,11 +770,11 @@ describe( 'StickyPanelView', () => { view.isActive = true; expect( view.isSticky ).to.be.true; - expect( view._isStickyToTheBottomOfLimiter ).to.be.false; + expect( view._stickyTopOffset ).to.not.equal( null ); expect( view._marginLeft ).to.equal( '-10px' ); } ); - it( 'is null if view._isStickyToTheBottomOfLimiter is true', () => { + it( 'is set if view._isStickyToTheBottomOfLimiter is true', () => { testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -30, bottom: 50, @@ -837,4 +820,13 @@ describe( 'StickyPanelView', () => { } ); } ); } ); + + 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 ); + } } ); From 8b731589d0fc8766301b4fd7f2e3dd0593ec097a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Jarz=C4=99bski?= Date: Mon, 10 Jul 2023 17:10:07 +0200 Subject: [PATCH 13/15] Cleaning up. --- .../src/panel/sticky/stickypanelview.ts | 122 ++++++++---------- .../tests/panel/sticky/stickypanelview.js | 32 ++--- 2 files changed, 68 insertions(+), 86 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 78b5d4856e2..80462ea7195 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -119,12 +119,20 @@ export default class StickyPanelView extends View { private _panelRect?: Rect; /** - * TODO + * Top offset of the panel when it is sticky to the top. + * + * @private + * @readonly + * @observable */ declare public _stickyTopOffset: number | null; /** - * TODO + * Bottom offset of the panel when it is sticky to the bottom. + * + * @private + * @readonly + * @observable */ declare public _stickyBottomOffset: number | null; @@ -226,24 +234,26 @@ export default class StickyPanelView extends View { super.render(); // Check if the panel should go into the sticky state immediately. - this._checkIfShouldBeSticky(); + this.checkIfShouldBeSticky(); // Update sticky state of the panel as the window and ancestors are being scrolled. this.listenTo( global.document, 'scroll', ( evt, data ) => { - this._checkIfShouldBeSticky( data.target as HTMLElement | Document ); + this.checkIfShouldBeSticky( data.target as HTMLElement | Document ); }, { useCapture: true } ); // Synchronize with `model.isActive` because sticking an inactive panel is pointless. this.listenTo( this, 'change:isActive', () => { - this._checkIfShouldBeSticky(); + this.checkIfShouldBeSticky(); } ); } /** - * Analyzes the environment to decide whether the panel should - * be sticky or not. + * Analyzes the environment to decide whether the panel should be sticky or not. + * Then handles the positioning of the panel. + * + * @param [scrollTarget] The element which is being scrolled. */ - private _checkIfShouldBeSticky( scrollTarget?: HTMLElement | Document ): void { + public checkIfShouldBeSticky( scrollTarget?: HTMLElement | Document ): void { // @if CK_DEBUG_STICKYPANEL // RectDrawer.clear(); this._panelRect = new Rect( this._contentPanel ); @@ -253,15 +263,13 @@ export default class StickyPanelView extends View { return; } - const scrollableAncestors = getScrollableAncestors( this.limiterElement ); + const scrollableAncestors = _getScrollableAncestors( this.limiterElement ); - // console.log( scrollTarget ); - // console.log( scrollableAncestors ); if ( scrollTarget && !scrollableAncestors.includes( scrollTarget ) ) { return; } - const visibleAncestorsRect = getVisibleAncestorsRect( scrollableAncestors, this.viewportTopOffset ); + const visibleAncestorsRect = _getVisibleAncestorsRect( scrollableAncestors, this.viewportTopOffset ); const limiterRect = new Rect( this.limiterElement ); // @if CK_DEBUG_STICKYPANEL // if ( visibleAncestorsRect ) { @@ -276,21 +284,10 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // 'Limiter' // @if CK_DEBUG_STICKYPANEL // ); - // console.log( visibleAncestorsRect, limiterRect.top < visibleAncestorsRect.top ); - - console.log( 'visibleAncestorsRect', visibleAncestorsRect ); - console.log( 'visibleAncestorsRect.top', visibleAncestorsRect.top ); - // // console.log( 'this._panelRect.height', this._panelRect.height ); - // console.log( 'this.limiterBottomOffset', this.limiterBottomOffset ); - // // console.log( 'visibleLimiterRect.bottom', visibleLimiterRect.bottom ); - // console.log( 'limiterRect.bottom', limiterRect.bottom ); - // console.log( 'limiterRect.top', limiterRect.top ); if ( visibleAncestorsRect && limiterRect.top < visibleAncestorsRect.top ) { const visibleLimiterRect = limiterRect.getIntersection( visibleAncestorsRect ); - console.log( 'a' ); if ( visibleLimiterRect ) { - console.log( 'b' ); // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleLimiterRect, // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'fuchsia', outlineOffset: '-3px', // @if CK_DEBUG_STICKYPANEL // backgroundColor: 'rgba(255, 0, 255, .3)' }, @@ -298,18 +295,9 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // ); const visibleAncestorsTop = visibleAncestorsRect.top; - console.log( 'visibleAncestorsTop', visibleAncestorsTop ); - console.log( 'this._panelRect.height', this._panelRect.height ); - console.log( 'this.limiterBottomOffset', this.limiterBottomOffset ); - console.log( 'visibleLimiterRect.bottom', visibleLimiterRect.bottom ); - console.log( 'limiterRect.bottom', limiterRect.bottom ); - console.log( 'limiterRect.top', limiterRect.top ); if ( visibleAncestorsTop + this._panelRect.height + this.limiterBottomOffset > visibleLimiterRect.bottom ) { - console.log( 'c' ); - console.log( visibleAncestorsRect.bottom ); const stickyBottomOffset = Math.max( limiterRect.bottom - visibleAncestorsRect.bottom, 0 ) + this.limiterBottomOffset; - console.log( 'stickyBottomOffset', stickyBottomOffset ); // @if CK_DEBUG_STICKYPANEL // const stickyBottomOffsetRect = new Rect( { // @if CK_DEBUG_STICKYPANEL // top: limiterRect.bottom - stickyBottomOffset, left: 0, right: 0, // @if CK_DEBUG_STICKYPANEL // bottom: limiterRect.bottom - stickyBottomOffset, width: 1000, height: 1 @@ -319,30 +307,22 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // 'Sticky bottom offset' // @if CK_DEBUG_STICKYPANEL // ); - console.log( 'result', limiterRect.bottom - stickyBottomOffset, limiterRect.top + this._panelRect.height ); if ( limiterRect.bottom - stickyBottomOffset > limiterRect.top + this._panelRect.height ) { - console.log( 'd' ); - this._stickToBottomOfLimiter( limiterRect, visibleAncestorsRect, stickyBottomOffset ); + this._stickToBottomOfLimiter( stickyBottomOffset ); } else { - console.log( 'e' ); this._unstick(); } } else { - console.log( 'f' ); if ( this._panelRect.height + this.limiterBottomOffset < limiterRect.height ) { - console.log( 'g' ); this._stickToTopOfAncestors( visibleAncestorsTop ); } else { - console.log( 'h' ); this._unstick(); } } } else { - console.log( 'i' ); this._unstick(); } } else { - console.log( 'j' ); this._unstick(); } @@ -354,32 +334,37 @@ export default class StickyPanelView extends View { } /** + * Sticks the panel at the given top offset. * - * @param limiterRect - * @param visibleAncestorsRect + * @private + * @param topOffset */ - private _stickToBottomOfLimiter( limiterRect: Rect, visibleAncestorsRect: Rect, stickyBottomOffset: number ) { + private _stickToTopOfAncestors( topOffset: number ) { this.isSticky = true; - this._isStickyToTheBottomOfLimiter = true; - this._stickyTopOffset = null; - this._stickyBottomOffset = stickyBottomOffset; + this._isStickyToTheBottomOfLimiter = false; + this._stickyTopOffset = topOffset; + this._stickyBottomOffset = null; this._marginLeft = toPx( -global.window.scrollX ); } /** + * Sticks the panel at the bottom of the limiter with a given bottom offset. * - * @param topOffset + * @private + * @param stickyBottomOffset */ - private _stickToTopOfAncestors( topOffset: number ) { + private _stickToBottomOfLimiter( stickyBottomOffset: number ) { this.isSticky = true; - this._isStickyToTheBottomOfLimiter = false; - this._stickyTopOffset = topOffset; - this._stickyBottomOffset = null; + this._isStickyToTheBottomOfLimiter = true; + this._stickyTopOffset = null; + this._stickyBottomOffset = stickyBottomOffset; this._marginLeft = toPx( -global.window.scrollX ); } /** - * TODO + * Unsticks the panel putting it back to its original position. + * + * @private */ private _unstick() { this.isSticky = false; @@ -390,13 +375,12 @@ export default class StickyPanelView extends View { } } -/** - * TODO - * - * @param element - * @returns - */ -function getScrollableAncestors( element: HTMLElement ) { +// Loops over the given element's ancestors to find all the scrollable elements. +// +// @private +// @param element +// @returns Array An array of scrollable element's ancestors. +function _getScrollableAncestors( element: HTMLElement ) { const scrollableAncestors = []; let scrollableAncestor = findClosestScrollableAncestor( element ); @@ -407,19 +391,17 @@ function getScrollableAncestors( element: HTMLElement ) { scrollableAncestors.push( global.document ); - console.log( scrollableAncestors.length ); - return scrollableAncestors; } -/** - * TODO - * - * @param scrollableAncestors - * @param viewportTopOffset - * @returns - */ -function getVisibleAncestorsRect( scrollableAncestors: Array, viewportTopOffset: number ) { +// Calculates the intersection rectangle of the given element and its scrollable ancestors. +// Also, takes into account the passed viewport top offset. +// +// @private +// @param scrollableAncestors +// @param viewportTopOffset +// @returns Rect +function _getVisibleAncestorsRect( scrollableAncestors: Array, viewportTopOffset: number ) { const scrollableAncestorsRects = scrollableAncestors.map( ancestor => { if ( ancestor instanceof Document ) { const windowRect = new Rect( global.window ); diff --git a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js index c0662c2de6a..3ead2c3bd56 100644 --- a/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js +++ b/packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js @@ -192,15 +192,15 @@ 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 document#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(); @@ -210,8 +210,8 @@ describe( 'StickyPanelView', () => { 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(); @@ -241,7 +241,7 @@ describe( 'StickyPanelView', () => { } ); } ); - describe( '_checkIfShouldBeSticky()', () => { + describe( 'checkIfShouldBeSticky()', () => { beforeEach( () => { view.limiterElement = limiterElement; } ); @@ -387,7 +387,7 @@ describe( 'StickyPanelView', () => { const separateElement = document.createElement( 'div' ); - view._checkIfShouldBeSticky( separateElement ); + view.checkIfShouldBeSticky( separateElement ); expect( view.isSticky ).to.be.false; } ); @@ -429,7 +429,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableContainer ); + view.checkIfShouldBeSticky( scrollableContainer ); sinon.assert.calledOnce( stickToTopSpy ); assureStickiness( { @@ -460,7 +460,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableContainer ); + view.checkIfShouldBeSticky( scrollableContainer ); expect( view.isSticky ).to.be.true; expect( view._isStickyToTheBottomOfLimiter ).to.be.true; @@ -496,7 +496,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableContainer ); + view.checkIfShouldBeSticky( scrollableContainer ); sinon.assert.notCalled( stickToBottomSpy ); sinon.assert.notCalled( stickToTopSpy ); @@ -529,7 +529,7 @@ describe( 'StickyPanelView', () => { height: 100 } ); - view._checkIfShouldBeSticky( scrollableContainer ); + view.checkIfShouldBeSticky( scrollableContainer ); sinon.assert.calledOnce( spy ); assureStickiness( { @@ -560,7 +560,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableContainer ); + view.checkIfShouldBeSticky( scrollableContainer ); sinon.assert.calledOnce( spy ); assureStickiness( { @@ -623,7 +623,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableOuterParent ); + view.checkIfShouldBeSticky( scrollableOuterParent ); sinon.assert.calledOnce( unstickSpy ); assureStickiness( { @@ -660,7 +660,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableOuterParent ); + view.checkIfShouldBeSticky( scrollableOuterParent ); sinon.assert.calledOnce( stickToTopSpy ); assureStickiness( { @@ -697,7 +697,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableOuterParent ); + view.checkIfShouldBeSticky( scrollableOuterParent ); sinon.assert.calledOnce( unstickSpy ); assureStickiness( { @@ -734,7 +734,7 @@ describe( 'StickyPanelView', () => { height: 20 } ); - view._checkIfShouldBeSticky( scrollableOuterParent ); + view.checkIfShouldBeSticky( scrollableOuterParent ); sinon.assert.calledOnce( unstickSpy ); assureStickiness( { From fcb6abb6cb452edbf9fdd0ab5f135b254efbb2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Jarz=C4=99bski?= Date: Mon, 10 Jul 2023 17:19:05 +0200 Subject: [PATCH 14/15] Improve manual test. --- .../tests/manual/stickypanel.html | 28 +++++++++++++++++++ .../tests/manual/stickypanel.md | 13 ++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-editor-classic/tests/manual/stickypanel.html b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.html index 1cbfa8e473c..384c0b613e6 100644 --- a/packages/ckeditor5-editor-classic/tests/manual/stickypanel.html +++ b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.html @@ -67,3 +67,31 @@

Heading 1

+ +

Unrelated scrollable container for testing.

+
+ +

Heading 1

+

Paragraph

+

Bold Italic Link

+
    +
  • UL List item 1
  • +
  • UL List item 2
  • +
+
    +
  1. OL List item 1
  2. +
  3. OL List item 2
  4. +
+
+ bar +
Caption
+
+
+

Quote

+
    +
  • Quoted UL List item 1
  • +
  • Quoted UL List item 2
  • +
+

Quote

+
+
diff --git a/packages/ckeditor5-editor-classic/tests/manual/stickypanel.md b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.md index 464090415c4..0981b035832 100644 --- a/packages/ckeditor5-editor-classic/tests/manual/stickypanel.md +++ b/packages/ckeditor5-editor-classic/tests/manual/stickypanel.md @@ -1 +1,12 @@ -# TODO +# Sticky panel + +Verify the behavior of the sticky panel inside the overflowing (scrollable) containers. +Best to run with `--debug=stickypanel` flag. + +Tests: +* Panel should stick only when it's active. +* Panel should not change its state when scrolling the element that isn't its ancestor (drawn rects will disappear though). +* Sticky toolbar inside the inner container. +* Sticking the toolbar when scrolling the outer container. +* Top viewport offset should be taken into account when scrolling the window. +* Panel should stick to the bottom of the container when it's near end. From 4f9ce3caa7aece5b6784987ea67a553d9e391d1f Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 12 Jul 2023 16:27:08 +0200 Subject: [PATCH 15/15] Docs. --- .../src/panel/sticky/stickypanelview.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts index 80462ea7195..0d4803d5c37 100644 --- a/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts +++ b/packages/ckeditor5-ui/src/panel/sticky/stickypanelview.ts @@ -119,7 +119,8 @@ export default class StickyPanelView extends View { private _panelRect?: Rect; /** - * Top offset of the panel when it is sticky to the top. + * The `top` CSS position of the panel when it is sticky to the top of the viewport or scrollable + * ancestors of the {@link #limiterElement}. * * @private * @readonly @@ -128,7 +129,7 @@ export default class StickyPanelView extends View { declare public _stickyTopOffset: number | null; /** - * Bottom offset of the panel when it is sticky to the bottom. + * The `bottom` CSS position of the panel when it is sticky to the bottom of the {@link #limiterElement}. * * @private * @readonly @@ -284,9 +285,14 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // 'Limiter' // @if CK_DEBUG_STICKYPANEL // ); + // Stick the panel only if + // * the limiter's ancestors are intersecting with each other so that some of their rects are visible, + // * and the limiter's top edge is above the visible ancestors' top edge. if ( visibleAncestorsRect && limiterRect.top < visibleAncestorsRect.top ) { const visibleLimiterRect = limiterRect.getIntersection( visibleAncestorsRect ); + // Sticky the panel only if the limiter's visible rect is at least partially visible in the + // visible ancestors' rects intersection. if ( visibleLimiterRect ) { // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleLimiterRect, // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'fuchsia', outlineOffset: '-3px', @@ -296,6 +302,7 @@ export default class StickyPanelView extends View { const visibleAncestorsTop = visibleAncestorsRect.top; + // Check if there's a change the panel can be sticky to the bottom of the limiter. if ( visibleAncestorsTop + this._panelRect.height + this.limiterBottomOffset > visibleLimiterRect.bottom ) { const stickyBottomOffset = Math.max( limiterRect.bottom - visibleAncestorsRect.bottom, 0 ) + this.limiterBottomOffset; // @if CK_DEBUG_STICKYPANEL // const stickyBottomOffsetRect = new Rect( { @@ -307,6 +314,8 @@ export default class StickyPanelView extends View { // @if CK_DEBUG_STICKYPANEL // 'Sticky bottom offset' // @if CK_DEBUG_STICKYPANEL // ); + // Check if sticking the panel to the bottom of the limiter does not cause it to suddenly + // move upwards if there's not enough space for it. if ( limiterRect.bottom - stickyBottomOffset > limiterRect.top + this._panelRect.height ) { this._stickToBottomOfLimiter( stickyBottomOffset ); } else { @@ -334,7 +343,7 @@ export default class StickyPanelView extends View { } /** - * Sticks the panel at the given top offset. + * Sticks the panel at the given CSS `top` offset. * * @private * @param topOffset @@ -348,7 +357,7 @@ export default class StickyPanelView extends View { } /** - * Sticks the panel at the bottom of the limiter with a given bottom offset. + * Sticks the panel at the bottom of the limiter with a given CSS `bottom` offset. * * @private * @param stickyBottomOffset @@ -394,7 +403,7 @@ function _getScrollableAncestors( element: HTMLElement ) { return scrollableAncestors; } -// Calculates the intersection rectangle of the given element and its scrollable ancestors. +// Calculates the intersection rectangle of the given element and its scrollable ancestors (including window). // Also, takes into account the passed viewport top offset. // // @private @@ -403,6 +412,7 @@ function _getScrollableAncestors( element: HTMLElement ) { // @returns Rect function _getVisibleAncestorsRect( scrollableAncestors: Array, 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 );