浏览代码

Merge branch 'master' into t/ckeditor5-dev/371

Piotrek Koszuliński 7 年之前
父节点
当前提交
9670f33868

+ 6 - 2
packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js

@@ -50,7 +50,7 @@ export default class BootstrapEditor extends Editor {
 		super( config );
 
 		// Remember the element the editor is created with.
-		this.element = element;
+		this.sourceElement = element;
 
 		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
@@ -69,9 +69,13 @@ export default class BootstrapEditor extends Editor {
 		this._elementReplacer = new ElementReplacer();
 	}
 
+	get element() {
+		return this.ui.view.element;
+	}
+
 	destroy() {
 		// When destroyed, editor sets the output of editor#getData() into editor#element...
-		this.updateElement();
+		this.updateSourceElement();
 
 		// ...and restores the original editor#element...
 		this._elementReplacer.restore();

+ 6 - 2
packages/ckeditor5-ui/docs/framework/guides/external-ui.md

@@ -68,7 +68,7 @@ export default class BootstrapEditor extends Editor {
 		super( config );
 
 		// Remember the element the editor is created with.
-		this.element = element;
+		this.sourceElement = element;
 
 		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
@@ -87,9 +87,13 @@ export default class BootstrapEditor extends Editor {
 		this._elementReplacer = new ElementReplacer();
 	}
 
+	get element() {
+		return this.ui.view.element;
+	}
+
 	destroy() {
 		// When destroyed, the editor sets the output of editor#getData() into editor#element...
-		this.updateElement();
+		this.updateSourceElement();
 
 		// ...and restores the original editor#element...
 		this._elementReplacer.restore();

+ 1 - 1
packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.js

@@ -21,7 +21,7 @@ export default class InlineEditableUIView extends EditableUIView {
 	 * @param {module:utils/locale~Locale} [locale] The locale instance.
 	 * @param {HTMLElement} [editableElement] The editable element. If not specified, the
 	 * {@link module:ui/editableui/editableuiview~EditableUIView}
-	 * should create it. Otherwise, the existing element should be used.
+	 * will create it. Otherwise, the existing element will be used.
 	 */
 	constructor( locale, editableElement ) {
 		super( locale, editableElement );

+ 73 - 61
packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js

@@ -11,6 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ContextualBalloon from '../../panel/balloon/contextualballoon';
 import ToolbarView from '../toolbarview';
 import BalloonPanelView from '../../panel/balloon/balloonpanelview.js';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import normalizeToolbarConfig from '../normalizetoolbarconfig';
@@ -40,31 +41,32 @@ export default class BalloonToolbar extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
-	init() {
-		const editor = this.editor;
+	constructor( editor ) {
+		super( editor );
 
 		/**
 		 * The toolbar view displayed in the balloon.
 		 *
-		 * @member {module:ui/toolbar/toolbarview~ToolbarView}
+		 * @type {module:ui/toolbar/toolbarview~ToolbarView}
 		 */
-		this.toolbarView = new ToolbarView( editor.locale );
-
-		this.toolbarView.extendTemplate( {
-			attributes: {
-				class: [
-					'ck-toolbar_floating'
-				]
-			}
-		} );
+		this.toolbarView = this._createToolbarView();
 
-		this.toolbarView.render();
+		/**
+		 * Tracks the focus of the {@link module:ui/editableui/editableuiview~EditableUIView#editableElement}
+		 * and the {@link #toolbarView}. When both are blurred then the toolbar should hide.
+		 *
+		 * @readonly
+		 * @type {module:utils:focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+		this.focusTracker.add( editor.ui.view.editableElement );
+		this.focusTracker.add( this.toolbarView.element );
 
 		/**
 		 * The contextual balloon plugin instance.
 		 *
 		 * @private
-		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
+		 * @type {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
 		 */
 		this._balloon = editor.plugins.get( ContextualBalloon );
 
@@ -75,20 +77,53 @@ export default class BalloonToolbar extends Plugin {
 		 * trailing debounced invocation on destroy.
 		 *
 		 * @private
-		 * @member {Function}
+		 * @type {Function}
 		 */
 		this._fireSelectionChangeDebounced = debounce( () => this.fire( '_selectionChangeDebounced' ), 200 );
 
-		// Attach lifecycle actions.
-		this._handleSelectionChange();
-		this._handleFocusChange();
-
 		// The appearance of the BalloonToolbar method is event–driven.
 		// It is possible to stop the #show event and this prevent the toolbar from showing up.
 		this.decorate( 'show' );
 	}
 
 	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const selection = editor.model.document.selection;
+
+		// Show/hide the toolbar on editable focus/blur.
+		this.listenTo( this.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
+			const isToolbarVisible = this._balloon.visibleView === this.toolbarView;
+
+			if ( !isFocused && isToolbarVisible ) {
+				this.hide();
+			} else if ( isFocused ) {
+				this.show();
+			}
+		} );
+
+		// Hide the toolbar when the selection is changed by a direct change or has changed to collapsed.
+		this.listenTo( selection, 'change:range', ( evt, data ) => {
+			if ( data.directChange || selection.isCollapsed ) {
+				this.hide();
+			}
+
+			// Fire internal `_selectionChangeDebounced` event to use it for showing
+			// the toolbar after the selection stops changing.
+			this._fireSelectionChangeDebounced();
+		} );
+
+		// Show the toolbar when the selection stops changing.
+		this.listenTo( this, '_selectionChangeDebounced', () => {
+			if ( this.editor.editing.view.document.isFocused ) {
+				this.show();
+			}
+		} );
+	}
+
+	/**
 	 * Creates toolbar components based on given configuration.
 	 * This needs to be done when all plugins are ready.
 	 *
@@ -102,52 +137,23 @@ export default class BalloonToolbar extends Plugin {
 	}
 
 	/**
-	 * Handles the editor focus change and hides the toolbar if it's needed.
+	 * Creates the toolbar view instance.
 	 *
 	 * @private
+	 * @returns {module:ui/toolbar/toolbarview~ToolbarView}
 	 */
-	_handleFocusChange() {
-		const editor = this.editor;
+	_createToolbarView() {
+		const toolbarView = new ToolbarView( this.editor.locale );
 
-		// Hide the panel View when editor loses focus but no the other way around.
-		this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
-			if ( this._balloon.visibleView === this.toolbarView && !isFocused ) {
-				this.hide();
+		toolbarView.extendTemplate( {
+			attributes: {
+				class: [ 'ck-toolbar_floating' ]
 			}
 		} );
-	}
 
-	/**
-	 * Handles {@link module:engine/model/document~Document#selection} change and show or hide toolbar.
-	 *
-	 * Note that in this case it's better to listen to {@link module:engine/model/document~Document model document}
-	 * selection instead of {@link module:engine/view/document~Document view document} selection because the first one
-	 * doesn't fire `change` event after text style change (like bold or italic) and toolbar doesn't blink.
-	 *
-	 * @private
-	 */
-	_handleSelectionChange() {
-		const selection = this.editor.model.document.selection;
-		const viewDocument = this.editor.editing.view.document;
+		toolbarView.render();
 
-		this.listenTo( selection, 'change:range', ( evt, data ) => {
-			// When the selection is not changed by a collaboration and when is not collapsed.
-			if ( data.directChange || selection.isCollapsed ) {
-				// Hide the toolbar when the selection starts changing.
-				this.hide();
-			}
-
-			// Fire internal `_selectionChangeDebounced` when the selection stops changing.
-			this._fireSelectionChangeDebounced();
-		} );
-
-		// Hide the toolbar when the selection stops changing.
-		this.listenTo( this, '_selectionChangeDebounced', () => {
-			// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
-			if ( viewDocument.isFocused && !viewDocument.selection.isCollapsed ) {
-				this.show();
-			}
-		} );
+		return toolbarView;
 	}
 
 	/**
@@ -156,20 +162,26 @@ export default class BalloonToolbar extends Plugin {
 	 * Fires {@link #event:show} event which can be stopped to prevent the toolbar from showing up.
 	 */
 	show() {
+		const editor = this.editor;
+
 		// Do not add the toolbar to the balloon stack twice.
 		if ( this._balloon.hasView( this.toolbarView ) ) {
 			return;
 		}
 
+		// Do not show the toolbar when the selection is collapsed.
+		if ( editor.model.document.selection.isCollapsed ) {
+			return;
+		}
+
 		// Don not show the toolbar when all components inside are disabled
 		// see https://github.com/ckeditor/ckeditor5-ui/issues/269.
 		if ( Array.from( this.toolbarView.items ).every( item => item.isEnabled !== undefined && !item.isEnabled ) ) {
 			return;
 		}
 
-		// Update the toolbar position upon change (e.g. external document changes)
-		// while it's visible.
-		this.listenTo( this.editor.editing.view, 'render', () => {
+		// Update the toolbar position when the editor ui should be refreshed.
+		this.listenTo( this.editor.ui, 'update', () => {
 			this._balloon.updatePosition( this._getBalloonPositionData() );
 		} );
 
@@ -186,7 +198,7 @@ export default class BalloonToolbar extends Plugin {
 	 */
 	hide() {
 		if ( this._balloon.hasView( this.toolbarView ) ) {
-			this.stopListening( this.editor.editing.view, 'render' );
+			this.stopListening( this.editor.ui, 'update' );
 			this._balloon.remove( this.toolbarView );
 		}
 	}

+ 107 - 83
packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.js

@@ -14,7 +14,6 @@ import BlockButtonView from './blockbuttonview';
 import BalloonPanelView from '../../panel/balloon/balloonpanelview';
 import ToolbarView from '../toolbarview';
 
-import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
 import clickOutsideHandler from '../../bindings/clickoutsidehandler';
 
 import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
@@ -66,10 +65,8 @@ export default class BlockToolbar extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
-	init() {
-		const editor = this.editor;
-
-		editor.editing.view.addObserver( ClickObserver );
+	constructor( editor ) {
+		super( editor );
 
 		/**
 		 * The toolbar view.
@@ -99,18 +96,39 @@ export default class BlockToolbar extends Plugin {
 			activator: () => this.panelView.isVisible,
 			callback: () => this._hidePanel()
 		} );
+	}
 
-		// Try to hide button when the editor switches to the read-only mode.
-		// Do not hide when panel if already visible to avoid a confusing UX when the panel
-		// unexpectedly disappears.
-		this.listenTo( editor, 'change:isReadOnly', () => {
-			if ( !this.panelView.isVisible ) {
-				this.buttonView.isVisible = false;
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		// Hides panel on a direct selection change.
+		this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => {
+			if ( data.directChange ) {
+				this._hidePanel();
 			}
 		} );
 
-		// Enable as default.
-		this._initListeners();
+		this.listenTo( editor.ui, 'update', () => this._updateButton() );
+		// `low` priority is used because of https://github.com/ckeditor/ckeditor5-core/issues/133.
+		this.listenTo( editor, 'change:isReadOnly', () => this._updateButton(), { priority: 'low' } );
+		this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => this._updateButton() );
+
+		// Reposition button on resize.
+		this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => {
+			if ( isVisible ) {
+				// Keep correct position of button and panel on window#resize.
+				this.buttonView.listenTo( window, 'resize', () => this._updateButton() );
+			} else {
+				// Stop repositioning button when is hidden.
+				this.buttonView.stopListening( window, 'resize' );
+
+				// Hide the panel when the button disappears.
+				this._hidePanel();
+			}
+		} );
 	}
 
 	/**
@@ -143,10 +161,15 @@ export default class BlockToolbar extends Plugin {
 
 		toolbarView.extendTemplate( {
 			attributes: {
-				class: [
-					// https://github.com/ckeditor/ckeditor5-editor-inline/issues/11
-					'ck-toolbar_floating'
-				]
+				// https://github.com/ckeditor/ckeditor5-editor-inline/issues/11
+				class: [ 'ck-toolbar_floating' ]
+			}
+		} );
+
+		// When toolbar lost focus then panel should hide.
+		toolbarView.focusTracker.on( 'change:isFocused', ( evt, name, is ) => {
+			if ( !is ) {
+				this._hidePanel();
 			}
 		} );
 
@@ -213,93 +236,62 @@ export default class BlockToolbar extends Plugin {
 	}
 
 	/**
-	 * Starts displaying the button next to allowed elements.
+	 * Shows or hides the button.
+	 * When the all conditions for displaying button are matched then shows the button. Hides otherwise.
 	 *
 	 * @private
 	 */
-	_initListeners() {
+	_updateButton() {
 		const editor = this.editor;
 		const model = editor.model;
 		const view = editor.editing.view;
-		let modelTarget, domTarget;
 
-		// Hides panel on a direct selection change.
-		this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => {
-			if ( data.directChange ) {
-				this._hidePanel();
-			}
-		} );
+		// Hides the button when the editor is not focused.
+		if ( !editor.ui.focusTracker.isFocused ) {
+			this._hideButton();
 
-		this.listenTo( view, 'render', () => {
-			// Get first selected block, button will be attached to this element.
-			modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ];
+			return;
+		}
 
-			// Do not attach block button when there is no enabled item in toolbar for current block element.
-			if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) {
-				this.buttonView.isVisible = false;
+		// Hides the button when the editor switches to the read-only mode.
+		if ( editor.isReadOnly ) {
+			this._hideButton();
 
-				return;
-			}
+			return;
+		}
 
-			// Get DOM target element.
-			domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) );
+		// Get the first selected block, button will be attached to this element.
+		const modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ];
 
-			// Show block button.
-			this.buttonView.isVisible = true;
+		// Hides the button when there is no enabled item in toolbar for the current block element.
+		if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) {
+			this._hideButton();
 
-			// Attach block button to target DOM element.
-			this._attachButtonToElement( domTarget );
+			return;
+		}
 
-			// When panel is opened then refresh it position to be properly aligned with block button.
-			if ( this.panelView.isVisible ) {
-				this._showPanel();
-			}
-		}, { priority: 'low' } );
+		// Get DOM target element.
+		const domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) );
 
-		this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => {
-			if ( isVisible ) {
-				// Keep correct position of button and panel on window#resize.
-				this.buttonView.listenTo( window, 'resize', () => this._attachButtonToElement( domTarget ) );
-			} else {
-				// Stop repositioning button when is hidden.
-				this.buttonView.stopListening( window, 'resize' );
+		// Show block button.
+		this.buttonView.isVisible = true;
 
-				// Hide the panel when the button disappears.
-				this._hidePanel();
-			}
-		} );
+		// Attach block button to target DOM element.
+		this._attachButtonToElement( domTarget );
+
+		// When panel is opened then refresh it position to be properly aligned with block button.
+		if ( this.panelView.isVisible ) {
+			this._showPanel();
+		}
 	}
 
 	/**
-	 * Attaches the {@link #buttonView} to the target block of content.
+	 * Hides the button.
 	 *
-	 * @protected
-	 * @param {HTMLElement} targetElement Target element.
+	 * @private
 	 */
-	_attachButtonToElement( targetElement ) {
-		const contentStyles = window.getComputedStyle( targetElement );
-
-		const editableRect = new Rect( this.editor.ui.view.editableElement );
-		const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 );
-		// When line height is not an integer then thread it as "normal".
-		// MDN says that 'normal' == ~1.2 on desktop browsers.
-		const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2;
-
-		const position = getOptimalPosition( {
-			element: this.buttonView.element,
-			target: targetElement,
-			positions: [
-				( contentRect, buttonRect ) => {
-					return {
-						top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
-						left: editableRect.left - buttonRect.width
-					};
-				}
-			]
-		} );
-
-		this.buttonView.top = position.top;
-		this.buttonView.left = position.left;
+	_hideButton() {
+		this.buttonView.isVisible = false;
 	}
 
 	/**
@@ -334,6 +326,38 @@ export default class BlockToolbar extends Plugin {
 			this.editor.editing.view.focus();
 		}
 	}
+
+	/**
+	 * Attaches the {@link #buttonView} to the target block of content.
+	 *
+	 * @protected
+	 * @param {HTMLElement} targetElement Target element.
+	 */
+	_attachButtonToElement( targetElement ) {
+		const contentStyles = window.getComputedStyle( targetElement );
+
+		const editableRect = new Rect( this.editor.ui.view.editableElement );
+		const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 );
+		// When line height is not an integer then thread it as "normal".
+		// MDN says that 'normal' == ~1.2 on desktop browsers.
+		const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2;
+
+		const position = getOptimalPosition( {
+			element: this.buttonView.element,
+			target: targetElement,
+			positions: [
+				( contentRect, buttonRect ) => {
+					return {
+						top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
+						left: editableRect.left - buttonRect.width
+					};
+				}
+			]
+		} );
+
+		this.buttonView.top = position.top;
+		this.buttonView.left = position.left;
+	}
 }
 
 /**

+ 31 - 0
packages/ckeditor5-ui/tests/manual/tickets/418/1.html

@@ -0,0 +1,31 @@
+<div class="wrapper">
+	<div id="editor">
+		<h2>The three greatest things you learn from traveling</h2>
+		<p>
+			Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons
+			I’ve learned over the years of traveling.
+		</p>
+
+		<h3>Appreciation of diversity</h3>
+		<p>
+			Getting used to an entirely different culture can be challenging. While it’s also nice to learn about
+			cultures online or from books, nothing comes close to experiencing cultural diversity in person.
+			You learn to appreciate each and every single one of the differences while you become more culturally fluid.
+		</p>
+	</div>
+</div>
+
+<style>
+	#editor {
+		margin: 0 auto;
+		max-width: 800px;
+	}
+
+	.wrapper {
+		padding: 50px 20px;
+	}
+
+	.ck-block-toolbar-button {
+		transform: translateX( -10px );
+	}
+</style>

+ 45 - 0
packages/ckeditor5-ui/tests/manual/tickets/418/1.js

@@ -0,0 +1,45 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window, document, console:false */
+
+import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
+import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
+import List from '@ckeditor/ckeditor5-list/src/list';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Heading from '@ckeditor/ckeditor5-heading/src/heading';
+import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui';
+import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+import Link from '@ckeditor/ckeditor5-link/src/link';
+
+import BlockToolbar from '../../../../src/toolbar/block/blocktoolbar';
+import BalloonToolbar from '../../../../src/toolbar/balloon/balloontoolbar';
+
+BalloonEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [
+			Essentials,
+			List,
+			Paragraph,
+			Heading,
+			HeadingButtonsUI,
+			ParagraphButtonUI,
+			Bold,
+			Italic,
+			Link,
+			BlockToolbar,
+			BalloonToolbar
+		],
+		blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList' ],
+		balloonToolbar: [ 'bold', 'italic', 'link' ]
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 5 - 0
packages/ckeditor5-ui/tests/manual/tickets/418/1.md

@@ -0,0 +1,5 @@
+## BlockToolbar and BalloonToolbar collision [#418](https://github.com/ckeditor/ckeditor5-ui/issues/418)
+
+- select some text, balloon toolbar should show up
+- click block toolbar button, balloon toolbar should hide
+- pres `Esc` to move focus back to editable, balloon toolbar should show up

+ 4 - 4
packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js

@@ -407,7 +407,7 @@ describe( 'ContextualBalloon', () => {
 				}
 			} );
 
-			balloon.view.pin.reset();
+			balloon.view.pin.resetHistory();
 
 			balloon.updatePosition();
 
@@ -419,7 +419,7 @@ describe( 'ContextualBalloon', () => {
 		} );
 
 		it( 'should set given position to the currently visible view and use position from the first view in the stack #1', () => {
-			balloon.view.pin.reset();
+			balloon.view.pin.resetHistory();
 
 			balloon.updatePosition( { target: 'new' } );
 
@@ -438,7 +438,7 @@ describe( 'ContextualBalloon', () => {
 				}
 			} );
 
-			balloon.view.pin.reset();
+			balloon.view.pin.resetHistory();
 
 			balloon.updatePosition( { target: 'new' } );
 
@@ -460,7 +460,7 @@ describe( 'ContextualBalloon', () => {
 		} );
 
 		it( 'should use a given position limiter instead of the default one', () => {
-			balloon.view.pin.reset();
+			balloon.view.pin.resetHistory();
 
 			balloon.updatePosition( {
 				target: 'new',

+ 70 - 52
packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js

@@ -8,6 +8,7 @@ import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
 import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
 import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
 import ToolbarView from '../../../src/toolbar/toolbarview';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
@@ -17,7 +18,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { stringify as viewStringify } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
-/* global document, setTimeout, window */
+/* global document, setTimeout, window, Event */
 
 describe( 'BalloonToolbar', () => {
 	let sandbox, editor, model, selection, editingView, balloonToolbar, balloon, editorElement;
@@ -143,6 +144,28 @@ describe( 'BalloonToolbar', () => {
 		} );
 	} );
 
+	describe( 'focusTracker', () => {
+		it( 'should be defined', () => {
+			expect( balloonToolbar.focusTracker ).to.instanceof( FocusTracker );
+		} );
+
+		it( 'it should track the focus of the #editableElement', () => {
+			expect( balloonToolbar.focusTracker.isFocused ).to.false;
+
+			editor.ui.view.editableElement.dispatchEvent( new Event( 'focus' ) );
+
+			expect( balloonToolbar.focusTracker.isFocused ).to.true;
+		} );
+
+		it( 'it should track the focus of the toolbarView#element', () => {
+			expect( balloonToolbar.focusTracker.isFocused ).to.false;
+
+			balloonToolbar.toolbarView.element.dispatchEvent( new Event( 'focus' ) );
+
+			expect( balloonToolbar.focusTracker.isFocused ).to.true;
+		} );
+	} );
+
 	describe( 'show()', () => {
 		let balloonAddSpy, backwardSelectionRect, forwardSelectionRect;
 
@@ -275,17 +298,17 @@ describe( 'BalloonToolbar', () => {
 			expect( targetRect ).to.deep.equal( backwardSelectionRect );
 		} );
 
-		it( 'should update balloon position on view#change event while balloon is added to the #_balloon', () => {
+		it( 'should update balloon position on ui#update event when #toolbarView is already added to the #_balloon', () => {
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
 			const spy = sandbox.spy( balloon, 'updatePosition' );
 
-			editingView.fire( 'render' );
+			editor.ui.fire( 'update' );
 
 			balloonToolbar.show();
 			sinon.assert.notCalled( spy );
 
-			editingView.fire( 'render' );
+			editor.ui.fire( 'update' );
 			sinon.assert.calledOnce( spy );
 		} );
 
@@ -297,6 +320,13 @@ describe( 'BalloonToolbar', () => {
 			sinon.assert.calledOnce( balloonAddSpy );
 		} );
 
+		it( 'should not add the #toolbarView to the #_balloon when the selection is collapsed', () => {
+			setData( model, '<paragraph>b[]ar</paragraph>' );
+
+			balloonToolbar.show();
+			sinon.assert.notCalled( balloonAddSpy );
+		} );
+
 		it( 'should not add #toolbarView to the #_balloon when all components inside #toolbarView are disabled', () => {
 			Array.from( balloonToolbar.toolbarView.items ).forEach( item => {
 				item.isEnabled = false;
@@ -319,37 +349,6 @@ describe( 'BalloonToolbar', () => {
 			balloonToolbar.show();
 			sinon.assert.calledOnce( balloonAddSpy );
 		} );
-
-		describe( 'on #_selectionChangeDebounced event', () => {
-			let showSpy;
-
-			beforeEach( () => {
-				showSpy = sandbox.spy( balloonToolbar, 'show' );
-			} );
-
-			it( 'should not be called when the editor is not focused', () => {
-				setData( model, '<paragraph>b[a]r</paragraph>' );
-				editingView.document.isFocused = false;
-
-				balloonToolbar.fire( '_selectionChangeDebounced' );
-				sinon.assert.notCalled( showSpy );
-			} );
-
-			it( 'should not be called when the selection is collapsed', () => {
-				setData( model, '<paragraph>b[]ar</paragraph>' );
-
-				balloonToolbar.fire( '_selectionChangeDebounced' );
-				sinon.assert.notCalled( showSpy );
-			} );
-
-			it( 'should be called when the selection is not collapsed and editor is focused', () => {
-				setData( model, '<paragraph>b[a]r</paragraph>' );
-				editingView.document.isFocused = true;
-
-				balloonToolbar.fire( '_selectionChangeDebounced' );
-				sinon.assert.calledOnce( showSpy );
-			} );
-		} );
 	} );
 
 	describe( 'hide()', () => {
@@ -369,7 +368,7 @@ describe( 'BalloonToolbar', () => {
 			sinon.assert.calledWithExactly( removeBalloonSpy, balloonToolbar.toolbarView );
 		} );
 
-		it( 'should stop update balloon position on ViewDocument#change event', () => {
+		it( 'should stop update balloon position on ui#update event', () => {
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
 			const spy = sandbox.spy( balloon, 'updatePosition' );
@@ -377,11 +376,11 @@ describe( 'BalloonToolbar', () => {
 			balloonToolbar.show();
 			balloonToolbar.hide();
 
-			editingView.fire( 'render' );
+			editor.ui.fire( 'update' );
 			sinon.assert.notCalled( spy );
 		} );
 
-		it( 'should not remove #ttolbarView when is not added to the #_balloon', () => {
+		it( 'should not remove #toolbarView when is not added to the #_balloon', () => {
 			balloonToolbar.hide();
 
 			sinon.assert.notCalled( removeBalloonSpy );
@@ -412,7 +411,7 @@ describe( 'BalloonToolbar', () => {
 		} );
 	} );
 
-	describe( 'showing and hiding', () => {
+	describe( 'show and hide triggers', () => {
 		let showPanelSpy, hidePanelSpy;
 
 		beforeEach( () => {
@@ -422,7 +421,7 @@ describe( 'BalloonToolbar', () => {
 			hidePanelSpy = sandbox.spy( balloonToolbar, 'hide' );
 		} );
 
-		it( 'should open when selection stops changing', () => {
+		it( 'should show when selection stops changing', () => {
 			sinon.assert.notCalled( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
 
@@ -432,7 +431,18 @@ describe( 'BalloonToolbar', () => {
 			sinon.assert.notCalled( hidePanelSpy );
 		} );
 
-		it( 'should close when selection starts changing by a directChange', () => {
+		it( 'should not show when the selection stops changing when the editable is blurred', () => {
+			sinon.assert.notCalled( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
+
+			editingView.document.isFocused = false;
+			balloonToolbar.fire( '_selectionChangeDebounced' );
+
+			sinon.assert.notCalled( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
+		} );
+
+		it( 'should hide when selection starts changing by a direct change', () => {
 			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			sinon.assert.calledOnce( showPanelSpy );
@@ -444,7 +454,7 @@ describe( 'BalloonToolbar', () => {
 			sinon.assert.calledOnce( hidePanelSpy );
 		} );
 
-		it( 'should not close when selection starts changing by not a directChange', () => {
+		it( 'should not hide when selection starts changing by an indirect change', () => {
 			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			sinon.assert.calledOnce( showPanelSpy );
@@ -456,7 +466,7 @@ describe( 'BalloonToolbar', () => {
 			sinon.assert.notCalled( hidePanelSpy );
 		} );
 
-		it( 'should close when selection starts changing by not a directChange but will become collapsed', () => {
+		it( 'should hide when selection starts changing by an indirect change but has changed to collapsed', () => {
 			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			sinon.assert.calledOnce( showPanelSpy );
@@ -472,17 +482,27 @@ describe( 'BalloonToolbar', () => {
 			sinon.assert.calledOnce( hidePanelSpy );
 		} );
 
-		it( 'should hide if the editor loses focus', () => {
-			editor.ui.focusTracker.isFocused = true;
+		it( 'should show on #focusTracker focus', () => {
+			balloonToolbar.focusTracker.isFocused = false;
 
-			balloonToolbar.fire( '_selectionChangeDebounced' );
+			sinon.assert.notCalled( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
+
+			balloonToolbar.focusTracker.isFocused = true;
+
+			sinon.assert.calledOnce( showPanelSpy );
+			sinon.assert.notCalled( hidePanelSpy );
+		} );
+
+		it( 'should hide on #focusTracker blur', () => {
+			balloonToolbar.focusTracker.isFocused = true;
 
 			const stub = sandbox.stub( balloon, 'visibleView' ).get( () => balloonToolbar.toolbarView );
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
 
-			editor.ui.focusTracker.isFocused = false;
+			balloonToolbar.focusTracker.isFocused = false;
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.calledOnce( hidePanelSpy );
@@ -490,17 +510,15 @@ describe( 'BalloonToolbar', () => {
 			stub.restore();
 		} );
 
-		it( 'should not hide if the editor loses focus and #toolbarView is not visible', () => {
-			editor.ui.focusTracker.isFocused = true;
-
-			balloonToolbar.fire( '_selectionChangeDebounced' );
+		it( 'should not hide on #focusTracker blur when toolbar is not in the balloon stack', () => {
+			balloonToolbar.focusTracker.isFocused = true;
 
 			const stub = sandbox.stub( balloon, 'visibleView' ).get( () => null );
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
 
-			editor.ui.focusTracker.isFocused = false;
+			balloonToolbar.focusTracker.isFocused = false;
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );

+ 58 - 22
packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js

@@ -5,7 +5,6 @@
 /* global document, window, Event */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
 
 import BlockToolbar from '../../../src/toolbar/block/blocktoolbar';
 import ToolbarView from '../../../src/toolbar/toolbarview';
@@ -39,6 +38,7 @@ describe( 'BlockToolbar', () => {
 		} ).then( newEditor => {
 			editor = newEditor;
 			blockToolbar = editor.plugins.get( BlockToolbar );
+			editor.ui.focusTracker.isFocused = true;
 		} );
 	} );
 
@@ -51,10 +51,6 @@ describe( 'BlockToolbar', () => {
 		expect( BlockToolbar.pluginName ).to.equal( 'BlockToolbar' );
 	} );
 
-	it( 'should register a click observer', () => {
-		expect( editor.editing.view.getObserver( ClickObserver ) ).to.be.instanceOf( ClickObserver );
-	} );
-
 	describe( 'child views', () => {
 		describe( 'panelView', () => {
 			it( 'should create a view instance', () => {
@@ -70,7 +66,7 @@ describe( 'BlockToolbar', () => {
 			} );
 
 			it( 'should add the #panelView to ui.focusTracker', () => {
-				expect( editor.ui.focusTracker.isFocused ).to.be.false;
+				editor.ui.focusTracker.isFocused = false;
 
 				blockToolbar.panelView.element.dispatchEvent( new Event( 'focus' ) );
 
@@ -137,6 +133,16 @@ describe( 'BlockToolbar', () => {
 
 				expect( blockToolbar.panelView.isVisible ).to.be.false;
 			} );
+
+			it( 'should hide the panel on toolbar blur', () => {
+				blockToolbar.buttonView.fire( 'execute' );
+
+				expect( blockToolbar.panelView.isVisible ).to.be.true;
+
+				blockToolbar.toolbarView.focusTracker.isFocused = false;
+
+				expect( blockToolbar.panelView.isVisible ).to.be.false;
+			} );
 		} );
 
 		describe( 'buttonView', () => {
@@ -149,7 +155,7 @@ describe( 'BlockToolbar', () => {
 			} );
 
 			it( 'should add the #buttonView to the ui.focusTracker', () => {
-				expect( editor.ui.focusTracker.isFocused ).to.be.false;
+				editor.ui.focusTracker.isFocused = false;
 
 				blockToolbar.buttonView.element.dispatchEvent( new Event( 'focus' ) );
 
@@ -226,7 +232,7 @@ describe( 'BlockToolbar', () => {
 			expect( blockToolbar.buttonView.isVisible ).to.be.true;
 		} );
 
-		it( 'should not display the button when the selection is placed in a root element', () => {
+		it( 'should not display the button when the selection is placed in the root element', () => {
 			editor.model.schema.extend( '$text', { allowIn: '$root' } );
 
 			setData( editor.model, '<paragraph>foo</paragraph>[]<paragraph>bar</paragraph>' );
@@ -291,7 +297,7 @@ describe( 'BlockToolbar', () => {
 				height: 100
 			} );
 
-			editor.editing.view.fire( 'render' );
+			editor.ui.fire( 'update' );
 
 			expect( blockToolbar.buttonView.top ).to.equal( 470 );
 			expect( blockToolbar.buttonView.left ).to.equal( 100 );
@@ -326,24 +332,24 @@ describe( 'BlockToolbar', () => {
 				height: 100
 			} );
 
-			editor.editing.view.fire( 'render' );
+			editor.ui.fire( 'update' );
 
 			expect( blockToolbar.buttonView.top ).to.equal( 472 );
 			expect( blockToolbar.buttonView.left ).to.equal( 100 );
 		} );
 
-		it( 'should reposition the #panelView when open on view#render', () => {
+		it( 'should reposition the #panelView when open on ui#update', () => {
 			blockToolbar.panelView.isVisible = false;
 
 			const spy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' );
 
-			editor.editing.view.fire( 'render' );
+			editor.ui.fire( 'update' );
 
 			sinon.assert.notCalled( spy );
 
 			blockToolbar.panelView.isVisible = true;
 
-			editor.editing.view.fire( 'render' );
+			editor.ui.fire( 'update' );
 
 			sinon.assert.calledWith( spy, {
 				target: blockToolbar.buttonView.element,
@@ -351,12 +357,12 @@ describe( 'BlockToolbar', () => {
 			} );
 		} );
 
-		it( 'should not reset the toolbar focus on view#render', () => {
+		it( 'should not reset the toolbar focus on ui#update', () => {
 			blockToolbar.panelView.isVisible = true;
 
 			const spy = testUtils.sinon.spy( blockToolbar.toolbarView, 'focus' );
 
-			editor.editing.view.fire( 'render' );
+			editor.ui.fire( 'update' );
 
 			sinon.assert.notCalled( spy );
 		} );
@@ -369,7 +375,7 @@ describe( 'BlockToolbar', () => {
 			expect( blockToolbar.panelView.isVisible ).to.be.false;
 		} );
 
-		it( 'should not hide the open panel on a indirect selection change', () => {
+		it( 'should not hide the open panel on an indirect selection change', () => {
 			blockToolbar.panelView.isVisible = true;
 
 			editor.model.document.selection.fire( 'change:range', { directChange: false } );
@@ -377,11 +383,11 @@ describe( 'BlockToolbar', () => {
 			expect( blockToolbar.panelView.isVisible ).to.be.true;
 		} );
 
-		it( 'should hide the UI when editor switches to readonly when the panel is not visible', () => {
+		it( 'should hide the UI when editor switched to readonly', () => {
 			setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
 
 			blockToolbar.buttonView.isVisible = true;
-			blockToolbar.panelView.isVisible = false;
+			blockToolbar.panelView.isVisible = true;
 
 			editor.isReadOnly = true;
 
@@ -389,16 +395,46 @@ describe( 'BlockToolbar', () => {
 			expect( blockToolbar.panelView.isVisible ).to.be.false;
 		} );
 
-		it( 'should not hide button when the editor switches to readonly when the panel is visible', () => {
+		it( 'should show the button when the editor switched back from readonly', () => {
 			setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
 
-			blockToolbar.buttonView.isVisible = true;
-			blockToolbar.panelView.isVisible = true;
+			expect( blockToolbar.buttonView.isVisible ).to.true;
 
 			editor.isReadOnly = true;
 
+			expect( blockToolbar.buttonView.isVisible ).to.false;
+
+			editor.isReadOnly = false;
+
 			expect( blockToolbar.buttonView.isVisible ).to.be.true;
-			expect( blockToolbar.panelView.isVisible ).to.be.true;
+		} );
+
+		it( 'should show/hide the button on the editor focus/blur', () => {
+			setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
+
+			editor.ui.focusTracker.isFocused = true;
+
+			expect( blockToolbar.buttonView.isVisible ).to.true;
+
+			editor.ui.focusTracker.isFocused = false;
+
+			expect( blockToolbar.buttonView.isVisible ).to.false;
+
+			editor.ui.focusTracker.isFocused = true;
+
+			expect( blockToolbar.buttonView.isVisible ).to.true;
+		} );
+
+		it( 'should hide the UI when the editor switched to the readonly when the panel is not visible', () => {
+			setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
+
+			blockToolbar.buttonView.isVisible = true;
+			blockToolbar.panelView.isVisible = false;
+
+			editor.isReadOnly = true;
+
+			expect( blockToolbar.buttonView.isVisible ).to.be.false;
+			expect( blockToolbar.panelView.isVisible ).to.be.false;
 		} );
 
 		it( 'should update the button position on browser resize only when the button is visible', () => {

+ 1 - 1
packages/ckeditor5-ui/theme/globals/_zindex.css

@@ -5,5 +5,5 @@
 
 :root {
 	--ck-z-default: 1;
-	--ck-z-modal: 999;
+	--ck-z-modal: calc( var(--ck-z-default) + 999 );
 }