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

Merged ckeditor5-ui-default into this package. Fixes #116.

Piotrek Koszuliński 9 лет назад
Родитель
Сommit
9395ba7c4e
72 измененных файлов с 5373 добавлено и 0 удалено
  1. 1 0
      packages/ckeditor5-ui/package.json
  2. 274 0
      packages/ckeditor5-ui/src/balloonpanel/balloonpanelview.js
  3. 30 0
      packages/ckeditor5-ui/src/bindings/clickoutsidehandler.js
  4. 28 0
      packages/ckeditor5-ui/src/bindings/escpresshandler.js
  5. 23 0
      packages/ckeditor5-ui/src/bindings/submithandler.js
  6. 216 0
      packages/ckeditor5-ui/src/button/buttonview.js
  7. 29 0
      packages/ckeditor5-ui/src/dropdown/createdropdown.js
  8. 38 0
      packages/ckeditor5-ui/src/dropdown/dropdownmodel.jsdoc
  9. 57 0
      packages/ckeditor5-ui/src/dropdown/dropdownpanelview.js
  10. 107 0
      packages/ckeditor5-ui/src/dropdown/dropdownview.js
  11. 73 0
      packages/ckeditor5-ui/src/dropdown/list/createlistdropdown.js
  12. 26 0
      packages/ckeditor5-ui/src/dropdown/list/listdropdownmodel.jsdoc
  13. 21 0
      packages/ckeditor5-ui/src/dropdown/list/listdropdownview.jsdoc
  14. 94 0
      packages/ckeditor5-ui/src/editableui/editableuiview.js
  15. 54 0
      packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.js
  16. 112 0
      packages/ckeditor5-ui/src/editorui/boxed/boxededitoruiview.js
  17. 98 0
      packages/ckeditor5-ui/src/editorui/editoruiview.js
  18. 79 0
      packages/ckeditor5-ui/src/icon/iconview.js
  19. 78 0
      packages/ckeditor5-ui/src/iconmanager/iconmanagerview.js
  20. 87 0
      packages/ckeditor5-ui/src/iframe/iframeview.js
  21. 65 0
      packages/ckeditor5-ui/src/inputtext/inputtextview.js
  22. 58 0
      packages/ckeditor5-ui/src/label/labelview.js
  23. 112 0
      packages/ckeditor5-ui/src/labeledinput/labeledinputview.js
  24. 68 0
      packages/ckeditor5-ui/src/list/listitemview.js
  25. 46 0
      packages/ckeditor5-ui/src/list/listview.js
  26. 215 0
      packages/ckeditor5-ui/src/toolbar/sticky/stickytoolbarview.js
  27. 38 0
      packages/ckeditor5-ui/src/toolbar/toolbarview.js
  28. 422 0
      packages/ckeditor5-ui/tests/balloonpanel/balloonpanelview.js
  29. 125 0
      packages/ckeditor5-ui/tests/bindings/clickoutsidehandler.js
  30. 141 0
      packages/ckeditor5-ui/tests/bindings/escpresshandler.js
  31. 39 0
      packages/ckeditor5-ui/tests/bindings/submithandler.js
  32. 170 0
      packages/ckeditor5-ui/tests/button/buttonview.js
  33. 65 0
      packages/ckeditor5-ui/tests/dropdown/createdropdown.js
  34. 52 0
      packages/ckeditor5-ui/tests/dropdown/dropdownpanelview.js
  35. 85 0
      packages/ckeditor5-ui/tests/dropdown/dropdownview.js
  36. 151 0
      packages/ckeditor5-ui/tests/dropdown/list/createlistdropdown.js
  37. 19 0
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.html
  38. 98 0
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js
  39. 30 0
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.md
  40. 84 0
      packages/ckeditor5-ui/tests/editableui/editableuiview.js
  41. 66 0
      packages/ckeditor5-ui/tests/editableui/inline/inlineeditableuiview.js
  42. 66 0
      packages/ckeditor5-ui/tests/editorui/boxed/boxededitoruiview.js
  43. 78 0
      packages/ckeditor5-ui/tests/editorui/editoruiview.js
  44. 59 0
      packages/ckeditor5-ui/tests/icon/iconview.js
  45. 41 0
      packages/ckeditor5-ui/tests/iconmanager/iconmanagerview.js
  46. 57 0
      packages/ckeditor5-ui/tests/iframe/iframeview.js
  47. 74 0
      packages/ckeditor5-ui/tests/inputtext/inputtextview.js
  48. 52 0
      packages/ckeditor5-ui/tests/label/labelview.js
  49. 74 0
      packages/ckeditor5-ui/tests/labeledinput/labeledinputview.js
  50. 62 0
      packages/ckeditor5-ui/tests/list/listitemview.js
  51. 30 0
      packages/ckeditor5-ui/tests/list/listview.js
  52. 16 0
      packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.html
  53. 131 0
      packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.js
  54. 5 0
      packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.md
  55. 15 0
      packages/ckeditor5-ui/tests/manual/icon/icon.html
  56. 81 0
      packages/ckeditor5-ui/tests/manual/icon/icon.js
  57. 7 0
      packages/ckeditor5-ui/tests/manual/icon/icon.md
  58. 37 0
      packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.html
  59. 122 0
      packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.js
  60. 6 0
      packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.md
  61. BIN
      packages/ckeditor5-ui/tests/manual/imagetoolbar/img/logo.png
  62. 55 0
      packages/ckeditor5-ui/tests/manual/stickytoolbarview/stickytoolbarview.html
  63. 29 0
      packages/ckeditor5-ui/tests/manual/stickytoolbarview/stickytoolbarview.js
  64. 16 0
      packages/ckeditor5-ui/tests/manual/stickytoolbarview/stickytoolbarview.md
  65. 85 0
      packages/ckeditor5-ui/tests/manual/tickets/126/1.html
  66. 21 0
      packages/ckeditor5-ui/tests/manual/tickets/126/1.js
  67. 13 0
      packages/ckeditor5-ui/tests/manual/tickets/126/1.md
  68. 15 0
      packages/ckeditor5-ui/tests/manual/tickets/76/1.html
  69. 23 0
      packages/ckeditor5-ui/tests/manual/tickets/76/1.js
  70. 8 0
      packages/ckeditor5-ui/tests/manual/tickets/76/1.md
  71. 385 0
      packages/ckeditor5-ui/tests/toolbar/sticky/stickytoolbarview.js
  72. 36 0
      packages/ckeditor5-ui/tests/toolbar/toolbarview.js

+ 1 - 0
packages/ckeditor5-ui/package.json

@@ -6,6 +6,7 @@
     "CKEditor"
   ],
   "dependencies": {
+    "ckeditor5-theme-lark": "ckeditor/ckeditor5-theme-lark",
     "ckeditor5-utils": "ckeditor/ckeditor5-utils"
   },
   "devDependencies": {

+ 274 - 0
packages/ckeditor5-ui/src/balloonpanel/balloonpanelview.js

@@ -0,0 +1,274 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/balloonpanel/balloonpanelview
+ */
+
+/* globals document */
+
+import View from '../view';
+import Template from '../template';
+import { getOptimalPosition } from 'ckeditor5-utils/src/dom/position';
+import toUnit from 'ckeditor5-utils/src/dom/tounit';
+
+const toPx = toUnit( 'px' );
+
+/**
+ * The balloon panel view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class BalloonPanelView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * The absolute top position of the balloon panel in pixels.
+		 *
+		 * @observable
+		 * @default 0
+		 * @member {Number} #top
+		 */
+		this.set( 'top', 0 );
+
+		/**
+		 * The absolute left position of the balloon panel in pixels.
+		 *
+		 * @observable
+		 * @default 0
+		 * @member {Number} #left
+		 */
+		this.set( 'left', 0 );
+
+		/**
+		 * Balloon panel's current position. The position name is reflected in the CSS class set
+		 * to the balloon, i.e. `.ck-balloon-panel_arrow_se` for "se" position. The class
+		 * controls the minor aspects of the balloon's visual appearance like placement
+		 * of the "arrow". To support a new position, an additional CSS must be created.
+		 *
+		 * Default position names correspond with {@link #defaultPositions}.
+		 *
+		 * @observable
+		 * @default 'se'
+		 * @member {'se'|'sw'|'ne'|'nw'} #position
+		 */
+		this.set( 'position', 'se' );
+
+		/**
+		 * Controls whether the balloon panel is visible or not.
+		 *
+		 * @observable
+		 * @default false
+		 * @member {Boolean} #isVisible
+		 */
+		this.set( 'isVisible', false );
+
+		/**
+		 * Max width of the balloon panel, as in CSS.
+		 *
+		 * @observable
+		 * @member {Number} #maxWidth
+		 */
+
+		/**
+		 * Collection of the child views which creates balloon panel contents.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.content = this.createCollection();
+
+		this.template = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-balloon-panel',
+					bind.to( 'position', ( value ) => `ck-balloon-panel_arrow_${ value }` ),
+					bind.if( 'isVisible', 'ck-balloon-panel_visible' )
+				],
+
+				style: {
+					top: bind.to( 'top', toPx ),
+					left: bind.to( 'left', toPx ),
+					maxWidth: bind.to( 'maxWidth', toPx )
+				},
+
+				// Make this element `focusable` to be available for adding to FocusTracker.
+				tabindex: -1
+			},
+
+			children: this.content
+		} );
+	}
+
+	/**
+	 * Shows the balloon panel.
+	 *
+	 * See {@link #isVisible}.
+	 */
+	show() {
+		this.isVisible = true;
+	}
+
+	/**
+	 * Hides the balloon panel.
+	 *
+	 * See {@link #isVisible}.
+	 */
+	hide() {
+		this.isVisible = false;
+	}
+
+	/**
+	 * Attaches the balloon panel to a specified DOM element or range with a smart heuristics.
+	 *
+	 * See {@link @link module:utils/dom/position~getOptimalPosition}.
+	 *
+	 * TODO: More docs and examples.
+	 *
+	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
+	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
+	 * {@link module:ui/balloonpanel/balloonpanelview~BalloonPanelView.defaultPositions}.
+	 */
+	attachTo( options ) {
+		this.show();
+
+		const defaultPositions = BalloonPanelView.defaultPositions;
+		const positionOptions = Object.assign( {}, {
+			element: this.element,
+			positions: [
+				defaultPositions.se,
+				defaultPositions.sw,
+				defaultPositions.ne,
+				defaultPositions.nw
+			],
+			limiter: document.body,
+			fitInViewport: true
+		}, options );
+
+		const { top, left, name: position } = getOptimalPosition( positionOptions );
+
+		Object.assign( this, { top, left, position } );
+	}
+}
+
+/**
+ * A horizontal offset of the arrow tip from the edge of the balloon. Controlled by CSS.
+ *
+ *		 +-----|---------...
+ *		 |     |
+ *		 |     |
+ *		 |     |
+ *		 |     |
+ *		 +--+  |  +------...
+ *		     \ | /
+ *		      \|/
+ *	    >|-----|<---------------- horizontal offset
+ *
+ * @default 30
+ * @member {Number} module:ui/balloonpanel/balloonpanelview~BalloonPanelView.arrowHorizontalOffset
+ */
+BalloonPanelView.arrowHorizontalOffset = 30;
+
+/**
+ * A vertical offset of the arrow from the edge of the balloon. Controlled by CSS.
+ *
+ *		 +-------------...
+ *		 |
+ *		 |
+ *		 |                      /-- vertical offset
+ *		 |                     V
+ *		 +--+    +-----...    ---------
+ *		     \  /              |
+ *		      \/               |
+ *		-------------------------------
+ *		                       ^
+ *
+ * @default 15
+ * @member {Number} module:ui/balloonpanel/balloonpanelview~BalloonPanelView.arrowVerticalOffset
+ */
+BalloonPanelView.arrowVerticalOffset = 15;
+
+/**
+ * A default set of positioning functions used by the balloon panel view
+ * when attaching using {@link #attachTo} method.
+ *
+ * The available positioning functions are as follows:
+ *
+ * * South east:
+ *
+ *		[ Target ]
+ *		    ^
+ *		+-----------------+
+ *		|     Balloon     |
+ *		+-----------------+
+ *
+ *
+ * * South west:
+ *
+ *		         [ Target ]
+ *		              ^
+ *		+-----------------+
+ *		|     Balloon     |
+ *		+-----------------+
+ *
+ *
+ * * North east:
+ *
+ *		+-----------------+
+ *		|     Balloon     |
+ *		+-----------------+
+ *		    V
+ *		[ Target ]
+ *
+ *
+ * * North west:
+ *
+ *		+-----------------+
+ *		|     Balloon     |
+ *		+-----------------+
+ *		              V
+ *		         [ Target ]
+ *
+ * See {@link #attachTo}.
+ *
+ * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
+ *
+ * The name that position function returns will be reflected in balloon panel's class that
+ * controls the placement of the "arrow". See {@link #position} to learn more.
+ *
+ * @member {Object} module:ui/balloonpanel/balloonpanelview~BalloonPanelView.defaultPositions
+ */
+BalloonPanelView.defaultPositions = {
+	se: ( targetRect ) => ( {
+		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
+		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
+		name: 'se'
+	} ),
+
+	sw: ( targetRect, balloonRect ) => ( {
+		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
+		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
+		name: 'sw'
+	} ),
+
+	ne: ( targetRect, balloonRect ) => ( {
+		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
+		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
+		name: 'ne'
+	} ),
+
+	nw: ( targetRect, balloonRect ) => ( {
+		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
+		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
+		name: 'nw'
+	} )
+};

+ 30 - 0
packages/ckeditor5-ui/src/bindings/clickoutsidehandler.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/bindings/clickoutsidehandler
+ */
+
+/* global document */
+
+/**
+ * Handles a DOM `click` event outside of specified element and fires an action.
+ *
+ * Note that it is not handled by a `click` event, this is to avoid situation when click on some trigger
+ * opens and closes element at the same time.
+ *
+ * @param {Object} options Configuration options.
+ * @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added.
+ * @param {Function} options.activator Function returning a `Boolean`, to determine whether handler is active.
+ * @param {HTMLElement} options.contextElement `HTMLElement` that clicking inside of which will not fire the callback.
+ * @param {Function} options.callback Function fired after clicking outside of a specified element.
+ */
+export default function clickOutsideHandler( { emitter, activator, callback, contextElement } ) {
+	emitter.listenTo( document, 'mouseup', ( evt, { target } ) => {
+		if ( activator() && !contextElement.contains( target ) ) {
+			callback();
+		}
+	} );
+}

+ 28 - 0
packages/ckeditor5-ui/src/bindings/escpresshandler.js

@@ -0,0 +1,28 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/bindings/escpresshandler
+ */
+
+/* global document */
+
+import { keyCodes } from 'ckeditor5-utils/src/keyboard';
+
+/**
+ * Detects <kbd>Esc</kbd> `keydown` DOM event and fires an action.
+ *
+ * @param {Object} options Configuration options.
+ * @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added.
+ * @param {Function} options.activator Function returning a `Boolean`, to determine whether handler is active.
+ * @param {Function} options.callback Function fired after <kbd>Esc</kbd> is pressed.
+ */
+export default function escPressHandler( { emitter, activator, callback } ) {
+	emitter.listenTo( document, 'keydown', ( evt, { keyCode } ) => {
+		if ( keyCode == keyCodes.esc && activator() ) {
+			callback();
+		}
+	} );
+}

+ 23 - 0
packages/ckeditor5-ui/src/bindings/submithandler.js

@@ -0,0 +1,23 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/bindings/submithandler
+ */
+
+/**
+ * Handles native DOM `submit` event by preventing it and firing
+ * the {@link module:ui/view~View view's} `submit` event, which can be then handled by the
+ * parent controller.
+ *
+ * @param {Object} [options] Configuration options.
+ * @param {module:ui/view~View} options.view The view to which this behavior should be added.
+ */
+export default function submitHandler( { view } ) {
+	view.listenTo( view.element, 'submit', ( evt, domEvt ) => {
+		domEvt.preventDefault();
+		view.fire( 'submit' );
+	}, { useCapture: true } );
+}

+ 216 - 0
packages/ckeditor5-ui/src/button/buttonview.js

@@ -0,0 +1,216 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/button/buttonview
+ */
+
+import View from '../view';
+import Template from '../template';
+import IconView from '../icon/iconview';
+
+import { getEnvKeystrokeText } from 'ckeditor5-utils/src/keyboard';
+
+/**
+ * The button view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class ButtonView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		/**
+		 * The label of the button view visible to the user.
+		 *
+		 * @observable
+		 * @member {String} #label
+		 */
+		this.set( 'label' );
+
+		/**
+		 * (Optional) The keystroke associated with the button, i.e. <kbd>CTRL+B</kbd>,
+		 * in the string format compatible with {@link module:utils/keyboard}.
+		 *
+		 * @observable
+		 * @member {Boolean} #keystroke.
+		 */
+		this.set( 'keystroke' );
+
+		/**
+		 * (Optional) Title of the button displayed in the tooltip, i.e. when
+		 * hovering the button with the mouse cursor. When `title` property is not defined
+		 * then combination of `label` and `keystroke` will be set as title.
+		 *
+		 * @observable
+		 * @member {Boolean} #title
+		 */
+		this.set( 'title' );
+
+		/**
+		 * 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", e.g. some feature which it represents
+		 * is currently enabled.
+		 *
+		 * @observable
+		 * @member {Boolean} #isOn
+		 */
+		this.set( 'isOn', false );
+
+		/**
+		 * Controls whether the button view is enabled (can be clicked).
+		 *
+		 * @observable
+		 * @member {Boolean} #isEnabled
+		 */
+		this.set( 'isEnabled', true );
+
+		/**
+		 * (Optional) Whether the label of the button is hidden (e.g. button with icon only).
+		 *
+		 * @observable
+		 * @member {Boolean} #withText
+		 */
+		this.set( 'withText', false );
+
+		/**
+		 * (Optional) The name of the icon of the button view.
+		 * See {@link module:ui/icon/iconview~IconView} and
+		 * {@link module:theme/iconmanagermodel~IconManagerModel}.
+		 *
+		 * @observable
+		 * @member {String} #icon
+		 */
+		this.set( 'icon' );
+
+		/**
+		 * Title of the button bound to the template.
+		 *
+		 * @see #title
+		 * @private
+		 * @observable
+		 * @member {Boolean} #_tooltip
+		 */
+		this.bind( '_tooltip' ).to( this, 'title', this, 'label', this, 'keystroke', this._getTooltip.bind( this ) );
+
+		/**
+		 * Icon of the button view.
+		 *
+		 * @readonly
+		 * @member {module:ui/icon/iconview~IconView} #iconView
+		 */
+
+		const bind = this.bindTemplate;
+
+		this.template = new Template( {
+			tag: 'button',
+
+			attributes: {
+				class: [
+					'ck-button',
+					'ck-tooltip_s',
+					bind.to( 'isEnabled', value => value ? 'ck-enabled' : 'ck-disabled' ),
+					bind.to( 'isOn', value => value ? 'ck-on' : 'ck-off' ),
+					bind.if( 'withText', 'ck-button_with-text' )
+				],
+				type: bind.to( 'type', value => value ? value : 'button' ),
+				'data-ck-tooltip': [
+					bind.to( '_tooltip' )
+				]
+			},
+
+			children: [
+				{
+					tag: 'span',
+
+					attributes: {
+						class: [ 'ck-button__label' ]
+					},
+
+					children: [
+						{
+							text: bind.to( 'label' )
+						}
+					]
+				}
+			],
+
+			on: {
+				mousedown: bind.to( evt => {
+					evt.preventDefault();
+				} ),
+
+				click: bind.to( evt => {
+					// We can't make the button disabled using the disabled attribute, because it won't be focusable.
+					// Though, shouldn't this condition be moved to the button controller?
+					if ( this.isEnabled ) {
+						this.fire( 'execute' );
+					} else {
+						// Prevent the default when button is disabled, to block e.g.
+						// automatic form submitting. See ckeditor/ckeditor5-link#74.
+						evt.preventDefault();
+					}
+				} )
+			}
+		} );
+
+		/**
+		 * Fired when the button view is clicked. It won't be fired when the button is disabled.
+		 *
+		 * @event #execute
+		 */
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		let promise = Promise.resolve();
+
+		if ( this.icon && !this.iconView ) {
+			const iconView = this.iconView = new IconView();
+
+			iconView.bind( 'content' ).to( this, 'icon' );
+
+			this.element.insertBefore( iconView.element, this.element.firstChild );
+
+			// Make sure the icon view will be destroyed along with button.
+			promise = promise.then( () => this.addChildren( iconView ) );
+		}
+
+		return promise.then( () => super.init() );
+	}
+
+	/**
+	 * Gets value for the `data-ck-tooltip` attribute from title, label and keystroke properties.
+	 *
+	 * @private
+	 * @param {String} title Button title.
+	 * @param {String} label Button label.
+	 * @param {String} keystroke Button keystroke.
+	 * @returns {String}
+	 */
+	_getTooltip( title, label, keystroke ) {
+		if ( title ) {
+			return title;
+		}
+
+		if ( keystroke ) {
+			label += ` (${ getEnvKeystrokeText( keystroke ) })`;
+		}
+
+		return label;
+	}
+}

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

@@ -0,0 +1,29 @@
+/**
+ * @license Copyright (c) 2003-2016, 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';
+
+/**
+ * Creates an instance of {@link module:ui/dropdown/dropdownview~DropdownView} class using
+ * defined model.
+ *
+ * @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 );
+	buttonView.bind( 'label', 'isOn', 'isEnabled', 'withText', 'keystroke' ).to( model );
+
+	const panelView = new DropdownPanelView( locale );
+
+	return new DropdownView( locale, buttonView, panelView );
+}

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

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * The basic dropdown model interface.
+ *
+ * @interface module:ui/dropdown/dropdownmodel~DropdownModel
+ */
+
+/**
+ * The label of the dropdown.
+ *
+ * @observable
+ * @member {String} #label
+ */
+
+/**
+ * Controls whether the dropdown is enabled (can be clicked).
+ *
+ * @observable
+ * @member {Boolean} #isEnabled
+ */
+
+/**
+ * Controls whether the {@link module:ui/dropdown/dropdownview~DropdownView#buttonView} is "pushed".
+ *
+ * @observable
+ * @member {Boolean} #isOn
+ */
+
+/**
+ * (Optional) Whether the label of the dropdown is visible. See {@link module:ui/button/buttonview~ButtonView#withText}.
+ *
+ * @observable
+ * @member {Boolean} #withText
+ */

+ 57 - 0
packages/ckeditor5-ui/src/dropdown/dropdownpanelview.js

@@ -0,0 +1,57 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/dropdownpanelview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The dropdown panel view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class DropdownPanelView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * Controls whether the panel is visible.
+		 *
+		 * @observable
+		 * @member {Boolean} #isVisible
+		 */
+		this.set( 'isVisible', false );
+
+		/**
+		 * Collection of the child views.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.children = this.createCollection();
+
+		this.template = new Template( {
+			tag: 'div',
+
+			attributes: {
+				class: [
+					'ck-reset',
+					'ck-dropdown__panel',
+					bind.if( 'isVisible', 'ck-dropdown__panel-visible' )
+				]
+			},
+
+			children: this.children
+		} );
+	}
+}

+ 107 - 0
packages/ckeditor5-ui/src/dropdown/dropdownview.js

@@ -0,0 +1,107 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/dropdownview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The dropdown view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class DropdownView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale, buttonView, panelView ) {
+		super( locale );
+
+		/**
+		 * Button of this dropdown view.
+		 *
+		 * @readonly
+		 * @member {ui.button.ButtonView} #buttonView
+		 */
+		this.addChildren( this.buttonView = buttonView );
+
+		/**
+		 * Panel of this dropdown view.
+		 *
+		 * @readonly
+		 * @member {module:ui/dropdown/dropdownpanelview~DropdownPanelView} #panelView
+		 */
+		this.addChildren( this.panelView = panelView );
+
+		/**
+		 * Controls whether the dropdown view is open, which also means its
+		 * {@link #panelView panel} is visible.
+		 *
+		 * @observable
+		 * @member {Boolean} #isOpen
+		 */
+		this.set( 'isOpen', false );
+
+		this.template = new Template( {
+			tag: 'div',
+
+			attributes: {
+				class: [
+					'ck-dropdown'
+				]
+			},
+
+			children: [
+				buttonView,
+				panelView
+			]
+		} );
+
+		Template.extend( buttonView.template, {
+			attributes: {
+				class: [
+					'ck-dropdown__button'
+				]
+			}
+		} );
+
+		// Toggle the the dropdown when it's button has been clicked.
+		this.listenTo( buttonView, 'execute', () => this.isOpen = !this.isOpen );
+
+		// Toggle the visibility of the panel when the dropdown becomes open.
+		panelView.bind( 'isVisible' ).to( this, 'isOpen' );
+
+		/**
+		 * The label of the dropdown.
+		 *
+		 * @observable
+		 * @member {String} #label
+		 */
+
+		/**
+		 * Controls whether the dropdown is enabled (can be clicked).
+		 *
+		 * @observable
+		 * @member {Boolean} #isEnabled
+		 */
+
+		/**
+		 * Controls whether the {@link #buttonView} is "pushed".
+		 *
+		 * @observable
+		 * @member {Boolean} #isOn
+		 */
+
+		/**
+		 * (Optional) Whether the label of the dropdown is visible. See {@link module:ui/button/buttonview~ButtonView#withText}.
+		 *
+		 * @observable
+		 * @member {Boolean} #withText
+		 */
+	}
+}

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

@@ -0,0 +1,73 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/list/createlistdropdown
+ */
+
+/* global document */
+
+import ListView from '../../list/listview';
+import ListItemView from '../../list/listitemview';
+import createDropdown from '../createdropdown';
+
+/**
+ * Creates an instance of {@link module:ui/dropdown/list/listdropdownview~ListDropdownView} class using
+ * defined model.
+ *
+ * @param {module:ui/dropdown/list/listdropdownmodel~ListDropdownModel} model Model of this 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 ).as( 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;
+	} );
+
+	// TODO: Delegate all events instead of just execute.
+	listView.items.delegate( 'execute' ).to( dropdownView );
+
+	dropdownView.panelView.children.add( listView );
+
+	dropdownView.on( 'change:isOpen', ( evt, name, value ) => {
+		if ( value ) {
+			attachDocumentClickListener( dropdownView );
+		} else {
+			dropdownView.stopListening( document );
+		}
+	} );
+
+	// Close the dropdown when one of the list items has been executed.
+	dropdownView.on( 'execute', () => {
+		dropdownView.isOpen = false;
+	} );
+
+	return dropdownView;
+}
+
+// Attaches a "click" listener in DOM to check if any element outside
+// the dropdown has been clicked.
+//
+// @private
+// @param {module:ui/dropdown/listdropdownview~ListDropdownView} dropdownView
+function attachDocumentClickListener( dropdownView ) {
+	// TODO: It will probably be focus/blur-based rather than click. It should be bound
+	// to focusmanager of some sort.
+	dropdownView.listenTo( document, 'click', ( evtInfo, { target: domEvtTarget } ) => {
+		// Collapse the dropdown when the webpage outside of the component is clicked.
+		if ( dropdownView.element != domEvtTarget && !dropdownView.element.contains( domEvtTarget ) ) {
+			dropdownView.isOpen = false;
+		}
+	} );
+}

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

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * 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} used to populate
+ * the inner {@link module:ui/list/listview~ListView} instance.
+ *
+ * @observable
+ * @member {Boolean} #items
+ */
+
+/**
+ * Fired when the list dropdown should be executed, usually when
+ * one of the list items in {@link #items} has been executed.
+ *
+ * @event #execute
+ */

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

@@ -0,0 +1,21 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * 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
+ */
+
+/**
+ * List of the list dropdown view.
+ *
+ * @readonly
+ * @member {module:ui/list/listview~ListView} #listView
+ */

+ 94 - 0
packages/ckeditor5-ui/src/editableui/editableuiview.js

@@ -0,0 +1,94 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/editableui/editableuiview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The editable UI view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class EditableUIView extends View {
+	/**
+	 * Creates an instance of EditableUIView class.
+	 *
+	 * @param {module:utils/locale~Locale} [locale] The locale instance.
+	 * @param {HTMLElement} [editableElement] The editable element. If not specified, this view
+	 * should create it. Otherwise, the existing element should be used.
+	 */
+	constructor( locale, editableElement ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		if ( editableElement ) {
+			this.element = this.editableElement = editableElement;
+		}
+
+		this.template = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					bind.to( 'isFocused', value => value ? 'ck-focused' : 'ck-blurred' ),
+					'ck-editor__editable'
+				],
+				contenteditable: bind.to( 'isReadOnly', value => !value ),
+			}
+		} );
+
+		/**
+		 * Controls whether the editable is writable or not.
+		 *
+		 * @observable
+		 * @member {Boolean} #isReadOnly
+		 */
+		this.set( 'isReadOnly', false );
+
+		/**
+		 * Controls whether the editable is focused, i.e. the user is typing in it.
+		 *
+		 * @observable
+		 * @member {Boolean} #isFocused
+		 */
+		this.set( 'isFocused', false );
+
+		/**
+		 * The element which is the main editable element (usually the one with `contentEditable="true"`).
+		 *
+		 * @readonly
+		 * @member {HTMLElement} #editableElement
+		 */
+	}
+
+	/**
+	 * Initializes the view by either applying the {@link #template} to the existing
+	 * {@link #editableElement} or assigning {@link #element} as {@link #editableElement}.
+	 *
+	 * @returns {Promise}
+	 */
+	init() {
+		if ( this.editableElement ) {
+			this.template.apply( this.editableElement );
+		} else {
+			this.editableElement = this.element;
+		}
+
+		return super.init();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		this.editableElement.contentEditable = false;
+
+		return super.destroy();
+	}
+}

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

@@ -0,0 +1,54 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/editableui/inline/inlineeditableuiview
+ */
+
+import EditableUIView from '../../editableui/editableuiview';
+import Template from '../../template';
+
+/**
+ * The inline editable UI class implementing an inline {@link module:ui/editableui/editableuiview~EditableUIView}.
+ *
+ * @extends module:ui/editableui/editableuiview~EditableUIView
+ */
+export default class InlineEditableUIView extends EditableUIView {
+	/**
+	 * Creates an instance of the InlineEditableUIView class.
+	 *
+	 * @param {module:utils/locale~Locale} [locale] The locale instance.
+	 * @param {HTMLElement} [editableElement] The editable element. If not specified, the
+	 * {@link module:ui/editableui/editableuiview~EditableUIView}
+	 * should create it. Otherwise, the existing element should be used.
+	 */
+	constructor( locale, editableElement ) {
+		super( locale, editableElement );
+
+		const bind = this.bindTemplate;
+		const t = this.t;
+
+		/**
+		 * The name of the editable UI view.
+		 *
+		 * @observable
+		 * @member {String} #name
+		 */
+		this.set( 'name', null );
+
+		const getLabel = ( value ) => {
+			return t( 'Rich Text Editor, %0', [ value ] );
+		};
+
+		Template.extend( this.template, {
+			attributes: {
+				role: 'textbox',
+				'aria-label': bind.to( 'name', getLabel ),
+				title: bind.to( 'name', getLabel ),
+				class: 'ck-editor__editable_inline'
+			}
+		} );
+	}
+}

+ 112 - 0
packages/ckeditor5-ui/src/editorui/boxed/boxededitoruiview.js

@@ -0,0 +1,112 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/editorui/boxed/boxededitoruiview
+ */
+
+import EditorUIView from '../../editorui/editoruiview';
+import uid from 'ckeditor5-utils/src/uid';
+import Template from '../../template';
+
+/**
+ * The boxed editor UI view class. This class represents an editor interface
+ * consisting of a toolbar and an editable area, enclosed within a box.
+ *
+ * @extends module:ui/editorui/editoruiview~EditorUIView
+ */
+export default class BoxedEditorUIView extends EditorUIView {
+	/**
+	 * Creates an instance of the boxed editor UI view class.
+	 *
+	 * @param {module:utils/locale~Locale} locale The locale instance..
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const t = this.t;
+		const ariaLabelUid = uid();
+
+		/**
+		 * The UI's width.
+		 *
+		 * @observable
+		 * @member {Number} #width
+		 */
+		this.set( 'width', null );
+
+		/**
+		 * The UI's height.
+		 *
+		 * @observable
+		 * @member {Number} #height
+		 */
+		this.set( 'height', null );
+
+		/**
+		 * Collection of the child views located in the top (`.ck-editor__top`)
+		 * area of the UI.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.top = this.createCollection();
+
+		/**
+		 * Collection of the child views located in the main (`.ck-editor__main`)
+		 * area of the UI.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.main = this.createCollection();
+
+		this.template = new Template( {
+			tag: 'div',
+
+			attributes: {
+				class: [
+					'ck-reset',
+					'ck-editor',
+					'ck-rounded-corners'
+				],
+				role: 'application',
+				dir: 'ltr',
+				lang: locale.lang,
+				'aria-labelledby': `cke-editor__aria-label_${ ariaLabelUid }`
+			},
+
+			children: [
+				{
+					tag: 'span',
+					attributes: {
+						id: `cke-editor__aria-label_${ ariaLabelUid }`,
+						class: 'cke-voice-label'
+					},
+					children: [
+						// TODO: Editor name?
+						t( 'Rich Text Editor' )
+					]
+				},
+				{
+					tag: 'div',
+					attributes: {
+						class: 'ck-editor__top ck-reset_all',
+						role: 'presentation'
+					},
+					children: this.top
+				},
+				{
+					tag: 'div',
+					attributes: {
+						class: 'ck-editor__main',
+						role: 'presentation'
+					},
+					children: this.main
+				}
+			]
+		} );
+	}
+}

+ 98 - 0
packages/ckeditor5-ui/src/editorui/editoruiview.js

@@ -0,0 +1,98 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/editorui/editoruiview
+ */
+
+/* globals document */
+
+import View from '../view';
+import Template from '../template';
+import IconManagerView from '../iconmanager/iconmanagerview';
+import iconManagerModel from 'ckeditor5-theme/src/iconmanagermodel';
+
+/**
+ * The editor UI view class. Base class for the editor main views.
+ *
+ * @extends module:ui/view~View
+ */
+export default class EditorUIView extends View {
+	/**
+	 * Creates an instance of the editor UI view class.
+	 *
+	 * @param {module:utils/locale~Locale} [locale] The locale instance.
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		/**
+		 * Collection of the child views, detached from the DOM
+		 * structure of the editor, like panels, icons etc.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection} #body
+		 */
+		this.body = this.createCollection();
+
+		/**
+		 * The element holding elements of the 'body' region.
+		 *
+		 * @private
+		 * @member {HTMLElement} #_bodyCollectionContainer
+		 */
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		return Promise.resolve()
+			.then( () => this._renderBodyCollection() )
+			.then( () => this._setupIconManager() )
+			.then( () => super.init() );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		this._bodyCollectionContainer.remove();
+
+		return super.destroy();
+	}
+
+	/**
+	 * Injects the {@link module:ui/iconmanager/iconmanagerview~IconManagerView} into DOM.
+	 *
+	 * @protected
+	 */
+	_setupIconManager() {
+		this.iconManagerView = new IconManagerView( iconManagerModel.sprite, iconManagerModel.icons );
+
+		return this.body.add( this.iconManagerView );
+	}
+
+	/**
+	 * Creates and appends to `<body>` the {@link #body} collection container.
+	 *
+	 * @private
+	 */
+	_renderBodyCollection() {
+		const bodyElement = this._bodyCollectionContainer = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-body',
+					'ck-rounded-corners',
+					'ck-reset_all'
+				]
+			},
+			children: this.body
+		} ).render();
+
+		document.body.appendChild( bodyElement );
+	}
+}

+ 79 - 0
packages/ckeditor5-ui/src/icon/iconview.js

@@ -0,0 +1,79 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global DOMParser */
+
+/**
+ * @module ui/icon/iconview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The icon view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class IconView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor() {
+		super();
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * The inline SVG or in legacy mode the name of the icon
+		 * which corresponds with the name of the file in the
+		 * {@link module:theme/iconmanagermodel~IconManagerModel}.
+		 *
+		 * @observable
+		 * @member {String} #content
+		 */
+		this.set( 'content' );
+
+		/**
+		 * This attribute specifies the boundaries to which the
+		 * icon content should stretch.
+		 *
+		 * @observable
+		 * @default '0 0 20 20'
+		 * @member {String} #viewBox
+		 */
+		this.set( 'viewBox', '0 0 20 20' );
+
+		this.template = new Template( {
+			tag: 'svg',
+			ns: 'http://www.w3.org/2000/svg',
+			attributes: {
+				class: 'ck-icon',
+				viewBox: bind.to( 'viewBox' )
+			}
+		} );
+
+		// This is a hack for lack of innerHTML binding.
+		// See: https://github.com/ckeditor/ckeditor5-ui/issues/99.
+		//
+		// Temporarily 2 types of icon content are supported:
+		//   * Inline SVG - content of svg file as plan text.
+		//   * Icon name (legacy) - name of icon corresponds with name of the
+		//     file in the {@link module:theme/iconmanagermodel~IconManagerModel}.
+		this.on( 'change:content', ( evt, name, value ) => {
+			if ( /</.test( value ) ) {
+				const svg = new DOMParser()
+					.parseFromString( value.trim(), 'image/svg+xml' )
+					.firstChild;
+
+				while ( svg.childNodes.length > 0 ) {
+					this.element.appendChild( svg.childNodes[ 0 ] );
+				}
+			} else {
+				this.element.innerHTML = `<use xlink:href="#ck-icon-${ value }"></use>`;
+			}
+		} );
+	}
+}

+ 78 - 0
packages/ckeditor5-ui/src/iconmanager/iconmanagerview.js

@@ -0,0 +1,78 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/iconmanager/iconmanagerview
+ */
+
+/* globals document */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The icon manager view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class IconManagerView extends View {
+	/**
+	 * Creates an instance of the icon manager view.
+	 *
+	 * @param {String} sprite The SVG (HTML) string of the icons to be injected into DOM.
+	 * @param {Array.<String>} icons List of icon names available in the manager.
+	 */
+	constructor( sprite, icons ) {
+		super();
+
+		this.template = new Template( {
+			tag: 'svg',
+			ns: 'http://www.w3.org/2000/svg',
+			attributes: {
+				class: 'ck-icon-manager__sprite'
+			}
+		} );
+
+		/**
+		 * The actual SVG (HTML) of the icons to be injected in DOM.
+		 *
+		 * @member {String}
+		 */
+		this.sprite = sprite;
+
+		/**
+		 * List of icon names available in the manager.
+		 *
+		 * @readonly
+		 * @member {Array.<String>}
+		 */
+		this.icons = icons;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		// Note: In MS Edge it's not enough to set:
+		//
+		//		this.element.innerHTML = this.sprite;
+		//
+		// because for some reason the browser won't parse the symbols string
+		// properly as svg content. Instead, an explicit parsing is needed (#55).
+		const tmp = document.createElement( 'div' );
+
+		tmp.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg">${ this.sprite }</svg>`;
+
+		const symbols = tmp.firstChild.childNodes;
+
+		// Pick symbols from the tmp and put them into icon manager.
+		// Note: MS Edge does not support forEach or Symbol.iterator for NodeList.
+		for ( let i = 0; i < symbols.length; ++i ) {
+			this.element.appendChild( document.importNode( symbols[ i ], true ) );
+		}
+
+		return super.init();
+	}
+}

+ 87 - 0
packages/ckeditor5-ui/src/iframe/iframeview.js

@@ -0,0 +1,87 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/iframe/iframeview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The iframe view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class IframeView extends View {
+	/**
+	 * Creates a new instance of the iframe view.
+	 *
+	 * @param {module:utils/locale~Locale} [locale] The locale instance.
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		this.template = new Template( {
+			tag: 'iframe',
+			attributes: {
+				class: [ 'ck-reset_all' ],
+				// It seems that we need to allow scripts in order to be able to listen to events.
+				// TODO: Research that. Perhaps the src must be set?
+				sandbox: 'allow-same-origin allow-scripts'
+			},
+			on: {
+				load: bind.to( 'loaded' )
+			}
+		} );
+
+		/**
+		 * A promise returned by {@link #init} since iframe loading may be asynchronous.
+		 *
+		 * **Note**: Listening to `load` in {@link #init} makes no sense because at this point
+		 * the element is already in the DOM and the `load` event might already be fired.
+		 *
+		 * See {@link #_iframeDeferred}.
+		 *
+		 * @private
+		 * @member {Promise}
+		 */
+		this._iframePromise = new Promise( ( resolve, reject ) => {
+			/**
+			 * A deferred object used to resolve the iframe promise associated with
+			 * asynchronous loading of `contentDocument`. See {@link #_iframePromise}.
+			 *
+			 * @private
+			 * @member {Object}
+			 */
+			this._iframeDeferred = { resolve, reject };
+		} );
+
+		this.on( 'loaded', () => {
+			this._iframeDeferred.resolve();
+		} );
+	}
+
+	/**
+	 * Initializes iframe {@link #element} and returns a `Promise` for asynchronous
+	 * child `contentDocument` loading process. See {@link #_iframePromise}.
+	 *
+	 * @returns {Promise} A promise which resolves once the iframe `contentDocument` has
+	 * been {@link #event:loaded}.
+	 */
+	init() {
+		super.init();
+
+		return this._iframePromise;
+	}
+}
+
+/**
+ * Fired when the DOM iframe's `contentDocument` finished loading.
+ *
+ * @event loaded
+ */

+ 65 - 0
packages/ckeditor5-ui/src/inputtext/inputtextview.js

@@ -0,0 +1,65 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/inputtext/inputtextview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The text input view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class InputTextView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		/**
+		 * The value of the input.
+		 *
+		 * @observable
+		 * @member {String} #value
+		 */
+		this.set( 'value' );
+
+		/**
+		 * The `id` attribute of the input (i.e. to pair with a `<label>` element).
+		 *
+		 * @observable
+		 * @member {String} #id
+		 */
+		this.set( 'id' );
+
+		const bind = this.bindTemplate;
+
+		this.template = new Template( {
+			tag: 'input',
+			attributes: {
+				type: 'text',
+				class: [
+					'ck-input',
+					'ck-input-text'
+				],
+				id: bind.to( 'id' )
+			}
+		} );
+
+		// Note: `value` cannot be an HTML attribute, because it doesn't change HTMLInputElement value once changed.
+		this.on( 'change:value', ( evt, propertyName, value ) => this.element.value = value || '' );
+	}
+
+	/**
+	 * Moves the focus to the input and selects the value.
+	 */
+	select() {
+		this.element.select();
+	}
+}

+ 58 - 0
packages/ckeditor5-ui/src/label/labelview.js

@@ -0,0 +1,58 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/label/labelview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The label view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class LabelView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		/**
+		 * The text of the label.
+		 *
+		 * @observable
+		 * @member {String} #text
+		 */
+		this.set( 'text' );
+
+		/**
+		 * The `for` attribute of the label (i.e. to pair with an `<input>` element).
+		 *
+		 * @observable
+		 * @member {String} #for
+		 */
+		this.set( 'for' );
+
+		const bind = this.bindTemplate;
+
+		this.template = new Template( {
+			tag: 'label',
+			attributes: {
+				class: [
+					'ck-label'
+				],
+				for: bind.to( 'for' )
+			},
+			children: [
+				{
+					text: bind.to( 'text' )
+				}
+			]
+		} );
+	}
+}

+ 112 - 0
packages/ckeditor5-ui/src/labeledinput/labeledinputview.js

@@ -0,0 +1,112 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/labeledinput/labeledinputview
+ */
+
+import View from '../view';
+import Template from '../template';
+import uid from 'ckeditor5-utils/src/uid';
+
+import LabelView from '../label/labelview';
+
+/**
+ * The labeled input view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class LabeledInputView extends View {
+	/**
+	 * Creates an instance of the labeled input view class.
+	 *
+	 * @param {module:utils/locale~Locale} locale The locale instance.
+	 * @param {Function} InputView Constructor of the input view.
+	 */
+	constructor( locale, InputView ) {
+		super( locale );
+
+		const id = `ck-input-${ uid() }`;
+
+		/**
+		 * The text of the label.
+		 *
+		 * @observable
+		 * @member {String} #label
+		 */
+		this.set( 'label' );
+
+		/**
+		 * The value of the input.
+		 *
+		 * @observable
+		 * @member {String} #value
+		 */
+		this.set( 'value' );
+
+		/**
+		 * The label view.
+		 *
+		 * @member {module:ui/label/labelview~LabelView} #labelView
+		 */
+		this.addChildren( this.labelView = this._createLabelView( id ) );
+
+		/**
+		 * The input view.
+		 *
+		 * @member {module:ui/view~View} #inputView
+		 */
+		this.addChildren( this.inputView = this._createInputView( InputView, id ) );
+
+		this.template = new Template( {
+			tag: 'div',
+
+			children: [
+				this.labelView,
+				this.inputView
+			]
+		} );
+	}
+
+	/**
+	 * Creates label view class instance and bind with view.
+	 *
+	 * @private
+	 * @param {String} id Unique id to set as labelView#for attribute.
+	 * @returns {module:ui/label/labelview~LabelView}
+	 */
+	_createLabelView( id ) {
+		const labelView = new LabelView( this.locale );
+
+		labelView.for = id;
+		labelView.bind( 'text' ).to( this, 'label' );
+
+		return labelView;
+	}
+
+	/**
+	 * Creates input view class instance and bind with view.
+	 *
+	 * @private
+	 * @param {Function} InputView Input view constructor.
+	 * @param {String} id Unique id to set as inputView#id attribute.
+	 * @returns {module:ui/inputtext/inputtextview~InputTextView}
+	 */
+	_createInputView( InputView, id ) {
+		const inputView = new InputView( this.locale );
+
+		inputView.id = id;
+		inputView.bind( 'value' ).to( this );
+
+		return inputView;
+	}
+
+	/**
+	 * Moves the focus to the input and selects the value.
+	 */
+	select() {
+		this.inputView.select();
+	}
+}

+ 68 - 0
packages/ckeditor5-ui/src/list/listitemview.js

@@ -0,0 +1,68 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/list/listitemview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The list item view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class ListItemView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor() {
+		super();
+
+		const bind = this.bindTemplate;
+
+		this.template = new Template( {
+			tag: 'li',
+
+			attributes: {
+				class: [
+					'ck-list__item'
+				],
+				style: bind.to( 'style' )
+			},
+
+			children: [
+				{
+					text: bind.to( 'label' )
+				}
+			],
+
+			on: {
+				click: bind.to( 'execute' )
+			}
+		} );
+
+		/**
+		 * The label of the list item.
+		 *
+		 * @observable
+		 * @member {String} #label
+		 */
+
+		/**
+		 * (Optional) The DOM style attribute of the list item.
+		 *
+		 * @observable
+		 * @member {String} #style
+		 */
+
+		/**
+		 * Fired when the list item has been clicked.
+		 *
+		 * @event #execute
+		 */
+	}
+}

+ 46 - 0
packages/ckeditor5-ui/src/list/listview.js

@@ -0,0 +1,46 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/list/listview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The list view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class ListView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor() {
+		super();
+
+		/**
+		 * Collection of the child list views.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.items = this.createCollection();
+
+		this.template = new Template( {
+			tag: 'ul',
+
+			attributes: {
+				class: [
+					'ck-reset',
+					'ck-list'
+				]
+			},
+
+			children: this.items
+		} );
+	}
+}

+ 215 - 0
packages/ckeditor5-ui/src/toolbar/sticky/stickytoolbarview.js

@@ -0,0 +1,215 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/toolbar/sticky/stickytoolbarview
+ */
+
+import global from 'ckeditor5-utils/src/dom/global';
+import Template from '../../template';
+import ToolbarView from '../toolbarview';
+import toUnit from 'ckeditor5-utils/src/dom/tounit';
+
+const toPx = toUnit( 'px' );
+
+/**
+ * The sticky toolbar view class.
+ *
+ * @extends module:ui/toolbar/toolbarview~ToolbarView
+ */
+export default class StickyToolbarView extends ToolbarView {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * Controls whether the sticky toolbar should be active. When any editable
+		 * is focused in the editor, toolbar becomes active.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isActive
+		 */
+		this.set( 'isActive', false );
+
+		/**
+		 * Controls whether the sticky toolbar is in the "sticky" state.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isSticky
+		 */
+		this.set( 'isSticky', false );
+
+		/**
+		 * The limiter element for the sticky toolbar instance. Its bounding rect limits
+		 * the "stickyness" of the toolbar, i.e. when the toolbar reaches the bottom
+		 * edge of the limiter, it becomes sticky to that edge and does not float
+		 * off the limiter. It is mandatory for the toolbar to work properly and once
+		 * set, it cannot be changed.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {HTMLElement} #limiterElement
+		 */
+		this.set( 'limiterElement', null );
+
+		/**
+		 * The offset from the bottom edge of {@link #limiterElement}
+		 * which stops the toolbar from stickying any further to prevent limiter's content
+		 * from being completely covered.
+		 *
+		 * @readonly
+		 * @observable
+		 * @default 50
+		 * @member {Number} #limiterOffset
+		 */
+		this.set( 'limiterOffset', 50 );
+
+		/**
+		 * Controls the `margin-left` CSS style of the toolbar.
+		 *
+		 * @protected
+		 * @readonly
+		 * @observable
+		 * @member {String} #_marginLeft
+		 */
+		this.set( '_marginLeft', null );
+
+		/**
+		 * Set `true` if the sticky toolbar reached the bottom edge of the
+		 * {@link #limiterElement}.
+		 *
+		 * @protected
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #_isStickyToTheLimiter
+		 */
+		this.set( '_isStickyToTheLimiter', false );
+
+		/**
+		 * The DOM bounding client rect of the {@link module:ui/view~View#element} of the toolbar.
+		 *
+		 * @protected
+		 * @member {Object} #_toolbarRect
+		 */
+
+		/**
+		 * The DOM bounding client rect of the {@link #limiterElement}
+		 * of the toolbar.
+		 *
+		 * @protected
+		 * @member {Object} #_limiterRect
+		 */
+
+		Template.extend( this.template, {
+			attributes: {
+				class: [
+					// Toggle class of the toolbar when "sticky" state changes in the view.
+					bind.if( 'isSticky', 'ck-toolbar_sticky' ),
+					bind.if( '_isStickyToTheLimiter', 'ck-toolbar_sticky_bottom-limit' ),
+				],
+				style: {
+					width: bind.to( 'isSticky', ( isSticky ) => {
+						return isSticky ? toPx( this._elementPlaceholder.getBoundingClientRect().width ) : null;
+					} ),
+
+					bottom: bind.to( '_isStickyToTheLimiter', ( _isStickyToTheLimiter ) => {
+						return _isStickyToTheLimiter ? toPx( this.limiterOffset ) : null;
+					} ),
+
+					marginLeft: bind.to( '_marginLeft' )
+				}
+			}
+		} );
+
+		/**
+		 * A dummy element which visually fills the space as long as the
+		 * actual toolbar is sticky. It prevents flickering of the UI.
+		 *
+		 * @private
+		 * @property {HTMLElement}
+		 */
+		this._elementPlaceholder = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-toolbar__placeholder'
+				],
+				style: {
+					display: bind.to( 'isSticky', isSticky => isSticky ? 'block' : 'none' ),
+					height: bind.to( 'isSticky', ( isSticky ) => {
+						return isSticky ? toPx( this._toolbarRect.height ) : null;
+					} )
+				}
+			}
+		} ).render();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		super.init();
+
+		this.element.parentNode.insertBefore( this._elementPlaceholder, this.element );
+
+		// Update sticky state of the toolbar as the window is being scrolled.
+		this.listenTo( global.window, 'scroll', () => {
+			this._checkIfShouldBeSticky();
+		} );
+
+		// Synchronize with `model.isActive` because sticking an inactive toolbar is pointless.
+		this.listenTo( this, 'change:isActive', () => {
+			this._checkIfShouldBeSticky();
+		} );
+	}
+
+	/**
+	 * Destroys the toolbar and removes the {@link #_elementPlaceholder}.
+	 */
+	destroy() {
+		return super.destroy().then( () => {
+			this._elementPlaceholder.remove();
+		} );
+	}
+
+	/**
+	 * Analyzes the environment to decide whether the toolbar should
+	 * be sticky or not.
+	 *
+	 * @protected
+	 */
+	_checkIfShouldBeSticky() {
+		const limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect();
+		const toolbarRect = this._toolbarRect = this.element.getBoundingClientRect();
+
+		// The toolbar must be active to become sticky.
+		this.isSticky = this.isActive &&
+			// The limiter's top edge must be beyond the upper edge of the visible viewport.
+			limiterRect.top < 0 &&
+			// The model#limiterElement's height mustn't be smaller than the toolbar's height and model#limiterOffset.
+			// There's no point in entering the sticky mode if the model#limiterElement is very, very small, because
+			// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterOffset, the toolbar
+			// would be positioned before the model#limiterElement.
+			this._toolbarRect.height + this.limiterOffset < limiterRect.height;
+
+		// Stick the toolbar to the top edge of the viewport simulating CSS position:sticky.
+		// TODO: Possibly replaced by CSS in the future http://caniuse.com/#feat=css-sticky
+		if ( this.isSticky ) {
+			this._isStickyToTheLimiter = limiterRect.bottom < toolbarRect.height + this.limiterOffset;
+			this._marginLeft = this._isStickyToTheLimiter ? null : toPx( -global.window.scrollX );
+		}
+		// Detach the toolbar from the top edge of the viewport.
+		else {
+			this._isStickyToTheLimiter = false;
+			this._marginLeft = null;
+		}
+	}
+}

+ 38 - 0
packages/ckeditor5-ui/src/toolbar/toolbarview.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/toolbar/toolbarview
+ */
+
+import View from '../view';
+import Template from '../template';
+
+/**
+ * The toolbar view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class ToolbarView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		this.items = this.createCollection();
+
+		this.template = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-toolbar'
+				]
+			},
+
+			children: this.items
+		} );
+	}
+}

+ 422 - 0
packages/ckeditor5-ui/tests/balloonpanel/balloonpanelview.js

@@ -0,0 +1,422 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global window, document */
+/* bender-tags: ui, balloonPanel, browser-only */
+
+import global from 'ckeditor5-utils/src/dom/global';
+import ViewCollection from 'ckeditor5-ui/src/viewcollection';
+import BalloonPanelView from 'ckeditor5-ui/src/balloonpanel/balloonpanelview';
+import ButtonView from 'ckeditor5-ui/src/button/buttonview';
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+import * as positionUtils from 'ckeditor5-utils/src/dom/position';
+
+testUtils.createSinonSandbox();
+
+describe( 'BalloonPanelView', () => {
+	let view, windowStub;
+
+	beforeEach( () => {
+		view = new BalloonPanelView();
+
+		view.set( 'maxWidth', 200 );
+
+		windowStub = {
+			innerWidth: 1000,
+			innerHeight: 1000,
+			scrollX: 0,
+			scrollY: 0,
+			getComputedStyle: ( el ) => {
+				return window.getComputedStyle( el );
+			}
+		};
+
+		testUtils.sinon.stub( global, 'window', windowStub );
+
+		return view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should create element from template', () => {
+			expect( view.element.tagName ).to.equal( 'DIV' );
+			expect( view.element.classList.contains( 'ck-balloon-panel' ) ).to.true;
+			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
+		} );
+
+		it( 'should set default values', () => {
+			expect( view.top ).to.equal( 0 );
+			expect( view.left ).to.equal( 0 );
+			expect( view.position ).to.equal( 'se' );
+			expect( view.isVisible ).to.equal( false );
+		} );
+
+		it( 'creates view#content collection', () => {
+			expect( view.content ).to.be.instanceOf( ViewCollection );
+		} );
+	} );
+
+	describe( 'DOM bindings', () => {
+		describe( 'arrow', () => {
+			it( 'should react on view#position', () => {
+				expect( view.element.classList.contains( 'ck-balloon-panel_arrow_se' ) ).to.true;
+
+				view.position = 'sw';
+
+				expect( view.element.classList.contains( 'ck-balloon-panel_arrow_sw' ) ).to.true;
+			} );
+		} );
+
+		describe( 'isVisible', () => {
+			it( 'should react on view#isvisible', () => {
+				expect( view.element.classList.contains( 'ck-balloon-panel_visible' ) ).to.false;
+
+				view.isVisible = true;
+
+				expect( view.element.classList.contains( 'ck-balloon-panel_visible' ) ).to.true;
+			} );
+		} );
+
+		describe( 'styles', () => {
+			it( 'should react on view#top', () => {
+				expect( view.element.style.top ).to.equal( '0px' );
+
+				view.top = 10;
+
+				expect( view.element.style.top ).to.equal( '10px' );
+			} );
+
+			it( 'should react on view#left', () => {
+				expect( view.element.style.left ).to.equal( '0px' );
+
+				view.left = 10;
+
+				expect( view.element.style.left ).to.equal( '10px' );
+			} );
+
+			it( 'should react on view#maxWidth', () => {
+				expect( view.element.style.maxWidth ).to.equal( '200px' );
+
+				view.maxWidth = 10;
+
+				expect( view.element.style.maxWidth ).to.equal( '10px' );
+			} );
+		} );
+
+		describe( 'children', () => {
+			it( 'should react on view#content', () => {
+				expect( view.element.childNodes.length ).to.equal( 0 );
+
+				const button = new ButtonView( { t() {} } );
+				view.content.add( button );
+
+				expect( view.element.childNodes.length ).to.equal( 1 );
+			} );
+		} );
+	} );
+
+	describe( 'isVisible', () => {
+		it( 'should return view#isVisible value', () => {
+			expect( view.isVisible ).to.false;
+
+			view.isVisible = true;
+
+			expect( view.isVisible ).to.true;
+		} );
+	} );
+
+	describe( 'show()', () => {
+		it( 'should set view#isVisible as true', () => {
+			view.isVisible = false;
+
+			view.show();
+
+			expect( view.isVisible ).to.true;
+		} );
+	} );
+
+	describe( 'hide()', () => {
+		it( 'should set view#isVisible as false', () => {
+			view.isVisible = true;
+
+			view.hide();
+
+			expect( view.isVisible ).to.false;
+		} );
+	} );
+
+	describe( 'attachTo()', () => {
+		let target, limiter;
+
+		beforeEach( () => {
+			limiter = document.createElement( 'div' );
+			target = document.createElement( 'div' );
+
+			// Mock balloon panel element dimensions.
+			mockBoundingBox( view.element, {
+				top: 0,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			// Make sure that limiter is fully visible in viewport.
+			Object.assign( windowStub, {
+				innerWidth: 500,
+				innerHeight: 500
+			} );
+		} );
+
+		it( 'should use default options', () => {
+			const spy = testUtils.sinon.spy( positionUtils, 'getOptimalPosition' );
+
+			view.attachTo( { target } );
+
+			sinon.assert.calledWithExactly( spy, sinon.match( {
+				element: view.element,
+				target: target,
+				positions: [
+					BalloonPanelView.defaultPositions.se,
+					BalloonPanelView.defaultPositions.sw,
+					BalloonPanelView.defaultPositions.ne,
+					BalloonPanelView.defaultPositions.nw
+				],
+				limiter: document.body,
+				fitInViewport: true
+			} ) );
+		} );
+
+		describe( 'limited by limiter element', () => {
+			beforeEach( () => {
+				// Mock limiter element dimensions.
+				mockBoundingBox( limiter, {
+					left: 0,
+					top: 0,
+					width: 500,
+					height: 500
+				} );
+			} );
+
+			it( 'should put balloon on the `south east` side of the target element at default', () => {
+				// Place target element at the center of the limiter.
+				mockBoundingBox( target, {
+					top: 225,
+					left: 225,
+					width: 50,
+					height: 50
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'se' );
+			} );
+
+			it( 'should put balloon on the `south east` side of the target element when target is on the top left side of the limiter', () => {
+				mockBoundingBox( target, {
+					top: 0,
+					left: 0,
+					width: 50,
+					height: 50
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'se' );
+			} );
+
+			it( 'should put balloon on the `south west` side of the target element when target is on the right side of the limiter', () => {
+				mockBoundingBox( target, {
+					top: 0,
+					left: 450,
+					width: 50,
+					height: 50
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'sw' );
+			} );
+
+			it( 'should put balloon on the `north east` side of the target element when target is on the bottom of the limiter ', () => {
+				mockBoundingBox( target, {
+					top: 450,
+					left: 0,
+					width: 50,
+					height: 50
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'ne' );
+			} );
+
+			it( 'should put balloon on the `north west` side of the target element when target is on the bottom right of the limiter', () => {
+				mockBoundingBox( target, {
+					top: 450,
+					left: 450,
+					width: 50,
+					height: 50
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'nw' );
+			} );
+
+			// #126
+			it( 'works in a positioned ancestor (position: absolute)', () => {
+				const positionedAncestor = document.createElement( 'div' );
+
+				positionedAncestor.style.position = 'absolute';
+				positionedAncestor.style.top = '100px';
+				positionedAncestor.style.left = '100px';
+				positionedAncestor.appendChild( view.element );
+
+				document.body.appendChild( positionedAncestor );
+				positionedAncestor.appendChild( view.element );
+
+				mockBoundingBox( positionedAncestor, {
+					top: 100,
+					left: 100,
+					width: 10,
+					height: 10
+				} );
+
+				mockBoundingBox( target, {
+					top: 0,
+					left: 0,
+					width: 100,
+					height: 100
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.top ).to.equal( 15 );
+				expect( view.left ).to.equal( -80 );
+			} );
+
+			// #126
+			it( 'works in a positioned ancestor (position: static)', () => {
+				const positionedAncestor = document.createElement( 'div' );
+
+				positionedAncestor.style.position = 'static';
+				positionedAncestor.appendChild( view.element );
+
+				document.body.appendChild( positionedAncestor );
+				positionedAncestor.appendChild( view.element );
+
+				mockBoundingBox( target, {
+					top: 0,
+					left: 0,
+					width: 100,
+					height: 100
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.top ).to.equal( 115 );
+				expect( view.left ).to.equal( 20 );
+			} );
+		} );
+
+		describe( 'limited by viewport', () => {
+			it( 'should put balloon on the `south west` position when `south east` is limited', () => {
+				mockBoundingBox( limiter, {
+					left: 0,
+					top: 0,
+					width: 500,
+					height: 500
+				} );
+
+				mockBoundingBox( target, {
+					top: 0,
+					left: 225,
+					width: 50,
+					height: 50
+				} );
+
+				Object.assign( windowStub, {
+					innerWidth: 275
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'sw' );
+			} );
+
+			it( 'should put balloon on the `south east` position when `south west` is limited', () => {
+				mockBoundingBox( limiter, {
+					top: 0,
+					left: -400,
+					width: 500,
+					height: 500
+				} );
+
+				mockBoundingBox( target, {
+					top: 0,
+					left: 0,
+					width: 50,
+					height: 50
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'se' );
+			} );
+
+			it( 'should put balloon on the `north east` position when `south east` is limited', () => {
+				mockBoundingBox( limiter, {
+					left: 0,
+					top: 0,
+					width: 500,
+					height: 500
+				} );
+
+				mockBoundingBox( target, {
+					top: 225,
+					left: 0,
+					width: 50,
+					height: 50
+				} );
+
+				Object.assign( windowStub, {
+					innerHeight: 275
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'ne' );
+			} );
+
+			it( 'should put balloon on the `south east` position when `north east` is limited', () => {
+				mockBoundingBox( limiter, {
+					left: 0,
+					top: -400,
+					width: 500,
+					height: 500
+				} );
+
+				mockBoundingBox( target, {
+					top: 0,
+					left: 0,
+					width: 50,
+					height: 50
+				} );
+
+				view.attachTo( { target, limiter } );
+
+				expect( view.position ).to.equal( 'se' );
+			} );
+		} );
+	} );
+} );
+
+function mockBoundingBox( element, data ) {
+	const boundingBox = Object.assign( {}, data );
+
+	boundingBox.right = boundingBox.left + boundingBox.width;
+	boundingBox.bottom = boundingBox.top + boundingBox.height;
+
+	testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( boundingBox );
+}

+ 125 - 0
packages/ckeditor5-ui/tests/bindings/clickoutsidehandler.js

@@ -0,0 +1,125 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, Event */
+
+import clickOutsideHandler from 'ckeditor5-ui/src/bindings/clickoutsidehandler';
+
+import DomEmitterMixin from 'ckeditor5-utils/src/dom/emittermixin';
+
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'clickOutsideHandler', () => {
+	let activator, actionSpy, contextElement;
+
+	beforeEach( () => {
+		activator = testUtils.sinon.stub().returns( false );
+		contextElement = document.createElement( 'div' );
+		actionSpy = testUtils.sinon.spy();
+
+		document.body.appendChild( contextElement );
+
+		clickOutsideHandler( {
+			emitter: Object.create( DomEmitterMixin ),
+			activator: activator,
+			contextElement: contextElement,
+			callback: actionSpy
+		} );
+	} );
+
+	afterEach( () => {
+		document.body.removeChild( contextElement );
+	} );
+
+	it( 'should fired callback after clicking out of context element when listener is active', () => {
+		activator.returns( true );
+
+		document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		sinon.assert.calledOnce( actionSpy );
+	} );
+
+	it( 'should not fired callback after clicking out of context element when listener is not active', () => {
+		activator.returns( false );
+
+		document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		sinon.assert.notCalled( actionSpy );
+	} );
+
+	it( 'should not fired callback after clicking on context element when listener is active', () => {
+		activator.returns( true );
+
+		contextElement.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		sinon.assert.notCalled( actionSpy );
+	} );
+
+	it( 'should not fired callback after clicking on context element when listener is not active', () => {
+		activator.returns( false );
+
+		contextElement.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		sinon.assert.notCalled( actionSpy );
+	} );
+
+	it( 'should listen when model initial `ifActive` value was `true`', () => {
+		const spy = testUtils.sinon.spy();
+
+		activator.returns( true );
+
+		clickOutsideHandler( {
+			emitter: Object.create( DomEmitterMixin ),
+			activator: activator,
+			contextElement: contextElement,
+			callback: spy
+		} );
+
+		document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		sinon.assert.calledOnce( spy );
+	} );
+
+	it( 'should not listen when model initial `ifActive` value was `false`', () => {
+		const spy = testUtils.sinon.spy();
+
+		activator.returns( false );
+
+		clickOutsideHandler( {
+			emitter: Object.create( DomEmitterMixin ),
+			activator: activator,
+			contextElement: contextElement,
+			callback: spy
+		} );
+
+		document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		sinon.assert.notCalled( spy );
+	} );
+
+	it( 'should react on model `ifActive` property change', () => {
+		activator.returns( true );
+
+		document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		sinon.assert.calledOnce( actionSpy );
+
+		activator.returns( false );
+
+		document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		// Still called once, was not called second time.
+		sinon.assert.calledOnce( actionSpy );
+
+		activator.returns( true );
+
+		document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+
+		// Called one more time.
+		sinon.assert.calledTwice( actionSpy );
+	} );
+} );

+ 141 - 0
packages/ckeditor5-ui/tests/bindings/escpresshandler.js

@@ -0,0 +1,141 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import escPressHandler from 'ckeditor5-ui/src/bindings/escpresshandler';
+
+import DomEmitterMixin from 'ckeditor5-utils/src/dom/emittermixin';
+import { keyCodes } from 'ckeditor5-utils/src/keyboard';
+
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'escPressHandler', () => {
+	let emitter, actionSpy, activator;
+
+	beforeEach( () => {
+		activator = testUtils.sinon.stub().returns( false );
+		actionSpy = testUtils.sinon.spy();
+		emitter = Object.create( DomEmitterMixin );
+
+		escPressHandler( {
+			emitter: emitter,
+			activator: activator,
+			callback: actionSpy
+		} );
+	} );
+
+	afterEach( () => {
+		emitter.stopListening();
+	} );
+
+	it( 'should fired callback after pressing `Esc` when listener is active', () => {
+		activator.returns( true );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
+
+		sinon.assert.calledOnce( actionSpy );
+	} );
+
+	it( 'should not fired callback after pressing a key different than `Esc`', () => {
+		activator.returns( true );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.ctrlKey );
+
+		sinon.assert.notCalled( actionSpy );
+	} );
+
+	it( 'should not fired callback after pressing Esc when listener is not active', () => {
+		activator.returns( false );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.enter );
+
+		sinon.assert.notCalled( actionSpy );
+	} );
+
+	it( 'should not fired callback after pressing other than Esc key when listener is active', () => {
+		activator.returns( false );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
+
+		sinon.assert.notCalled( actionSpy );
+	} );
+
+	it( 'should listen when model initial `ifActive` value was `true`', () => {
+		const spy = testUtils.sinon.spy();
+
+		activator.returns( true );
+
+		emitter = Object.create( DomEmitterMixin );
+
+		escPressHandler( {
+			emitter: emitter,
+			activator: activator,
+			callback: spy
+		} );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
+
+		sinon.assert.calledOnce( spy );
+	} );
+
+	it( 'should not listen when model initial `ifActive` value was `false`', () => {
+		const spy = testUtils.sinon.spy();
+
+		activator.returns( false );
+
+		emitter = Object.create( DomEmitterMixin );
+
+		escPressHandler( {
+			emitter: emitter,
+			activator: activator,
+			callback: spy
+		} );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
+
+		sinon.assert.notCalled( spy );
+	} );
+
+	it( 'should react on model `ifActive` property change', () => {
+		activator.returns( true );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
+
+		sinon.assert.calledOnce( actionSpy );
+
+		activator.returns( false );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
+
+		// Still called once, was not called second time.
+		sinon.assert.calledOnce( actionSpy );
+
+		activator.returns( true );
+
+		dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
+
+		// Called one more time.
+		sinon.assert.calledTwice( actionSpy );
+	} );
+} );
+
+// Creates and dispatches keyboard event with specified keyCode.
+//
+// @private
+// @param {EventTarget} eventTarget
+// @param {String} eventName
+// @param {Number} keyCode
+function dispatchKeyboardEvent( eventTarget, eventName, keyCode ) {
+	const event = document.createEvent( 'Events' );
+
+	event.initEvent( eventName, true, true );
+	event.which = keyCode;
+	event.keyCode = keyCode;
+
+	eventTarget.dispatchEvent( event );
+}

+ 39 - 0
packages/ckeditor5-ui/tests/bindings/submithandler.js

@@ -0,0 +1,39 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, Event */
+
+import submitHandler from 'ckeditor5-ui/src/bindings/submithandler';
+
+import View from 'ckeditor5-ui/src/view';
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'submitHandler', () => {
+	let view;
+
+	beforeEach( () => {
+		view = new View();
+		view.element = document.createElement( 'div' );
+		view.element.child = document.createElement( 'input' );
+
+		view.element.appendChild( view.element.child );
+
+		submitHandler( { view } );
+	} );
+
+	it( 'should fire #submit event on the view and prevent the native DOM #submit', ( done ) => {
+		const evt = new Event( 'submit' );
+		const spy = sinon.spy( evt, 'preventDefault' );
+
+		view.on( 'submit', () => {
+			sinon.assert.calledOnce( spy );
+			done();
+		} );
+
+		view.element.child.dispatchEvent( evt );
+	} );
+} );

+ 170 - 0
packages/ckeditor5-ui/tests/button/buttonview.js

@@ -0,0 +1,170 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals Event */
+/* bender-tags: ui, button */
+
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+import ButtonView from 'ckeditor5-ui/src/button/buttonview';
+import IconView from 'ckeditor5-ui/src/icon/iconview';
+
+testUtils.createSinonSandbox();
+
+describe( 'ButtonView', () => {
+	let locale, view;
+
+	beforeEach( () => {
+		locale = { t() {} };
+
+		return ( view = new ButtonView( locale ) ).init();
+	} );
+
+	describe( '<button> bindings', () => {
+		describe( 'class', () => {
+			it( 'is set initially', () => {
+				expect( view.element.classList ).to.have.length( 4 );
+				expect( view.element.classList.contains( 'ck-button' ) ).to.true;
+				expect( view.element.classList.contains( 'ck-off' ) ).to.true;
+				expect( view.element.classList.contains( 'ck-tooltip_s' ) ).to.true;
+			} );
+
+			it( 'reacts on view#isEnabled', () => {
+				view.isEnabled = true;
+				expect( view.element.classList.contains( 'ck-disabled' ) ).to.false;
+
+				view.isEnabled = false;
+				expect( view.element.classList.contains( 'ck-disabled' ) ).to.true;
+			} );
+
+			it( 'reacts on view#isOn', () => {
+				view.isOn = true;
+				expect( view.element.classList.contains( 'ck-on' ) ).to.true;
+
+				view.isOn = false;
+				expect( view.element.classList.contains( 'ck-on' ) ).to.false;
+			} );
+
+			it( 'reacts on view#withText', () => {
+				view.withText = true;
+				expect( view.element.classList.contains( 'ck-button_with-text' ) ).to.true;
+
+				view.withText = false;
+				expect( view.element.classList.contains( 'ck-button_with-text' ) ).to.false;
+			} );
+
+			it( 'reacts on view#type', () => {
+				// Default value.
+				expect( view.element.getAttribute( 'type' ) ).to.equal( 'button' );
+
+				view.type = 'submit';
+				expect( view.element.getAttribute( 'type' ) ).to.equal( 'submit' );
+
+				// Default value.
+				view.type = null;
+				expect( view.element.getAttribute( 'type' ) ).to.equal( 'button' );
+			} );
+		} );
+
+		describe( 'tooltip', () => {
+			it( 'is not initially set ', () => {
+				expect( view.element.dataset.ckTooltip ).to.undefined;
+			} );
+
+			it( 'is always equal to view#title if is defined', () => {
+				view.title = 'bar';
+				view.label = 'foo';
+				view.keystroke = 'A';
+
+				expect( view.element.dataset.ckTooltip ).to.equal( 'bar' );
+			} );
+
+			it( 'is equal to view#label when view#title is not defined', () => {
+				view.label = 'bar';
+
+				expect( view.element.dataset.ckTooltip ).to.equal( 'bar' );
+			} );
+
+			it( 'contains keystroke when view#label and view#keystroke is defined', () => {
+				view.label = 'bar';
+				view.keystroke = 'A';
+
+				expect( view.element.dataset.ckTooltip ).to.equal( 'bar (A)' );
+			} );
+		} );
+
+		describe( 'text', () => {
+			it( 'is not initially set ', () => {
+				expect( view.element.textContent ).to.equal( '' );
+			} );
+
+			it( 'reacts on view#label', () => {
+				view.label = 'bar';
+
+				expect( view.element.textContent ).to.equal( 'bar' );
+			} );
+		} );
+
+		describe( 'mousedown event', () => {
+			it( 'should be prevented', () => {
+				const ret = view.element.dispatchEvent( new Event( 'mousedown', { cancelable: true } ) );
+
+				expect( ret ).to.false;
+			} );
+		} );
+
+		describe( 'execute event', () => {
+			it( 'triggers view#execute event if button is not disabled', () => {
+				const spy = sinon.spy();
+
+				view.on( 'execute', spy );
+				view.set( 'isEnabled', true );
+
+				view.element.dispatchEvent( new Event( 'click' ) );
+				sinon.assert.callCount( spy, 1 );
+
+				view.isEnabled = false;
+
+				view.element.dispatchEvent( new Event( 'click' ) );
+				sinon.assert.callCount( spy, 1 );
+			} );
+		} );
+	} );
+
+	describe( 'icon', () => {
+		it( 'is not initially set', () => {
+			expect( view.element.childNodes ).to.have.length( 1 );
+			expect( view.iconView ).to.undefined;
+		} );
+
+		it( 'is set when view#icon is defined', () => {
+			view = new ButtonView( locale );
+			view.icon = 'foo';
+
+			return view.init().then( () => {
+				expect( view.element.childNodes ).to.have.length( 2 );
+				expect( view.element.childNodes[ 0 ] ).to.equal( view.iconView.element );
+
+				expect( view.iconView ).to.instanceOf( IconView );
+				expect( view.iconView.content ).to.equal( 'foo' );
+
+				view.icon = 'bar';
+				expect( view.iconView.content ).to.equal( 'bar' );
+			} );
+		} );
+
+		it( 'is destroyed with the view', () => {
+			view = new ButtonView( locale );
+			view.icon = 'foo';
+
+			return view.init().then( () => {
+				const spy = sinon.spy( view.iconView, 'destroy' );
+
+				return view.destroy().then( () => {
+					sinon.assert.calledOnce( spy );
+				} );
+			} );
+		} );
+	} );
+} );

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

@@ -0,0 +1,65 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, dropdown */
+
+import utilsTestUtils from 'ckeditor5-utils/tests/_utils/utils';
+import createDropdown from 'ckeditor5-ui/src/dropdown/createdropdown';
+import Model from 'ckeditor5-ui/src/model';
+import DropdownView from 'ckeditor5-ui/src/dropdown/dropdownview';
+import DropdownPanelView from 'ckeditor5-ui/src/dropdown/dropdownpanelview';
+import ButtonView from 'ckeditor5-ui/src/button/buttonview';
+
+const assertBinding = utilsTestUtils.assertBinding;
+
+describe( 'createDropdown', () => {
+	it( 'accepts model', () => {
+		const modelDef = {
+			label: 'foo',
+			isOn: false,
+			isEnabled: true,
+			withText: false
+		};
+
+		const model = new Model( modelDef );
+		const view = createDropdown( model );
+
+		assertBinding( view.buttonView,
+			modelDef,
+			[
+				[ model, { label: 'bar', isOn: true, isEnabled: false, withText: true } ]
+			],
+			{ label: 'bar', isOn: true, isEnabled: false, withText: true }
+		);
+	} );
+
+	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 );
+	} );
+} );
+

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

@@ -0,0 +1,52 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, dropdown */
+
+import ViewCollection from 'ckeditor5-ui/src/viewcollection';
+import DropdownPanelView from 'ckeditor5-ui/src/dropdown/dropdownpanelview';
+
+describe( 'DropdownPanelView', () => {
+	let view, locale;
+
+	beforeEach( () => {
+		locale = { t() {} };
+
+		return ( view = new DropdownPanelView( locale ) ).init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets view#locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'sets view#isVisible false', () => {
+			expect( view.isVisible ).to.be.false;
+		} );
+
+		it( 'creates view#children collection', () => {
+			expect( view.children ).to.be.instanceOf( ViewCollection );
+		} );
+
+		it( 'creates element from template', () => {
+			expect( view.element.classList.contains( 'ck-reset' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-dropdown__panel' ) ).to.be.true;
+		} );
+
+		describe( 'template bindings', () => {
+			describe( 'class', () => {
+				it( 'reacts on view#isVisible', () => {
+					expect( view.element.classList.contains( 'ck-dropdown__panel-visible' ) ).to.be.false;
+
+					view.isVisible = true;
+					expect( view.element.classList.contains( 'ck-dropdown__panel-visible' ) ).to.be.true;
+
+					view.isVisible = false;
+					expect( view.element.classList.contains( 'ck-dropdown__panel-visible' ) ).to.be.false;
+				} );
+			} );
+		} );
+	} );
+} );

+ 85 - 0
packages/ckeditor5-ui/tests/dropdown/dropdownview.js

@@ -0,0 +1,85 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, dropdown */
+
+import DropdownView from 'ckeditor5-ui/src/dropdown/dropdownview';
+import ButtonView from 'ckeditor5-ui/src/button/buttonview';
+import DropdownPanelView from 'ckeditor5-ui/src/dropdown/dropdownpanelview';
+
+describe( 'DropdownView', () => {
+	let view, buttonView, panelView, locale;
+
+	beforeEach( () => {
+		locale = { t() {} };
+
+		buttonView = new ButtonView( locale );
+		panelView = new DropdownPanelView( locale );
+
+		return ( view = new DropdownView( locale, buttonView, panelView ) ).init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets view#locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'sets view#buttonView', () => {
+			expect( view.buttonView ).to.equal( buttonView );
+		} );
+
+		it( 'sets view#panelView', () => {
+			expect( view.panelView ).to.equal( panelView );
+		} );
+
+		it( 'sets view#isOpen false', () => {
+			expect( view.isOpen ).to.be.false;
+		} );
+
+		it( 'creates #element from template', () => {
+			expect( view.element.classList.contains( 'ck-dropdown' ) ).to.be.true;
+			expect( view.element.firstChild ).to.equal( buttonView.element );
+			expect( view.element.lastChild ).to.equal( panelView.element );
+		} );
+
+		it( 'sets view#buttonView class', () => {
+			expect( view.buttonView.element.classList.contains( 'ck-dropdown__button' ) ).to.be.true;
+		} );
+
+		describe( 'bindings', () => {
+			describe( 'view#isOpen to view.buttonView#execute', () => {
+				it( 'is activated', () => {
+					const values = [];
+
+					view.on( 'change:isOpen', () => {
+						values.push( view.isOpen );
+					} );
+
+					view.buttonView.fire( 'execute' );
+					view.buttonView.fire( 'execute' );
+					view.buttonView.fire( 'execute' );
+
+					expect( values ).to.have.members( [ true, false, true ] );
+				} );
+			} );
+
+			describe( 'view.panelView#isVisible to view#isOpen', () => {
+				it( 'is activated', () => {
+					const values = [];
+
+					view.listenTo( view.panelView, 'change:isVisible', () => {
+						values.push( view.isOpen );
+					} );
+
+					view.isOpen = true;
+					view.isOpen = false;
+					view.isOpen = true;
+
+					expect( values ).to.have.members( [ true, false, true ] );
+				} );
+			} );
+		} );
+	} );
+} );

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

@@ -0,0 +1,151 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document, Event */
+/* bender-tags: ui, dropdown */
+
+import Model from 'ckeditor5-ui/src/model';
+import createListDropdown from 'ckeditor5-ui/src/dropdown/list/createlistdropdown';
+import Collection from 'ckeditor5-utils/src/collection';
+import ListView from 'ckeditor5-ui/src/list/listview';
+import ListItemView from 'ckeditor5-ui/src/list/listitemview';
+
+describe( 'createListDropdown', () => {
+	let view, model, locale, items;
+
+	beforeEach( () => {
+		locale = { t() {} };
+		items = new Collection();
+		model = new Model( {
+			isEnabled: true,
+			items: items,
+			isOn: false,
+			label: 'foo'
+		} );
+
+		return ( view = createListDropdown( model, locale ) ).init().then( () => {
+			document.body.appendChild( view.element );
+		} );
+	} );
+
+	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;
+			expect( Object.keys( view._listeningTo ) ).to.have.length( 2 );
+
+			// Fire event from outside of the dropdown.
+			document.body.dispatchEvent( new Event( 'click', {
+				bubbles: true
+			} ) );
+
+			// Closed the dropdown.
+			expect( view.isOpen ).to.be.false;
+			expect( Object.keys( view._listeningTo ) ).to.have.length( 1 );
+
+			// Fire event from outside of the dropdown.
+			document.body.dispatchEvent( new Event( 'click', {
+				bubbles: true
+			} ) );
+
+			// Dropdown is still closed.
+			expect( view.isOpen ).to.be.false;
+			expect( Object.keys( view._listeningTo ) ).to.have.length( 1 );
+		} );
+
+		it( 'listens to view#isOpen and reacts to DOM events (invalid target)', () => {
+			// Open the dropdown.
+			view.isOpen = true;
+			expect( Object.keys( view._listeningTo ) ).to.have.length( 2 );
+
+			// Event from view.element should be discarded.
+			view.element.dispatchEvent( new Event( 'click', {
+				bubbles: true
+			} ) );
+
+			// Dropdown is still open.
+			expect( view.isOpen ).to.be.true;
+			expect( Object.keys( view._listeningTo ) ).to.have.length( 2 );
+
+			// Event from within view.element should be discarded.
+			const child = document.createElement( 'div' );
+			view.element.appendChild( child );
+
+			child.dispatchEvent( new Event( 'click', {
+				bubbles: true
+			} ) );
+
+			// Dropdown is still open.
+			expect( view.isOpen ).to.be.true;
+			expect( Object.keys( view._listeningTo ) ).to.have.length( 2 );
+		} );
+	} );
+} );

+ 19 - 0
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.html

@@ -0,0 +1,19 @@
+<head>
+	<link rel="stylesheet" href="/theme/ckeditor.css">
+</head>
+
+<h2>Empty</h2>
+
+<div id="dropdown" class="ck-reset_all"></div>
+
+<h2>ListDropdown</h2>
+
+<div id="list-dropdown" class="ck-reset_all"></div>
+
+<h2>Shared Model</h2>
+
+<div id="dropdown-shared" class="ck-reset_all"></div>
+
+<h2>Long label (truncated)</h2>
+
+<div id="dropdown-label" class="ck-reset_all"></div>

+ 98 - 0
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js

@@ -0,0 +1,98 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document, window */
+
+import Model from 'ckeditor5-ui/src/model';
+import Collection from 'ckeditor5-utils/src/collection';
+
+import createDropdown from 'ckeditor5-ui/src/dropdown/createDropdown';
+import createListDropdown from 'ckeditor5-ui/src/dropdown/list/createListDropdown';
+
+function renderInto( selector, view ) {
+	view.init().then( () => {
+		document.querySelector( selector ).appendChild( view.element );
+	} );
+}
+
+function testEmpty() {
+	const dropdownView = createDropdown( new Model( {
+		label: 'Dropdown',
+		isEnabled: true,
+		isOn: false,
+		withText: true
+	} ) );
+
+	dropdownView.panelView.element.innerHTML = 'Empty panel. There is no child view in this DropdownPanelView.';
+
+	renderInto( '#dropdown', dropdownView );
+}
+
+function testList() {
+	const collection = new Collection( { idProperty: 'label' } );
+
+	[ '0.8em', '1em', '1.2em', '1.5em', '2.0em', '3.0em' ].forEach( font => {
+		collection.add( new Model( {
+			label: font,
+			style: `font-size: ${ font }`
+		} ) );
+	} );
+
+	const model = new Model( {
+		label: 'ListDropdown',
+		isEnabled: true,
+		isOn: false,
+		withText: true,
+		items: collection
+	} );
+
+	const dropdownView = createListDropdown( model );
+
+	dropdownView.on( 'execute', ( evt ) => {
+		/* global console */
+		console.log( 'List#execute:', evt.source.label );
+	} );
+
+	renderInto( '#list-dropdown', 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 );
+
+	renderInto( '#dropdown-shared', dropdownView1 );
+	renderInto( '#dropdown-shared', dropdownView2 );
+
+	dropdownView1.panelView.element.innerHTML = dropdownView2.panelView.element.innerHTML = 'Empty panel.';
+}
+
+function testLongLabel() {
+	const dropdownView = createDropdown( new Model( {
+		label: 'Dropdown with a very long label',
+		isEnabled: true,
+		isOn: false,
+		withText: true
+	} ) );
+
+	renderInto( '#dropdown-label', dropdownView );
+
+	dropdownView.panelView.element.innerHTML = 'Empty panel. There is no child view in this DropdownPanelView.';
+}
+
+testEmpty();
+testList();
+testSharedModel();
+testLongLabel();

+ 30 - 0
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.md

@@ -0,0 +1,30 @@
+1. Make sure each dropdown opens when clicked.
+2. The panel that shows up should be positioned precisely below and to the left edge of the parent dropdown.
+4. The panel that shows up should float **over** the rest of the page.
+5. Each dropdown should have **equal** width.
+
+## ListDropdown
+
+1. `ListDropdown` should close when any of the dropdown items is clicked. Clicked item's model should be logged in console.
+2. `ListDropdown` should close when clicked beyond the dropdown like page body or other elements.
+
+## Long label
+
+1. Make sure that the label is truncated and followed by *"..."*.
+2. Dropdown button "triangle" should be placed after *"..."*.
+3. Text should not wrap to the next line.
+
+### Notes:
+
+* Play with `listDropdownModel.isOn` to control its "on/off" state.
+* Play with `listDropdownModel.isEnabled` to control whether it's disabled.
+* Play with `listDropdownModel.label` to control dropdown label.
+* Play with `listDropdownCollection`, i.e. add new item:
+```
+listDropdownCollection.add(
+	new window.Model( {
+		label: '2.5em',
+		style: 'font-size: 2.5em'
+	} )
+);
+```

+ 84 - 0
packages/ckeditor5-ui/tests/editableui/editableuiview.js

@@ -0,0 +1,84 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+/* bender-tags: editable */
+
+import EditableUIView from 'ckeditor5-ui/src/editableui/editableuiview';
+import Locale from 'ckeditor5-utils/src/locale';
+
+describe( 'EditableUIView', () => {
+	let view, editableElement, locale;
+
+	beforeEach( () => {
+		locale = new Locale( 'en' );
+		editableElement = document.createElement( 'div' );
+
+		return ( view = new EditableUIView( locale ) ).init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets initial values of attributes', () => {
+			expect( view.isReadOnly ).to.be.false;
+			expect( view.isFocused ).to.be.false;
+		} );
+
+		it( 'renders element from template when no editableElement', () => {
+			view = new EditableUIView( locale );
+
+			return view.init().then( () => {
+				expect( view.element ).to.equal( view.editableElement );
+				expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
+			} );
+		} );
+
+		it( 'accepts editableElement as an argument', () => {
+			view = new EditableUIView( locale, editableElement );
+
+			return view.init().then( () => {
+				expect( view.element ).to.equal( editableElement );
+				expect( view.element ).to.equal( view.editableElement );
+				expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
+			} );
+		} );
+	} );
+
+	describe( 'View bindings', () => {
+		describe( 'class', () => {
+			it( 'reacts on view#isFocused', () => {
+				view.isFocused = true;
+
+				expect( view.element.classList.contains( 'ck-focused' ) ).to.be.true;
+				expect( view.element.classList.contains( 'ck-blurred' ) ).to.be.false;
+
+				view.isFocused = false;
+				expect( view.element.classList.contains( 'ck-focused' ) ).to.be.false;
+				expect( view.element.classList.contains( 'ck-blurred' ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'contenteditable', () => {
+			it( 'reacts on view#isReadOnly', () => {
+				view.isReadOnly = true;
+				expect( view.element.hasAttribute( 'contenteditable' ) ).to.be.false;
+
+				view.isReadOnly = false;
+				expect( view.element.hasAttribute( 'contenteditable' ) ).to.be.true;
+			} );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'updates contentEditable property of editableElement', () => {
+			return new EditableUIView( locale, editableElement ).init().then( () => {
+				expect( view.editableElement.contentEditable ).to.equal( 'true' );
+			} )
+			.then( () => view.destroy() )
+			.then( () => {
+				expect( view.editableElement.contentEditable ).to.equal( 'false' );
+			} );
+		} );
+	} );
+} );

+ 66 - 0
packages/ckeditor5-ui/tests/editableui/inline/inlineeditableuiview.js

@@ -0,0 +1,66 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+/* bender-tags: editable */
+
+import InlineEditableUIView from 'ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
+import Locale from 'ckeditor5-utils/src/locale';
+
+describe( 'InlineEditableUIView', () => {
+	let view, editableElement, locale;
+
+	beforeEach( () => {
+		locale = new Locale( 'en' );
+		editableElement = document.createElement( 'div' );
+
+		return ( view = new InlineEditableUIView( locale ) ).init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'accepts locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'sets initial values of attributes', () => {
+			expect( view.name ).to.be.null;
+		} );
+
+		it( 'accepts editableElement', () => {
+			view = new InlineEditableUIView( locale, editableElement );
+
+			expect( view.element ).to.equal( editableElement );
+		} );
+
+		it( 'creates view#element from template when no editableElement provided', () => {
+			expect( view.template ).to.be.an( 'object' );
+		} );
+	} );
+
+	describe( 'editableElement', () => {
+		const ariaLabel = 'Rich Text Editor, foo';
+
+		beforeEach( () => {
+			view.name = 'foo';
+		} );
+
+		it( 'has proper accessibility role', () => {
+			expect( view.element.attributes.getNamedItem( 'role' ).value ).to.equal( 'textbox' );
+		} );
+
+		it( 'has proper ARIA label', () => {
+			expect( view.element.getAttribute( 'aria-label' ) ).to.equal( ariaLabel );
+		} );
+
+		it( 'has proper title', () => {
+			expect( view.element.getAttribute( 'title' ) ).to.equal( ariaLabel );
+		} );
+
+		it( 'has proper class name', () => {
+			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-editor__editable_inline' ) ).to.be.true;
+		} );
+	} );
+} );

+ 66 - 0
packages/ckeditor5-ui/tests/editorui/boxed/boxededitoruiview.js

@@ -0,0 +1,66 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Editor from 'ckeditor5-core/src/editor/editor';
+import BoxedEditorUIView from 'ckeditor5-ui/src/editorui/boxed/boxededitoruiview';
+import Locale from 'ckeditor5-utils/src/locale';
+import ViewCollection from 'ckeditor5-ui/src/viewcollection';
+
+describe( 'BoxedEditorUIView', () => {
+	let view, element, editor;
+
+	beforeEach( () => {
+		editor = new Editor();
+		view = new BoxedEditorUIView( new Locale( 'en' ) );
+		element = view.element;
+
+		return view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'adds view collections', () => {
+			expect( view.top ).to.be.instanceof( ViewCollection );
+			expect( view.main ).to.be.instanceof( ViewCollection );
+		} );
+
+		it( 'sets "width" and "height" attributes', () => {
+			expect( view.width ).to.equal( null );
+			expect( view.height ).to.equal( null );
+		} );
+
+		it( 'bootstraps the view element from template', () => {
+			expect( view.element.classList.contains( 'ck-editor' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-reset' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
+			expect( element.attributes[ 'aria-labelledby' ].value )
+				.to.equal( view.element.firstChild.id )
+				.to.match( /^cke-editor__aria-label_\w+$/ );
+		} );
+
+		it( 'bootstraps the voice label from template', () => {
+			const voiceLabel = view.element.firstChild;
+
+			expect( voiceLabel.classList.contains( 'cke-voice-label' ) ).to.be.true;
+			expect( voiceLabel.textContent ).to.equal( 'Rich Text Editor' );
+		} );
+
+		it( 'setups accessibility of the view element', () => {
+			expect( element.attributes.getNamedItem( 'aria-labelledby' ).value ).to.equal(
+				view.element.firstChild.id );
+			expect( element.attributes.getNamedItem( 'role' ).value ).to.equal( 'application' );
+			expect( element.attributes.getNamedItem( 'lang' ).value ).to.equal( 'en' );
+		} );
+
+		it( 'bootstraps the view region elements from template', () => {
+			expect( element.childNodes[ 1 ].classList.contains( 'ck-editor__top' ) ).to.be.true;
+			expect( element.childNodes[ 2 ].classList.contains( 'ck-editor__main' ) ).to.be.true;
+		} );
+
+		it( 'setups accessibility of the view region elements', () => {
+			expect( element.childNodes[ 1 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
+			expect( element.childNodes[ 2 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
+		} );
+	} );
+} );

+ 78 - 0
packages/ckeditor5-ui/tests/editorui/editoruiview.js

@@ -0,0 +1,78 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+import EditorUIView from 'ckeditor5-ui/src/editorui/editoruiview';
+import ViewCollection from 'ckeditor5-ui/src/viewcollection';
+import Locale from 'ckeditor5-utils/src/locale';
+
+testUtils.createSinonSandbox();
+
+describe( 'EditorUIView', () => {
+	let view, locale;
+
+	beforeEach( () => {
+		locale = new Locale( 'en' );
+		view = new EditorUIView( locale );
+
+		return view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'accepts locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'sets all the properties', () => {
+			expect( view.body ).to.be.instanceof( ViewCollection );
+		} );
+
+		it( 'sets the right class set to the body region', () => {
+			const el = view._bodyCollectionContainer;
+
+			expect( el.classList.contains( 'ck-body' ) ).to.be.true;
+			expect( el.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
+			expect( el.classList.contains( 'ck-reset_all' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'init', () => {
+		it( 'calls view#_setupIconManager', () => {
+			view = new EditorUIView( locale );
+			const spy = testUtils.sinon.spy( view, '_setupIconManager' );
+
+			return view.init().then( () => {
+				expect( spy.calledOnce ).to.be.true;
+			} );
+		} );
+	} );
+
+	describe( 'view#_setupIconManager', () => {
+		it( 'injects the manager into DOM', () => {
+			view._setupIconManager().then( () => {
+				const iconManagerElement = view._bodyCollectionContainer.firstChild;
+
+				expect( iconManagerElement.classList.contains( 'ck-icon-manager__sprite' ) ).to.be.true;
+			} );
+		} );
+
+		it( 'sets view#icon property', () => {
+			view._setupIconManager().then( () => {
+				expect( view.icons ).to.be.an( 'array' );
+				expect( view.icons ).to.not.be.empty;
+			} );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'removes the body region container', () => {
+			const el = view._bodyCollectionContainer;
+
+			return view.destroy().then( () => {
+				expect( el.parentNode ).to.be.null;
+			} );
+		} );
+	} );
+} );

+ 59 - 0
packages/ckeditor5-ui/tests/icon/iconview.js

@@ -0,0 +1,59 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, icon */
+
+import IconView from 'ckeditor5-ui/src/icon/iconview';
+
+describe( 'IconView', () => {
+	let view;
+
+	beforeEach( () => {
+		return ( view = new IconView() ).init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'creates element from template', () => {
+			expect( view.element.tagName ).to.equal( 'svg' );
+			expect( view.element.getAttribute( 'class' ) ).to.equal( 'ck-icon' );
+			expect( view.element.getAttribute( 'viewBox' ) ).to.equal( '0 0 20 20' );
+		} );
+	} );
+
+	describe( '<svg> bindings', () => {
+		describe( 'viewBox', () => {
+			it( 'should react to changes in view#viewBox', () => {
+				expect( view.element.getAttribute( 'viewBox' ) ).to.equal( '0 0 20 20' );
+
+				view.viewBox = '1 2 3 4';
+
+				expect( view.element.getAttribute( 'viewBox' ) ).to.equal( '1 2 3 4' );
+			} );
+		} );
+
+		describe( 'inline svg', () => {
+			it( 'should react to changes in view#content', () => {
+				view.content = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="test"></g></svg>';
+
+				expect( view.element.innerHTML = '<g id="test"></g>' );
+			} );
+		} );
+
+		describe( 'legacy xlink:href', () => {
+			it( 'reacts to changes in view#content', () => {
+				const svgHrefNs = 'http://www.w3.org/1999/xlink';
+				let svgUseElement;
+
+				view.content = 'foo';
+				svgUseElement = view.element.firstChild;
+				expect( svgUseElement.getAttributeNS( svgHrefNs, 'href' ) ).to.equal( '#ck-icon-foo' );
+
+				view.content = 'abc';
+				svgUseElement = view.element.firstChild;
+				expect( svgUseElement.getAttributeNS( svgHrefNs, 'href' ) ).to.equal( '#ck-icon-abc' );
+			} );
+		} );
+	} );
+} );

+ 41 - 0
packages/ckeditor5-ui/tests/iconmanager/iconmanagerview.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, iconmanager */
+
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+import IconManagerView from 'ckeditor5-ui/src/iconmanager/iconmanagerview';
+
+testUtils.createSinonSandbox();
+
+describe( 'IconManagerView', () => {
+	let view, sprite, icons;
+
+	beforeEach( () => {
+		sprite = '<symbol><title>foo</title></symbol>';
+		icons = [ 'foo' ];
+		view = new IconManagerView( sprite, icons );
+
+		return view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets initial view attribute values', () => {
+			expect( view.sprite ).to.equal( sprite );
+			expect( view.icons ).to.equal( icons );
+		} );
+
+		it( 'creates element from template', () => {
+			expect( view.element.tagName ).to.equal( 'svg' );
+			expect( view.element.getAttribute( 'class' ) ).to.equal( 'ck-icon-manager__sprite' );
+		} );
+	} );
+
+	describe( 'init', () => {
+		it( 'initializes the sprite', () => {
+			expect( view.element.innerHTML ).to.equal( '<symbol><title>foo</title></symbol>' );
+		} );
+	} );
+} );

+ 57 - 0
packages/ckeditor5-ui/tests/iframe/iframeview.js

@@ -0,0 +1,57 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+/* bender-tags: ui, iframe */
+
+import IframeView from 'ckeditor5-ui/src/iframe/iframeview';
+
+describe( 'IframeView', () => {
+	let view;
+
+	describe( 'constructor()', () => {
+		it( 'creates view element from the template', () => {
+			view = new IframeView();
+			document.body.appendChild( view.element );
+
+			expect( view.element.classList.contains( 'ck-reset_all' ) ).to.be.true;
+			expect( view.element.attributes.getNamedItem( 'sandbox' ).value ).to.equal( 'allow-same-origin allow-scripts' );
+		} );
+	} );
+
+	describe( 'init', () => {
+		it( 'returns promise', () => {
+			view = new IframeView();
+
+			expect( view.init() ).to.be.an.instanceof( Promise );
+		} );
+
+		it( 'returns promise which is resolved when iframe finished loading', () => {
+			view = new IframeView();
+
+			const promise = view.init().then( () => {
+				expect( view.element.contentDocument.readyState ).to.equal( 'complete' );
+			} );
+
+			// Moving iframe into DOM trigger creation of a document inside iframe.
+			document.body.appendChild( view.element );
+
+			return promise;
+		} );
+	} );
+
+	describe( 'loaded event', () => {
+		it( 'is fired when frame finished loading', ( done ) => {
+			view = new IframeView();
+
+			view.on( 'loaded', () => done() );
+
+			view.init();
+
+			// Moving iframe into DOM trigger creation of a document inside iframe.
+			document.body.appendChild( view.element );
+		} );
+	} );
+} );

+ 74 - 0
packages/ckeditor5-ui/tests/inputtext/inputtextview.js

@@ -0,0 +1,74 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, input */
+
+import InputTextView from 'ckeditor5-ui/src/inputtext/inputtextview';
+
+describe( 'InputTextView', () => {
+	let view;
+
+	beforeEach( () => {
+		view = new InputTextView();
+
+		view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should creates element from template', () => {
+			expect( view.element.tagName ).to.equal( 'INPUT' );
+			expect( view.element.type ).to.equal( 'text' );
+			expect( view.element.classList.contains( 'ck-input' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-input-text' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'DOM bindings', () => {
+		beforeEach( () => {
+			view.value = 'foo';
+			view.id = 'bar';
+		} );
+
+		describe( 'value', () => {
+			it( 'should react on view#value', () => {
+				expect( view.element.value ).to.equal( 'foo' );
+
+				view.value = 'baz';
+
+				expect( view.element.value ).to.equal( 'baz' );
+			} );
+
+			it( 'should set to empty string when using `falsy` values', () => {
+				[ undefined, false, null ].forEach( ( value ) => {
+					view.value = value;
+
+					expect( view.element.value ).to.equal( '' );
+				} );
+			} );
+		} );
+
+		describe( 'id', () => {
+			it( 'should react on view#id', () => {
+				expect( view.element.id ).to.equal( 'bar' );
+
+				view.id = 'baz';
+
+				expect( view.element.id ).to.equal( 'baz' );
+			} );
+		} );
+	} );
+
+	describe( 'select', () => {
+		it( 'should select input value', () => {
+			const selectSpy = sinon.spy( view.element, 'select' );
+
+			view.select();
+
+			expect( selectSpy.calledOnce ).to.true;
+
+			selectSpy.restore();
+		} );
+	} );
+} );

+ 52 - 0
packages/ckeditor5-ui/tests/label/labelview.js

@@ -0,0 +1,52 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, label */
+
+import LabelView from 'ckeditor5-ui/src/label/labelview';
+
+describe( 'LabelView', () => {
+	let view;
+
+	beforeEach( () => {
+		view = new LabelView();
+
+		view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should create element from template', () => {
+			expect( view.element.tagName ).to.equal( 'LABEL' );
+			expect( view.element.classList.contains( 'ck-label' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'DOM bindings', () => {
+		beforeEach( () => {
+			view.text = 'foo';
+			view.for = 'bar';
+		} );
+
+		describe( 'text content', () => {
+			it( 'should react on view#text', () => {
+				expect( view.element.textContent ).to.equal( 'foo' );
+
+				view.text = 'baz';
+
+				expect( view.element.textContent ).to.equal( 'baz' );
+			} );
+		} );
+
+		describe( 'for attribute', () => {
+			it( 'should react on view#for', () => {
+				expect( view.element.getAttribute( 'for' ) ).to.equal( 'bar' );
+
+				view.for = 'baz';
+
+				expect( view.element.getAttribute( 'for' ) ).to.equal( 'baz' );
+			} );
+		} );
+	} );
+} );

+ 74 - 0
packages/ckeditor5-ui/tests/labeledinput/labeledinputview.js

@@ -0,0 +1,74 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, input */
+
+import LabeledInputView from 'ckeditor5-ui/src/labeledinput/labeledinputview';
+import InputView from 'ckeditor5-ui/src/inputtext/inputtextview';
+import LabelView from 'ckeditor5-ui/src/label/labelview';
+
+describe( 'LabeledInputView', () => {
+	const locale = {};
+
+	let view;
+
+	beforeEach( () => {
+		view = new LabeledInputView( locale, InputView );
+
+		view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should set view#locale', () => {
+			expect( view.locale ).to.deep.equal( locale );
+		} );
+
+		it( 'should create view#inputView', () => {
+			expect( view.inputView ).to.instanceOf( InputView );
+		} );
+
+		it( 'should create view#labelView', () => {
+			expect( view.labelView ).to.instanceOf( LabelView );
+		} );
+
+		it( 'should pair inputView and labelView by unique id', () => {
+			expect( view.labelView.for ).to.equal( view.inputView.id ).to.ok;
+		} );
+	} );
+
+	describe( 'template', () => {
+		it( 'should have label view', () => {
+			expect( view.template.children.get( 0 ) ).to.equal( view.labelView );
+		} );
+
+		it( 'should have input view', () => {
+			expect( view.template.children.get( 1 ) ).to.equal( view.inputView );
+		} );
+	} );
+
+	describe( 'binding', () => {
+		it( 'should bind view#text to view.labelView#label', () => {
+			view.label = 'Foo bar';
+
+			expect( view.labelView.text ).to.equal( 'Foo bar' );
+		} );
+
+		it( 'should bind view#value to view.inputView#value', () => {
+			view.value = 'Lorem ipsum';
+
+			expect( view.inputView.value ).to.equal( 'Lorem ipsum' );
+		} );
+	} );
+
+	describe( 'select', () => {
+		it( 'should select input value', () => {
+			const spy = sinon.spy( view.inputView, 'select' );
+
+			view.select();
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+} );

+ 62 - 0
packages/ckeditor5-ui/tests/list/listitemview.js

@@ -0,0 +1,62 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals Event */
+/* bender-tags: ui, list */
+
+import ListItemView from 'ckeditor5-ui/src/list/listitemview';
+
+describe( 'ListItemView', () => {
+	let view;
+
+	beforeEach( () => {
+		view = new ListItemView();
+		view.set( {
+			style: 'foo',
+			label: 'bar'
+		} );
+
+		return view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'creates element from template', () => {
+			expect( view.element.classList.contains( 'ck-list__item' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'DOM bindings', () => {
+		describe( '"style" attribute', () => {
+			it( 'reacts on view#style', () => {
+				expect( view.element.attributes.getNamedItem( 'style' ).value ).to.equal( 'foo' );
+
+				view.style = 'color: red';
+
+				expect( view.element.attributes.getNamedItem( 'style' ).value ).to.equal( 'color: red' );
+			} );
+		} );
+
+		describe( 'text content', () => {
+			it( 'reacts on view#label', () => {
+				expect( view.element.innerHTML ).to.equal( 'bar' );
+
+				view.label = 'baz';
+
+				expect( view.element.innerHTML ).to.equal( 'baz' );
+			} );
+		} );
+
+		describe( 'view#execute event', () => {
+			it( 'triggers view#execute event when "click" is fired in DOM', () => {
+				const spy = sinon.spy();
+
+				view.on( 'execute', spy );
+
+				view.element.dispatchEvent( new Event( 'click' ) );
+				expect( spy.calledOnce ).to.be.true;
+			} );
+		} );
+	} );
+} );

+ 30 - 0
packages/ckeditor5-ui/tests/list/listview.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, list */
+
+import ViewCollection from 'ckeditor5-ui/src/viewcollection';
+import ListView from 'ckeditor5-ui/src/list/listview';
+
+describe( 'ListView', () => {
+	let view;
+
+	beforeEach( () => {
+		return ( view = new ListView() ).init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'creates element from template', () => {
+			expect( view.element.classList.contains( 'ck-reset' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-list' ) ).to.be.true;
+		} );
+
+		it( 'creates view#children collection', () => {
+			expect( view.items ).to.be.instanceOf( ViewCollection );
+			expect( view.template.children ).to.have.length( 1 );
+			expect( view.template.children.get( 0 ) ).to.equal( view.items );
+		} );
+	} );
+} );

+ 16 - 0
packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.html

@@ -0,0 +1,16 @@
+<head>
+	<link rel="stylesheet" href="/theme/ckeditor.css">
+</head>
+
+<div id="editor">
+	<p><em>This</em> is a <strong>first line</strong> of text.</p>
+	<p><em>This</em> is a <strong>second line</strong> of text.</p>
+	<p><em>This</em> is the <strong>end</strong> of text.</p>
+</div>
+
+<style>
+	.ck-editor {
+		margin: 5em auto;
+		max-width: 70%;
+	}
+</style>

+ 131 - 0
packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.js

@@ -0,0 +1,131 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window, document, console:false */
+
+import ClassicEditor from 'ckeditor5-editor-classic/src/classic';
+import DomEventObserver from 'ckeditor5-engine/src/view/observer/domeventobserver';
+import Enter from 'ckeditor5-enter/src/enter';
+import Typing from 'ckeditor5-typing/src/typing';
+import Paragraph from 'ckeditor5-paragraph/src/paragraph';
+import Undo from 'ckeditor5-undo/src/undo';
+import Bold from 'ckeditor5-basic-styles/src/bold';
+import Italic from 'ckeditor5-basic-styles/src/italic';
+
+import Template from 'ckeditor5-ui/src/template';
+import ToolbarView from 'ckeditor5-ui/src/toolbar/toolbarview';
+import BalloonPanelView from 'ckeditor5-ui/src/balloonpanel/balloonpanelview';
+
+const arrowVOffset = BalloonPanelView.arrowVerticalOffset;
+const positions = {
+	//     [text range]
+	//                ^
+	//       +-----------------+
+	//       |     Balloon     |
+	//       +-----------------+
+	forwardSelection: ( targetRect, balloonRect ) => ( {
+		top: targetRect.bottom + arrowVOffset,
+		left: targetRect.right - balloonRect.width / 2,
+		name: 's'
+	} ),
+
+	//	+-----------------+
+	//	|     Balloon     |
+	//	+-----------------+
+	//	        V
+	//	        [text range]
+	backwardSelection: ( targetRect, balloonRect ) => ( {
+		top: targetRect.top - balloonRect.height - arrowVOffset,
+		left: targetRect.left - balloonRect.width / 2,
+		name: 'n'
+	} )
+};
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	plugins: [ Enter, Typing, Paragraph, Undo, Bold, Italic ],
+	toolbar: [ 'bold', 'italic', 'undo', 'redo' ]
+} )
+.then( editor => {
+	createContextualToolbar( editor );
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );
+
+function createContextualToolbar( editor ) {
+	// Create a plain toolbar instance.
+	const toolbar = new ToolbarView();
+
+	// Create a BalloonPanelView instance.
+	const panel = new BalloonPanelView( editor.locale );
+
+	Template.extend( panel.template, {
+		attributes: {
+			class: [
+				'ck-toolbar__container',
+			]
+		}
+	} );
+
+	// Putting the toolbar inside of the balloon panel.
+	panel.content.add( toolbar );
+
+	editor.ui.view.body.add( panel ).then( () => {
+		const editingView = editor.editing.view;
+
+		// Fill the toolbar with some buttons. Simply copy default editor toolbar.
+		for ( let name of editor.config.get( 'toolbar' ) ) {
+			toolbar.items.add( editor.ui.componentFactory.create( name ) );
+		}
+
+		// Let the focusTracker know about new focusable UI element.
+		editor.ui.focusTracker.add( panel.element );
+
+		// Hide the panel when editor loses focus but no the other way around.
+		panel.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is, was ) => {
+			if ( was && !is ) {
+				panel.hide();
+			}
+		} );
+
+		// Add "mouseup" event observer. It's enought to use ClickObserver in Chrome
+		// but Firefox requires "mouseup" to work properly.
+		editingView.addObserver( class extends DomEventObserver {
+			get domEventType() {
+				return [ 'mouseup' ];
+			}
+
+			onDomEvent( domEvent ) {
+				this.fire( domEvent.type, domEvent );
+			}
+		} );
+
+		// Position the panel each time the user clicked in editable.
+		editor.listenTo( editingView, 'mouseup', () => {
+			// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
+			if ( !editingView.selection.isCollapsed ) {
+				const isBackward = editingView.selection.isBackward;
+
+				// getBoundingClientRect() makes no sense when the selection spans across number
+				// of lines of text. Using getClientRects() allows us to browse micro–ranges
+				// that would normally make up the bounding client rect.
+				const rangeRects = editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ).getClientRects();
+
+				// Select the proper range rect depending on the direction of the selection.
+				const rangeRect = isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 );
+
+				panel.attachTo( {
+					target: rangeRect,
+					positions: [
+						positions[ isBackward ? 'backwardSelection' : 'forwardSelection' ]
+					]
+				} );
+			} else {
+				panel.hide();
+			}
+		} );
+	} );
+}

+ 5 - 0
packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.md

@@ -0,0 +1,5 @@
+## Contextual toolbar demo
+
+1. Create a non–collapsed selection.
+2. Create another non–collapsed selection but in another direction.
+3. For each selection, a contextual toolbar should appear and the beginning/end of the selection, duplicating main editor toolbar.

+ 15 - 0
packages/ckeditor5-ui/tests/manual/icon/icon.html

@@ -0,0 +1,15 @@
+<head>
+	<link rel="stylesheet" href="/theme/ckeditor.css">
+
+	<style>
+		#inline-svg li {
+			width: 150px;
+			text-align: center;
+			min-height: 60px;
+		}
+	</style>
+</head>
+
+<h2>Icons with embed SVG</h2>
+
+<ol id="inline-svg"></ol>

+ 81 - 0
packages/ckeditor5-ui/tests/manual/icon/icon.js

@@ -0,0 +1,81 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import IconView from 'ckeditor5-ui/src/icon/iconview';
+
+const wrapper = document.querySelector( '#inline-svg' );
+
+const icon = `
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
+    <!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch -->
+    <title>image</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
+        <g id="image" sketch:type="MSArtboardGroup" fill="#454545">
+            <g id="icon:image" sketch:type="MSLayerGroup" transform="translate(2.000000, 3.000000)">
+                <path d="M0,11.9941413 C0,13.1019465 0.894513756,14 1.99406028,14 L14.0059397,14 C15.1072288,14 16,13.1029399 16,11.9941413
+                 L16,2.00585866 C16,0.898053512 15.1054862,0 14.0059397,0 L1.99406028,0 C0.892771196,0 0,0.897060126 0,2.00585866 
+                 L0,11.9941413 Z M1,2.00247329 C1,1.44882258 1.44994876,1 2.00684547,1 L13.9931545,1 C14.5492199,1 15,1.45576096 
+                 15,2.00247329 L15,11.9975267 C15,12.5511774 14.5500512,13 13.9931545,13 L2.00684547,13 C1.45078007,13 1,12.544239 
+                 1,11.9975267 L1,2.00247329 Z M2.0237314,12.0028573 L14,12.0028573 L14,8.90598928 L11.1099289,4.64285714 
+                 L8.01350775,9.9000001 L5.01091767,7.79714291 L2,10.9601769 L2.0237314,12.0028573 Z M4.40625001,3 C3.62959688,3
+                  3,3.62360071 3,4.39285714 C3,5.16210429 3.62959688,5.78571429 4.40625001,5.78571429 C5.18289376,5.78571429 
+                  5.81250002,5.16210429 5.81250002,4.39285714 C5.81250002,3.62360071 5.18289376,3 4.40625001,3 L4.40625001,3 Z" 
+                  id="path4700" sketch:type="MSShapeGroup"></path>
+            </g>
+        </g>
+    </g>
+</svg>`;
+
+// Small.
+addCase( renderIcon( icon, 20 ) );
+
+// Medium.
+addCase( renderIcon( icon, 40 ) );
+
+// Large.
+addCase( renderIcon( icon, 60 ) );
+
+// Color.
+addCase( renderIcon( icon, 60, 'red' ) );
+
+// Inherited color.
+const iconWrapper = document.createElement( 'p' );
+iconWrapper.style.color = 'blue';
+iconWrapper.appendChild( document.createTextNode( 'foo' ) );
+iconWrapper.appendChild( renderIcon( icon, 60 ) );
+iconWrapper.appendChild( document.createTextNode( 'bar' ) );
+addCase( iconWrapper );
+
+function renderIcon( content, size, color ) {
+	const iconView = new IconView();
+
+	iconView.content = content;
+	iconView.init();
+
+	if ( size ) {
+		iconView.element.style.width = `${ size }px`;
+		iconView.element.style.height = `${ size }px`;
+	}
+
+	if ( color ) {
+		iconView.element.style.color = color;
+	}
+
+	return iconView.element;
+}
+
+function addCase( el ) {
+	const item = document.createElement( 'li' );
+
+	item.appendChild( el );
+
+	wrapper.appendChild( item );
+}

+ 7 - 0
packages/ckeditor5-ui/tests/manual/icon/icon.md

@@ -0,0 +1,7 @@
+## Icons with inline SVG
+
+1. First icon should be small size and dark color.
+1. Second icon should be medium size and dark color.
+1. Third icon should be large size and dark color.
+1. Fourth icon should be large size and red color.
+1. Fifth icon should be large size and blue color (inherited).

+ 37 - 0
packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.html

@@ -0,0 +1,37 @@
+<head>
+	<link rel="stylesheet" href="/theme/ckeditor.css">
+</head>
+
+<div id="editor">
+	<p><em>This</em> is a <strong>first line</strong> of text.</p>
+	<figure class="image">
+		<img src="img/logo.png" alt="CKEditor logo" />
+	</figure>
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <i>Sint ista Graecorum;</i> Istam voluptatem, inquit, Epicurus ignorat? <b>Idem iste, inquam, de voluptate quid sentit?</b> <mark>Peccata paria.</mark> Omnes enim iucundum motum, quo sensus hilaretur. Quae iam oratio non a philosopho aliquo, sed a censore opprimenda est. </p>
+	<p>Sed hoc sane concedamus. Quamvis enim depravatae non sint, pravae tamen esse possunt. Non dolere, inquam, istud quam vim habeat postea videro; Nam adhuc, meo fortasse vitio, quid ego quaeram non perspicis. Expectoque quid ad id, quod quaerebam, respondeas. Quod idem cum vestri faciant, non satis magnam tribuunt inventoribus gratiam. </p>
+	<p>Parvi enim primo ortu sic iacent, tamquam omnino sine animo sint. Vide, quantum, inquam, fallare, Torquate. Bonum integritas corporis: misera debilitas. Quae autem natura suae primae institutionis oblita est? </p>
+	<p>Nemo igitur esse beatus potest. Etsi ea quidem, quae adhuc dixisti, quamvis ad aetatem recte isto modo dicerentur. <i>Sed fortuna fortis;</i> <b>An nisi populari fama?</b> Paupertas si malum est, mendicus beatus esse nemo potest, quamvis sit sapiens. Ait enim se, si uratur, Quam hoc suave! dicturum. <i>Minime vero, inquit ille, consentit.</i> Esse enim, nisi eris, non potes. </p>
+	<p>Cum autem venissemus in Academiae non sine causa nobilitata spatia, solitudo erat ea, quam volueramus. <i>Duo Reges: constructio interrete.</i> Non enim solum Torquatus dixit quid sentiret, sed etiam cur. Obsecro, inquit, Torquate, haec dicit Epicurus? Primum divisit ineleganter; </p>
+	<p><em>This</em> is the <strong>end</strong> of text.</p>
+	<figure class="image">
+		<img src="img/logo.png" alt="CKEditor logo" />
+	</figure>
+</div>
+
+<style>
+	.ck-editor {
+		margin: 5em auto;
+		max-width: 70%;
+	}
+
+	.ck-editor .image {
+		font-size: 0;
+		line-height: 0;
+		max-width: 80%;
+		margin: 0 auto;
+	}
+
+	.ck-editor .image img {
+		max-width: 100%;
+	}
+</style>

+ 122 - 0
packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.js

@@ -0,0 +1,122 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window, document, console:false */
+
+import ClassicEditor from 'ckeditor5-editor-classic/src/classic';
+import ClickObserver from 'ckeditor5-engine/src/view/observer/clickobserver';
+import Enter from 'ckeditor5-enter/src/enter';
+import Typing from 'ckeditor5-typing/src/typing';
+import Paragraph from 'ckeditor5-paragraph/src/paragraph';
+import Undo from 'ckeditor5-undo/src/undo';
+import Image from 'ckeditor5-image/src/image';
+import Bold from 'ckeditor5-basic-styles/src/bold';
+import Italic from 'ckeditor5-basic-styles/src/italic';
+
+import Template from 'ckeditor5-ui/src/template';
+import ToolbarView from 'ckeditor5-ui/src/toolbar/toolbarview';
+import BalloonPanelView from 'ckeditor5-ui/src/balloonpanel/balloonpanelview';
+
+const arrowVOffset = BalloonPanelView.arrowVerticalOffset;
+const positions = {
+	//          [text range]
+	//                ^
+	//       +-----------------+
+	//       |     Balloon     |
+	//       +-----------------+
+	south: ( targetRect, balloonRect ) => ( {
+		top: targetRect.bottom + arrowVOffset,
+		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
+		name: 's'
+	} ),
+
+	//	+-----------------+
+	//	|     Balloon     |
+	//	+-----------------+
+	//	        V
+	//	   [text range]
+	north: ( targetRect, balloonRect ) => ( {
+		top: targetRect.top - balloonRect.height - arrowVOffset,
+		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
+		name: 'n'
+	} )
+};
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	plugins: [ Enter, Typing, Paragraph, Undo, Bold, Italic, Image ],
+	toolbar: [ 'bold', 'italic', 'undo', 'redo' ]
+} )
+.then( editor => {
+	createImageToolbar( editor );
+
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );
+
+function createImageToolbar( editor ) {
+	// Create a plain toolbar instance.
+	const toolbar = new ToolbarView();
+
+	// Create a BalloonPanelView instance.
+	const panel = new BalloonPanelView( editor.locale );
+
+	Template.extend( panel.template, {
+		attributes: {
+			class: [
+				'ck-toolbar__container',
+			]
+		}
+	} );
+
+	// Putting the toolbar inside of the balloon panel.
+	panel.content.add( toolbar );
+
+	editor.ui.view.body.add( panel ).then( () => {
+		const editingView = editor.editing.view;
+
+		// Fill the toolbar with some buttons. Simply copy default editor toolbar.
+		for ( let name of editor.config.get( 'toolbar' ) ) {
+			toolbar.items.add( editor.ui.componentFactory.create( name ) );
+		}
+
+		// Let the focusTracker know about new focusable UI element.
+		editor.ui.focusTracker.add( panel.element );
+
+		// Hide the panel when editor loses focus but no the other way around.
+		panel.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is, was ) => {
+			if ( was && !is ) {
+				panel.hide();
+			}
+		} );
+
+		editingView.addObserver( ClickObserver );
+
+		// Check if the toolbar should be displayed each time the user clicked in editable.
+		editor.listenTo( editingView, 'click', () => {
+			if ( editingView.selection.isFake ) {
+				attachToolbar();
+
+				// TODO: These 2 need interval–based event debouncing for performance
+				// reasons. I guess even lodash offers such a helper.
+				editor.ui.view.listenTo( window, 'scroll', attachToolbar );
+				editor.ui.view.listenTo( window, 'resize', attachToolbar );
+			} else {
+				panel.hide();
+
+				editor.ui.view.stopListening( window, 'scroll', attachToolbar );
+				editor.ui.view.stopListening( window, 'resize', attachToolbar );
+			}
+		} );
+
+		function attachToolbar() {
+			panel.attachTo( {
+				target: editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ),
+				positions: [ positions.north, positions.south ]
+			} );
+		}
+	} );
+}

+ 6 - 0
packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.md

@@ -0,0 +1,6 @@
+## Image toolbar demo
+
+1. Click on the image (select it).
+1. A toolbar should appear, pinned to the horizontal middle point on the top of the image.
+1. Scroll the window – the toolbar should change it's position to remain visible for as long as possible.
+1. Resize the window – the toolbar should remain pinned to the image.

BIN
packages/ckeditor5-ui/tests/manual/imagetoolbar/img/logo.png


+ 55 - 0
packages/ckeditor5-ui/tests/manual/stickytoolbarview/stickytoolbarview.html

@@ -0,0 +1,55 @@
+<head>
+	<link rel="stylesheet" href="/theme/ckeditor.css">
+</head>
+
+<div class="ck-reset ck-editor position-relative">
+	<div class="ck-reset_all ck-editor__top">
+	</div>
+	<p class="ck-reset">
+		An editable content mock–up.
+		An editable content mock–up.
+		An editable content mock–up.
+		An editable content mock–up.
+		An editable content mock–up.
+		An editable content mock–up.
+		An editable content mock–up.
+		An editable content mock–up.
+	</p>
+	<div class="ck-reset_all offset-visualizer">
+	</div>
+</div>
+
+<style>
+	.ck-editor {
+		height: 200px;
+		max-width: 400px;
+		margin: 5em;
+		background: #ccc;
+	}
+
+	.ck-toolbar {
+		background: #fff !important;
+	}
+
+	.ck-toolbar:after {
+		content: "A toolbar mock–up.";
+	}
+
+	.offset-visualizer {
+		height: 50px;
+		width: 100%;
+		position: absolute;
+		bottom: 0;
+		background: red;
+		color: #fff;
+	}
+
+	.offset-visualizer:after {
+		content: "An offset mock–up. Toolbar should *never* cover the red area.";
+	}
+
+	body {
+		width: 3000px;
+		height: 3000px;
+	}
+</style>

+ 29 - 0
packages/ckeditor5-ui/tests/manual/stickytoolbarview/stickytoolbarview.js

@@ -0,0 +1,29 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console:false */
+
+import testUtils from 'ckeditor5-ui/tests/_utils/utils';
+import StickyToolbarView from 'ckeditor5-ui/src/toolbar/sticky/stickytoolbarview';
+
+testUtils.createTestUIView( {
+	top: '.ck-editor__top'
+} )
+.then( ui => {
+	createToolbar( ui.top );
+} )
+.catch( err => {
+	console.error( err.stack );
+} );
+
+function createToolbar( collection ) {
+	const toolbar = new StickyToolbarView();
+
+	toolbar.limiterElement = collection._parentElement.parentNode;
+
+	collection.add( toolbar ).then( () => {
+		toolbar.isActive = true;
+	} );
+}

+ 16 - 0
packages/ckeditor5-ui/tests/manual/stickytoolbarview/stickytoolbarview.md

@@ -0,0 +1,16 @@
+## Vertical scrolling
+
+1. When the page is scrolled vertically, the toolbar should
+	1. stick to the top of the viewport first,
+	1. then disappear beyond the upper edge of the viewport as it touches the red area
+	1. but never cover the red area or go beyond the upper edge of editor mock–up.
+
+## Horizontal scrolling
+
+1. The toolbar should always fit horizontally within the editor mock–up, regardless of the position of the h– and v–scrolls of the web page.
+
+## On–load positioning
+
+1. Scroll the web page vertically and horizontally, so the toolbar is sticky.
+1. Reload the web page.
+1. The toolbar should get sticky as the page reloads, if the position of scrollbars and the geometry of the viewport creates such a need.

+ 85 - 0
packages/ckeditor5-ui/tests/manual/tickets/126/1.html

@@ -0,0 +1,85 @@
+<head>
+	<link rel="stylesheet" href="/theme/ckeditor.css">
+</head>
+
+<button id="scroll" type="button">Scroll to the squares</button>
+
+<div id="anchors">
+	<div id="anchor-static" class="anchor"></div>
+	<div id="anchor-relative" class="anchor"></div>
+	<div id="anchor-absolute" class="anchor"></div>
+	<div id="anchor-fixed" class="anchor"></div>
+
+	<button id="attach" type="button">Attach the panels</button>
+</div>
+
+<div class="ck-reset_all" id="static-container"></div>
+<div class="ck-reset_all" id="relative-container"></div>
+<div class="ck-reset_all" id="absolute-container"></div>
+<div class="ck-reset_all" id="fixed-container"></div>
+
+<script>
+( () => {
+	/* global document, window  */
+
+	document.querySelector( '#scroll' ).addEventListener( 'click', () => {
+		window.scrollTo( 900, 900 );
+	} );
+
+	document.querySelector( '#attach' ).addEventListener( 'click', () => {
+		window.createPanel( 'static' );
+		window.createPanel( 'relative' );
+		window.createPanel( 'absolute' );
+		window.createPanel( 'fixed' );
+	} );
+} )();
+</script>
+
+<style>
+	body {
+		width: 10000px;
+		height: 10000px;
+	}
+
+	#anchors {
+		margin-top: 1000px;
+		margin-left: 1000px;
+	}
+
+	.anchor {
+		width: 10px;
+		height: 10px;
+		background: red;
+		margin: 5em;
+	}
+
+	#static-container {
+		margin: 5em;
+		padding: 5em;
+		position: static;
+	}
+
+	#relative-container {
+		margin: 5em;
+		padding: 5em;
+		position: relative;
+		top: 10em;
+		left: 10em;
+	}
+
+	#absolute-container {
+		margin: 5em;
+		padding: 5em;
+		position: absolute;
+		top: 4em;
+		right: 4em;
+	}
+
+	#fixed-container {
+		margin: 5em;
+		padding: 5em;
+		position: fixed;
+		right: 14em;
+		top: 6em;
+	}
+</style>

+ 21 - 0
packages/ckeditor5-ui/tests/manual/tickets/126/1.js

@@ -0,0 +1,21 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, window */
+
+import BalloonPanelView from 'ckeditor5-ui/src/balloonpanel/balloonpanelview';
+
+window.createPanel = ( selector ) => {
+	const view = new BalloonPanelView();
+
+	view.element.innerHTML = `Parent of this panel has position:${ selector }.`;
+	view.init().then( () => {
+		document.querySelector( `#${ selector }-container` ).appendChild( view.element );
+
+		view.attachTo( {
+			target: document.querySelector( `#anchor-${ selector }` )
+		} );
+	} );
+};

+ 13 - 0
packages/ckeditor5-ui/tests/manual/tickets/126/1.md

@@ -0,0 +1,13 @@
+### Invalid `BalloonPanelView` positioning [#126](https://github.com/ckeditor/ckeditor5-ui-default/issues/126)
+
+### Pre–requirements
+
+1. Click "**Scroll to the squares**" button.
+1. There should be 4 red squares displayed in this test.
+
+### Test
+
+1. Click "**Attach the panels**" button.
+1. There should be 4 balloon panels displayed in this test.
+1. Each balloon panel should be **perfectly** attached to a square.
+1. Each balloon panel's triangle should hit directly into the spot (square).

+ 15 - 0
packages/ckeditor5-ui/tests/manual/tickets/76/1.html

@@ -0,0 +1,15 @@
+<head>
+    <link rel="stylesheet" href="/theme/ckeditor.css">
+</head>
+
+<div id="editor">
+    <p><a href="http://ckeditor.com">First link</a></p>
+    <p><a href="http://ckeditor.com">First link</a></p>
+    <p></p>
+    <p>SCROLL HERE</p>
+    <p></p>
+    <p><a href="http://ckeditor.com">Third link</a></p>
+</div>
+
+<div style="height: 3000px;"></div>
+<p>Bottom content</p>

+ 23 - 0
packages/ckeditor5-ui/tests/manual/tickets/76/1.js

@@ -0,0 +1,23 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console:false, document, window */
+
+import ClassicEditor from 'ckeditor5-editor-classic/src/classic';
+import Enter from 'ckeditor5-enter/src/enter';
+import Typing from 'ckeditor5-typing/src/typing';
+import Heading from 'ckeditor5-heading/src/heading';
+import Link from 'ckeditor5-link/src/link';
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	plugins: [ Enter, Typing, Heading, Link ],
+	toolbar: [ 'link' ]
+} )
+.then( editor => {
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );

+ 8 - 0
packages/ckeditor5-ui/tests/manual/tickets/76/1.md

@@ -0,0 +1,8 @@
+### Issue [#76](https://github.com/ckeditor/ckeditor5-ui-default/issues/76) manual test
+
+ - make sure that entire editor is visible in browser viewport,
+ - click on the `First link` and check if balloon panel appeared on bottom of the selection,
+ - close panel, click on the `Second link` and check if balloon panel appeared on bottom of the selection,
+ - close panel, click on the `Third link` and check if balloon panel appeared on top of the selection,
+ - close panel, scroll page to the `SCROLL HERE` marker, click on the `Third link` and check
+ if balloon panel appeared on bottom of the selection.

+ 385 - 0
packages/ckeditor5-ui/tests/toolbar/sticky/stickytoolbarview.js

@@ -0,0 +1,385 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+/* bender-tags: ui, toolbar */
+
+import global from 'ckeditor5-utils/src/dom/global';
+import testUtils from 'ckeditor5-core/tests/_utils/utils';
+import StickyToolbarView from 'ckeditor5-ui/src/toolbar/sticky/stickytoolbarview';
+import ToolbarView from 'ckeditor5-ui/src/toolbar/toolbarview';
+import DomEmitterMixin from 'ckeditor5-utils/src/dom/emittermixin';
+
+testUtils.createSinonSandbox();
+
+describe( 'StickyToolbarView', () => {
+	let view, element, limiterElement, locale, windowStub;
+
+	beforeEach( () => {
+		locale = {};
+		limiterElement = document.createElement( 'div' );
+
+		view = new StickyToolbarView( locale );
+		element = view.element;
+
+		// Dummy values just to let non–geometrical tests pass without reference errors.
+		view._toolbarRect = { top: 10, right: 20, bottom: 30, left: 40, width: 50, height: 60 };
+		view._limiterRect = { top: 5, right: 10, bottom: 15, left: 20, width: 25, height: 30 };
+
+		windowStub = Object.create( DomEmitterMixin );
+
+		Object.assign( windowStub, {
+			scrollX: 0,
+			scrollY: 0
+		} );
+
+		testUtils.sinon.stub( global, 'window', windowStub );
+
+		document.body.appendChild( element );
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'inherits from ToolbarView', () => {
+			expect( view ).to.be.instanceof( ToolbarView );
+		} );
+
+		it( 'sets view attributes', () => {
+			expect( view.isSticky ).to.be.false;
+			expect( view.limiterElement ).to.be.null;
+			expect( view.limiterOffset ).to.equal( 50 );
+
+			expect( view._isStickyToTheLimiter ).to.be.false;
+			expect( view._marginLeft ).to.be.null;
+		} );
+
+		it( 'accepts the locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'creates the _elementPlaceholder', () => {
+			expect( view._elementPlaceholder.classList.contains( 'ck-toolbar__placeholder' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'element view bindings', () => {
+		beforeEach( () => {
+			view.limiterElement = limiterElement;
+		} );
+
+		it( 'update the class on view#isSticky change', () => {
+			view.isSticky = false;
+			expect( element.classList.contains( 'ck-toolbar_sticky' ) ).to.be.false;
+
+			view.isSticky = true;
+			expect( element.classList.contains( 'ck-toolbar_sticky' ) ).to.be.true;
+		} );
+
+		it( 'update the class on view#_isStickyToTheLimiter change', () => {
+			view._isStickyToTheLimiter = false;
+			expect( element.classList.contains( 'ck-toolbar_sticky_bottom-limit' ) ).to.be.false;
+
+			view._isStickyToTheLimiter = true;
+			expect( element.classList.contains( 'ck-toolbar_sticky_bottom-limit' ) ).to.be.true;
+		} );
+
+		it( 'update the styles.width on view#isSticky change', () => {
+			testUtils.sinon.stub( view._elementPlaceholder, 'getBoundingClientRect' ).returns( { width: 100 } );
+
+			view.isSticky = false;
+			expect( element.style.width ).to.equal( '' );
+
+			view.isSticky = true;
+			expect( element.style.width ).to.equal( '100px' );
+		} );
+
+		it( 'update the styles.bottom on view#_isStickyToTheLimiter change', () => {
+			view._isStickyToTheLimiter = false;
+			expect( element.style.bottom ).to.equal( '' );
+
+			view._isStickyToTheLimiter = true;
+			expect( element.style.bottom ).to.equal( '50px' );
+		} );
+
+		it( 'update the styles.marginLeft on view#marginLeft change', () => {
+			view._marginLeft = '30px';
+			expect( element.style.marginLeft ).to.equal( '30px' );
+
+			view._marginLeft = '10px';
+			expect( element.style.marginLeft ).to.equal( '10px' );
+		} );
+	} );
+
+	describe( '_elementPlaceholder view bindings', () => {
+		it( 'update the styles.display on view#isSticky change', () => {
+			view.isSticky = false;
+			expect( view._elementPlaceholder.style.display ).to.equal( 'none' );
+
+			view.isSticky = true;
+			expect( view._elementPlaceholder.style.display ).to.equal( 'block' );
+		} );
+
+		it( 'update the styles.height on view#isSticky change', () => {
+			view._toolbarRect = { height: 50 };
+
+			view.isSticky = false;
+			expect( view._elementPlaceholder.style.height ).to.equal( '' );
+
+			view.isSticky = true;
+			expect( view._elementPlaceholder.style.height ).to.equal( '50px' );
+		} );
+	} );
+
+	describe( 'init()', () => {
+		beforeEach( () => {
+			view.limiterElement = limiterElement;
+		} );
+
+		afterEach( () => {
+			return view.destroy();
+		} );
+
+		it( 'calls init on parent class', () => {
+			const spy = testUtils.sinon.spy( ToolbarView.prototype, 'init' );
+
+			view.init();
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
+		it( 'puts the view._elementPlaceholder before view.element', () => {
+			view.init();
+			expect( element.previousSibling ).to.equal( view._elementPlaceholder );
+		} );
+
+		it( 'listens to window#scroll event and calls view._checkIfShouldBeSticky', () => {
+			const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
+
+			view.init();
+			global.window.fire( 'scroll' );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
+		it( 'listens to view.isActive and calls view._checkIfShouldBeSticky', () => {
+			const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
+			expect( spy.notCalled ).to.be.true;
+
+			view.init();
+			view.isActive = true;
+			expect( spy.calledOnce ).to.be.true;
+
+			view.isActive = false;
+			expect( spy.calledTwice ).to.be.true;
+		} );
+	} );
+
+	describe( 'destroy()', () => {
+		it( 'should return a promise', () => {
+			expect( view.destroy() ).to.be.instanceof( Promise );
+		} );
+
+		it( 'calls destroy on parent class', () => {
+			const spy = testUtils.sinon.spy( ToolbarView.prototype, 'destroy' );
+
+			return view.destroy().then( () => {
+				expect( spy.calledOnce ).to.be.true;
+			} );
+		} );
+
+		it( 'removes view._elementPlaceholder from DOM', () => {
+			return view.destroy().then( () => {
+				expect( view._elementPlaceholder.parentNode ).to.be.null;
+			} );
+		} );
+	} );
+
+	describe( '_checkIfShouldBeSticky', () => {
+		beforeEach( () => {
+			view.limiterElement = limiterElement;
+		} );
+
+		describe( 'view.isSticky', () => {
+			beforeEach( () => {
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+			} );
+
+			it( 'is true if beyond the top of the viewport (toolbar is active)', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } );
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+			} );
+
+			it( 'is false if beyond the top of the viewport (toolbar is inactive)', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } );
+				view.isActive = false;
+
+				expect( view.isSticky ).to.be.false;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.false;
+			} );
+
+			it( 'is false if in the viewport (toolbar is active)', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: 10, height: 100 } );
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.false;
+			} );
+
+			it( 'is false if view.limiterElement is smaller than the toolbar and view.limiterOffset (toolbar is active)', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 60 } );
+				view.isActive = true;
+				view.limiterOffset = 50;
+
+				expect( view.isSticky ).to.be.false;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.false;
+			} );
+		} );
+
+		describe( 'view._isStickyToTheLimiter', () => {
+			it( 'is true if view.isSticky is true and reached the bottom edge of view.limiterElement', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: -10,
+					bottom: 10,
+					height: 100
+				} );
+
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+				expect( view._isStickyToTheLimiter ).to.be.true;
+			} );
+
+			it( 'is false if view.isSticky is true and not reached the bottom edge of view.limiterElement', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: -10,
+					bottom: 90,
+					height: 100
+				} );
+
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+			} );
+
+			it( 'is false if view.isSticky is false', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: 10,
+				} );
+
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+			} );
+		} );
+
+		describe( 'view._marginLeft', () => {
+			it( 'is set if view.isSticky is true view._isStickyToTheLimiter is false', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: -10,
+					bottom: 80,
+					height: 100
+				} );
+
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+
+				Object.assign( windowStub, {
+					scrollX: 10,
+					scrollY: 0
+				} );
+
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+				expect( view._marginLeft ).to.equal( null );
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+				expect( view._marginLeft ).to.equal( '-10px' );
+			} );
+
+			it( 'is not set if view._isStickyToTheLimiter is true', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: -10,
+					bottom: 10,
+					left: 60,
+					height: 100
+				} );
+
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+
+				testUtils.sinon.stub( document.body, 'getBoundingClientRect' ).returns( {
+					left: 40
+				} );
+
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+				expect( view._marginLeft ).to.equal( null );
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+				expect( view._isStickyToTheLimiter ).to.be.true;
+				expect( view._marginLeft ).to.equal( null );
+			} );
+
+			it( 'is not set if view.isSticky is false', () => {
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: 10,
+				} );
+
+				view.isActive = true;
+
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+				expect( view._marginLeft ).to.equal( null );
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.false;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+				expect( view._marginLeft ).to.equal( null );
+			} );
+		} );
+	} );
+} );

+ 36 - 0
packages/ckeditor5-ui/tests/toolbar/toolbarview.js

@@ -0,0 +1,36 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: ui, toolbar */
+
+import ToolbarView from 'ckeditor5-ui/src/toolbar/toolbarview';
+import ViewCollection from 'ckeditor5-ui/src/viewcollection';
+
+describe( 'ToolbarView', () => {
+	let locale, view;
+
+	beforeEach( () => {
+		locale = {};
+		view = new ToolbarView( locale );
+
+		return view.init();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should set view#locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'should create view#children collection', () => {
+			expect( view.items ).to.be.instanceOf( ViewCollection );
+		} );
+	} );
+
+	describe( 'template', () => {
+		it( 'should create element from template', () => {
+			expect( view.element.classList.contains( 'ck-toolbar' ) ).to.true;
+		} );
+	} );
+} );