Przeglądaj źródła

Code refactoring - used composition in the ToolbarView.

Aleksander Nowodzinski 6 lat temu
rodzic
commit
b9ccab05ae
1 zmienionych plików z 226 dodań i 244 usunięć
  1. 226 244
      packages/ckeditor5-ui/src/toolbar/toolbarview.js

+ 226 - 244
packages/ckeditor5-ui/src/toolbar/toolbarview.js

@@ -86,14 +86,6 @@ export default class ToolbarView extends View {
 		 */
 		this.keystrokes = new KeystrokeHandler();
 
-		/**
-		 * Controls the orientation of toolbar items.
-		 *
-		 * @observable
-		 * @member {Boolean} #isVertical
-		 */
-		this.set( 'isVertical', false );
-
 		/**
 		 * An additional CSS class added to the {@link #element}.
 		 *
@@ -103,91 +95,28 @@ export default class ToolbarView extends View {
 		this.set( 'class' );
 
 		/**
-		 * A subset of of toolbar {@link #items}. Aggregates items that fit into a single row of the toolbar
-		 * and were not {@link #_groupedItems grouped} into a {@link #_groupedItemsDropdown dropdown}.
-		 * Items of this collection are displayed in a {@link #_ungroupedItemsView dedicated view}.
-		 *
-		 * When none of the {@link #items} were grouped, it matches the {@link #items} collection in size and order.
-		 *
-		 * **Note**: Grouping occurs only when the toolbar was
-		 * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull configured}.
+		 * A view containing {@link #ungroupedItems ungrouped toolbar items} (as opposed to the
+		 * {@link #groupedItemsDropdown} containing {@link #groupedItems grouped toolbar items}).
 		 *
 		 * See the {@link #_itemsManager} to learn more.
 		 *
 		 * @private
 		 * @readonly
-		 * @member {module:ui/viewcollection~ViewCollection}
+		 * @member {module:ui/toolbar/toolbarview~ItemsView}
 		 */
-		this._ungroupedItems = this.createCollection();
-
-		/**
-		 * A subset of of toolbar {@link #items}. A collection of the toolbar items that do not fit into a
-		 * single row of the toolbar. Grouped items are displayed in a dedicated {@link #_groupedItemsDropdown dropdown}.
-		 *
-		 * When none of the {@link #items} were grouped, this collection is empty.
-		 *
-		 * **Note**: Grouping occurs only when the toolbar was
-		 * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull configured}.
-		 *
-		 * See the {@link #_itemsManager} to learn more.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:ui/viewcollection~ViewCollection}
-		 */
-		this._groupedItems = this.createCollection();
-
-		/**
-		 * A view containing {@link #_ungroupedItems ungrouped toolbar items} (as opposed to the
-		 * {@link #_groupedItemsDropdown} containing {@link #_groupedItems grouped toolbar items}).
-		 *
-		 * See the {@link #_itemsManager} to learn more.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:ui/toolbar/toolbarview~UngrouppedItemsView}
-		 */
-		this._ungroupedItemsView = this._createUngrouppedItemsView();
-
-		/**
-		 * The dropdown that aggregates {@link #_groupedItems grouped items} that do not fit into a single
-		 * row of the toolbar. It is displayed on demand at the end of the toolbar and offers another
-		 * (nested) toolbar which displays items that would normally overflow.
-		 *
-		 * See the {@link #_itemsManager} to learn more.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:ui/dropdown/dropdownview~DropdownView}
-		 */
-		this._groupedItemsDropdown = this._createGrouppedItemsDropdown();
-
-		/**
-		 * An instance of the utility responsible for managing the toolbar {@link #items}.
-		 *
-		 * For instance, it controls which of the {@link #items} should be {@link #_ungroupedItems ungrouped} or
-		 * {@link #_groupedItems grouped} depending on the configuration of the toolbar and its geometry.
-		 *
-		 * **Note**: The instance is created upon {@link #render} when the {@link #element} of the toolbar
-		 * starts to exist.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:ui/toolbar/toolbarview~ToolbarItemsManager}
-		 */
-		this._itemsManager = null;
+		this._itemsView = this._createItemsView();
 
 		/**
 		 * A top–level collection aggregating building blocks of the toolbar. It mainly exists to
-		 * make sure {@link #_ungroupedItems} do not mix up with the {@link #_groupedItemsDropdown}.
+		 * make sure {@link #ungroupedItems} do not mix up with the {@link #groupedItemsDropdown}.
 		 *
 		 * It helps a lot when the {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull grouping}
 		 * logic is on (no re–ordering issues, exclusions, etc.).
 		 *
 		 *	┌───────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
 		 *	| ┌─────────────────────────────────────── #_components ───────────────────────────────────────┐ |
-		 *	| |   ┌── #_ungroupedItemsView ───┐ ┌──────────────────────┐ ┌── #_groupedItemsDropdown ───┐   | |
-		 *	| |   |     #_ungroupedItems      | | ToolbarSeparatorView | |        #_groupedItems       |   | |
+		 *	| |   ┌────── #_itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐   | |
+		 *	| |   |     #ungroupedItems      | | ToolbarSeparatorView | |        #groupedItems       |   | |
 		 *	| |   └──────────────────────────-┘ └──────────────────────┘ └─────────────────────────────┘   | |
 		 *	| |                                  \--------- only when toolbar items overflow ---------/    | |
 		 *	| └────────────────────────────────────────────────────────────────────────────────────────────┘ |
@@ -198,21 +127,21 @@ export default class ToolbarView extends View {
 		 * @member {module:ui/viewcollection~ViewCollection}
 		 */
 		this._components = this.createCollection();
-		this._components.add( this._ungroupedItemsView );
+		this._components.add( this._itemsView );
 
 		/**
 		 * A helper collection that aggregates a subset of {@link #items} that is subject to the focus cycling
 		 * (e.g. navigation using the keyboard).
 		 *
-		 * It contains all the items from {@link #_ungroupedItems} plus (optionally) the {@link #_groupedItemsDropdown}
+		 * It contains all the items from {@link #ungroupedItems} plus (optionally) the {@link #groupedItemsDropdown}
 		 * at the end.
 		 *
-		 * This collection is dynamic and responds to the changes in {@link #_ungroupedItems} and {@link #_components}
+		 * This collection is dynamic and responds to the changes in {@link #ungroupedItems} and {@link #_components}
 		 * so the {@link #_focusCycler focus cycler} logic operates on the up–to–date collection of items that
 		 * are actually available for the user to focus and navigate at this particular moment.
 		 *
 		 * This collection is necessary because the {@link #_itemsManager} can dynamically change the content
-		 * of the {@link #_ungroupedItems} and also spontaneously display the {@link #_groupedItemsDropdown}
+		 * of the {@link #ungroupedItems} and also spontaneously display the {@link #groupedItemsDropdown}
 		 * (also focusable and "cycleable").
 		 *
 		 * @private
@@ -221,14 +150,6 @@ export default class ToolbarView extends View {
 		 */
 		this._focusCycleableItems = this.createCollection();
 
-		// Make sure all #items visible in the main space of the toolbar are cycleable.
-		this._ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
-		this._ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
-
-		// Make sure the #_groupedItemsDropdown is also included in cycling when it appears.
-		this._components.on( 'add', this._updateFocusCycleableItems.bind( this ) );
-		this._components.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
-
 		/**
 		 * Helps cycling over focusable {@link #items} in the toolbar.
 		 *
@@ -249,14 +170,27 @@ export default class ToolbarView extends View {
 			}
 		} );
 
+		/**
+		 * An instance of the utility responsible for managing the toolbar {@link #items}.
+		 *
+		 * For instance, it controls which of the {@link #items} should be {@link #ungroupedItems ungrouped} or
+		 * {@link #groupedItems grouped} depending on the configuration of the toolbar and its geometry.
+		 *
+		 * **Note**: The instance is created upon {@link #render} when the {@link #element} of the toolbar
+		 * starts to exist.
+		 *
+		 * @private
+		 * @readonly
+		 * @member {module:ui/toolbar/toolbarview~DynamicGroupingToolbar}
+		 */
+		this._kind = this.options.shouldGroupWhenFull ? new DynamicGroupingToolbar( this ) : new StaticToolbar( this );
+
 		this.setTemplate( {
 			tag: 'div',
 			attributes: {
 				class: [
 					'ck',
 					'ck-toolbar',
-					this.options.shouldGroupWhenFull ? 'ck-toolbar_grouping' : '',
-					bind.if( 'isVertical', 'ck-toolbar_vertical' ),
 					bind.to( 'class' )
 				],
 				role: 'toolbar',
@@ -270,6 +204,8 @@ export default class ToolbarView extends View {
 				mousedown: preventDefault( this )
 			}
 		} );
+
+		this._kind.extendTemplate();
 	}
 
 	/**
@@ -294,38 +230,14 @@ export default class ToolbarView extends View {
 		// Start listening for the keystrokes coming from #element.
 		this.keystrokes.listenTo( this.element );
 
-		// Initialize the utility that manages toolbar items.
-		this._itemsManager = new ToolbarItemsManager( {
-			shouldGroupWhenFull: this.options.shouldGroupWhenFull,
-			items: this.items,
-			ungroupedItems: this._ungroupedItems,
-			groupedItems: this._groupedItems,
-			element: this.element,
-			uiLanguageDirection: this.locale.uiLanguageDirection,
-
-			onGroupStart: () => {
-				this._components.add( new ToolbarSeparatorView() );
-				this._components.add( this._groupedItemsDropdown );
-				this.focusTracker.add( this._groupedItemsDropdown.element );
-			},
-
-			onGroupEnd: () => {
-				this._components.remove( this._groupedItemsDropdown );
-				this._components.remove( this._components.last );
-				this.focusTracker.remove( this._groupedItemsDropdown.element );
-			}
-		} );
+		this._kind.render();
 	}
 
 	/**
 	 * @inheritDoc
 	 */
 	destroy() {
-		// The dropdown may not be in #_components at the moment of toolbar destruction
-		// so let's make sure it's actually destroyed along with the toolbar.
-		this._groupedItemsDropdown.destroy();
-
-		this._itemsManager.destroy();
+		this._kind.destroy();
 
 		return super.destroy();
 	}
@@ -385,81 +297,26 @@ export default class ToolbarView extends View {
 	}
 
 	/**
-	 * Creates the {@link #_ungroupedItemsView} that hosts the members of the {@link #_ungroupedItems} collection.
+	 * Creates the {@link #_itemsView} that hosts the members of the {@link #ungroupedItems} collection.
 	 *
 	 * @private
-	 * @returns {module:ui/toolbar/toolbarview~UngrouppedItemsView}
+	 * @returns {module:ui/toolbar/toolbarview~ItemsView}
 	 */
-	_createUngrouppedItemsView() {
-		const ungrouppedItemsView = new UngrouppedItemsView( this.locale );
-
-		// 1:1 pass–through binding.
-		ungrouppedItemsView.items.bindTo( this._ungroupedItems ).using( item => item );
+	_createItemsView() {
+		const ungrouppedItemsView = new ItemsView( this.locale );
 
 		return ungrouppedItemsView;
 	}
-
-	/**
-	 * Creates the {@link #_groupedItemsDropdown} that hosts the members of the {@link #_groupedItems}
-	 * collection when there is not enough space in the toolbar to display all items in a single row.
-	 *
-	 * @private
-	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
-	 */
-	_createGrouppedItemsDropdown() {
-		if ( !this.options.shouldGroupWhenFull ) {
-			return null;
-		}
-
-		const t = this.t;
-		const locale = this.locale;
-		const groupedItemsDropdown = createDropdown( locale );
-
-		groupedItemsDropdown.class = 'ck-toolbar__grouped-dropdown';
-		addToolbarToDropdown( groupedItemsDropdown, [] );
-
-		groupedItemsDropdown.buttonView.set( {
-			label: t( 'Show more items' ),
-			tooltip: true,
-			icon: verticalDotsIcon
-		} );
-
-		// 1:1 pass–through binding.
-		groupedItemsDropdown.toolbarView.items.bindTo( this._groupedItems ).using( item => item );
-
-		return groupedItemsDropdown;
-	}
-
-	/**
-	 * A method that updates the {@link #_focusCycleableItems focus–cycleable items}
-	 * collection so it represents the up–to–date state of the UI from the perspective of the user.
-	 *
-	 * See the {@link #_focusCycleableItems collection} documentation to learn more about the purpose
-	 * of this method.
-	 *
-	 * @private
-	 */
-	_updateFocusCycleableItems() {
-		this._focusCycleableItems.clear();
-
-		this._ungroupedItems.map( item => {
-			this._focusCycleableItems.add( item );
-		} );
-
-		if ( this._groupedItemsDropdown && this._components.has( this._groupedItemsDropdown ) ) {
-			this._focusCycleableItems.add( this._groupedItemsDropdown );
-		}
-	}
 }
 
 /**
  * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
- * {@link module:ui/toolbar/toolbarview~ToolbarView#_ungroupedItems ungrouped items}.
+ * {@link module:ui/toolbar/toolbarview~ToolbarView#ungroupedItems ungrouped items}.
  *
  * @private
  * @extends module:ui/view~View
  */
-class UngrouppedItemsView extends View {
+class ItemsView extends View {
 	/**
 	 * @inheritDoc
 	 */
@@ -487,19 +344,54 @@ class UngrouppedItemsView extends View {
 	}
 }
 
+class StaticToolbar {
+	constructor( view ) {
+		this.view = view;
+
+		/**
+		 * Controls the orientation of toolbar items.
+		 *
+		 * @observable
+		 * @member {Boolean} #isVertical
+		 */
+		view.set( 'isVertical', false );
+
+		view._focusCycleableItems.bindTo( view.items );
+
+		// 1:1 pass–through binding.
+		view._itemsView.items.bindTo( view.items ).using( item => item );
+	}
+
+	extendTemplate() {
+		const bind = this.view.bindTemplate;
+
+		this.view.extendTemplate( {
+			attributes: {
+				class: [
+					bind.if( 'isVertical', 'ck-toolbar_vertical' )
+				]
+			}
+		} );
+	}
+
+	render() {
+		// Nothing to do here?
+	}
+}
+
 /**
  * A helper class that manages the presentation layer of the {@link module:ui/toolbar/toolbarview~ToolbarView}.
  *
  * In a nutshell, it distributes the toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}
- * among its {@link module:ui/toolbar/toolbarview~ToolbarView#_groupedItems} and
- * {@link module:ui/toolbar/toolbarview~ToolbarView#_ungroupedItems}
+ * among its {@link module:ui/toolbar/toolbarview~ToolbarView#groupedItems} and
+ * {@link module:ui/toolbar/toolbarview~ToolbarView#ungroupedItems}
  * depending on the configuration of the toolbar, the geometry and the number of items.
  *
  * @private
  */
-class ToolbarItemsManager {
+class DynamicGroupingToolbar {
 	/**
-	 * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarItemsManager} class.
+	 * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGroupingToolbar} class.
 	 *
 	 * @param {Object} options The configuration of the helper.
 	 * @param {Boolean} options.shouldGroupWhenFull Corresponds to
@@ -507,58 +399,64 @@ class ToolbarItemsManager {
 	 * @param {module:ui/viewcollection~ViewCollection} options.items Corresponds to
 	 * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
 	 * @param {module:ui/viewcollection~ViewCollection} options.ungroupedItems Corresponds to
-	 * {@link module:ui/toolbar/toolbarview~ToolbarView#_ungroupedItems}.
+	 * {@link module:ui/toolbar/toolbarview~ToolbarView#ungroupedItems}.
 	 * @param {module:ui/viewcollection~ViewCollection} options.groupedItems Corresponds to
-	 * {@link module:ui/toolbar/toolbarview~ToolbarView#_groupedItems}/
+	 * {@link module:ui/toolbar/toolbarview~ToolbarView#groupedItems}/
 	 * @param {HTMLElement} options.element Corresponds to {@link module:ui/toolbar/toolbarview~ToolbarView#element}.
 	 * @param {String} options.uiLanguageDirection Corresponds to {@link module:utils/locale~Locale#uiLanguageDirection}.
 	 * @param {Function} options.onGroupStart Executed when the first ungrouped toolbar item gets grouped.
 	 * @param {Function} options.onGroupEnd Executed when the last of the grouped toolbar items just got ungrouped.
 	 */
-	constructor( options ) {
-		Object.assign( this, options );
-
-		/**
-		 * @readonly
-		 * @member {Boolean} #shouldGroupWhenFull
-		 */
-
-		/**
-		 * @readonly
-		 * @member {module:ui/viewcollection~ViewCollection} #items
-		 */
-
-		/**
-		 * @readonly
-		 * @member {module:ui/viewcollection~ViewCollection} #ungroupedItems
-		 */
-
-		/**
-		 * @readonly
-		 * @member {module:ui/viewcollection~ViewCollection} #groupedItems
-		 */
-
-		/**
-		 * @readonly
-		 * @member {HTMLElement} #element
-		 */
+	constructor( view ) {
+		this.view = view;
 
 		/**
+		 * A subset of of toolbar {@link #items}. Aggregates items that fit into a single row of the toolbar
+		 * and were not {@link #groupedItems grouped} into a {@link #groupedItemsDropdown dropdown}.
+		 * Items of this collection are displayed in a {@link #_itemsView dedicated view}.
+		 *
+		 * When none of the {@link #items} were grouped, it matches the {@link #items} collection in size and order.
+		 *
+		 * **Note**: Grouping occurs only when the toolbar was
+		 * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull configured}.
+		 *
+		 * See the {@link #_itemsManager} to learn more.
+		 *
+		 * @private
 		 * @readonly
-		 * @member {String} #uiLanguageDirection
+		 * @member {module:ui/viewcollection~ViewCollection}
 		 */
+		this.ungroupedItems = view.createCollection();
 
 		/**
+		 * A subset of of toolbar {@link #items}. A collection of the toolbar items that do not fit into a
+		 * single row of the toolbar. Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
+		 *
+		 * When none of the {@link #items} were grouped, this collection is empty.
+		 *
+		 * **Note**: Grouping occurs only when the toolbar was
+		 * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull configured}.
+		 *
+		 * See the {@link #_itemsManager} to learn more.
+		 *
+		 * @private
 		 * @readonly
-		 * @member {Function} #onGroupStart Executed when the first ungrouped toolbar item gets grouped.
-		 * Called by {@link #groupLastItem}.
+		 * @member {module:ui/viewcollection~ViewCollection}
 		 */
+		this.groupedItems = view.createCollection();
 
 		/**
+		 * The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
+		 * row of the toolbar. It is displayed on demand at the end of the toolbar and offers another
+		 * (nested) toolbar which displays items that would normally overflow.
+		 *
+		 * See the {@link #_itemsManager} to learn more.
+		 *
+		 * @private
 		 * @readonly
-		 * @member {Function} #onGroupEnd Executed when the last of the grouped toolbar items just
-		 * got ungrouped. Called by {@link #ungroupFirstItem}.
+		 * @member {module:ui/dropdown/dropdownview~DropdownView}
 		 */
+		this.groupedItemsDropdown = this._createGrouppedItemsDropdown();
 
 		/**
 		 * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
@@ -600,9 +498,20 @@ class ToolbarItemsManager {
 		 */
 		this._cachedPadding = null;
 
+		// 1:1 pass–through binding.
+		view._itemsView.items.bindTo( this.ungroupedItems ).using( item => item );
+
+		// Make sure all #items visible in the main space of the toolbar are cycleable.
+		this.ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
+		this.ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
+
+		// Make sure the #groupedItemsDropdown is also included in cycling when it appears.
+		view._components.on( 'add', this._updateFocusCycleableItems.bind( this ) );
+		view._components.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
+
 		// ToolbarView#items is dynamic. When an item is added, it should be automatically
 		// represented in either grouped or ungrouped items at the right index.
-		this.items.on( 'add', ( evt, item, index ) => {
+		view.items.on( 'add', ( evt, item, index ) => {
 			if ( index > this.ungroupedItems.length ) {
 				this.groupedItems.add( item, index - this.ungroupedItems.length );
 			} else {
@@ -610,15 +519,13 @@ class ToolbarItemsManager {
 
 				// When a new ungrouped item joins in, there's a chance it causes the toolbar to overflow.
 				// Let's check this out and do the grouping if necessary.
-				if ( options.shouldGroupWhenFull ) {
-					this.updateGrouping();
-				}
+				this.updateGrouping();
 			}
 		} );
 
 		// When an item is removed from ToolbarView#items, it should be automatically
 		// removed from either grouped or ungrouped items.
-		this.items.on( 'remove', ( evt, item ) => {
+		view.items.on( 'remove', ( evt, item ) => {
 			if ( this.groupedItems.has( item ) ) {
 				this.groupedItems.remove( item );
 			} else if ( this.ungroupedItems.has( item ) ) {
@@ -627,14 +534,33 @@ class ToolbarItemsManager {
 
 			// Whether removed from grouped or ungrouped items, there is a chance
 			// some new space is available and we could do some ungrouping.
-			if ( options.shouldGroupWhenFull ) {
-				this.updateGrouping();
+			this.updateGrouping();
+		} );
+	}
+
+	render() {
+		this.enableGroupingOnResize();
+	}
+
+	extendTemplate() {
+		this.view.extendTemplate( {
+			attributes: {
+				class: [
+					'ck-toolbar_grouping'
+				]
 			}
 		} );
+	}
 
-		if ( options.shouldGroupWhenFull ) {
-			this.enableGroupingOnResize();
-		}
+	/**
+	 * Cleans up after the manager when its parent toolbar is destroyed.
+	 */
+	destroy() {
+		// The dropdown may not be in #_components at the moment of toolbar destruction
+		// so let's make sure it's actually destroyed along with the toolbar.
+		this.groupedItemsDropdown.destroy();
+
+		this._resizeObserver.disconnect();
 	}
 
 	/**
@@ -653,11 +579,13 @@ class ToolbarItemsManager {
 			return;
 		}
 
+		const view = this.view;
+
 		// Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
 		// for instance before #render(), or after render but without a parent or a parent detached
 		// from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
 		// nothing else.
-		if ( !this.element.ownerDocument.body.contains( this.element ) ) {
+		if ( !view.element.ownerDocument.body.contains( view.element ) ) {
 			return;
 		}
 
@@ -706,6 +634,8 @@ class ToolbarItemsManager {
 	 * on the visual space available.
 	 */
 	enableGroupingOnResize() {
+		const view = this.view;
+
 		let previousWidth;
 
 		// TODO: Consider debounce.
@@ -717,20 +647,11 @@ class ToolbarItemsManager {
 			}
 		} );
 
-		this._resizeObserver.observe( this.element );
+		this._resizeObserver.observe( view.element );
 
 		this.updateGrouping();
 	}
 
-	/**
-	 * Cleans up after the manager when its parent toolbar is destroyed.
-	 */
-	destroy() {
-		if ( this._resizeObserver ) {
-			this._resizeObserver.disconnect();
-		}
-	}
-
 	/**
 	 * Returns `true` when {@link #element} children visually overflow, for instance if the
 	 * toolbar is narrower than its members. `false` otherwise.
@@ -743,12 +664,14 @@ class ToolbarItemsManager {
 			return false;
 		}
 
-		const uiLanguageDirection = this.uiLanguageDirection;
-		const lastChildRect = new Rect( this.element.lastChild );
-		const toolbarRect = new Rect( this.element );
+		const view = this.view;
+		const element = view.element;
+		const uiLanguageDirection = view.locale.uiLanguageDirection;
+		const lastChildRect = new Rect( element.lastChild );
+		const toolbarRect = new Rect( element );
 
 		if ( !this._cachedPadding ) {
-			const computedStyle = global.window.getComputedStyle( this.element );
+			const computedStyle = global.window.getComputedStyle( element );
 			const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
 
 			// parseInt() is essential because of quirky floating point numbers logic and DOM.
@@ -773,8 +696,12 @@ class ToolbarItemsManager {
 	 * @protected
 	 */
 	groupLastItem() {
+		const view = this.view;
+
 		if ( !this.groupedItems.length ) {
-			this.onGroupStart();
+			view._components.add( new ToolbarSeparatorView() );
+			view._components.add( this.groupedItemsDropdown );
+			view.focusTracker.add( this.groupedItemsDropdown.element );
 		}
 
 		this.groupedItems.add( this.ungroupedItems.remove( this.ungroupedItems.last ), 0 );
@@ -789,10 +716,65 @@ class ToolbarItemsManager {
 	 * @protected
 	 */
 	ungroupFirstItem() {
+		const view = this.view;
+
 		this.ungroupedItems.add( this.groupedItems.remove( this.groupedItems.first ) );
 
 		if ( !this.groupedItems.length ) {
-			this.onGroupEnd();
+			view._components.remove( this.groupedItemsDropdown );
+			view._components.remove( view._components.last );
+			view.focusTracker.remove( this.groupedItemsDropdown.element );
+		}
+	}
+
+	/**
+	 * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
+	 * collection when there is not enough space in the toolbar to display all items in a single row.
+	 *
+	 * @private
+	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
+	 */
+	_createGrouppedItemsDropdown() {
+		const view = this.view;
+		const t = view.t;
+		const locale = view.locale;
+		const groupedItemsDropdown = createDropdown( locale );
+
+		groupedItemsDropdown.class = 'ck-toolbar__grouped-dropdown';
+		addToolbarToDropdown( groupedItemsDropdown, [] );
+
+		groupedItemsDropdown.buttonView.set( {
+			label: t( 'Show more items' ),
+			tooltip: true,
+			icon: verticalDotsIcon
+		} );
+
+		// 1:1 pass–through binding.
+		groupedItemsDropdown.toolbarView.items.bindTo( this.groupedItems ).using( item => item );
+
+		return groupedItemsDropdown;
+	}
+
+	/**
+	 * A method that updates the {@link #_focusCycleableItems focus–cycleable items}
+	 * collection so it represents the up–to–date state of the UI from the perspective of the user.
+	 *
+	 * See the {@link #_focusCycleableItems collection} documentation to learn more about the purpose
+	 * of this method.
+	 *
+	 * @private
+	 */
+	_updateFocusCycleableItems() {
+		const view = this.view;
+
+		view._focusCycleableItems.clear();
+
+		this.ungroupedItems.map( item => {
+			view._focusCycleableItems.add( item );
+		} );
+
+		if ( this.groupedItems.length ) {
+			view._focusCycleableItems.add( this.groupedItemsDropdown );
 		}
 	}
 }