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 = [ 'foo', 'bar' ].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: 'baz', - buttons - } ); - - view = createButtonDropdown( model, locale ); - view.render(); - - expect( view.buttonView.icon ).to.equal( 'baz' ); - } ); - - it( 'should not bind icons if staticIcon is set', () => { - const model = new Model( { - defaultIcon: 'baz', - staticIcon: true, - buttons - } ); - - view = createButtonDropdown( model, locale ); - view.render(); - - expect( view.buttonView.icon ).to.equal( 'baz' ); - view.toolbarView.items.get( 1 ).isOn = true; - - expect( view.buttonView.icon ).to.equal( 'baz' ); - } ); - } ); - } ); -} ); 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 = [ 'foo', 'bar' ].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 {