8
0
Просмотр исходного кода

Merge branch 'master' into t/ckeditor5-engine/1210

Szymon Kupś 8 лет назад
Родитель
Сommit
9fabc1d4d3
31 измененных файлов с 1573 добавлено и 1217 удалено
  1. 128 0
      packages/ckeditor5-ui/src/button/button.jsdoc
  2. 15 120
      packages/ckeditor5-ui/src/button/buttonview.js
  3. 0 71
      packages/ckeditor5-ui/src/dropdown/button/buttondropdownmodel.jsdoc
  4. 0 26
      packages/ckeditor5-ui/src/dropdown/button/buttondropdownview.jsdoc
  5. 0 113
      packages/ckeditor5-ui/src/dropdown/button/createbuttondropdown.js
  6. 22 0
      packages/ckeditor5-ui/src/dropdown/button/dropdownbutton.jsdoc
  7. 82 0
      packages/ckeditor5-ui/src/dropdown/button/dropdownbuttonview.js
  8. 212 0
      packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.js
  9. 0 54
      packages/ckeditor5-ui/src/dropdown/createdropdown.js
  10. 0 60
      packages/ckeditor5-ui/src/dropdown/dropdownmodel.jsdoc
  11. 1 1
      packages/ckeditor5-ui/src/dropdown/dropdownpanelfocusable.jsdoc
  12. 31 3
      packages/ckeditor5-ui/src/dropdown/dropdownpanelview.js
  13. 76 27
      packages/ckeditor5-ui/src/dropdown/dropdownview.js
  14. 0 73
      packages/ckeditor5-ui/src/dropdown/list/createlistdropdown.js
  15. 0 33
      packages/ckeditor5-ui/src/dropdown/list/listdropdownmodel.jsdoc
  16. 0 26
      packages/ckeditor5-ui/src/dropdown/list/listdropdownview.jsdoc
  17. 204 32
      packages/ckeditor5-ui/src/dropdown/utils.js
  18. 1 1
      packages/ckeditor5-ui/src/view.js
  19. 0 219
      packages/ckeditor5-ui/tests/dropdown/button/createbuttondropdown.js
  20. 47 0
      packages/ckeditor5-ui/tests/dropdown/button/dropdownbuttonview.js
  21. 225 0
      packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js
  22. 0 109
      packages/ckeditor5-ui/tests/dropdown/createdropdown.js
  23. 53 0
      packages/ckeditor5-ui/tests/dropdown/dropdownpanelview.js
  24. 6 13
      packages/ckeditor5-ui/tests/dropdown/dropdownview.js
  25. 0 185
      packages/ckeditor5-ui/tests/dropdown/list/createlistdropdown.js
  26. 7 7
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.html
  27. 63 41
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js
  28. 8 2
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.md
  29. 354 0
      packages/ckeditor5-ui/tests/dropdown/utils.js
  30. 31 0
      packages/ckeditor5-ui/theme/components/dropdown/splitbutton.css
  31. 7 1
      packages/ckeditor5-ui/theme/components/dropdown/toolbardropdown.css

+ 128 - 0
packages/ckeditor5-ui/src/button/button.jsdoc

@@ -0,0 +1,128 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/button/button
+ */
+
+/**
+ * The button interface. Implemented by, among others, {@link module:ui/button/buttonview~ButtonView},
+ * {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} and
+ * {@link module:ui/dropdown/button/dropdownbuttonview~DropdownButtonView}.
+ *
+ * @interface module:ui/button/button~Button
+ */
+
+/**
+ * The label of the button view visible to the user when {@link #withText} is `true`.
+ * It can also be used to create a {@link #tooltip}.
+ *
+ * @observable
+ * @member {String} #label
+ */
+
+/**
+ * (Optional) The keystroke associated with the button, i.e. <kbd>CTRL+B</kbd>,
+ * in the string format compatible with {@link module:utils/keyboard}.
+ *
+ * @observable
+ * @member {Boolean} #keystroke
+ */
+
+/**
+ * (Optional) Tooltip of the button, i.e. displayed when hovering the button with the mouse cursor.
+ *
+ * * If defined as a `Boolean` (e.g. `true`), then combination of `label` and `keystroke` will be set as a tooltip.
+ * * If defined as a `String`, tooltip will equal the exact text of that `String`.
+ * * If defined as a `Function`, `label` and `keystroke` will be passed to that function, which is to return
+ * a string with the tooltip text.
+ *
+ *		const view = new ButtonView( locale );
+ *		view.tooltip = ( label, keystroke ) => `A tooltip for ${ label } and ${ keystroke }.`
+ *
+ * @observable
+ * @default false
+ * @member {Boolean|String|Function} #tooltip
+ */
+
+/**
+ * (Optional) The position of the tooltip. See {@link module:ui/tooltip/tooltipview~TooltipView#position}
+ * to learn more about the available position values.
+ *
+ * **Note:** It makes sense only when the {@link #tooltip `tooltip` attribute} is defined.
+ *
+ * @observable
+ * @default 's'
+ * @member {'s'|'n'} #tooltipPosition
+ */
+
+/**
+ * The HTML type of the button. Default `button`.
+ *
+ * @observable
+ * @member {'button'|'submit'|'reset'|'menu'} #type
+ */
+
+/**
+ * Controls whether the button view is "on". It makes sense when a feature it represents
+ * is currently active, e.g. a bold button is "on" when the selection is in the bold text.
+ *
+ * To disable the button, use {@link #isEnabled} instead.
+ *
+ * @observable
+ * @default true
+ * @member {Boolean} #isOn
+ */
+
+/**
+ * Controls whether the button view is enabled, i.e. it can be clicked and execute an action.
+ *
+ * To change the "on" state of the button, use {@link #isOn} instead.
+ *
+ * @observable
+ * @default true
+ * @member {Boolean} #isEnabled
+ */
+
+/**
+ * Controls whether the button view is visible. Visible by default, buttons are hidden
+ * using a CSS class.
+ *
+ * @observable
+ * @default true
+ * @member {Boolean} #isVisible
+ */
+
+/**
+ * (Optional) Controls whether the label of the button is hidden (e.g. an icon–only button).
+ *
+ * @observable
+ * @default false
+ * @member {Boolean} #withText
+ */
+
+/**
+ * (Optional) An XML {@link module:ui/icon/iconview~IconView#content content} of the icon.
+ * When defined, an `iconView` should be added to the button.
+ *
+ * @observable
+ * @member {String} #icon
+ */
+
+/**
+ * (Optional) Controls the `tabindex` HTML attribute of the button. By default, the button is focusable
+ * but does not included in the <kbd>Tab</kbd> order.
+ *
+ * @observable
+ * @default -1
+ * @member {String} #tabindex
+ */
+
+/**
+ * Fired when the button view is clicked. It won't be fired when the button {@link #isEnabled}
+ * is `false`.
+ *
+ * @event execute
+ */

+ 15 - 120
packages/ckeditor5-ui/src/button/buttonview.js

@@ -32,6 +32,7 @@ import '../../theme/components/button/button.css';
  *		document.body.append( view.element );
  *
  * @extends module:ui/view~View
+ * @implements module:ui/button/button~Button
  */
 export default class ButtonView extends View {
 	/**
@@ -42,118 +43,19 @@ export default class ButtonView extends View {
 
 		const bind = this.bindTemplate;
 
-		/**
-		 * The label of the button view visible to the user when {@link #withText} is `true`.
-		 * It can also be used to create a {@link #tooltip}.
-		 *
-		 * @observable
-		 * @member {String} #label
-		 */
-		this.set( 'label' );
-
-		/**
-		 * (Optional) The keystroke associated with the button, i.e. <kbd>CTRL+B</kbd>,
-		 * in the string format compatible with {@link module:utils/keyboard}.
-		 *
-		 * @observable
-		 * @member {Boolean} #keystroke
-		 */
+		// Implement the Button interface.
+		this.set( 'icon' );
+		this.set( 'isEnabled', true );
+		this.set( 'isOn', false );
+		this.set( 'isVisible', true );
 		this.set( 'keystroke' );
-
-		/**
-		 * (Optional) Tooltip of the button, i.e. displayed when hovering the button with the mouse cursor.
-		 *
-		 * * If defined as a `Boolean` (e.g. `true`), then combination of `label` and `keystroke` will be set as a tooltip.
-		 * * If defined as a `String`, tooltip will equal the exact text of that `String`.
-		 * * If defined as a `Function`, `label` and `keystroke` will be passed to that function, which is to return
-		 * a string with the tooltip text.
-		 *
-		 *		const view = new ButtonView( locale );
-		 *		view.tooltip = ( label, keystroke ) => `A tooltip for ${ label } and ${ keystroke }.`
-		 *
-		 * @observable
-		 * @default false
-		 * @member {Boolean|String|Function} #tooltip
-		 */
+		this.set( 'label' );
+		this.set( 'tabindex', -1 );
 		this.set( 'tooltip' );
-
-		/**
-		 * (Optional) The position of the tooltip. See {@link module:ui/tooltip/tooltipview~TooltipView#position}
-		 * to learn more about the available position values.
-		 *
-		 * **Note:** It makes sense only when the {@link #tooltip `tooltip` attribute} is defined.
-		 *
-		 * @observable
-		 * @default 's'
-		 * @member {'s'|'n'} #position
-		 */
 		this.set( 'tooltipPosition', 's' );
-
-		/**
-		 * The HTML type of the button. Default `button`.
-		 *
-		 * @observable
-		 * @member {'button'|'submit'|'reset'|'menu'} #type
-		 */
 		this.set( 'type', 'button' );
-
-		/**
-		 * Controls whether the button view is "on". It makes sense when a feature it represents
-		 * is currently active, e.g. a bold button is "on" when the selection is in the bold text.
-		 *
-		 * To disable the button, use {@link #isEnabled} instead.
-		 *
-		 * @observable
-		 * @member {Boolean} #isOn
-		 */
-		this.set( 'isOn', false );
-
-		/**
-		 * Controls whether the button view is enabled, i.e. it can be clicked and execute an action.
-		 *
-		 * To change the "on" state of the button, use {@link #isOn} instead.
-		 *
-		 * @observable
-		 * @member {Boolean} #isEnabled
-		 */
-		this.set( 'isEnabled', true );
-
-		/**
-		 * Controls whether the button view is visible. Visible by default, buttons are hidden
-		 * using a CSS class.
-		 *
-		 * @observable
-		 * @member {Boolean} #isVisible
-		 */
-		this.set( 'isVisible', true );
-
-		/**
-		 * (Optional) Controls whether the label of the button is hidden (e.g. an icon–only button).
-		 *
-		 * @observable
-		 * @member {Boolean} #withText
-		 */
 		this.set( 'withText', false );
 
-		/**
-		 * (Optional) An XML {@link module:ui/icon/iconview~IconView#content content} of the icon.
-		 * When defined, an {@link #iconView} will be added to the button.
-		 *
-		 * @observable
-		 * @member {String} #icon
-		 */
-		this.set( 'icon' );
-
-		/**
-		 * (Optional) Controls the `tabindex` HTML attribute of the button. By default, the button is focusable
-		 * but does not included in the <kbd>Tab</kbd> order.
-		 *
-		 * @observable
-		 * @default -1
-		 * @member {String} #tabindex
-		 */
-		this.set( 'tabindex', -1 );
-
 		/**
 		 * Collection of the child views inside of the button {@link #element}.
 		 *
@@ -178,6 +80,13 @@ export default class ButtonView extends View {
 		 */
 		this.labelView = this._createLabelView();
 
+		/**
+		 * (Optional) The icon view of the button. Only present when the {@link #icon icon attribute} is defined.
+		 *
+		 * @readonly
+		 * @member {module:ui/icon/iconview~IconView} #iconView
+		 */
+
 		/**
 		 * Tooltip of the button bound to the template.
 		 *
@@ -194,13 +103,6 @@ export default class ButtonView extends View {
 			this._getTooltipString.bind( this )
 		);
 
-		/**
-		 * (Optional) The icon view of the button. Only present when the {@link #icon icon attribute} is defined.
-		 *
-		 * @readonly
-		 * @member {module:ui/icon/iconview~IconView} #iconView
-		 */
-
 		this.setTemplate( {
 			tag: 'button',
 
@@ -236,13 +138,6 @@ export default class ButtonView extends View {
 				} )
 			}
 		} );
-
-		/**
-		 * Fired when the button view is clicked. It won't be fired when the button {@link #isEnabled}
-		 * is `false`.
-		 *
-		 * @event execute
-		 */
 	}
 
 	/**

+ 0 - 71
packages/ckeditor5-ui/src/dropdown/button/buttondropdownmodel.jsdoc

@@ -1,71 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/button/buttondropdownmodel
- */
-
-/**
- * The button dropdown model interface.
- *
- * @implements module:ui/dropdown/dropdownmodel~DropdownModel
- * @interface module:ui/dropdown/button/buttondropdownmodel~ButtonDropdownModel
- */
-
-/**
- * List of buttons to be included in dropdown
- *
- * @observable
- * @member {Array.<module:ui/button/buttonview~ButtonView>} #buttons
- */
-
-/**
- * Fired when the button dropdown is executed. It fires when one of the buttons
- * {@link module:ui/button/buttonview~ButtonView#event:execute executed}.
- *
- * @event #execute
- */
-
-/**
- * Controls dropdown direction.
- *
- * @observable
- * @member {Boolean} #isVertical=false
- */
-
-/**
- * Disables automatic button icon binding. If set to true dropdown's button {@link #icon} will be set to {@link #defaultIcon}.
- *
- * @observable
- * @member {Boolean} #staticIcon=false
- */
-
-/**
- * Defines default icon which is used when no button is active.
- *
- * Also see {@link #icon}.
- *
- * @observable
- * @member {String} #defaultIcon
- */
-
-/**
- * Button dropdown icon is set from inner button views.
- *
- * Also see {@link #defaultIcon} and {@link #staticIcon}.
- *
- * @observable
- * @member {String} #icon
- */
-
-/**
- * (Optional) A CSS class set to
- * {@link module:ui/dropdown/button/buttondropdownview~ButtonDropdownView#toolbarView}.
- *
- * Also see {@link module:ui/toolbar/toolbarview~ToolbarView#className `ToolbarView#className`}.
- *
- * @observable
- * @member {String} #toolbarClassName
- */

+ 0 - 26
packages/ckeditor5-ui/src/dropdown/button/buttondropdownview.jsdoc

@@ -1,26 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/button/createbuttondropdown
- */
-
-/**
- * The button dropdown view.
- *
- * See {@link module:ui/dropdown/button/createbuttondropdown~createButtonDropdown}.
- *
- * @abstract
- * @class module:ui/dropdown/button/buttondropdownview~ButtonDropdownView
- * @extends module:ui/dropdown/dropdownview~DropdownView
- */
-
-/**
- * A child toolbar of the dropdown located in the
- * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
- *
- * @readonly
- * @member {module:ui/toolbar/toolbarview~ToolbarView} #toolbarView
- */

+ 0 - 113
packages/ckeditor5-ui/src/dropdown/button/createbuttondropdown.js

@@ -1,113 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/button/createbuttondropdown
- */
-
-import createDropdown from '../createdropdown';
-
-import ToolbarView from '../../toolbar/toolbarview';
-import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownContentsOnArrows } from '../utils';
-
-import '../../../theme/components/dropdown/buttondropdown.css';
-
-/**
- * Creates an instance of {@link module:ui/dropdown/button/buttondropdownview~ButtonDropdownView} class using
- * a provided {@link module:ui/dropdown/button/buttondropdownmodel~ButtonDropdownModel}.
- *
- *		const buttons = [];
- *
- *		buttons.push( new ButtonView() );
- *		buttons.push( editor.ui.componentFactory.get( 'someButton' ) );
- *
- *		const model = new Model( {
- *			label: 'A button dropdown',
- *			isVertical: true,
- *			buttons
- *		} );
- *
- *		const dropdown = createButtonDropdown( model, locale );
- *
- *		// Will render a vertical button dropdown labeled "A button dropdown"
- *		// with a button group in the panel containing two buttons.
- *		dropdown.render()
- *		document.body.appendChild( dropdown.element );
- *
- * The model instance remains in control of the dropdown after it has been created. E.g. changes to the
- * {@link module:ui/dropdown/dropdownmodel~DropdownModel#label `model.label`} will be reflected in the
- * dropdown button's {@link module:ui/button/buttonview~ButtonView#label} attribute and in DOM.
- *
- * See {@link module:ui/dropdown/createdropdown~createDropdown}.
- *
- * @param {module:ui/dropdown/button/buttondropdownmodel~ButtonDropdownModel} model Model of the list dropdown.
- * @param {module:utils/locale~Locale} locale The locale instance.
- * @returns {module:ui/dropdown/button/buttondropdownview~ButtonDropdownView} The button dropdown view instance.
- * @returns {module:ui/dropdown/dropdownview~DropdownView}
- */
-export default function createButtonDropdown( model, locale ) {
-	// Make disabled when all buttons are disabled
-	model.bind( 'isEnabled' ).to(
-		// Bind to #isEnabled of each command...
-		...getBindingTargets( model.buttons, 'isEnabled' ),
-		// ...and set it true if any command #isEnabled is true.
-		( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled )
-	);
-
-	// If defined `staticIcon` use the `defautlIcon` without binding it to active a button.
-	if ( model.staticIcon ) {
-		model.bind( 'icon' ).to( model, 'defaultIcon' );
-	} else {
-		// Make dropdown icon as any active button.
-		model.bind( 'icon' ).to(
-			// Bind to #isOn of each button...
-			...getBindingTargets( model.buttons, 'isOn' ),
-			// ...and chose the title of the first one which #isOn is true.
-			( ...areActive ) => {
-				const index = areActive.findIndex( value => value );
-
-				// If none of the commands is active, display either defaultIcon or first button icon.
-				if ( index < 0 && model.defaultIcon ) {
-					return model.defaultIcon;
-				}
-
-				return model.buttons[ index < 0 ? 0 : index ].icon;
-			}
-		);
-	}
-
-	const dropdownView = createDropdown( model, locale );
-	const toolbarView = dropdownView.toolbarView = new ToolbarView();
-
-	toolbarView.bind( 'isVertical', 'className' ).to( model, 'isVertical', 'toolbarClassName' );
-
-	model.buttons.map( view => toolbarView.items.add( view ) );
-
-	dropdownView.extendTemplate( {
-		attributes: {
-			class: [ 'ck-buttondropdown' ]
-		}
-	} );
-
-	dropdownView.panelView.children.add( toolbarView );
-
-	closeDropdownOnBlur( dropdownView );
-	closeDropdownOnExecute( dropdownView, toolbarView.items );
-	focusDropdownContentsOnArrows( dropdownView, toolbarView );
-
-	return dropdownView;
-}
-
-// Returns an array of binding components for
-// {@link module:utils/observablemixin~Observable#bind} from a set of iterable
-// buttons.
-//
-// @private
-// @param {Iterable.<module:ui/button/buttonview~ButtonView>} buttons
-// @param {String} attribute
-// @returns {Array.<String>}
-function getBindingTargets( buttons, attribute ) {
-	return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) );
-}

+ 22 - 0
packages/ckeditor5-ui/src/dropdown/button/dropdownbutton.jsdoc

@@ -0,0 +1,22 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/button/dropdownbutton
+ */
+
+/**
+ * The dropdown button interface.
+ *
+ * @interface module:ui/dropdown/button/dropdownbutton~DropdownButton
+ * @extends module:ui/button/button~Button
+ */
+
+/**
+ * Fired when the dropdown should be opened.
+ * It will not be fired when the button {@link #isEnabled is disabled}.
+ *
+ * @event open
+ */

+ 82 - 0
packages/ckeditor5-ui/src/dropdown/button/dropdownbuttonview.js

@@ -0,0 +1,82 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/button/dropdownbuttonview
+ */
+
+import ButtonView from '../../button/buttonview';
+
+import dropdownArrowIcon from '../../../theme/icons/dropdown-arrow.svg';
+import IconView from '../../icon/iconview';
+
+/**
+ * The default dropdown button view class.
+ *
+ *		const view = new DropdownButtonView();
+ *
+ *		view.set( {
+ *			label: 'A button',
+ *			keystroke: 'Ctrl+B',
+ *			tooltip: true
+ *		} );
+ *
+ *		view.render();
+ *
+ *		document.body.append( view.element );
+ *
+ * Also see the {@link module:ui/dropdown/utils~createDropdown `createDropdown()` util}.
+ *
+ * @implements module:ui/dropdown/button/dropdownbutton~DropdownButton
+ * @extends module:ui/button/buttonview~ButtonView
+ */
+export default class DropdownButtonView extends ButtonView {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		/**
+		 * An icon that displays arrow to indicate a dropdown button.
+		 *
+		 * @readonly
+		 * @member {module:ui/icon/iconview~IconView}
+		 */
+		this.arrowView = this._createArrowView();
+
+		// The DropdownButton interface expects the open event upon which will open the dropdown.
+		this.delegate( 'execute' ).to( this, 'open' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		this.children.add( this.arrowView );
+	}
+
+	/**
+	 * Creates a {@link module:ui/icon/iconview~IconView} instance as {@link #arrowView}.
+	 *
+	 * @private
+	 * @returns {module:ui/icon/iconview~IconView}
+	 */
+	_createArrowView() {
+		const arrowView = new IconView();
+
+		arrowView.content = dropdownArrowIcon;
+
+		arrowView.extendTemplate( {
+			attributes: {
+				class: 'ck-dropdown__arrow'
+			}
+		} );
+
+		return arrowView;
+	}
+}

+ 212 - 0
packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.js

@@ -0,0 +1,212 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/button/splitbuttonview
+ */
+
+import View from '../../view';
+import ButtonView from '../../button/buttonview';
+
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+
+import dropdownArrowIcon from '../../../theme/icons/dropdown-arrow.svg';
+
+import '../../../theme/components/dropdown/splitbutton.css';
+
+/**
+ * The split button view class.
+ *
+ *		const view = new SplitButtonView();
+ *
+ *		view.set( {
+ *			label: 'A button',
+ *			keystroke: 'Ctrl+B',
+ *			tooltip: true
+ *		} );
+ *
+ *		view.render();
+ *
+ *		document.body.append( view.element );
+ *
+ * Also see the {@link module:ui/dropdown/utils~createDropdown `createDropdown()` util}.
+ *
+ * @implements module:ui/dropdown/button/dropdownbutton~DropdownButton
+ * @extends module:ui/view~View
+ */
+export default class SplitButtonView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		// Implement the Button interface.
+		this.set( 'icon' );
+		this.set( 'isEnabled', true );
+		this.set( 'isOn', false );
+		this.set( 'isVisible', true );
+		this.set( 'keystroke' );
+		this.set( 'label' );
+		this.set( 'tabindex', -1 );
+		this.set( 'tooltip' );
+		this.set( 'tooltipPosition', 's' );
+		this.set( 'type', 'button' );
+		this.set( 'withText', false );
+
+		/**
+		 * Collection of the child views inside of the split button {@link #element}.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.children = this.createCollection();
+
+		/**
+		 * A main button of split button.
+		 *
+		 * @readonly
+		 * @member {module:ui/button/buttonview~ButtonView}
+		 */
+		this.actionView = this._createActionView();
+
+		/**
+		 * A secondary button of split button that opens dropdown.
+		 *
+		 * @readonly
+		 * @member {module:ui/button/buttonview~ButtonView}
+		 */
+		this.arrowView = this._createArrowView();
+
+		/**
+		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. It manages
+		 * keystrokes of the split button:
+		 *
+		 * * <kbd>▶</kbd> moves focus to arrow view when action view is focused,
+		 * * <kbd>◀</kbd> moves focus to action view when arrow view is focused.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * Tracks information about DOM focus in the dropdown.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		this.setTemplate( {
+			tag: 'div',
+
+			attributes: {
+				class: [
+					'ck-splitbutton',
+					bind.if( 'isVisible', 'ck-hidden', value => !value )
+				]
+			},
+
+			children: this.children
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		this.children.add( this.actionView );
+		this.children.add( this.arrowView );
+
+		this.focusTracker.add( this.actionView.element );
+		this.focusTracker.add( this.arrowView.element );
+
+		this.keystrokes.listenTo( this.element );
+
+		// Overrides toolbar focus cycling behavior.
+		this.keystrokes.set( 'arrowright', ( evt, cancel ) => {
+			if ( this.focusTracker.focusedElement === this.actionView.element ) {
+				this.arrowView.focus();
+
+				cancel();
+			}
+		} );
+
+		// Overrides toolbar focus cycling behavior.
+		this.keystrokes.set( 'arrowleft', ( evt, cancel ) => {
+			if ( this.focusTracker.focusedElement === this.arrowView.element ) {
+				this.actionView.focus();
+
+				cancel();
+			}
+		} );
+	}
+
+	/**
+	 * Focuses the {@link #actionView#element} of the action part of split button.
+	 */
+	focus() {
+		this.actionView.focus();
+	}
+
+	/**
+	 * Creates a {@link module:ui/button/buttonview~ButtonView} instance as {@link #actionView} and binds it with main split button
+	 * attributes.
+	 *
+	 * @private
+	 * @returns {module:ui/button/buttonview~ButtonView}
+	 */
+	_createActionView() {
+		const buttonView = new ButtonView();
+
+		buttonView.bind(
+			'icon',
+			'isEnabled',
+			'isOn',
+			'keystroke',
+			'label',
+			'tabindex',
+			'tooltip',
+			'tooltipPosition',
+			'type',
+			'withText'
+		).to( this );
+
+		buttonView.delegate( 'execute' ).to( this );
+
+		return buttonView;
+	}
+
+	/**
+	 * Creates a {@link module:ui/button/buttonview~ButtonView} instance as {@link #arrowView} and binds it with main split button
+	 * attributes.
+	 *
+	 * @private
+	 * @returns {module:ui/button/buttonview~ButtonView}
+	 */
+	_createArrowView() {
+		const arrowView = new ButtonView();
+
+		arrowView.icon = dropdownArrowIcon;
+
+		arrowView.extendTemplate( {
+			attributes: {
+				class: 'ck-splitbutton-arrow'
+			}
+		} );
+
+		arrowView.bind( 'isEnabled' ).to( this );
+
+		arrowView.delegate( 'execute' ).to( this, 'open' );
+
+		return arrowView;
+	}
+}

+ 0 - 54
packages/ckeditor5-ui/src/dropdown/createdropdown.js

@@ -1,54 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/createdropdown
- */
-
-import ButtonView from '../button/buttonview';
-import DropdownView from './dropdownview';
-import DropdownPanelView from './dropdownpanelview';
-
-/**
- * A helper which creates an instance of {@link module:ui/dropdown/dropdownview~DropdownView} class using
- * a provided {@link module:ui/dropdown/dropdownmodel~DropdownModel}.
- *
- *		const model = new Model( {
- *			label: 'A dropdown',
- *			isEnabled: true,
- *			isOn: false,
- *			withText: true
- *		} );
- *
- *		const dropdown = createDropdown( model );
- *
- *		dropdown.render();
- *
- *		// Will render a dropdown labeled "A dropdown" with an empty panel.
- *		document.body.appendChild( dropdown.element );
- *
- * The model instance remains in control of the dropdown after it has been created. E.g. changes to the
- * {@link module:ui/dropdown/dropdownmodel~DropdownModel#label `model.label`} will be reflected in the
- * dropdown button's {@link module:ui/button/buttonview~ButtonView#label} attribute and in DOM.
- *
- * Also see {@link module:ui/dropdown/list/createlistdropdown~createListDropdown}.
- *
- * @param {module:ui/dropdown/dropdownmodel~DropdownModel} model Model of this dropdown.
- * @param {module:utils/locale~Locale} locale The locale instance.
- * @returns {module:ui/dropdown/dropdownview~DropdownView} The dropdown view instance.
- */
-export default function createDropdown( model, locale ) {
-	const buttonView = new ButtonView( locale );
-	const panelView = new DropdownPanelView( locale );
-	const dropdownView = new DropdownView( locale, buttonView, panelView );
-
-	dropdownView.bind( 'isEnabled' ).to( model );
-	buttonView.bind( 'label', 'isEnabled', 'withText', 'keystroke', 'tooltip', 'icon' ).to( model );
-	buttonView.bind( 'isOn' ).to( model, 'isOn', dropdownView, 'isOpen', ( isOn, isOpen ) => {
-		return isOn || isOpen;
-	} );
-
-	return dropdownView;
-}

+ 0 - 60
packages/ckeditor5-ui/src/dropdown/dropdownmodel.jsdoc

@@ -1,60 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/dropdownmodel
- */
-
-/**
- * The basic dropdown model interface.
- *
- * @interface module:ui/dropdown/dropdownmodel~DropdownModel
- */
-
-/**
- * The label of the dropdown.
- *
- * Also see {@link module:ui/button/buttonview~ButtonView#label}.
- *
- * @observable
- * @member {String} #label
- */
-
-/**
- * Controls whether the dropdown is enabled, i.e. it opens the panel when clicked.
- *
- * Also see {@link module:ui/button/buttonview~ButtonView#isEnabled}.
- *
- * @observable
- * @member {Boolean} #isEnabled
- */
-
-/**
- * Controls whether the dropdown is "on". It makes sense when a feature it represents
- * is currently active.
- *
- * Also see {@link module:ui/button/buttonview~ButtonView#isOn}.
- *
- * @observable
- * @member {Boolean} #isOn
- */
-
-/**
- * (Optional) Controls whether the label of the dropdown is visible.
- *
- * Also see {@link module:ui/button/buttonview~ButtonView#withText}.
- *
- * @observable
- * @member {Boolean} #withText
- */
-
-/**
- * (Optional) Controls the icon of the dropdown.
- *
- * Also see {@link module:ui/button/buttonview~ButtonView#withText}.
- *
- * @observable
- * @member {Boolean} #icon
- */

+ 1 - 1
packages/ckeditor5-ui/src/dropdown/dropdownpanelfocusable.jsdoc

@@ -8,7 +8,7 @@
  */
 
 /**
- * The dropdown panel interface interface for focusable contents. It provides two methods for managing focus of the contents
+ * The dropdown panel interface for focusable contents. It provides two methods for managing focus of the contents
  * of dropdown's panel.
  *
  * @interface module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable

+ 31 - 3
packages/ckeditor5-ui/src/dropdown/dropdownpanelview.js

@@ -36,9 +36,9 @@ export default class DropdownPanelView extends View {
 		/**
 		 * Collection of the child views in this panel.
 		 *
-		 * A common child type is the {@link module:list/list~List}. See
-		 * {@link module:ui/dropdown/list/createlistdropdown~createListDropdown} to learn more
-		 * about list dropdowns.
+		 * A common child type is the {@link module:ui/list/listview~ListView} and {@link module:ui/toolbar/toolbarview~ToolbarView}.
+		 * See {@link module:ui/dropdown/utils~addListToDropdown} and
+		 * {@link module:ui/dropdown/utils~addToolbarToDropdown} to learn more about child views of dropdowns.
 		 *
 		 * @readonly
 		 * @member {module:ui/viewcollection~ViewCollection}
@@ -65,4 +65,32 @@ export default class DropdownPanelView extends View {
 			}
 		} );
 	}
+
+	/**
+	 * Focuses the view element or first item in view collection on opening dropdown's panel.
+	 *
+	 * See also {@link module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable}.
+	 */
+	focus() {
+		if ( this.children.length ) {
+			this.children.first.focus();
+		}
+	}
+
+	/**
+	 * Focuses the view element or last item in view collection on opening dropdown's panel.
+	 *
+	 * See also {@link module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable}.
+	 */
+	focusLast() {
+		if ( this.children.length ) {
+			const lastChild = this.children.last;
+
+			if ( typeof lastChild.focusLast === 'function' ) {
+				lastChild.focusLast();
+			} else {
+				lastChild.focus();
+			}
+		}
+	}
 }

+ 76 - 27
packages/ckeditor5-ui/src/dropdown/dropdownview.js

@@ -8,21 +8,42 @@
  */
 
 import View from '../view';
-import IconView from '../icon/iconview';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
-import dropdownArrowIcon from '../../theme/icons/dropdown-arrow.svg';
 
 import '../../theme/components/dropdown/dropdown.css';
 
 /**
- * The dropdown view class.
+ * The dropdown view class. It manages the dropdown button and dropdown panel.
  *
- *		const button = new ButtonView( locale );
+ * In most cases, the easiest way to create a dropdown is by using the {@link module:ui/dropdown/utils~createDropdown}
+ * util:
+ *
+ *		const dropdown = createDropdown( locale );
+ *
+ *		// Configure dropdown's button properties:
+ *		dropdown.buttonView.set( {
+ *			label: 'A dropdown',
+ *			withText: true
+ *		} );
+ *
+ *		dropdown.render();
+ *
+ *		dropdown.panelView.element.textContent = 'Content of the panel';
+ *
+ *		// Will render a dropdown with a panel containing a "Content of the panel" text.
+ *		document.body.appendChild( dropdown.element );
+ *
+ * If you want to add a richer content to the dropdown panel, you can use the {@link module:ui/dropdown/utils~addListToDropdown}
+ * and {@link module:ui/dropdown/utils~addToolbarToDropdown} helpers. See more examples in
+ * {@link module:ui/dropdown/utils~createDropdown} documentation.
+ *
+ * If you want to create a completely custom dropdown, then you can compose it manually:
+ *
+ *		const button = new DropdownButtonView( locale );
  *		const panel = new DropdownPanelView( locale );
  *		const dropdown = new DropdownView( locale, button, panel );
  *
- *		panel.element.textContent = 'Content of the panel';
  *		button.set( {
  *			label: 'A dropdown',
  *			withText: true
@@ -30,18 +51,27 @@ import '../../theme/components/dropdown/dropdown.css';
  *
  *		dropdown.render();
  *
+ *		panel.element.textContent = 'Content of the panel';
+ *
  *		// Will render a dropdown with a panel containing a "Content of the panel" text.
  *		document.body.appendChild( dropdown.element );
  *
- * Also see {@link module:ui/dropdown/createdropdown~createDropdown} and
- * {@link module:ui/dropdown/list/createlistdropdown~createListDropdown} to learn about different
- * dropdown creation helpers.
+ * However, dropdown created this way will contain little behavior. You will need to implement handlers for actions
+ * such as {@link module:ui/bindings/clickoutsidehandler~clickOutsideHandler clicking outside an open dropdown}
+ * (which should close it) and support for arrow keys inside the panel. Therefore, unless you really know what
+ * you do and you really need to do it, it is recommended to use the {@link module:ui/dropdown/utils~createDropdown} helper.
  *
  * @extends module:ui/view~View
  */
 export default class DropdownView extends View {
 	/**
-	 * @inheritDoc
+	 * Creates an instance of the dropdown.
+	 *
+	 * Also see {@link #render}.
+	 *
+	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
+	 * @param {module:ui/dropdown/button/dropdownbutton~DropdownButton} buttonView
+	 * @param {module:ui/dropdown/dropdownpanelview~DropdownPanelView} panelView
 	 */
 	constructor( locale, buttonView, panelView ) {
 		super( locale );
@@ -110,14 +140,6 @@ export default class DropdownView extends View {
 		 */
 		this.keystrokes = new KeystrokeHandler();
 
-		/**
-		 * The arrow icon of the dropdown.
-		 *
-		 * @readonly
-		 * @member {module:ui/icon/iconview~IconView} #arrowView
-		 */
-		const arrowView = this.arrowView = new IconView();
-
 		this.setTemplate( {
 			tag: 'div',
 
@@ -130,18 +152,10 @@ export default class DropdownView extends View {
 
 			children: [
 				buttonView,
-				arrowView,
 				panelView
 			]
 		} );
 
-		arrowView.content = dropdownArrowIcon;
-		arrowView.extendTemplate( {
-			attributes: {
-				class: 'ck-dropdown__arrow'
-			}
-		} );
-
 		buttonView.extendTemplate( {
 			attributes: {
 				class: [
@@ -149,6 +163,41 @@ export default class DropdownView extends View {
 				]
 			}
 		} );
+
+		/**
+		 * A child {@link module:ui/list/listview~ListView list view} of the dropdown located
+		 * in its {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
+		 *
+		 * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addListToDropdown}.
+		 *
+		 * @readonly
+		 * @member {module:ui/list/listview~ListView} #listView
+		 */
+
+		/**
+		 * A child toolbar of the dropdown located in the
+		 * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
+		 *
+		 * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addToolbarToDropdown}.
+		 *
+		 * @readonly
+		 * @member {module:ui/toolbar/toolbarview~ToolbarView} #toolbarView
+		 */
+
+		/**
+		 * Fired when the toolbar button or list item is executed.
+		 *
+		 * For {@link #listView} It fires when one of the list items has been
+		 * {@link module:ui/list/listitemview~ListItemView#event:execute executed}.
+		 *
+		 * For {@link #toolbarView} It fires when one of the buttons has been
+		 * {@link module:ui/button/buttonview~ButtonView#event:execute executed}.
+		 *
+		 * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addListToDropdown}
+		 * or {@link module:ui/dropdown/utils~addToolbarToDropdown}.
+		 *
+		 * @event #execute
+		 */
 	}
 
 	/**
@@ -157,8 +206,8 @@ export default class DropdownView extends View {
 	render() {
 		super.render();
 
-		// Toggle the the dropdown when it's button has been clicked.
-		this.listenTo( this.buttonView, 'execute', () => {
+		// Toggle the dropdown when its button has been clicked.
+		this.listenTo( this.buttonView, 'open', () => {
 			this.isOpen = !this.isOpen;
 		} );
 

+ 0 - 73
packages/ckeditor5-ui/src/dropdown/list/createlistdropdown.js

@@ -1,73 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/list/createlistdropdown
- */
-
-import ListView from '../../list/listview';
-import ListItemView from '../../list/listitemview';
-import createDropdown from '../createdropdown';
-import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownContentsOnArrows } from '../utils';
-
-/**
- * Creates an instance of {@link module:ui/dropdown/list/listdropdownview~ListDropdownView} class using
- * a provided {@link module:ui/dropdown/list/listdropdownmodel~ListDropdownModel}.
- *
- *		const items = new Collection();
- *
- *		items.add( new Model( { label: 'First item', style: 'color: red' } ) );
- *		items.add( new Model( { label: 'Second item', style: 'color: green', class: 'foo' } ) );
- *
- *		const model = new Model( {
- *			isEnabled: true,
- *			items,
- *			isOn: false,
- *			label: 'A dropdown'
- *		} );
- *
- *		const dropdown = createListDropdown( model, locale );
- *
- *		// Will render a dropdown labeled "A dropdown" with a list in the panel
- *		// containing two items.
- *		dropdown.render()
- *		document.body.appendChild( dropdown.element );
- *
- * The model instance remains in control of the dropdown after it has been created. E.g. changes to the
- * {@link module:ui/dropdown/dropdownmodel~DropdownModel#label `model.label`} will be reflected in the
- * dropdown button's {@link module:ui/button/buttonview~ButtonView#label} attribute and in DOM.
- *
- * The
- * {@link module:ui/dropdown/list/listdropdownmodel~ListDropdownModel#items items collection}
- * of the {@link module:ui/dropdown/list/listdropdownmodel~ListDropdownModel model} also controls the
- * presence and attributes of respective {@link module:ui/list/listitemview~ListItemView list items}.
- *
- * See {@link module:ui/dropdown/createdropdown~createDropdown} and {@link module:list/list~List}.
- *
- * @param {module:ui/dropdown/list/listdropdownmodel~ListDropdownModel} model Model of the list dropdown.
- * @param {module:utils/locale~Locale} locale The locale instance.
- * @returns {module:ui/dropdown/list/listdropdownview~ListDropdownView} The list dropdown view instance.
- */
-export default function createListDropdown( model, locale ) {
-	const dropdownView = createDropdown( model, locale );
-	const listView = dropdownView.listView = new ListView( locale );
-
-	listView.items.bindTo( model.items ).using( itemModel => {
-		const item = new ListItemView( locale );
-
-		// Bind all attributes of the model to the item view.
-		item.bind( ...Object.keys( itemModel ) ).to( itemModel );
-
-		return item;
-	} );
-
-	dropdownView.panelView.children.add( listView );
-
-	closeDropdownOnBlur( dropdownView );
-	closeDropdownOnExecute( dropdownView, listView.items );
-	focusDropdownContentsOnArrows( dropdownView, listView );
-
-	return dropdownView;
-}

+ 0 - 33
packages/ckeditor5-ui/src/dropdown/list/listdropdownmodel.jsdoc

@@ -1,33 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/list/listdropdownmodel
- */
-
-/**
- * The list dropdown model interface.
- *
- * @implements module:ui/dropdown/dropdownmodel~DropdownModel
- * @interface module:ui/dropdown/list/listdropdownmodel~ListDropdownModel
- */
-
-/**
- * A {@link module:utils/collection~Collection} of {@link module:utils/observablemixin~Observable}
- * that the inner dropdown {@link module:ui/list/listview~ListView} children are created from.
- *
- * Usually, it is a collection of {@link module:ui/model~Model models}.
- *
- * @observable
- * @member {module:utils/collection~Collection.<module:utils/observablemixin~Observable>} #items
- */
-
-/**
- * Fired when the list dropdown is executed. It fires when one of the list items in
- * {@link #items the collection} has been
- * {@link module:ui/list/listitemview~ListItemView#event:execute executed}.
- *
- * @event #execute
- */

+ 0 - 26
packages/ckeditor5-ui/src/dropdown/list/listdropdownview.jsdoc

@@ -1,26 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/dropdown/list/createlistdropdown
- */
-
-/**
- * The list dropdown view.
- *
- * See {@link module:ui/dropdown/list/createlistdropdown~createListDropdown}.
- *
- * @abstract
- * @class module:ui/dropdown/list/listdropdownview~ListDropdownView
- * @extends module:ui/dropdown/dropdownview~DropdownView
- */
-
-/**
- * A child {@link module:ui/list/listview~ListView list view} of the dropdown located
- * in its {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
- *
- * @readonly
- * @member {module:ui/list/listview~ListView} #listView
- */

+ 204 - 32
packages/ckeditor5-ui/src/dropdown/utils.js

@@ -7,56 +7,197 @@
  * @module ui/dropdown/utils
  */
 
+import DropdownPanelView from './dropdownpanelview';
+import DropdownView from './dropdownview';
+import DropdownButtonView from './button/dropdownbuttonview';
+import ToolbarView from '../toolbar/toolbarview';
+import ListView from '../list/listview';
+import ListItemView from '../list/listitemview';
+
 import clickOutsideHandler from '../bindings/clickoutsidehandler';
 
+import '../../theme/components/dropdown/toolbardropdown.css';
+
 /**
- * Adds a behavior to a dropdownView that focuses dropdown panel view contents on keystrokes.
+ * A helper for creating dropdowns. It creates an instance of a {@link module:ui/dropdown/dropdownview~DropdownView dropdown},
+ * with a {@link module:ui/dropdown/button/dropdownbutton~DropdownButton button},
+ * {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView panel} and all standard dropdown's behaviors.
+ *
+ * # Creating dropdowns
+ *
+ * By default, the default {@link module:ui/dropdown/button/dropdownbuttonview~DropdownButtonView} class is used as
+ * definition of the button:
+ *
+ *		const dropdown = createDropdown( model );
+ *
+ *		// Configure dropdown's button properties:
+ *		dropdown.buttonView.set( {
+ *			label: 'A dropdown',
+ *			withText: true
+ *		} );
+ *
+ *		dropdown.render();
+ *
+ *		// Will render a dropdown labeled "A dropdown" with an empty panel.
+ *		document.body.appendChild( dropdown.element );
+ *
+ * You can also provide other button views (they need to implement the
+ * {module:ui/dropdown/button/dropdownbutton~DropdownButton} interface). For instance, you can use
+ * {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} to create a dropdown with a split button.
+ *
+ *		const dropdown = createDropdown( model, SplitButtonView );
+ *
+ *		// Configure dropdown's button properties:
+ *		dropdown.buttonView.set( {
+ *			label: 'A dropdown',
+ *			withText: true
+ *		} );
  *
- * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
- * @param {module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable} panelViewContents
+ *		dropdown.buttonView.on( 'execute', () => {
+ *			// Add the behavior of the "action part" of the split button.
+ *			// Split button consists of the "action part" and "arrow part".
+ *			// The arrow opens the dropdown while the action part can have some other behavior.
+ * 		} );
+ *
+ *		dropdown.render();
+ *
+ *		// Will render a dropdown labeled "A dropdown" with an empty panel.
+ *		document.body.appendChild( dropdown.element );
+ *
+ * # Adding content to the dropdown's panel
+ *
+ * The content of the panel can be inserted directly into the `dropdown.panelView.element`:
+ *
+ *		dropdown.panelView.element.textContent = 'Content of the panel';
+ *
+ * However, most of the time you will want to add there either a {@link module:ui/list/listview~ListView list of options}
+ * or a list of buttons (i.e. a {@link module:ui/toolbar/toolbarview~ToolbarView toolbar}).
+ * To simplify the task, you can use, respectively, {@link module:ui/dropdown/utils~addListToDropdown} or
+ * {@link module:ui/dropdown/utils~addToolbarToDropdown} utils.
+ *
+ * @param {module:utils/locale~Locale} locale The locale instance.
+ * @param {Function} ButtonClass The dropdown button view class. Needs to implement the
+ * {@link module:ui/dropdown/button/dropdownbutton~DropdownButton} interface.
+ * @returns {module:ui/dropdown/dropdownview~DropdownView} The dropdown view instance.
  */
-export function focusDropdownContentsOnArrows( dropdownView, panelViewContents ) {
-	// If the dropdown panel is already open, the arrow down key should
-	// focus the first element in list.
-	dropdownView.keystrokes.set( 'arrowdown', ( data, cancel ) => {
-		if ( dropdownView.isOpen ) {
-			panelViewContents.focus();
-			cancel();
-		}
-	} );
+export function createDropdown( locale, ButtonClass = DropdownButtonView ) {
+	const buttonView = new ButtonClass( locale );
 
-	// If the dropdown panel is already open, the arrow up key should
-	// focus the last element in the list.
-	dropdownView.keystrokes.set( 'arrowup', ( data, cancel ) => {
-		if ( dropdownView.isOpen ) {
-			panelViewContents.focusLast();
-			cancel();
-		}
-	} );
+	const panelView = new DropdownPanelView( locale );
+	const dropdownView = new DropdownView( locale, buttonView, panelView );
+
+	buttonView.bind( 'isEnabled' ).to( dropdownView );
+
+	if ( buttonView instanceof DropdownButtonView ) {
+		buttonView.bind( 'isOn' ).to( dropdownView, 'isOpen' );
+	} else {
+		buttonView.arrowView.bind( 'isOn' ).to( dropdownView, 'isOpen' );
+	}
+
+	addDefaultBehavior( dropdownView );
+
+	return dropdownView;
 }
 
 /**
- * Adds a behavior to a dropdownView that closes dropdown view on any view collection item's "execute" event.
+ * Adds an instance of {@link module:ui/toolbar/toolbarview~ToolbarView} to a dropdown.
  *
- * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
- * @param {module:ui/viewcollection~ViewCollection} viewCollection
+ *		const buttons = [];
+ *
+ *		// Either create a new ButtonView instance or create existing.
+ *		buttons.push( new ButtonView() );
+ *		buttons.push( editor.ui.componentFactory.get( 'someButton' ) );
+ *
+ *		const dropdown = createDropdown( locale );
+ *
+ *		addToolbarToDropdown( dropdown, buttons );
+ *
+ *		dropdown.toolbarView.isVertical = true;
+ *
+ *		// Will render a vertical button dropdown labeled "A button dropdown"
+ *		// with a button group in the panel containing two buttons.
+ *		dropdown.render()
+ *		document.body.appendChild( dropdown.element );
+ *
+ * See {@link module:ui/dropdown/utils~createDropdown} and {@link module:ui/toolbar/toolbarview~ToolbarView}.
+ *
+ * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdown instance to which `ToolbarView` will be added.
+ * @param {Iterable.<module:ui/button/buttonview~ButtonView>} buttons
  */
-export function closeDropdownOnExecute( dropdownView, viewCollection ) {
-	// TODO: Delegate all events instead of just execute.
-	viewCollection.delegate( 'execute' ).to( dropdownView );
+export function addToolbarToDropdown( dropdownView, buttons ) {
+	const toolbarView = dropdownView.toolbarView = new ToolbarView();
 
-	// Close the dropdown when one of the list items has been executed.
-	dropdownView.on( 'execute', () => {
-		dropdownView.isOpen = false;
+	dropdownView.extendTemplate( {
+		attributes: {
+			class: [ 'ck-toolbar-dropdown' ]
+		}
 	} );
+
+	buttons.map( view => toolbarView.items.add( view ) );
+
+	dropdownView.panelView.children.add( toolbarView );
+	toolbarView.items.delegate( 'execute' ).to( dropdownView );
 }
 
 /**
- * Adds a behavior to a dropdownView that closes opened dropdown on user click outside the dropdown.
+ * Adds an instance of {@link module:ui/list/listview~ListView} to a dropdown.
+ *
+ *		const items = new Collection();
+ *
+ *		items.add( new Model( { label: 'First item', style: 'color: red' } ) );
+ *		items.add( new Model( { label: 'Second item', style: 'color: green', class: 'foo' } ) );
+ *
+ *		const dropdown = createDropdown( locale );
+ *
+ *		addListToDropdown( dropdown, items );
+ *
+ *		// Will render a dropdown with a list in the panel containing two items.
+ *		dropdown.render()
+ *		document.body.appendChild( dropdown.element );
+ *
+ * The `items` collection passed to this methods controls the presence and attributes of respective
+ * {@link module:ui/list/listitemview~ListItemView list items}.
+ *
+ *
+ * See {@link module:ui/dropdown/utils~createDropdown} and {@link module:list/list~List}.
  *
- * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
+ * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdown instance to which `ListVIew` will be added.
+ * @param {module:utils/collection~Collection} items
+ * that the inner dropdown {@link module:ui/list/listview~ListView} children are created from.
+ *
+ * Usually, it is a collection of {@link module:ui/model~Model models}.
  */
-export function closeDropdownOnBlur( dropdownView ) {
+export function addListToDropdown( dropdownView, items ) {
+	const locale = dropdownView.locale;
+	const listView = dropdownView.listView = new ListView( locale );
+
+	listView.items.bindTo( items ).using( itemModel => {
+		const item = new ListItemView( locale );
+
+		// Bind all attributes of the model to the item view.
+		item.bind( ...Object.keys( itemModel ) ).to( itemModel );
+
+		return item;
+	} );
+
+	dropdownView.panelView.children.add( listView );
+
+	listView.items.delegate( 'execute' ).to( dropdownView );
+}
+
+// Add a set of default behaviors to dropdown view.
+//
+// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
+function addDefaultBehavior( dropdownView ) {
+	closeDropdownOnBlur( dropdownView );
+	closeDropdownOnExecute( dropdownView );
+	focusDropdownContentsOnArrows( dropdownView );
+}
+
+// Adds a behavior to a dropdownView that closes opened dropdown when user clicks outside the dropdown.
+//
+// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
+function closeDropdownOnBlur( dropdownView ) {
 	dropdownView.on( 'render', () => {
 		clickOutsideHandler( {
 			emitter: dropdownView,
@@ -68,3 +209,34 @@ export function closeDropdownOnBlur( dropdownView ) {
 		} );
 	} );
 }
+
+// Adds a behavior to a dropdownView that closes the dropdown view on "execute" event.
+//
+// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
+function closeDropdownOnExecute( dropdownView ) {
+	// Close the dropdown when one of the list items has been executed.
+	dropdownView.on( 'execute', () => {
+		dropdownView.isOpen = false;
+	} );
+}
+
+// Adds a behavior to a dropdownView that focuses the dropdown's panel view contents on keystrokes.
+//
+// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
+function focusDropdownContentsOnArrows( dropdownView ) {
+	// If the dropdown panel is already open, the arrow down key should focus the first child of the #panelView.
+	dropdownView.keystrokes.set( 'arrowdown', ( data, cancel ) => {
+		if ( dropdownView.isOpen ) {
+			dropdownView.panelView.focus();
+			cancel();
+		}
+	} );
+
+	// If the dropdown panel is already open, the arrow up key should focus the last child of the #panelView.
+	dropdownView.keystrokes.set( 'arrowup', ( data, cancel ) => {
+		if ( dropdownView.isOpen ) {
+			dropdownView.panelView.focusLast();
+			cancel();
+		}
+	} );
+}

+ 1 - 1
packages/ckeditor5-ui/src/view.js

@@ -111,7 +111,7 @@ export default class View {
 		 *
 		 *		const view = new SampleView();
 		 *
-		 *		// Renders the #template
+		 *		// Renders the #template.
 		 *		view.render();
 		 *
 		 *		// Append the HTML element of the view to <body>.

+ 0 - 219
packages/ckeditor5-ui/tests/dropdown/button/createbuttondropdown.js

@@ -1,219 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals document, Event */
-
-import Model from '../../../src/model';
-import createButtonDropdown from '../../../src/dropdown/button/createbuttondropdown';
-
-import ButtonView from '../../../src/button/buttonview';
-import ToolbarView from '../../../src/toolbar/toolbarview';
-
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
-
-describe( 'createButtonDropdown', () => {
-	let view, model, locale, buttons;
-
-	beforeEach( () => {
-		locale = { t() {} };
-		buttons = [ '<svg>foo</svg>', '<svg>bar</svg>' ].map( icon => {
-			const button = new ButtonView();
-			button.icon = icon;
-
-			return button;
-		} );
-
-		model = new Model( {
-			isVertical: true,
-			buttons
-		} );
-
-		view = createButtonDropdown( model, locale );
-		view.render();
-		document.body.appendChild( view.element );
-	} );
-
-	afterEach( () => {
-		view.element.remove();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'sets view#locale', () => {
-			expect( view.locale ).to.equal( locale );
-		} );
-
-		describe( 'view#toolbarView', () => {
-			it( 'is created', () => {
-				const panelChildren = view.panelView.children;
-
-				expect( panelChildren ).to.have.length( 1 );
-				expect( panelChildren.get( 0 ) ).to.equal( view.toolbarView );
-				expect( view.toolbarView ).to.be.instanceof( ToolbarView );
-			} );
-
-			it( 'delegates view.toolbarView#execute to the view', done => {
-				view.on( 'execute', evt => {
-					expect( evt.source ).to.equal( view.toolbarView.items.get( 0 ) );
-					expect( evt.path ).to.deep.equal( [ view.toolbarView.items.get( 0 ), view ] );
-
-					done();
-				} );
-
-				view.toolbarView.items.get( 0 ).fire( 'execute' );
-			} );
-
-			it( 'reacts on model#isVertical', () => {
-				model.isVertical = false;
-				expect( view.toolbarView.isVertical ).to.be.false;
-
-				model.isVertical = true;
-				expect( view.toolbarView.isVertical ).to.be.true;
-			} );
-
-			it( 'reacts on model#toolbarClassName', () => {
-				expect( view.toolbarView.className ).to.be.undefined;
-
-				model.set( 'toolbarClassName', 'foo' );
-				expect( view.toolbarView.className ).to.equal( 'foo' );
-			} );
-		} );
-
-		it( 'changes view#isOpen on view#execute', () => {
-			view.isOpen = true;
-
-			view.fire( 'execute' );
-			expect( view.isOpen ).to.be.false;
-
-			view.fire( 'execute' );
-			expect( view.isOpen ).to.be.false;
-		} );
-
-		it( 'listens to view#isOpen and reacts to DOM events (valid target)', () => {
-			// Open the dropdown.
-			view.isOpen = true;
-
-			// Fire event from outside of the dropdown.
-			document.body.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Closed the dropdown.
-			expect( view.isOpen ).to.be.false;
-
-			// Fire event from outside of the dropdown.
-			document.body.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Dropdown is still closed.
-			expect( view.isOpen ).to.be.false;
-		} );
-
-		it( 'listens to view#isOpen and reacts to DOM events (invalid target)', () => {
-			// Open the dropdown.
-			view.isOpen = true;
-
-			// Event from view.element should be discarded.
-			view.element.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Dropdown is still open.
-			expect( view.isOpen ).to.be.true;
-
-			// Event from within view.element should be discarded.
-			const child = document.createElement( 'div' );
-			view.element.appendChild( child );
-
-			child.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Dropdown is still open.
-			expect( view.isOpen ).to.be.true;
-		} );
-
-		describe( 'activates keyboard navigation for the dropdown', () => {
-			it( 'so "arrowdown" focuses the #toolbarView if dropdown is open', () => {
-				const keyEvtData = {
-					keyCode: keyCodes.arrowdown,
-					preventDefault: sinon.spy(),
-					stopPropagation: sinon.spy()
-				};
-				const spy = sinon.spy( view.toolbarView, 'focus' );
-
-				view.isOpen = false;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.notCalled( spy );
-
-				view.isOpen = true;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.calledOnce( spy );
-			} );
-
-			it( 'so "arrowup" focuses the last #item in #toolbarView if dropdown is open', () => {
-				const keyEvtData = {
-					keyCode: keyCodes.arrowup,
-					preventDefault: sinon.spy(),
-					stopPropagation: sinon.spy()
-				};
-				const spy = sinon.spy( view.toolbarView, 'focusLast' );
-
-				view.isOpen = false;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.notCalled( spy );
-
-				view.isOpen = true;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.calledOnce( spy );
-			} );
-		} );
-
-		describe( 'icon', () => {
-			it( 'should be set to first button\'s icon if no defaultIcon defined', () => {
-				expect( view.buttonView.icon ).to.equal( view.toolbarView.items.get( 0 ).icon );
-			} );
-
-			it( 'should be bound to first button that is on', () => {
-				view.toolbarView.items.get( 1 ).isOn = true;
-
-				expect( view.buttonView.icon ).to.equal( view.toolbarView.items.get( 1 ).icon );
-
-				view.toolbarView.items.get( 0 ).isOn = true;
-				view.toolbarView.items.get( 1 ).isOn = false;
-
-				expect( view.buttonView.icon ).to.equal( view.toolbarView.items.get( 0 ).icon );
-			} );
-
-			it( 'should be set to defaultIcon if defined and on button is on', () => {
-				const model = new Model( {
-					defaultIcon: '<svg>baz</svg>',
-					buttons
-				} );
-
-				view = createButtonDropdown( model, locale );
-				view.render();
-
-				expect( view.buttonView.icon ).to.equal( '<svg>baz</svg>' );
-			} );
-
-			it( 'should not bind icons if staticIcon is set', () => {
-				const model = new Model( {
-					defaultIcon: '<svg>baz</svg>',
-					staticIcon: true,
-					buttons
-				} );
-
-				view = createButtonDropdown( model, locale );
-				view.render();
-
-				expect( view.buttonView.icon ).to.equal( '<svg>baz</svg>' );
-				view.toolbarView.items.get( 1 ).isOn = true;
-
-				expect( view.buttonView.icon ).to.equal( '<svg>baz</svg>' );
-			} );
-		} );
-	} );
-} );

+ 47 - 0
packages/ckeditor5-ui/tests/dropdown/button/dropdownbuttonview.js

@@ -0,0 +1,47 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import IconView from '../../../src/icon/iconview';
+import DropdownButtonView from '../../../src/dropdown/button/dropdownbuttonview';
+
+testUtils.createSinonSandbox();
+
+describe( 'DropdownButtonView', () => {
+	let locale, view;
+
+	beforeEach( () => {
+		locale = { t() {} };
+
+		view = new DropdownButtonView( locale );
+		view.render();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets view#locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'creates view#arrowView', () => {
+			expect( view.arrowView ).to.be.instanceOf( IconView );
+		} );
+
+		it( 'creates element from template', () => {
+			expect( view.element.tagName ).to.equal( 'BUTTON' );
+		} );
+	} );
+
+	describe( 'bindings', () => {
+		it( 'delegates view#execute to view#open', () => {
+			const spy = sinon.spy();
+
+			view.on( 'open', spy );
+
+			view.fire( 'execute' );
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+} );

+ 225 - 0
packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js

@@ -0,0 +1,225 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import ButtonView from '../../../src/button/buttonview';
+import SplitButtonView from '../../../src/dropdown/button/splitbuttonview';
+
+testUtils.createSinonSandbox();
+
+describe( 'SplitButtonView', () => {
+	let locale, view;
+
+	beforeEach( () => {
+		locale = { t() {} };
+
+		view = new SplitButtonView( locale );
+		view.render();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets view#locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'creates view#actionView', () => {
+			expect( view.actionView ).to.be.instanceOf( ButtonView );
+		} );
+
+		it( 'creates view#arrowView', () => {
+			expect( view.arrowView ).to.be.instanceOf( ButtonView );
+			expect( view.arrowView.element.classList.contains( 'ck-splitbutton-arrow' ) ).to.be.true;
+			expect( view.arrowView.icon ).to.be.not.undefined;
+		} );
+
+		it( 'creates element from template', () => {
+			expect( view.element.tagName ).to.equal( 'DIV' );
+			expect( view.element.classList.contains( 'ck-splitbutton' ) ).to.be.true;
+		} );
+
+		it( 'binds #isVisible to the template', () => {
+			expect( view.element.classList.contains( 'ck-hidden' ) ).to.be.false;
+
+			view.isVisible = false;
+
+			expect( view.element.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+			// There should be no binding to the action view. Only the entire split button should react.
+			expect( view.actionView.element.classList.contains( 'ck-hidden' ) ).to.be.false;
+		} );
+
+		describe( 'activates keyboard navigation for the toolbar', () => {
+			it( 'so "arrowright" on view#arrowView does nothing', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.arrowright,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				view.focusTracker.isFocused = true;
+				view.focusTracker.focusedElement = view.arrowView.element;
+
+				const spy = sinon.spy( view.actionView, 'focus' );
+
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.notCalled( spy );
+				sinon.assert.notCalled( keyEvtData.preventDefault );
+				sinon.assert.notCalled( keyEvtData.stopPropagation );
+			} );
+
+			it( 'so "arrowleft" on view#arrowView focuses view#actionView', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.arrowleft,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				view.focusTracker.isFocused = true;
+				view.focusTracker.focusedElement = view.arrowView.element;
+
+				const spy = sinon.spy( view.actionView, 'focus' );
+
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+			} );
+
+			it( 'so "arrowright" on view#actionView focuses view#arrowView', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.arrowright,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				view.focusTracker.isFocused = true;
+				view.focusTracker.focusedElement = view.actionView.element;
+
+				const spy = sinon.spy( view.arrowView, 'focus' );
+
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+			} );
+
+			it( 'so "arrowleft" on view#actionsView does nothing', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.arrowleft,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				view.focusTracker.isFocused = true;
+				view.focusTracker.focusedElement = view.actionView.element;
+
+				const spy = sinon.spy( view.arrowView, 'focus' );
+
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.notCalled( spy );
+				sinon.assert.notCalled( keyEvtData.preventDefault );
+				sinon.assert.notCalled( keyEvtData.stopPropagation );
+			} );
+		} );
+	} );
+
+	describe( 'bindings', () => {
+		it( 'delegates actionView#execute to view#execute', () => {
+			const spy = sinon.spy();
+
+			view.on( 'execute', spy );
+
+			view.actionView.fire( 'execute' );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'binds actionView#icon to view', () => {
+			expect( view.actionView.icon ).to.be.undefined;
+
+			view.icon = 'foo';
+
+			expect( view.actionView.icon ).to.equal( 'foo' );
+		} );
+
+		it( 'binds actionView#isEnabled to view', () => {
+			expect( view.actionView.isEnabled ).to.be.true;
+
+			view.isEnabled = false;
+
+			expect( view.actionView.isEnabled ).to.be.false;
+		} );
+
+		it( 'binds actionView#label to view', () => {
+			expect( view.actionView.label ).to.be.undefined;
+
+			view.label = 'foo';
+
+			expect( view.actionView.label ).to.equal( 'foo' );
+		} );
+
+		it( 'delegates arrowView#execute to view#open', () => {
+			const spy = sinon.spy();
+
+			view.on( 'open', spy );
+
+			view.arrowView.fire( 'execute' );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'binds arrowView#isEnabled to view', () => {
+			expect( view.arrowView.isEnabled ).to.be.true;
+
+			view.isEnabled = false;
+
+			expect( view.arrowView.isEnabled ).to.be.false;
+		} );
+
+		it( 'binds actionView#tabindex to view', () => {
+			expect( view.actionView.tabindex ).to.equal( -1 );
+
+			view.tabindex = 1;
+
+			expect( view.actionView.tabindex ).to.equal( 1 );
+		} );
+
+		// Makes little sense for split button but the Button interface specifies it, so let's support it.
+		it( 'binds actionView#type to view', () => {
+			expect( view.actionView.type ).to.equal( 'button' );
+
+			view.type = 'submit';
+
+			expect( view.actionView.type ).to.equal( 'submit' );
+		} );
+
+		it( 'binds actionView#withText to view', () => {
+			expect( view.actionView.withText ).to.equal( false );
+
+			view.withText = true;
+
+			expect( view.actionView.withText ).to.equal( true );
+		} );
+
+		it( 'binds actionView#tooltipPosition to view', () => {
+			expect( view.actionView.tooltipPosition ).to.equal( 's' );
+
+			view.tooltipPosition = 'n';
+
+			expect( view.actionView.tooltipPosition ).to.equal( 'n' );
+		} );
+	} );
+
+	describe( 'focus()', () => {
+		it( 'focuses the actionButton', () => {
+			const spy = sinon.spy( view.actionView, 'focus' );
+
+			view.focus();
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+} );

+ 0 - 109
packages/ckeditor5-ui/tests/dropdown/createdropdown.js

@@ -1,109 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import utilsTestUtils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
-import createDropdown from '../../src/dropdown/createdropdown';
-import Model from '../../src/model';
-import DropdownView from '../../src/dropdown/dropdownview';
-import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
-import ButtonView from '../../src/button/buttonview';
-
-const assertBinding = utilsTestUtils.assertBinding;
-
-describe( 'createDropdown', () => {
-	it( 'binds button attributes to the model', () => {
-		const modelDef = {
-			label: 'foo',
-			isOn: false,
-			isEnabled: true,
-			withText: false,
-			tooltip: false
-		};
-
-		const model = new Model( modelDef );
-		const view = createDropdown( model );
-
-		assertBinding( view.buttonView,
-			modelDef,
-			[
-				[ model, { label: 'bar', isEnabled: false, isOn: true, withText: true, tooltip: true } ]
-			],
-			{ label: 'bar', isEnabled: false, isOn: true, withText: true, tooltip: true }
-		);
-	} );
-
-	it( 'binds button#isOn do dropdown #isOpen and model #isOn', () => {
-		const modelDef = {
-			label: 'foo',
-			isOn: false,
-			isEnabled: true,
-			withText: false,
-			tooltip: false
-		};
-
-		const model = new Model( modelDef );
-		const view = createDropdown( model );
-
-		view.isOpen = false;
-		expect( view.buttonView.isOn ).to.be.false;
-
-		model.isOn = true;
-		expect( view.buttonView.isOn ).to.be.true;
-
-		view.isOpen = true;
-		expect( view.buttonView.isOn ).to.be.true;
-
-		model.isOn = false;
-		expect( view.buttonView.isOn ).to.be.true;
-	} );
-
-	it( 'binds dropdown#isEnabled to the model', () => {
-		const modelDef = {
-			label: 'foo',
-			isEnabled: true,
-			withText: false,
-			tooltip: false
-		};
-
-		const model = new Model( modelDef );
-		const view = createDropdown( model );
-
-		assertBinding( view,
-			{ isEnabled: true },
-			[
-				[ model, { isEnabled: false } ]
-			],
-			{ isEnabled: false }
-		);
-	} );
-
-	it( 'accepts locale', () => {
-		const locale = {};
-		const view = createDropdown( new Model(), locale );
-
-		expect( view.locale ).to.equal( locale );
-		expect( view.buttonView.locale ).to.equal( locale );
-		expect( view.panelView.locale ).to.equal( locale );
-	} );
-
-	it( 'returns view', () => {
-		const view = createDropdown( new Model() );
-
-		expect( view ).to.be.instanceOf( DropdownView );
-	} );
-
-	it( 'creates dropdown#buttonView out of ButtonView', () => {
-		const view = createDropdown( new Model() );
-
-		expect( view.buttonView ).to.be.instanceOf( ButtonView );
-	} );
-
-	it( 'creates dropdown#panelView out of DropdownPanelView', () => {
-		const view = createDropdown( new Model() );
-
-		expect( view.panelView ).to.be.instanceOf( DropdownPanelView );
-	} );
-} );
-

+ 53 - 0
packages/ckeditor5-ui/tests/dropdown/dropdownpanelview.js

@@ -1,3 +1,4 @@
+
 /**
  * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
@@ -7,6 +8,7 @@
 
 import ViewCollection from '../../src/viewcollection';
 import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
+import View from '../../src/view';
 
 describe( 'DropdownPanelView', () => {
 	let view, locale;
@@ -63,4 +65,55 @@ describe( 'DropdownPanelView', () => {
 			} );
 		} );
 	} );
+
+	describe( 'focus()', () => {
+		it( 'does nothing for empty panel', () => {
+			expect( () => view.focus() ).to.not.throw();
+		} );
+
+		it( 'focuses first child view', () => {
+			const firstChildView = new View();
+
+			firstChildView.focus = sinon.spy();
+
+			view.children.add( firstChildView );
+			view.children.add( new View() );
+
+			view.focus();
+
+			sinon.assert.calledOnce( firstChildView.focus );
+		} );
+	} );
+
+	describe( 'focusLast()', () => {
+		it( 'does nothing for empty panel', () => {
+			expect( () => view.focusLast() ).to.not.throw();
+		} );
+
+		it( 'focuses last child view', () => {
+			const lastChildView = new View();
+
+			lastChildView.focusLast = sinon.spy();
+
+			view.children.add( new View() );
+			view.children.add( lastChildView );
+
+			view.focusLast();
+
+			sinon.assert.calledOnce( lastChildView.focusLast );
+		} );
+
+		it( 'focuses last child view even if it does not have focusLast() method', () => {
+			const lastChildView = new View();
+
+			lastChildView.focus = sinon.spy();
+
+			view.children.add( new View() );
+			view.children.add( lastChildView );
+
+			view.focusLast();
+
+			sinon.assert.calledOnce( lastChildView.focus );
+		} );
+	} );
 } );

+ 6 - 13
packages/ckeditor5-ui/tests/dropdown/dropdownview.js

@@ -8,7 +8,6 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import ButtonView from '../../src/button/buttonview';
-import IconView from '../../src/icon/iconview';
 import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
 
 describe( 'DropdownView', () => {
@@ -55,23 +54,17 @@ describe( 'DropdownView', () => {
 
 		it( 'creates #element from template', () => {
 			expect( view.element.classList.contains( 'ck-dropdown' ) ).to.be.true;
-			expect( view.element.children ).to.have.length( 3 );
+			expect( view.element.children ).to.have.length( 2 );
 			expect( view.element.children[ 0 ] ).to.equal( buttonView.element );
-			expect( view.element.children[ 1 ] ).to.equal( view.arrowView.element );
-			expect( view.element.children[ 2 ] ).to.equal( panelView.element );
+			expect( view.element.children[ 1 ] ).to.equal( panelView.element );
 		} );
 
 		it( 'sets view#buttonView class', () => {
 			expect( view.buttonView.element.classList.contains( 'ck-dropdown__button' ) ).to.be.true;
 		} );
 
-		it( 'creates #arrowView icon instance', () => {
-			expect( view.arrowView ).to.be.instanceOf( IconView );
-			expect( view.arrowView.element.classList.contains( 'ck-dropdown__arrow' ) );
-		} );
-
 		describe( 'bindings', () => {
-			describe( 'view#isOpen to view.buttonView#execute', () => {
+			describe( 'view#isOpen to view.buttonView#select', () => {
 				it( 'is activated', () => {
 					const values = [];
 
@@ -79,9 +72,9 @@ describe( 'DropdownView', () => {
 						values.push( view.isOpen );
 					} );
 
-					view.buttonView.fire( 'execute' );
-					view.buttonView.fire( 'execute' );
-					view.buttonView.fire( 'execute' );
+					view.buttonView.fire( 'open' );
+					view.buttonView.fire( 'open' );
+					view.buttonView.fire( 'open' );
 
 					expect( values ).to.have.members( [ true, false, true ] );
 				} );

+ 0 - 185
packages/ckeditor5-ui/tests/dropdown/list/createlistdropdown.js

@@ -1,185 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals document, Event */
-
-import Model from '../../../src/model';
-import createListDropdown from '../../../src/dropdown/list/createlistdropdown';
-import Collection from '@ckeditor/ckeditor5-utils/src/collection';
-import ListView from '../../../src/list/listview';
-import ListItemView from '../../../src/list/listitemview';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
-
-describe( 'createListDropdown', () => {
-	let view, model, locale, items;
-
-	beforeEach( () => {
-		locale = { t() {} };
-		items = new Collection();
-		model = new Model( {
-			isEnabled: true,
-			items,
-			isOn: false,
-			label: 'foo'
-		} );
-
-		view = createListDropdown( model, locale );
-		view.render();
-		document.body.appendChild( view.element );
-	} );
-
-	afterEach( () => {
-		view.element.remove();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'sets view#locale', () => {
-			expect( view.locale ).to.equal( locale );
-		} );
-
-		describe( 'view#listView', () => {
-			it( 'is created', () => {
-				const panelChildren = view.panelView.children;
-
-				expect( panelChildren ).to.have.length( 1 );
-				expect( panelChildren.get( 0 ) ).to.equal( view.listView );
-				expect( view.listView ).to.be.instanceof( ListView );
-			} );
-
-			it( 'is bound to model#items', () => {
-				items.add( new Model( { label: 'a', style: 'b' } ) );
-				items.add( new Model( { label: 'c', style: 'd' } ) );
-
-				expect( view.listView.items ).to.have.length( 2 );
-				expect( view.listView.items.get( 0 ) ).to.be.instanceOf( ListItemView );
-				expect( view.listView.items.get( 1 ).label ).to.equal( 'c' );
-				expect( view.listView.items.get( 1 ).style ).to.equal( 'd' );
-
-				items.remove( 1 );
-				expect( view.listView.items ).to.have.length( 1 );
-				expect( view.listView.items.get( 0 ).label ).to.equal( 'a' );
-				expect( view.listView.items.get( 0 ).style ).to.equal( 'b' );
-			} );
-
-			it( 'binds all attributes in model#items', () => {
-				const itemModel = new Model( { label: 'a', style: 'b', foo: 'bar', baz: 'qux' } );
-
-				items.add( itemModel );
-
-				const item = view.listView.items.get( 0 );
-
-				expect( item.foo ).to.equal( 'bar' );
-				expect( item.baz ).to.equal( 'qux' );
-
-				itemModel.baz = 'foo?';
-				expect( item.baz ).to.equal( 'foo?' );
-			} );
-
-			it( 'delegates view.listView#execute to the view', done => {
-				items.add( new Model( { label: 'a', style: 'b' } ) );
-
-				view.on( 'execute', evt => {
-					expect( evt.source ).to.equal( view.listView.items.get( 0 ) );
-					expect( evt.path ).to.deep.equal( [ view.listView.items.get( 0 ), view ] );
-
-					done();
-				} );
-
-				view.listView.items.get( 0 ).fire( 'execute' );
-			} );
-		} );
-
-		it( 'changes view#isOpen on view#execute', () => {
-			view.isOpen = true;
-
-			view.fire( 'execute' );
-			expect( view.isOpen ).to.be.false;
-
-			view.fire( 'execute' );
-			expect( view.isOpen ).to.be.false;
-		} );
-
-		it( 'listens to view#isOpen and reacts to DOM events (valid target)', () => {
-			// Open the dropdown.
-			view.isOpen = true;
-
-			// Fire event from outside of the dropdown.
-			document.body.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Closed the dropdown.
-			expect( view.isOpen ).to.be.false;
-
-			// Fire event from outside of the dropdown.
-			document.body.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Dropdown is still closed.
-			expect( view.isOpen ).to.be.false;
-		} );
-
-		it( 'listens to view#isOpen and reacts to DOM events (invalid target)', () => {
-			// Open the dropdown.
-			view.isOpen = true;
-
-			// Event from view.element should be discarded.
-			view.element.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Dropdown is still open.
-			expect( view.isOpen ).to.be.true;
-
-			// Event from within view.element should be discarded.
-			const child = document.createElement( 'div' );
-			view.element.appendChild( child );
-
-			child.dispatchEvent( new Event( 'mousedown', {
-				bubbles: true
-			} ) );
-
-			// Dropdown is still open.
-			expect( view.isOpen ).to.be.true;
-		} );
-
-		describe( 'activates keyboard navigation for the dropdown', () => {
-			it( 'so "arrowdown" focuses the #listView if dropdown is open', () => {
-				const keyEvtData = {
-					keyCode: keyCodes.arrowdown,
-					preventDefault: sinon.spy(),
-					stopPropagation: sinon.spy()
-				};
-				const spy = sinon.spy( view.listView, 'focus' );
-
-				view.isOpen = false;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.notCalled( spy );
-
-				view.isOpen = true;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.calledOnce( spy );
-			} );
-
-			it( 'so "arrowup" focuses the last #item in #listView if dropdown is open', () => {
-				const keyEvtData = {
-					keyCode: keyCodes.arrowup,
-					preventDefault: sinon.spy(),
-					stopPropagation: sinon.spy()
-				};
-				const spy = sinon.spy( view.listView, 'focusLast' );
-
-				view.isOpen = false;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.notCalled( spy );
-
-				view.isOpen = true;
-				view.keystrokes.press( keyEvtData );
-				sinon.assert.calledOnce( spy );
-			} );
-		} );
-	} );
-} );

+ 7 - 7
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.html

@@ -2,18 +2,18 @@
 
 <div id="dropdown" class="ck-reset_all"></div>
 
-<h2>ListDropdown</h2>
+<h2>Dropdown with ListView</h2>
 
 <div id="list-dropdown" class="ck-reset_all"></div>
 
-<h2>Shared Model</h2>
-
-<div id="dropdown-shared" class="ck-reset_all"></div>
-
 <h2>Long label (truncated)</h2>
 
 <div id="dropdown-label" class="ck-reset_all"></div>
 
-<h2>ButtonDropdown</h2>
+<h2>Dropdown with ToolbarView</h2>
+
+<div id="dropdown-toolbar" class="ck-reset_all"></div>
+
+<h2>SplitButton Dropdown</h2>
 
-<div id="button-dropown" class="ck-reset_all"></div>
+<div id="dropdown-splitbutton" class="ck-reset_all"></div>

+ 63 - 41
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js

@@ -8,33 +8,33 @@
 import Model from '../../../src/model';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
-import createDropdown from '../../../src/dropdown/createdropdown';
-import createListDropdown from '../../../src/dropdown/list/createlistdropdown';
-
 import testUtils from '../../_utils/utils';
 
 import alignLeftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
 import alignRightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
 import alignCenterIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
 import ButtonView from '../../../src/button/buttonview';
+import SplitButtonView from '../../../src/dropdown/button/splitbuttonview';
 
-import createButtonDropdown from '../../../src/dropdown/button/createbuttondropdown';
+import { createDropdown, addToolbarToDropdown, addListToDropdown } from '../../../src/dropdown/utils';
 
 const ui = testUtils.createTestUIView( {
 	dropdown: '#dropdown',
 	listDropdown: '#list-dropdown',
-	dropdownShared: '#dropdown-shared',
 	dropdownLabel: '#dropdown-label',
-	buttonDropdown: '#button-dropown'
+	toolbarDropdown: '#dropdown-toolbar',
+	splitButton: '#dropdown-splitbutton'
 } );
 
 function testEmpty() {
-	const dropdownView = createDropdown( new Model( {
+	const dropdownView = createDropdown( {} );
+
+	dropdownView.buttonView.set( {
 		label: 'Dropdown',
 		isEnabled: true,
 		isOn: false,
 		withText: true
-	} ) );
+	} );
 
 	ui.dropdown.add( dropdownView );
 
@@ -51,15 +51,16 @@ function testList() {
 		} ) );
 	} );
 
-	const model = new Model( {
+	const dropdownView = createDropdown( {} );
+
+	dropdownView.buttonView.set( {
 		label: 'ListDropdown',
 		isEnabled: true,
 		isOn: false,
-		withText: true,
-		items: collection
+		withText: true
 	} );
 
-	const dropdownView = createListDropdown( model );
+	addListToDropdown( dropdownView, collection );
 
 	dropdownView.on( 'execute', evt => {
 		/* global console */
@@ -68,42 +69,26 @@ function testList() {
 
 	ui.listDropdown.add( dropdownView );
 
-	window.listDropdownModel = model;
 	window.listDropdownCollection = collection;
 	window.Model = Model;
 }
 
-function testSharedModel() {
-	const model = new Model( {
-		label: 'Shared Model',
-		isEnabled: true,
-		isOn: false,
-		withText: true
-	} );
-
-	const dropdownView1 = createDropdown( model );
-	const dropdownView2 = createDropdown( model );
-
-	ui.dropdownShared.add( dropdownView1 );
-	ui.dropdownShared.add( dropdownView2 );
-
-	dropdownView1.panelView.element.innerHTML = dropdownView2.panelView.element.innerHTML = 'Empty panel.';
-}
-
 function testLongLabel() {
-	const dropdownView = createDropdown( new Model( {
+	const dropdownView = createDropdown( {} );
+
+	dropdownView.buttonView.set( {
 		label: 'Dropdown with a very long label',
 		isEnabled: true,
 		isOn: false,
 		withText: true
-	} ) );
+	} );
 
 	ui.dropdownLabel.add( dropdownView );
 
 	dropdownView.panelView.element.innerHTML = 'Empty panel. There is no child view in this DropdownPanelView.';
 }
 
-function testButton() {
+function testToolbar() {
 	const locale = {};
 
 	const icons = { left: alignLeftIcon, right: alignRightIcon, center: alignCenterIcon };
@@ -122,21 +107,58 @@ function testButton() {
 			return buttonView;
 		} );
 
-	const buttonDropdownModel = new Model( {
-		isVertical: true,
-		buttons: buttonViews
+	const toolbarDropdown = createDropdown( locale );
+	toolbarDropdown.set( 'isVertical', true );
+
+	addToolbarToDropdown( toolbarDropdown, buttonViews );
+
+	// This will change icon to button with `isOn = true`.
+	toolbarDropdown.buttonView.bind( 'icon' ).toMany( buttons, 'isOn', ( ...areActive ) => {
+		// Get the index of an active button.
+		const index = areActive.findIndex( value => value );
+
+		// If none of the commands is active, display either defaultIcon or the first button's icon.
+		if ( index < 0 ) {
+			return buttons[ 0 ].icon;
+		}
+
+		// Return active button's icon.
+		return buttons[ index ].icon;
 	} );
 
-	const buttonDropdown = createButtonDropdown( buttonDropdownModel, {} );
+	// This will disable dropdown button when all buttons have `isEnabled = false`.
+	toolbarDropdown.bind( 'isEnabled' ).toMany( buttons, 'isEnabled', ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) );
 
-	ui.buttonDropdown.add( buttonDropdown );
+	ui.toolbarDropdown.add( toolbarDropdown );
 
 	window.buttons = buttons;
-	window.buttonDropdownModel = buttonDropdownModel;
+}
+
+function testSplitButton() {
+	const dropdownView = createDropdown( {}, SplitButtonView );
+
+	dropdownView.buttonView.set( {
+		label: 'Dropdown',
+		icon: alignCenterIcon
+	} );
+
+	ui.splitButton.add( dropdownView );
+
+	dropdownView.panelView.element.innerHTML = 'Empty panel. There is no child view in this DropdownPanelView.';
+
+	dropdownView.buttonView.on( 'execute', () => {
+		/* global console */
+		console.log( 'SplitButton#execute' );
+	} );
+
+	dropdownView.buttonView.on( 'open', () => {
+		/* global console */
+		console.log( 'SplitButton#open' );
+	} );
 }
 
 testEmpty();
 testList();
-testSharedModel();
 testLongLabel();
-testButton();
+testToolbar();
+testSplitButton();

+ 8 - 2
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.md

@@ -29,7 +29,13 @@ listDropdownCollection.add(
 );
 ```
 
-## Button Dropdown
+## Toolbar Dropdown
 1. Play with `buttons[ n ].isOn` to control buttonDropdown active icon.
 2. Play with `buttons[ n ].isEnabled` to control buttonDropdown disabled state (all buttons must be set to `false`).
-3. Play with `buttonDropdownModel.isVertical` to control buttonDropdown vertical/horizontal alignment.
+3. Play with `toolbarDropdown.toolbarView.isVertical` to control buttonDropdown vertical/horizontal alignment.
+
+## SplitButton Dropdown
+
+* It should have two distinct fields: action button and arrow.
+* Click on action button should be logged in console.
+* Click on arrow button should open panel and should be logged in console.

+ 354 - 0
packages/ckeditor5-ui/tests/dropdown/utils.js

@@ -0,0 +1,354 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document Event */
+
+import utilsTestUtils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+
+import Model from '../../src/model';
+
+import ButtonView from '../../src/button/buttonview';
+import DropdownView from '../../src/dropdown/dropdownview';
+import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
+import SplitButtonView from '../../src/dropdown/button/splitbuttonview';
+import View from '../../src/view';
+import ToolbarView from '../../src/toolbar/toolbarview';
+import { createDropdown, addToolbarToDropdown, addListToDropdown } from '../../src/dropdown/utils';
+import ListItemView from '../../src/list/listitemview';
+import ListView from '../../src/list/listview';
+
+const assertBinding = utilsTestUtils.assertBinding;
+
+describe( 'utils', () => {
+	let locale, dropdownView;
+
+	beforeEach( () => {
+		locale = { t() {} };
+	} );
+
+	describe( 'createDropdown()', () => {
+		beforeEach( () => {
+			dropdownView = createDropdown( locale );
+		} );
+
+		it( 'accepts locale', () => {
+			expect( dropdownView.locale ).to.equal( locale );
+			expect( dropdownView.panelView.locale ).to.equal( locale );
+		} );
+
+		it( 'returns view', () => {
+			expect( dropdownView ).to.be.instanceOf( DropdownView );
+		} );
+
+		it( 'creates dropdown#panelView out of DropdownPanelView', () => {
+			expect( dropdownView.panelView ).to.be.instanceOf( DropdownPanelView );
+		} );
+
+		it( 'creates dropdown#buttonView out of ButtonView', () => {
+			expect( dropdownView.buttonView ).to.be.instanceOf( ButtonView );
+		} );
+
+		it( 'creates dropdown#buttonView out of passed SplitButtonView', () => {
+			dropdownView = createDropdown( locale, SplitButtonView );
+
+			expect( dropdownView.buttonView ).to.be.instanceOf( SplitButtonView );
+		} );
+
+		it( 'binds #isEnabled to the buttonView', () => {
+			dropdownView = createDropdown( locale );
+
+			assertBinding( dropdownView.buttonView,
+				{ isEnabled: true },
+				[
+					[ dropdownView, { isEnabled: false } ]
+				],
+				{ isEnabled: false }
+			);
+		} );
+
+		it( 'binds button#isOn to dropdown #isOpen', () => {
+			dropdownView = createDropdown( locale );
+			dropdownView.buttonView.isEnabled = true;
+
+			dropdownView.isOpen = false;
+			expect( dropdownView.buttonView.isOn ).to.be.false;
+
+			dropdownView.isOpen = true;
+			expect( dropdownView.buttonView.isOn ).to.be.true;
+		} );
+
+		describe( '#buttonView', () => {
+			it( 'accepts locale', () => {
+				expect( dropdownView.buttonView.locale ).to.equal( locale );
+			} );
+
+			it( 'is a ButtonView instance', () => {
+				expect( dropdownView.buttonView ).to.be.instanceof( ButtonView );
+			} );
+		} );
+
+		describe( 'has default behavior', () => {
+			describe( 'closeDropdownOnBlur()', () => {
+				beforeEach( () => {
+					dropdownView.render();
+					document.body.appendChild( dropdownView.element );
+				} );
+
+				afterEach( () => {
+					dropdownView.element.remove();
+				} );
+
+				it( 'listens to view#isOpen and reacts to DOM events (valid target)', () => {
+					// Open the dropdown.
+					dropdownView.isOpen = true;
+					// Fire event from outside of the dropdown.
+					document.body.dispatchEvent( new Event( 'mousedown', {
+						bubbles: true
+					} ) );
+					// Closed the dropdown.
+					expect( dropdownView.isOpen ).to.be.false;
+					// Fire event from outside of the dropdown.
+					document.body.dispatchEvent( new Event( 'mousedown', {
+						bubbles: true
+					} ) );
+					// Dropdown is still closed.
+					expect( dropdownView.isOpen ).to.be.false;
+				} );
+
+				it( 'listens to view#isOpen and reacts to DOM events (invalid target)', () => {
+					// Open the dropdown.
+					dropdownView.isOpen = true;
+
+					// Event from view.element should be discarded.
+					dropdownView.element.dispatchEvent( new Event( 'mousedown', {
+						bubbles: true
+					} ) );
+
+					// Dropdown is still open.
+					expect( dropdownView.isOpen ).to.be.true;
+
+					// Event from within view.element should be discarded.
+					const child = document.createElement( 'div' );
+					dropdownView.element.appendChild( child );
+
+					child.dispatchEvent( new Event( 'mousedown', {
+						bubbles: true
+					} ) );
+
+					// Dropdown is still open.
+					expect( dropdownView.isOpen ).to.be.true;
+				} );
+			} );
+
+			describe( 'closeDropdownOnExecute()', () => {
+				beforeEach( () => {
+					dropdownView.render();
+					document.body.appendChild( dropdownView.element );
+				} );
+
+				afterEach( () => {
+					dropdownView.element.remove();
+				} );
+
+				it( 'changes view#isOpen on view#execute', () => {
+					dropdownView.isOpen = true;
+
+					dropdownView.fire( 'execute' );
+					expect( dropdownView.isOpen ).to.be.false;
+
+					dropdownView.fire( 'execute' );
+					expect( dropdownView.isOpen ).to.be.false;
+				} );
+			} );
+
+			describe( 'focusDropdownContentsOnArrows()', () => {
+				let panelChildView;
+
+				beforeEach( () => {
+					panelChildView = new View();
+					panelChildView.setTemplate( { tag: 'div' } );
+					panelChildView.focus = () => {};
+					panelChildView.focusLast = () => {};
+
+					dropdownView.panelView.children.add( panelChildView );
+
+					dropdownView.render();
+					document.body.appendChild( dropdownView.element );
+				} );
+
+				afterEach( () => {
+					dropdownView.element.remove();
+				} );
+
+				it( '"arrowdown" focuses the #innerPanelView if dropdown is open', () => {
+					const keyEvtData = {
+						keyCode: keyCodes.arrowdown,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+					const spy = sinon.spy( panelChildView, 'focus' );
+
+					dropdownView.isOpen = false;
+					dropdownView.keystrokes.press( keyEvtData );
+					sinon.assert.notCalled( spy );
+
+					dropdownView.isOpen = true;
+					dropdownView.keystrokes.press( keyEvtData );
+
+					sinon.assert.calledOnce( spy );
+				} );
+
+				it( '"arrowup" focuses the last #item in #innerPanelView if dropdown is open', () => {
+					const keyEvtData = {
+						keyCode: keyCodes.arrowup,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+					const spy = sinon.spy( panelChildView, 'focusLast' );
+
+					dropdownView.isOpen = false;
+					dropdownView.keystrokes.press( keyEvtData );
+					sinon.assert.notCalled( spy );
+
+					dropdownView.isOpen = true;
+					dropdownView.keystrokes.press( keyEvtData );
+					sinon.assert.calledOnce( spy );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'addToolbarToDropdown()', () => {
+		let buttons;
+
+		beforeEach( () => {
+			buttons = [ '<svg>foo</svg>', '<svg>bar</svg>' ].map( icon => {
+				const button = new ButtonView();
+
+				button.icon = icon;
+
+				return button;
+			} );
+
+			dropdownView = createDropdown( locale );
+
+			addToolbarToDropdown( dropdownView, buttons );
+
+			dropdownView.render();
+			document.body.appendChild( dropdownView.element );
+		} );
+
+		afterEach( () => {
+			dropdownView.element.remove();
+		} );
+
+		it( 'sets view#locale', () => {
+			expect( dropdownView.locale ).to.equal( locale );
+		} );
+
+		it( 'sets view class', () => {
+			expect( dropdownView.element.classList.contains( 'ck-toolbar-dropdown' ) ).to.be.true;
+		} );
+
+		describe( 'view#toolbarView', () => {
+			it( 'is created', () => {
+				const panelChildren = dropdownView.panelView.children;
+
+				expect( panelChildren ).to.have.length( 1 );
+				expect( panelChildren.get( 0 ) ).to.equal( dropdownView.toolbarView );
+				expect( dropdownView.toolbarView ).to.be.instanceof( ToolbarView );
+			} );
+
+			it( 'delegates view.toolbarView.items#execute to the view', done => {
+				dropdownView.on( 'execute', evt => {
+					expect( evt.source ).to.equal( dropdownView.toolbarView.items.get( 0 ) );
+					expect( evt.path ).to.deep.equal( [ dropdownView.toolbarView.items.get( 0 ), dropdownView ] );
+
+					done();
+				} );
+
+				dropdownView.toolbarView.items.get( 0 ).fire( 'execute' );
+			} );
+		} );
+	} );
+
+	describe( 'addListToDropdown()', () => {
+		let items;
+
+		beforeEach( () => {
+			items = new Collection();
+
+			dropdownView = createDropdown( locale );
+			dropdownView.buttonView.set( {
+				isEnabled: true,
+				isOn: false,
+				label: 'foo'
+			} );
+
+			addListToDropdown( dropdownView, items );
+
+			dropdownView.render();
+			document.body.appendChild( dropdownView.element );
+		} );
+
+		afterEach( () => {
+			dropdownView.element.remove();
+		} );
+
+		describe( 'view#listView', () => {
+			it( 'is created', () => {
+				const panelChildren = dropdownView.panelView.children;
+
+				expect( panelChildren ).to.have.length( 1 );
+				expect( panelChildren.get( 0 ) ).to.equal( dropdownView.listView );
+				expect( dropdownView.listView ).to.be.instanceof( ListView );
+			} );
+
+			it( 'is bound to model#items', () => {
+				items.add( new Model( { label: 'a', style: 'b' } ) );
+				items.add( new Model( { label: 'c', style: 'd' } ) );
+
+				expect( dropdownView.listView.items ).to.have.length( 2 );
+				expect( dropdownView.listView.items.get( 0 ) ).to.be.instanceOf( ListItemView );
+				expect( dropdownView.listView.items.get( 1 ).label ).to.equal( 'c' );
+				expect( dropdownView.listView.items.get( 1 ).style ).to.equal( 'd' );
+
+				items.remove( 1 );
+				expect( dropdownView.listView.items ).to.have.length( 1 );
+				expect( dropdownView.listView.items.get( 0 ).label ).to.equal( 'a' );
+				expect( dropdownView.listView.items.get( 0 ).style ).to.equal( 'b' );
+			} );
+
+			it( 'binds all attributes in model#items', () => {
+				const itemModel = new Model( { label: 'a', style: 'b', foo: 'bar', baz: 'qux' } );
+
+				items.add( itemModel );
+
+				const item = dropdownView.listView.items.get( 0 );
+
+				expect( item.foo ).to.equal( 'bar' );
+				expect( item.baz ).to.equal( 'qux' );
+
+				itemModel.baz = 'foo?';
+				expect( item.baz ).to.equal( 'foo?' );
+			} );
+
+			it( 'delegates view.listView#execute to the view', done => {
+				items.add( new Model( { label: 'a', style: 'b' } ) );
+
+				dropdownView.on( 'execute', evt => {
+					expect( evt.source ).to.equal( dropdownView.listView.items.get( 0 ) );
+					expect( evt.path ).to.deep.equal( [ dropdownView.listView.items.get( 0 ), dropdownView ] );
+
+					done();
+				} );
+
+				dropdownView.listView.items.get( 0 ).fire( 'execute' );
+			} );
+		} );
+	} );
+} );

+ 31 - 0
packages/ckeditor5-ui/theme/components/dropdown/splitbutton.css

@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+.ck-rounded-corners .ck-splitbutton > .ck-button:not(.ck-splitbutton-arrow) {
+	border-top-right-radius: unset;
+	border-bottom-right-radius: unset;
+
+	&:focus {
+		z-index: calc(var(--ck-z-default) + 1);
+	}
+}
+
+.ck-rounded-corners .ck-splitbutton > .ck-splitbutton-arrow {
+	border-top-left-radius: unset;
+	border-bottom-left-radius: unset;
+}
+
+.ck-dropdown {
+	/* Enable font size inheritance, which allows fluid UI scaling. */
+	font-size: inherit;
+
+	& .ck-splitbutton .ck-splitbutton-arrow svg {
+		position: static;
+		top: initial;
+		transform: initial;
+		right: auto;
+		width: var(--ck-dropdown-icon-size);
+	}
+}

+ 7 - 1
packages/ckeditor5-ui/theme/components/dropdown/buttondropdown.css → packages/ckeditor5-ui/theme/components/dropdown/toolbardropdown.css

@@ -3,9 +3,15 @@
  * For licensing, see LICENSE.md.
  */
 
-.ck-buttondropdown {
+.ck-toolbar-dropdown {
 	& .ck-toolbar {
 		flex-wrap: nowrap;
+
+		& .ck-toolbar__separator {
+			height: calc(var(--ck-line-height-base) * var(--ck-font-size-normal));
+			margin-top: 0;
+			margin-left: var(--ck-spacing-small);
+		}
 	}
 
 	& .ck-dropdown__panel .ck-button {