diff --git a/src/button/button.jsdoc b/src/button/button.jsdoc
new file mode 100644
index 00000000..fc6a9b0e
--- /dev/null
+++ b/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. CTRL+B,
+ * 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 Tab 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
+ */
diff --git a/src/button/buttonview.js b/src/button/buttonview.js
index cd3291ed..2c0d702f 100644
--- a/src/button/buttonview.js
+++ b/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. CTRL+B,
- * 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 Tab 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
- */
}
/**
diff --git a/src/dropdown/button/buttondropdownmodel.jsdoc b/src/dropdown/button/buttondropdownmodel.jsdoc
deleted file mode 100644
index cabe9edd..00000000
--- a/src/dropdown/button/buttondropdownmodel.jsdoc
+++ /dev/null
@@ -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.} #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
- */
diff --git a/src/dropdown/button/buttondropdownview.jsdoc b/src/dropdown/button/buttondropdownview.jsdoc
deleted file mode 100644
index cd148969..00000000
--- a/src/dropdown/button/buttondropdownview.jsdoc
+++ /dev/null
@@ -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
- */
diff --git a/src/dropdown/button/createbuttondropdown.js b/src/dropdown/button/createbuttondropdown.js
deleted file mode 100644
index b3232c2f..00000000
--- a/src/dropdown/button/createbuttondropdown.js
+++ /dev/null
@@ -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.} buttons
-// @param {String} attribute
-// @returns {Array.}
-function getBindingTargets( buttons, attribute ) {
- return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) );
-}
diff --git a/src/dropdown/button/dropdownbutton.jsdoc b/src/dropdown/button/dropdownbutton.jsdoc
new file mode 100644
index 00000000..cbe57a4e
--- /dev/null
+++ b/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
+ */
diff --git a/src/dropdown/button/dropdownbuttonview.js b/src/dropdown/button/dropdownbuttonview.js
new file mode 100644
index 00000000..17428960
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/dropdown/button/splitbuttonview.js b/src/dropdown/button/splitbuttonview.js
new file mode 100644
index 00000000..b50ae1d5
--- /dev/null
+++ b/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:
+ *
+ * * ▶ moves focus to arrow view when action view is focused,
+ * * ◀ 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;
+ }
+}
diff --git a/src/dropdown/createdropdown.js b/src/dropdown/createdropdown.js
deleted file mode 100644
index b1eae9e3..00000000
--- a/src/dropdown/createdropdown.js
+++ /dev/null
@@ -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;
-}
diff --git a/src/dropdown/dropdownmodel.jsdoc b/src/dropdown/dropdownmodel.jsdoc
deleted file mode 100644
index 40497977..00000000
--- a/src/dropdown/dropdownmodel.jsdoc
+++ /dev/null
@@ -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
- */
diff --git a/src/dropdown/dropdownpanelfocusable.jsdoc b/src/dropdown/dropdownpanelfocusable.jsdoc
index 509809b7..7d4bf1b0 100644
--- a/src/dropdown/dropdownpanelfocusable.jsdoc
+++ b/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
diff --git a/src/dropdown/dropdownpanelview.js b/src/dropdown/dropdownpanelview.js
index 66385ccd..7218206f 100644
--- a/src/dropdown/dropdownpanelview.js
+++ b/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();
+ }
+ }
+ }
}
diff --git a/src/dropdown/dropdownview.js b/src/dropdown/dropdownview.js
index 1c71cd61..a7ab9440 100644
--- a/src/dropdown/dropdownview.js
+++ b/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;
} );
diff --git a/src/dropdown/list/createlistdropdown.js b/src/dropdown/list/createlistdropdown.js
deleted file mode 100644
index a1295272..00000000
--- a/src/dropdown/list/createlistdropdown.js
+++ /dev/null
@@ -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;
-}
diff --git a/src/dropdown/list/listdropdownmodel.jsdoc b/src/dropdown/list/listdropdownmodel.jsdoc
deleted file mode 100644
index d20f72d1..00000000
--- a/src/dropdown/list/listdropdownmodel.jsdoc
+++ /dev/null
@@ -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.} #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
- */
diff --git a/src/dropdown/list/listdropdownview.jsdoc b/src/dropdown/list/listdropdownview.jsdoc
deleted file mode 100644
index cc498dfa..00000000
--- a/src/dropdown/list/listdropdownview.jsdoc
+++ /dev/null
@@ -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
- */
diff --git a/src/dropdown/utils.js b/src/dropdown/utils.js
index b2b68ba8..150545fe 100644
--- a/src/dropdown/utils.js
+++ b/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.} 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();
+ }
+ } );
+}
diff --git a/src/view.js b/src/view.js
index 1f5bb6dc..d8806955 100644
--- a/src/view.js
+++ b/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 .
diff --git a/tests/dropdown/button/createbuttondropdown.js b/tests/dropdown/button/createbuttondropdown.js
deleted file mode 100644
index 839b721e..00000000
--- a/tests/dropdown/button/createbuttondropdown.js
+++ /dev/null
@@ -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 = [ '', '' ].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: '',
- buttons
- } );
-
- view = createButtonDropdown( model, locale );
- view.render();
-
- expect( view.buttonView.icon ).to.equal( '' );
- } );
-
- it( 'should not bind icons if staticIcon is set', () => {
- const model = new Model( {
- defaultIcon: '',
- staticIcon: true,
- buttons
- } );
-
- view = createButtonDropdown( model, locale );
- view.render();
-
- expect( view.buttonView.icon ).to.equal( '' );
- view.toolbarView.items.get( 1 ).isOn = true;
-
- expect( view.buttonView.icon ).to.equal( '' );
- } );
- } );
- } );
-} );
diff --git a/tests/dropdown/button/dropdownbuttonview.js b/tests/dropdown/button/dropdownbuttonview.js
new file mode 100644
index 00000000..8bf9425d
--- /dev/null
+++ b/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 );
+ } );
+ } );
+} );
diff --git a/tests/dropdown/button/splitbuttonview.js b/tests/dropdown/button/splitbuttonview.js
new file mode 100644
index 00000000..d34ff756
--- /dev/null
+++ b/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 );
+ } );
+ } );
+} );
diff --git a/tests/dropdown/createdropdown.js b/tests/dropdown/createdropdown.js
deleted file mode 100644
index 7cb0f74a..00000000
--- a/tests/dropdown/createdropdown.js
+++ /dev/null
@@ -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 );
- } );
-} );
-
diff --git a/tests/dropdown/dropdownpanelview.js b/tests/dropdown/dropdownpanelview.js
index c1ee8551..aee1b4f5 100644
--- a/tests/dropdown/dropdownpanelview.js
+++ b/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 );
+ } );
+ } );
} );
diff --git a/tests/dropdown/dropdownview.js b/tests/dropdown/dropdownview.js
index 48624516..123f0630 100644
--- a/tests/dropdown/dropdownview.js
+++ b/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 ] );
} );
diff --git a/tests/dropdown/list/createlistdropdown.js b/tests/dropdown/list/createlistdropdown.js
deleted file mode 100644
index 9b709902..00000000
--- a/tests/dropdown/list/createlistdropdown.js
+++ /dev/null
@@ -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 );
- } );
- } );
- } );
-} );
diff --git a/tests/dropdown/manual/dropdown.html b/tests/dropdown/manual/dropdown.html
index 7b14edfa..8722cc3b 100644
--- a/tests/dropdown/manual/dropdown.html
+++ b/tests/dropdown/manual/dropdown.html
@@ -2,18 +2,18 @@ Empty
-ListDropdown
+Dropdown with ListView
-Shared Model
-
-
-
Long label (truncated)
-ButtonDropdown
+Dropdown with ToolbarView
+
+
+
+SplitButton Dropdown
-
+
diff --git a/tests/dropdown/manual/dropdown.js b/tests/dropdown/manual/dropdown.js
index 4c3b912c..a04c3770 100644
--- a/tests/dropdown/manual/dropdown.js
+++ b/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();
diff --git a/tests/dropdown/manual/dropdown.md b/tests/dropdown/manual/dropdown.md
index a17c8cc4..e864797f 100644
--- a/tests/dropdown/manual/dropdown.md
+++ b/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.
diff --git a/tests/dropdown/utils.js b/tests/dropdown/utils.js
new file mode 100644
index 00000000..ec22fe52
--- /dev/null
+++ b/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 = [ '', '' ].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' );
+ } );
+ } );
+ } );
+} );
diff --git a/theme/components/dropdown/splitbutton.css b/theme/components/dropdown/splitbutton.css
new file mode 100644
index 00000000..1bcd63c3
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/theme/components/dropdown/buttondropdown.css b/theme/components/dropdown/toolbardropdown.css
similarity index 58%
rename from theme/components/dropdown/buttondropdown.css
rename to theme/components/dropdown/toolbardropdown.css
index 171cd086..692210e8 100644
--- a/theme/components/dropdown/buttondropdown.css
+++ b/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 {