From 61e59f917b58b4ad81547d14afce8440b90d3893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20Nowodzi=C5=84ski?= Date: Wed, 9 Oct 2024 16:54:05 +0200 Subject: [PATCH 1/4] Made the page unscrollable while the modal is visible. --- .../ckeditor5-ui/components/dialog/dialog.css | 5 ++ packages/ckeditor5-ui/src/dialog/dialog.ts | 31 +++++++ .../dialog/dialog-body-scroll-lock.html | 53 ++++++++++++ .../manual/dialog/dialog-body-scroll-lock.md | 8 ++ .../manual/dialog/dialog-body-scroll-lock.ts | 86 +++++++++++++++++++ .../tests/manual/dialog/dialog.ts | 2 +- 6 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html create mode 100644 packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md create mode 100644 packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.ts diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/dialog/dialog.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/dialog/dialog.css index cffabae6f88..42c4a7a075b 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/dialog/dialog.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/dialog/dialog.css @@ -32,12 +32,17 @@ max-height: var(--ck-dialog-max-height); max-width: var(--ck-dialog-max-width); border: 1px solid var(--ck-color-base-border); + overscroll-behavior: contain; & .ck.ck-form__header { border-bottom: 1px solid var(--ck-color-dialog-form-header-border); } } +.ck-dialog-body-scroll-locked { + overflow: hidden; +} + @keyframes ck-dialog-fade-in { 0% { background: hsla( 0, 0%, 0%, 0 ); diff --git a/packages/ckeditor5-ui/src/dialog/dialog.ts b/packages/ckeditor5-ui/src/dialog/dialog.ts index fe4d8bdff14..0cc7022db7e 100644 --- a/packages/ckeditor5-ui/src/dialog/dialog.ts +++ b/packages/ckeditor5-ui/src/dialog/dialog.ts @@ -87,6 +87,15 @@ export default class Dialog extends Plugin { } ); } + /** + * @inheritDoc + */ + public override destroy(): void { + super.destroy(); + + this._unlockBodyScroll(); + } + /** * Initiates listeners for the `show` and `hide` events emitted by this plugin. * @@ -295,6 +304,10 @@ export default class Dialog extends Plugin { position = isModal ? DialogViewPosition.SCREEN_CENTER : DialogViewPosition.EDITOR_CENTER; } + if ( isModal ) { + this._lockBodyScroll(); + } + view.set( { position, _isVisible: true, @@ -342,6 +355,10 @@ export default class Dialog extends Plugin { const editor = this.editor; const view = this.view; + if ( view.isModal ) { + this._unlockBodyScroll(); + } + // Reset the content view to prevent its children from being destroyed in the standard // View#destroy() (and collections) chain. If the content children were left in there, // they would have to be re-created by the feature using the dialog every time the dialog @@ -361,6 +378,20 @@ export default class Dialog extends Plugin { this.isOpen = false; Dialog._visibleDialogPlugin = null; } + + /** + * Makes the unscrollable (e.g. when the modal shows up). + */ + private _lockBodyScroll(): void { + document.body.classList.add( 'ck-dialog-body-scroll-locked' ); + } + + /** + * Makes the scrollable again (e.g. once the modal hides). + */ + private _unlockBodyScroll(): void { + document.body.classList.remove( 'ck-dialog-body-scroll-locked' ); + } } /** diff --git a/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html new file mode 100644 index 00000000000..6fcfbc1e8dd --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html @@ -0,0 +1,53 @@ +
+
+ Is document scrollable? +
+ + +
+
+ + +
+
+
+ Has <body> position? +
+ + +
+
+ + +
+
+
+ +

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

+ +
Editor content.
+ +

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

+ + diff --git a/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md new file mode 100644 index 00000000000..aa6efb386a2 --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md @@ -0,0 +1,8 @@ +# Locking page scroll when the modal is open + +1. Click the ">>> Open modal <<<" button. +2. Ensure that the entire page underneath does not scroll in different browsers. +3. Ensure the same applies when various options are turned on. + +Known issues: +* In Safari iOS, the page is still scrollable when the modal is visible. diff --git a/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.ts b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.ts new file mode 100644 index 00000000000..391a58a20d9 --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.ts @@ -0,0 +1,86 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import { Plugin } from '@ckeditor/ckeditor5-core'; +import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; +import { ButtonView, Dialog, TextareaView } from '../../../src/index.js'; +import { Essentials } from '@ckeditor/ckeditor5-essentials'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; + +declare global { + interface Window { editor: any } +} + +interface FormElements extends HTMLFormControlsCollection { + isScrollable: RadioNodeList; + hasPosition: RadioNodeList; +} + +class PluginWithModal extends Plugin { + public static get requires() { + return [ Dialog ] as const; + } + + public init(): void { + this.editor.ui.componentFactory.add( 'modal', locale => { + const button = new ButtonView( locale ); + const dialog = this.editor.plugins.get( 'Dialog' ); + const textareaView = new TextareaView( locale ); + + textareaView.minRows = 5; + textareaView.maxRows = 10; + textareaView.value = + `Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor + quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean + ultricies mi vitae est. Mauris placerat eleifend leo.`.repeat( 10 ); + + button.label = '>>> Open modal <<<'; + button.withText = true; + button.on( 'execute', () => { + dialog.show( { + id: 'modalWithText', + isModal: true, + title: 'A modal', + content: textareaView, + actionButtons: [ + { + label: 'Close', + class: 'ck-button-action', + withText: true, + onExecute: () => dialog.hide() + } + ], + onShow() { + textareaView.element!.style.margin = '10px'; + textareaView.element!.style.width = '400px'; + } + } ); + } ); + + return button; + } ); + } +} + +ClassicEditor.create( document.querySelector( '#editor' ) as HTMLElement, { + extraPlugins: [ Essentials, Heading, Bold, Italic, PluginWithModal ], + toolbar: [ 'heading', '|', 'bold', 'italic', '|', 'modal' ] +} ).then( editor => { + window.editor = editor; +} ); + +const form = document.querySelector( 'form' )!; + +form.addEventListener( 'change', () => { + const elements = form.elements as FormElements; + + document.body.style.height = elements.isScrollable.value === 'yes' ? '3000px' : ''; + document.body.style.width = elements.isScrollable.value === 'yes' ? '3000px' : ''; + + document.body.style.position = elements.hasPosition.value === 'yes' ? 'absolute' : ''; + document.body.style.top = elements.hasPosition.value === 'yes' ? '100px' : ''; + document.body.style.left = elements.hasPosition.value === 'yes' ? '100px' : ''; +} ); diff --git a/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts b/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts index 1d15932328e..af2f92389d3 100644 --- a/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts +++ b/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts @@ -446,7 +446,7 @@ class MultiRootEditorIntegration extends Plugin { this.listenTo( view, 'execute', () => { const root = editor.model.document.selection.getFirstRange()!.root; - editor.detachRoot( root, true ); + editor.detachRoot( root.rootName!, true ); } ); return view; From 292c73ed26ccde0de9d7c5014a6aeb229c55cde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20Nowodzi=C5=84ski?= Date: Wed, 9 Oct 2024 16:55:18 +0200 Subject: [PATCH 2/4] Tests: Updated a test description. --- .../tests/manual/dialog/dialog-body-scroll-lock.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md index aa6efb386a2..ab9e99976d4 100644 --- a/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md +++ b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.md @@ -2,7 +2,8 @@ 1. Click the ">>> Open modal <<<" button. 2. Ensure that the entire page underneath does not scroll in different browsers. -3. Ensure the same applies when various options are turned on. +3. Ensure the same applies when various test options are turned on. +4. Play with the global scroll of the webpage before opening the modal. Known issues: * In Safari iOS, the page is still scrollable when the modal is visible. From d03717acd312b495d35530710291ec042e7c3fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20Nowodzi=C5=84ski?= Date: Thu, 17 Oct 2024 12:40:22 +0200 Subject: [PATCH 3/4] Tests: Addressed an issue in the dialog scroll lock test. --- .../manual/dialog/dialog-body-scroll-lock.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html index 6fcfbc1e8dd..4eeb133ae45 100644 --- a/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html +++ b/packages/ckeditor5-ui/tests/manual/dialog/dialog-body-scroll-lock.html @@ -2,23 +2,23 @@
Is document scrollable?
- - + +
- - + +
Has <body> position?
- - + +
- - + +
From d3cb8e64bfe78e3d661dcf74db12be87554cb629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20Nowodzi=C5=84ski?= Date: Thu, 17 Oct 2024 12:47:50 +0200 Subject: [PATCH 4/4] Tests: Added unit tests. --- packages/ckeditor5-ui/tests/dialog/dialog.js | 55 ++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/ckeditor5-ui/tests/dialog/dialog.js b/packages/ckeditor5-ui/tests/dialog/dialog.js index 1c81fbd7191..f645c5490fa 100644 --- a/packages/ckeditor5-ui/tests/dialog/dialog.js +++ b/packages/ckeditor5-ui/tests/dialog/dialog.js @@ -258,6 +258,22 @@ describe( 'Dialog', () => { } ); } ); + describe( 'destroy()', () => { + it( 'should unlock scrolling on the document if modal was displayed', () => { + dialogPlugin._show( { + position: DialogViewPosition.EDITOR_CENTER, + isModal: true, + className: 'foo' + } ); + + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.true; + + dialogPlugin.destroy(); + + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.false; + } ); + } ); + describe( 'show()', () => { it( 'should fire the `show` event with id in namespace', () => { const spy = sinon.spy(); @@ -430,6 +446,29 @@ describe( 'Dialog', () => { expect( dialogPlugin._onHide, '`_onHide` should be set' ).to.be.a( 'function' ); expect( Dialog._visibleDialogPlugin, '`_visibleDialogPlugin` instance should be set' ).to.equal( dialogPlugin ); } ); + + it( 'should lock document scroll if the dialog is a modal', () => { + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.false; + + dialogPlugin._show( { + position: DialogViewPosition.EDITOR_CENTER, + isModal: true, + className: 'foo' + } ); + + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.true; + } ); + + it( 'should not lock document scroll if the dialog is not a modal', () => { + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.false; + + dialogPlugin._show( { + position: DialogViewPosition.EDITOR_CENTER, + className: 'foo' + } ); + + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.false; + } ); } ); describe( 'hide()', () => { @@ -500,5 +539,21 @@ describe( 'Dialog', () => { expect( dialogPlugin._onHide, '`_onHide` should be reset' ).to.be.undefined; expect( Dialog._visibleDialogPlugin, '`_visibleDialogPlugin` instance should be reset' ).to.be.null; } ); + + it( 'should unlock document scroll if the dialog is a modal', () => { + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.false; + + dialogPlugin._show( { + position: DialogViewPosition.EDITOR_CENTER, + isModal: true, + className: 'foo' + } ); + + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.true; + + dialogPlugin._hide(); + + expect( document.body.classList.contains( 'ck-dialog-body-scroll-locked' ) ).to.be.false; + } ); } ); } );