Browse Source

Merge branch 'master' into t/ckeditor5/416d

Aleksander Nowodzinski 6 years ago
parent
commit
2616c97eb2
42 changed files with 547 additions and 158 deletions
  1. 23 1
      packages/ckeditor5-ui/CHANGELOG.md
  2. 1 1
      packages/ckeditor5-ui/docs/api/ui.md
  3. 8 0
      packages/ckeditor5-ui/docs/features/blocktoolbar.md
  4. 3 1
      packages/ckeditor5-ui/lang/contexts.json
  5. 4 4
      packages/ckeditor5-ui/lang/translations/da.po
  6. 41 0
      packages/ckeditor5-ui/lang/translations/en-gb.po
  7. 4 4
      packages/ckeditor5-ui/lang/translations/nl.po
  8. 3 3
      packages/ckeditor5-ui/lang/translations/sr-latn.po
  9. 3 3
      packages/ckeditor5-ui/lang/translations/sr.po
  10. 18 18
      packages/ckeditor5-ui/package.json
  11. 8 0
      packages/ckeditor5-ui/src/button/button.jsdoc
  12. 2 1
      packages/ckeditor5-ui/src/button/buttonview.js
  13. 2 0
      packages/ckeditor5-ui/src/button/switchbuttonview.js
  14. 4 0
      packages/ckeditor5-ui/src/colorgrid/colorgridview.js
  15. 5 1
      packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.js
  16. 28 9
      packages/ckeditor5-ui/src/dropdown/dropdownview.js
  17. 5 1
      packages/ckeditor5-ui/src/dropdown/utils.js
  18. 3 1
      packages/ckeditor5-ui/src/editableui/editableuiview.js
  19. 2 2
      packages/ckeditor5-ui/src/editorui/boxed/boxededitoruiview.js
  20. 3 1
      packages/ckeditor5-ui/src/editorui/editoruiview.js
  21. 2 2
      packages/ckeditor5-ui/src/template.js
  22. 2 0
      packages/ckeditor5-ui/src/toolbar/block/blockbuttonview.js
  23. 18 2
      packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.js
  24. 17 2
      packages/ckeditor5-ui/src/toolbar/toolbarview.js
  25. 4 4
      packages/ckeditor5-ui/src/viewcollection.js
  26. 10 1
      packages/ckeditor5-ui/tests/button/buttonview.js
  27. 4 0
      packages/ckeditor5-ui/tests/button/switchbuttonview.js
  28. 17 0
      packages/ckeditor5-ui/tests/colorgrid/colorgridview.js
  29. 16 0
      packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js
  30. 88 82
      packages/ckeditor5-ui/tests/dropdown/dropdownview.js
  31. 1 1
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js
  32. 5 1
      packages/ckeditor5-ui/tests/dropdown/utils.js
  33. 58 5
      packages/ckeditor5-ui/tests/editableui/editableuiview.js
  34. 1 1
      packages/ckeditor5-ui/tests/editableui/inline/inlineeditableuiview.js
  35. 12 1
      packages/ckeditor5-ui/tests/editorui/boxed/boxededitoruiview.js
  36. 20 1
      packages/ckeditor5-ui/tests/editorui/editoruiview.js
  37. 4 0
      packages/ckeditor5-ui/tests/manual/blocktoolbar/blocktoolbar.html
  38. 4 0
      packages/ckeditor5-ui/tests/toolbar/block/blockbuttonview.js
  39. 37 0
      packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js
  40. 2 2
      packages/ckeditor5-ui/tests/toolbar/enabletoolbarkeyboardfocus.js
  41. 45 2
      packages/ckeditor5-ui/tests/toolbar/toolbarview.js
  42. 10 0
      packages/ckeditor5-ui/theme/mixins/_dir.css

+ 23 - 1
packages/ckeditor5-ui/CHANGELOG.md

@@ -1,11 +1,33 @@
 Changelog
 =========
 
+## [14.0.0](https://github.com/ckeditor/ckeditor5-ui/compare/v13.0.2...v14.0.0) (2019-08-26)
+
+### Features
+
+* Brought support for right–to–left (RTL) languages to various UI components. See [ckeditor/ckeditor5#1151](https://github.com/ckeditor/ckeditor5/issues/1151). ([d6c7f55](https://github.com/ckeditor/ckeditor5-ui/commit/d6c7f55))
+
+### Bug fixes
+
+* `ColorGrid` should set the `#isOn` property value of new `ColorTiles`. See [ckeditor/ckeditor5-font#51](https://github.com/ckeditor/ckeditor5-font/issues/51). ([e89ad60](https://github.com/ckeditor/ckeditor5-ui/commit/e89ad60))
+* All editor toolbars should come with the `role` and `aria-label` attributes. Closes [ckeditor/ckeditor5#1404](https://github.com/ckeditor/ckeditor5/issues/1404). ([bdede90](https://github.com/ckeditor/ckeditor5-ui/commit/bdede90))
+* Screen reader will now properly prompt errors for text inputs. Closes [ckeditor/ckeditor5#1406](https://github.com/ckeditor/ckeditor5/issues/1406). ([3a164b7](https://github.com/ckeditor/ckeditor5-ui/commit/3a164b7))
+
+### Other changes
+
+* The issue tracker for this package was moved to https://github.com/ckeditor/ckeditor5/issues. See [ckeditor/ckeditor5#1988](https://github.com/ckeditor/ckeditor5/issues/1988). ([451df7b](https://github.com/ckeditor/ckeditor5-ui/commit/451df7b))
+* Updated translations. ([eca9fe6](https://github.com/ckeditor/ckeditor5-ui/commit/eca9fe6))
+
+### BREAKING CHANGES
+
+* The `ToolbarView` class requires the [editor locale](https://ckeditor.com/docs/ckeditor5/latest/api/module_utils_locale-Locale.html) to be passed into the constructor.
+
+
 ## [13.0.2](https://github.com/ckeditor/ckeditor5-ui/compare/v13.0.1...v13.0.2) (2019-07-10)
 
 ### Other changes
 
-* Updated translations. ([ad189b6](https://github.com/ckeditor/ckeditor5-ui/commit/ad189b6)) 
+* Updated translations. ([ad189b6](https://github.com/ckeditor/ckeditor5-ui/commit/ad189b6))
 
 
 ## [13.0.1](https://github.com/ckeditor/ckeditor5-ui/compare/v13.0.0...v13.0.1) (2019-07-04)

+ 1 - 1
packages/ckeditor5-ui/docs/api/ui.md

@@ -30,5 +30,5 @@ The source code of this package is available on GitHub in https://github.com/cke
 
 * [`@ckeditor/ckeditor5-ui` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui)
 * [`ckeditor/ckeditor5-ui` on GitHub](https://github.com/ckeditor/ckeditor5-ui)
-* [Issue tracker](https://github.com/ckeditor/ckeditor5-ui/issues)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5/issues)
 * [Changelog](https://github.com/ckeditor/ckeditor5-ui/blob/master/CHANGELOG.md)

+ 8 - 0
packages/ckeditor5-ui/docs/features/blocktoolbar.md

@@ -31,6 +31,14 @@ To adjust the position of the block toolbar button to match the styles of your w
 }
 ```
 
+If you plan to run the editor in a right–to–left (RTL) language, keep in mind the button will be attached to the **right** boundary of the editable area. In that case, make sure the CSS position adjustment works properly by adding the following styles:
+
+```css
+.ck[dir="rtl"] .ck-block-toolbar-button {
+	transform: translateX( 10px );
+}
+```
+
 ## Installation
 
 <info-box hint>

+ 3 - 1
packages/ckeditor5-ui/lang/contexts.json

@@ -4,5 +4,7 @@
 	"Edit block": "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)",
 	"Next": "Label for a button showing the next thing (tab, page, etc.).",
 	"Previous": "Label for a button showing the previous thing (tab, page, etc.).",
-	"%0 of %1": "Label for a 'page X of Y' status of a typical next/previous page navigation."
+	"%0 of %1": "Label for a 'page X of Y' status of a typical next/previous page navigation.",
+	"Editor toolbar": "Label used by assistive technologies describing a generic editor toolbar.",
+	"Dropdown toolbar": "Label used by assistive technologies describing a toolbar displayed inside a dropdown."
 }

+ 4 - 4
packages/ckeditor5-ui/lang/translations/da.po

@@ -26,16 +26,16 @@ msgstr "Wysiwyg editor"
 
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
-msgstr ""
+msgstr "Redigér blok"
 
 msgctxt "Label for a button showing the next thing (tab, page, etc.)."
 msgid "Next"
-msgstr ""
+msgstr "Næste"
 
 msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
 msgid "Previous"
-msgstr ""
+msgstr "Forrige"
 
 msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
 msgid "%0 of %1"
-msgstr ""
+msgstr "%0 af %1"

+ 41 - 0
packages/ckeditor5-ui/lang/translations/en-gb.po

@@ -0,0 +1,41 @@
+# Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: English (United Kingdom) (https://www.transifex.com/ckeditor/teams/11143/en_GB/)\n"
+"Language: en_GB\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Title of the CKEditor5 editor."
+msgid "Rich Text Editor, %0"
+msgstr "Rich Text Editor, %0"
+
+msgctxt "Title of the CKEditor5 editor."
+msgid "Rich Text Editor"
+msgstr "Rich Text Editor"
+
+msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
+msgid "Edit block"
+msgstr "Edit block"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr "Next"
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr "Previous"
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr "%0 of %1"

+ 4 - 4
packages/ckeditor5-ui/lang/translations/nl.po

@@ -26,16 +26,16 @@ msgstr "Tekstbewerker"
 
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
-msgstr ""
+msgstr "Blok aanpassen"
 
 msgctxt "Label for a button showing the next thing (tab, page, etc.)."
 msgid "Next"
-msgstr ""
+msgstr "Volgende"
 
 msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
 msgid "Previous"
-msgstr ""
+msgstr "Vorige"
 
 msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
 msgid "%0 of %1"
-msgstr ""
+msgstr "0% van 1%"

+ 3 - 3
packages/ckeditor5-ui/lang/translations/sr-latn.po

@@ -30,12 +30,12 @@ msgstr "Blok uređivač"
 
 msgctxt "Label for a button showing the next thing (tab, page, etc.)."
 msgid "Next"
-msgstr ""
+msgstr "Sledeći"
 
 msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
 msgid "Previous"
-msgstr ""
+msgstr "Prethodni"
 
 msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
 msgid "%0 of %1"
-msgstr ""
+msgstr "%0 of %1"

+ 3 - 3
packages/ckeditor5-ui/lang/translations/sr.po

@@ -30,12 +30,12 @@ msgstr "Блок уређивач"
 
 msgctxt "Label for a button showing the next thing (tab, page, etc.)."
 msgid "Next"
-msgstr ""
+msgstr "Следећи"
 
 msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
 msgid "Previous"
-msgstr ""
+msgstr "Претходни"
 
 msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
 msgid "%0 of %1"
-msgstr ""
+msgstr "%0 of %1"

+ 18 - 18
packages/ckeditor5-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-ui",
-  "version": "13.0.2",
+  "version": "14.0.0",
   "description": "The UI framework and standard UI library of CKEditor 5.",
   "keywords": [
     "ckeditor",
@@ -9,25 +9,25 @@
     "ckeditor5-lib"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^12.2.1",
-    "@ckeditor/ckeditor5-utils": "^13.0.1",
+    "@ckeditor/ckeditor5-core": "^12.3.0",
+    "@ckeditor/ckeditor5-utils": "^14.0.0",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.3",
-    "@ckeditor/ckeditor5-block-quote": "^11.1.2",
-    "@ckeditor/ckeditor5-editor-balloon": "^12.2.1",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.3",
-    "@ckeditor/ckeditor5-engine": "^13.2.1",
-    "@ckeditor/ckeditor5-enter": "^11.0.4",
-    "@ckeditor/ckeditor5-essentials": "^11.0.4",
-    "@ckeditor/ckeditor5-heading": "^11.0.4",
-    "@ckeditor/ckeditor5-image": "^13.1.2",
-    "@ckeditor/ckeditor5-link": "^11.1.1",
-    "@ckeditor/ckeditor5-list": "^12.0.4",
-    "@ckeditor/ckeditor5-mention": "^12.0.1",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.4",
-    "@ckeditor/ckeditor5-typing": "^12.1.1",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.4",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.3",
+    "@ckeditor/ckeditor5-editor-balloon": "^12.2.2",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.4",
+    "@ckeditor/ckeditor5-engine": "^14.0.0",
+    "@ckeditor/ckeditor5-enter": "^11.1.0",
+    "@ckeditor/ckeditor5-essentials": "^11.0.5",
+    "@ckeditor/ckeditor5-heading": "^11.0.5",
+    "@ckeditor/ckeditor5-image": "^14.0.0",
+    "@ckeditor/ckeditor5-link": "^11.1.2",
+    "@ckeditor/ckeditor5-list": "^12.1.0",
+    "@ckeditor/ckeditor5-mention": "^13.0.0",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.5",
+    "@ckeditor/ckeditor5-typing": "^12.2.0",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
@@ -40,7 +40,7 @@
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
   "homepage": "https://ckeditor.com/ckeditor-5",
-  "bugs": "https://github.com/ckeditor/ckeditor5-ui/issues",
+  "bugs": "https://github.com/ckeditor/ckeditor5/issues",
   "repository": {
     "type": "git",
     "url": "https://github.com/ckeditor/ckeditor5-ui.git"

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

@@ -95,6 +95,14 @@
  * @member {Boolean} #isVisible
  */
 
+/**
+ * Controls whether the button view is a toggle button (two–state) for assistive technologies.
+ *
+ * @observable
+ * @default false
+ * @member {Boolean} #isToggleable
+ */
+
 /**
  * (Optional) Controls whether the label of the button is hidden (e.g. an icon–only button).
  *

+ 2 - 1
packages/ckeditor5-ui/src/button/buttonview.js

@@ -52,6 +52,7 @@ export default class ButtonView extends View {
 		this.set( 'isEnabled', true );
 		this.set( 'isOn', false );
 		this.set( 'isVisible', true );
+		this.set( 'isToggleable', false );
 		this.set( 'keystroke' );
 		this.set( 'label' );
 		this.set( 'tabindex', -1 );
@@ -132,7 +133,7 @@ export default class ButtonView extends View {
 				tabindex: bind.to( 'tabindex' ),
 				'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`,
 				'aria-disabled': bind.if( 'isEnabled', true, value => !value ),
-				'aria-pressed': bind.if( 'isOn', true )
+				'aria-pressed': bind.to( 'isOn', value => this.isToggleable ? String( value ) : false )
 			},
 
 			children: this.children,

+ 2 - 0
packages/ckeditor5-ui/src/button/switchbuttonview.js

@@ -35,6 +35,8 @@ export default class SwitchButtonView extends ButtonView {
 	constructor( locale ) {
 		super( locale );
 
+		this.isToggleable = true;
+
 		/**
 		 * The toggle switch of the button.
 		 *

+ 4 - 0
packages/ckeditor5-ui/src/colorgrid/colorgridview.js

@@ -90,6 +90,10 @@ export default class ColorGridView extends View {
 			}
 		} );
 
+		this.items.on( 'add', ( evt, colorTile ) => {
+			colorTile.isOn = colorTile.color === this.selectedColor;
+		} );
+
 		colorDefinitions.forEach( item => {
 			const colorTile = new ColorTileView();
 

+ 5 - 1
packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.js

@@ -50,6 +50,7 @@ export default class SplitButtonView extends View {
 		this.set( 'icon' );
 		this.set( 'isEnabled', true );
 		this.set( 'isOn', false );
+		this.set( 'isToggleable', false );
 		this.set( 'isVisible', true );
 		this.set( 'keystroke' );
 		this.set( 'label' );
@@ -173,6 +174,7 @@ export default class SplitButtonView extends View {
 			'icon',
 			'isEnabled',
 			'isOn',
+			'isToggleable',
 			'keystroke',
 			'label',
 			'tabindex',
@@ -202,13 +204,15 @@ export default class SplitButtonView extends View {
 	 */
 	_createArrowView() {
 		const arrowView = new ButtonView();
+		const bind = arrowView.bindTemplate;
 
 		arrowView.icon = dropdownArrowIcon;
 
 		arrowView.extendTemplate( {
 			attributes: {
 				class: 'ck-splitbutton__arrow',
-				'aria-haspopup': true
+				'aria-haspopup': true,
+				'aria-expanded': bind.to( 'isOn', value => String( value ) )
 			}
 		} );
 

+ 28 - 9
packages/ckeditor5-ui/src/dropdown/dropdownview.js

@@ -253,18 +253,11 @@ export default class DropdownView extends View {
 			// If "auto", find the best position of the panel to fit into the viewport.
 			// Otherwise, simply assign the static position.
 			if ( this.panelPosition === 'auto' ) {
-				const defaultPanelPositions = DropdownView.defaultPanelPositions;
-
-				this.panelView.position = getOptimalPosition( {
+				this.panelView.position = DropdownView._getOptimalPosition( {
 					element: this.panelView.element,
 					target: this.buttonView.element,
 					fitInViewport: true,
-					positions: [
-						defaultPanelPositions.southEast,
-						defaultPanelPositions.southWest,
-						defaultPanelPositions.northEast,
-						defaultPanelPositions.northWest
-					]
+					positions: this._panelPositions
 				} ).name;
 			} else {
 				this.panelView.position = this.panelPosition;
@@ -312,6 +305,24 @@ export default class DropdownView extends View {
 	focus() {
 		this.buttonView.focus();
 	}
+
+	/**
+	 * Returns {@link #panelView panel} positions to be used by the
+	 * {@link module:utils/dom/position~getOptimalPosition `getOptimalPosition()`}
+	 * utility considering the direction of the language the UI of the editor is displayed in.
+	 *
+	 * @type {module:utils/dom/position~Options#positions}
+	 * @private
+	 */
+	get _panelPositions() {
+		const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
+
+		if ( this.locale.uiLanguageDirection === 'ltr' ) {
+			return [ southEast, southWest, northEast, northWest ];
+		} else {
+			return [ southWest, southEast, northWest, northEast ];
+		}
+	}
 }
 
 /**
@@ -392,3 +403,11 @@ DropdownView.defaultPanelPositions = {
 		};
 	}
 };
+
+/**
+ * A function used to calculate the optimal position for the dropdown panel.
+ *
+ * @protected
+ * @member {Function} module:ui/dropdown/dropdownview~DropdownView._getOptimalPosition
+ */
+DropdownView._getOptimalPosition = getOptimalPosition;

+ 5 - 1
packages/ckeditor5-ui/src/dropdown/utils.js

@@ -129,7 +129,11 @@ export function createDropdown( locale, ButtonClass = DropdownButtonView ) {
  * @param {Iterable.<module:ui/button/buttonview~ButtonView>} buttons
  */
 export function addToolbarToDropdown( dropdownView, buttons ) {
-	const toolbarView = dropdownView.toolbarView = new ToolbarView();
+	const locale = dropdownView.locale;
+	const t = locale.t;
+	const toolbarView = dropdownView.toolbarView = new ToolbarView( locale );
+
+	toolbarView.set( 'ariaLabel', t( 'Dropdown toolbar' ) );
 
 	dropdownView.extendTemplate( {
 		attributes: {

+ 3 - 1
packages/ckeditor5-ui/src/editableui/editableuiview.js

@@ -34,7 +34,9 @@ export default class EditableUIView extends View {
 					'ck-content',
 					'ck-editor__editable',
 					'ck-rounded-corners'
-				]
+				],
+				lang: locale.contentLanguage,
+				dir: locale.contentLanguageDirection
 			}
 		} );
 

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

@@ -66,8 +66,8 @@ export default class BoxedEditorUIView extends EditorUIView {
 					'ck-rounded-corners'
 				],
 				role: 'application',
-				dir: 'ltr',
-				lang: locale.language,
+				dir: locale.uiLanguageDirection,
+				lang: locale.uiLanguage,
 				'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`
 			},
 

+ 3 - 1
packages/ckeditor5-ui/src/editorui/editoruiview.js

@@ -69,6 +69,7 @@ export default class EditorUIView extends View {
 	 * @private
 	 */
 	_renderBodyCollection() {
+		const locale = this.locale;
 		const bodyElement = this._bodyCollectionContainer = new Template( {
 			tag: 'div',
 			attributes: {
@@ -77,7 +78,8 @@ export default class EditorUIView extends View {
 					'ck-reset_all',
 					'ck-body',
 					'ck-rounded-corners'
-				]
+				],
+				dir: locale.uiLanguageDirection,
 			},
 			children: this.body
 		} ).render();

+ 2 - 2
packages/ckeditor5-ui/src/template.js

@@ -802,8 +802,8 @@ export default class Template {
 	 *
 	 * @protected
 	 * @param {HTMLElement|Text} node A node to be reverted.
-	 * @param {module:ui/template~RenderData#revertData} revertData Stores information about
-	 * what changes have been made by {@link #apply} to the node.
+	 * @param {Object} revertData An object that stores information about what changes have been made by
+	 * {@link #apply} to the node. See {@link module:ui/template~RenderData#revertData} for more information.
 	 */
 	_revertTemplateFromNode( node, revertData ) {
 		for ( const binding of revertData.bindings ) {

+ 2 - 0
packages/ckeditor5-ui/src/toolbar/block/blockbuttonview.js

@@ -34,6 +34,8 @@ export default class BlockButtonView extends ButtonView {
 		// Hide button on init.
 		this.isVisible = false;
 
+		this.isToggleable = true;
+
 		/**
 		 * Top offset.
 		 *

+ 18 - 2
packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.js

@@ -53,6 +53,14 @@ import iconPilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
  * 		      |  block of content that the button is
  * 		      |  attached to.
  *
+ * **Note**: If you plan to run the editor in a right–to–left (RTL) language, keep in mind the button
+ * will be attached to the **right** boundary of the editable area. In that case, make sure the
+ * CSS position adjustment works properly by adding the following styles:
+ *
+ * 		.ck[dir="rtl"] .ck-block-toolbar-button {
+ * 			transform: translateX( 10px );
+ * 		}
+ *
  * @extends module:core/plugin~Plugin
  */
 export default class BlockToolbar extends Plugin {
@@ -361,9 +369,17 @@ export default class BlockToolbar extends Plugin {
 			target: targetElement,
 			positions: [
 				( contentRect, buttonRect ) => {
+					let left;
+
+					if ( this.editor.locale.uiLanguageDirection === 'ltr' ) {
+						left = editableRect.left - buttonRect.width;
+					} else {
+						left = editableRect.right;
+					}
+
 					return {
-						top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
-						left: editableRect.left - buttonRect.width
+						top: contentRect.top + contentPaddingTop + ( contentLineHeight - buttonRect.height ) / 2,
+						left
 					};
 				}
 			]

+ 17 - 2
packages/ckeditor5-ui/src/toolbar/toolbarview.js

@@ -32,12 +32,25 @@ import '../../theme/components/toolbar/toolbar.css';
  */
 export default class ToolbarView extends View {
 	/**
-	 * @inheritDoc
+	 * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
+	 *
+	 * Also see {@link #render}.
+	 *
+	 * @param {module:utils/locale~Locale} locale The localization services instance.
 	 */
 	constructor( locale ) {
 		super( locale );
 
 		const bind = this.bindTemplate;
+		const t = this.t;
+
+		/**
+		 * Label used by assistive technologies to describe this toolbar element.
+		 *
+		 * @default 'Editor toolbar'
+		 * @member {String} #ariaLabel
+		 */
+		this.set( 'ariaLabel', t( 'Editor toolbar' ) );
 
 		/**
 		 * Collection of the toolbar items (like buttons).
@@ -189,7 +202,9 @@ export default class ToolbarView extends View {
 					bind.if( 'isVertical', 'ck-toolbar_vertical' ),
 					bind.if( 'shouldGroupWhenFull', 'ck-toolbar_grouping' ),
 					bind.to( 'class' )
-				]
+				],
+				role: 'toolbar',
+				'aria-label': bind.to( 'ariaLabel' )
 			},
 
 			children: this._components,

+ 4 - 4
packages/ckeditor5-ui/src/viewcollection.js

@@ -139,13 +139,13 @@ export default class ViewCollection extends Collection {
 	 *
 	 *		viewA.fire( 'eventY', customData );
 	 *
-	 * See {@link module:utils/emittermixin~EmitterMixin#delegate}.
+	 * See {@link module:utils/emittermixin~Emitter#delegate}.
 	 *
 	 * @param {...String} events {@link module:ui/view~View} event names to be delegated to another
 	 * {@link module:utils/emittermixin~Emitter}.
 	 * @returns {Object}
 	 * @returns {Function} return.to A function which accepts the destination of
-	 * {@link module:utils/emittermixin~EmitterMixin#delegate delegated} events.
+	 * {@link module:utils/emittermixin~Emitter#delegate delegated} events.
 	 */
 	delegate( ...events ) {
 		if ( !events.length || !isStringArray( events ) ) {
@@ -162,11 +162,11 @@ export default class ViewCollection extends Collection {
 
 		return {
 			/**
-			 * Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
+			 * Selects destination for {@link module:utils/emittermixin~Emitter#delegate} events.
 			 *
 			 * @memberOf module:ui/viewcollection~ViewCollection#delegate
 			 * @function module:ui/viewcollection~ViewCollection#delegate.to
-			 * @param {module:utils/emittermixin~EmitterMixin} dest An `EmitterMixin` instance which is
+			 * @param {module:utils/emittermixin~Emitter} dest An `Emitter` instance which is
 			 * the destination for delegated events.
 			 */
 			to: dest => {

+ 10 - 1
packages/ckeditor5-ui/tests/button/buttonview.js

@@ -245,11 +245,20 @@ describe( 'ButtonView', () => {
 			} );
 
 			it( '-pressed reacts to #isOn', () => {
+				view.isToggleable = true;
 				view.isOn = true;
 				expect( view.element.attributes[ 'aria-pressed' ].value ).to.equal( 'true' );
 
 				view.isOn = false;
-				expect( view.element.attributes[ 'aria-pressed' ] ).to.be.undefined;
+				expect( view.element.attributes[ 'aria-pressed' ].value ).to.equal( 'false' );
+			} );
+
+			it( '-pressed is not present for non–toggleable button', () => {
+				view.isOn = true;
+				expect( view.element.hasAttribute( 'aria-pressed' ) ).to.be.false;
+
+				view.isOn = false;
+				expect( view.element.hasAttribute( 'aria-pressed' ) ).to.be.false;
 			} );
 		} );
 

+ 4 - 0
packages/ckeditor5-ui/tests/button/switchbuttonview.js

@@ -24,6 +24,10 @@ describe( 'SwitchButtonView', () => {
 		it( 'sets CSS class', () => {
 			expect( view.element.classList.contains( 'ck-switchbutton' ) ).to.be.true;
 		} );
+
+		it( 'sets isToggleable flag to true', () => {
+			expect( view.isToggleable ).to.be.true;
+		} );
 	} );
 
 	describe( 'render', () => {

+ 17 - 0
packages/ckeditor5-ui/tests/colorgrid/colorgridview.js

@@ -106,6 +106,23 @@ describe( 'ColorGridView', () => {
 			expect( view.items.get( 2 ).isOn ).to.be.false;
 		} );
 
+		it( 'should determine #isOn value when a ColorTileView is added', () => {
+			view.selectedColor = 'gold';
+
+			const tile = new ColorTileView();
+			tile.set( {
+				color: 'gold',
+				label: 'Gold',
+				options: {
+					hasBorder: false
+				}
+			} );
+
+			view.items.add( tile );
+
+			expect( view.items.get( 3 ).isOn ).to.be.true;
+		} );
+
 		describe( 'add colors from definition as child items', () => {
 			it( 'has proper number of elements', () => {
 				expect( view.items.length ).to.equal( 3 );

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

@@ -30,6 +30,14 @@ describe( 'SplitButtonView', () => {
 			expect( view.actionView.element.classList.contains( 'ck-splitbutton__action' ) ).to.be.true;
 		} );
 
+		it( 'adds isToggleable to view#actionView', () => {
+			expect( view.actionView.isToggleable ).to.be.false;
+
+			view.isToggleable = true;
+
+			expect( view.actionView.isToggleable ).to.be.true;
+		} );
+
 		it( 'creates view#arrowView', () => {
 			expect( view.arrowView ).to.be.instanceOf( ButtonView );
 			expect( view.arrowView.element.classList.contains( 'ck-splitbutton__arrow' ) ).to.be.true;
@@ -62,6 +70,14 @@ describe( 'SplitButtonView', () => {
 			expect( view.element.classList.contains( 'ck-splitbutton_open' ) ).to.be.false;
 		} );
 
+		it( 'binds arrowView aria-expanded attribute to #isOn', () => {
+			view.arrowView.isOn = true;
+			expect( view.arrowView.element.getAttribute( 'aria-expanded' ) ).to.equal( 'true' );
+
+			view.arrowView.isOn = false;
+			expect( view.arrowView.element.getAttribute( 'aria-expanded' ) ).to.equal( 'false' );
+		} );
+
 		describe( 'activates keyboard navigation for the toolbar', () => {
 			it( 'so "arrowright" on view#arrowView does nothing', () => {
 				const keyEvtData = {

+ 88 - 82
packages/ckeditor5-ui/tests/dropdown/dropdownview.js

@@ -10,7 +10,6 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import ButtonView from '../../src/button/buttonview';
 import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 describe( 'DropdownView', () => {
@@ -19,7 +18,10 @@ describe( 'DropdownView', () => {
 	testUtils.createSinonSandbox();
 
 	beforeEach( () => {
-		locale = { t() {} };
+		locale = {
+			uiLanguageDirection: 'ltr',
+			t() {}
+		};
 
 		buttonView = new ButtonView( locale );
 		panelView = new DropdownPanelView( locale );
@@ -116,7 +118,7 @@ describe( 'DropdownView', () => {
 			} );
 
 			describe( 'view.panelView#position to view#panelPosition', () => {
-				it( 'does not update until the dropdown is opened', () => {
+				it( 'does not update until the dropdown is open', () => {
 					view.isOpen = false;
 					view.panelPosition = 'nw';
 
@@ -128,86 +130,37 @@ describe( 'DropdownView', () => {
 				} );
 
 				describe( 'in "auto" mode', () => {
-					beforeEach( () => {
-						// Bloat the panel a little to give the positioning algorithm something to
-						// work with. If the panel was empty, any smart positioning is pointless.
-						// Placing an empty element in the viewport isn't that hard, right?
-						panelView.element.style.width = '200px';
-						panelView.element.style.height = '200px';
-					} );
-
-					it( 'defaults to "south-east" when there is a plenty of space around', () => {
-						const windowRect = new Rect( global.window );
-
-						// "Put" the dropdown in the middle of the viewport.
-						stubElementClientRect( view.buttonView.element, {
-							top: windowRect.height / 2,
-							left: windowRect.width / 2,
-							width: 10,
-							height: 10
-						} );
-
-						view.isOpen = true;
-
-						expect( panelView.position ).to.equal( 'se' );
-					} );
-
-					it( 'when the dropdown in the north-west corner of the viewport', () => {
-						stubElementClientRect( view.buttonView.element, {
-							top: 0,
-							left: 0,
-							width: 100,
-							height: 10
-						} );
+					it( 'uses _getOptimalPosition() and a dedicated set of positions (LTR)', () => {
+						const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
+						const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
 
 						view.isOpen = true;
 
-						expect( panelView.position ).to.equal( 'se' );
+						sinon.assert.calledWithExactly( spy, sinon.match( {
+							element: panelView.element,
+							target: buttonView.element,
+							positions: [
+								southEast, southWest, northEast, northWest
+							],
+							fitInViewport: true
+						} ) );
 					} );
 
-					it( 'when the dropdown in the north-east corner of the viewport', () => {
-						const windowRect = new Rect( global.window );
-
-						stubElementClientRect( view.buttonView.element, {
-							top: 0,
-							left: windowRect.right - 100,
-							width: 100,
-							height: 10
-						} );
+					it( 'uses _getOptimalPosition() and a dedicated set of positions (RTL)', () => {
+						const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
+						const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
 
+						view.locale.uiLanguageDirection = 'rtl';
 						view.isOpen = true;
 
-						expect( panelView.position ).to.equal( 'sw' );
-					} );
-
-					it( 'when the dropdown in the south-west corner of the viewport', () => {
-						const windowRect = new Rect( global.window );
-
-						stubElementClientRect( view.buttonView.element, {
-							top: windowRect.bottom - 10,
-							left: 0,
-							width: 100,
-							height: 10
-						} );
-
-						view.isOpen = true;
-
-						expect( panelView.position ).to.equal( 'ne' );
-					} );
-
-					it( 'when the dropdown in the south-east corner of the viewport', () => {
-						const windowRect = new Rect( global.window );
-
-						stubElementClientRect( view.buttonView.element, {
-							top: windowRect.bottom - 10,
-							left: windowRect.right - 100,
-							width: 100,
-							height: 10
-						} );
-
-						view.isOpen = true;
-
-						expect( panelView.position ).to.equal( 'nw' );
+						sinon.assert.calledWithExactly( spy, sinon.match( {
+							element: panelView.element,
+							target: buttonView.element,
+							positions: [
+								southWest, southEast, northWest, northEast
+							],
+							fitInViewport: true
+						} ) );
 					} );
 				} );
 			} );
@@ -372,13 +325,66 @@ describe( 'DropdownView', () => {
 			sinon.assert.calledOnce( spy );
 		} );
 	} );
-} );
 
-function stubElementClientRect( element, data ) {
-	const clientRect = Object.assign( {}, data );
+	describe( 'DropdownView.defaultPanelPositions', () => {
+		let positions, buttonRect, panelRect;
+
+		beforeEach( () => {
+			positions = DropdownView.defaultPanelPositions;
+
+			buttonRect = {
+				top: 100,
+				bottom: 200,
+				left: 100,
+				right: 200,
+				width: 100,
+				height: 100
+			};
+
+			panelRect = {
+				top: 0,
+				bottom: 0,
+				left: 0,
+				right: 0,
+				width: 50,
+				height: 50
+			};
+		} );
+
+		it( 'should have a proper length', () => {
+			expect( Object.keys( positions ) ).to.have.length( 4 );
+		} );
+
+		it( 'should define the "southEast" position', () => {
+			expect( positions.southEast( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 200,
+				left: 100,
+				name: 'se'
+			} );
+		} );
+
+		it( 'should define the "southWest" position', () => {
+			expect( positions.southWest( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 200,
+				left: 150,
+				name: 'sw'
+			} );
+		} );
 
-	clientRect.right = clientRect.left + clientRect.width;
-	clientRect.bottom = clientRect.top + clientRect.height;
+		it( 'should define the "northEast" position', () => {
+			expect( positions.northEast( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 50,
+				left: 100,
+				name: 'ne'
+			} );
+		} );
 
-	testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( clientRect );
-}
+		it( 'should define the "northWest" position', () => {
+			expect( positions.northWest( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 150,
+				left: 150,
+				name: 'nw'
+			} );
+		} );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js

@@ -93,7 +93,7 @@ function testLongLabel() {
 }
 
 function testToolbar() {
-	const locale = {};
+	const locale = { t: langString => langString };
 
 	const icons = { left: alignLeftIcon, right: alignRightIcon, center: alignCenterIcon };
 

+ 5 - 1
packages/ckeditor5-ui/tests/dropdown/utils.js

@@ -27,7 +27,7 @@ describe( 'utils', () => {
 	let locale, dropdownView;
 
 	beforeEach( () => {
-		locale = { t() {} };
+		locale = { t: langString => langString };
 	} );
 
 	describe( 'createDropdown()', () => {
@@ -272,6 +272,10 @@ describe( 'utils', () => {
 			expect( dropdownView.element.classList.contains( 'ck-toolbar-dropdown' ) ).to.be.true;
 		} );
 
+		it( 'sets aria-label', () => {
+			expect( dropdownView.toolbarView.element.getAttribute( 'aria-label' ) ).to.equal( 'Dropdown toolbar' );
+		} );
+
 		describe( 'view#toolbarView', () => {
 			it( 'is created', () => {
 				const panelChildren = dropdownView.panelView.children;

+ 58 - 5
packages/ckeditor5-ui/tests/editableui/editableuiview.js

@@ -18,7 +18,7 @@ describe( 'EditableUIView', () => {
 	testUtils.createSinonSandbox();
 
 	beforeEach( () => {
-		locale = new Locale( 'en' );
+		locale = new Locale();
 		editableElement = document.createElement( 'div' );
 
 		editingView = new EditingView();
@@ -31,14 +31,21 @@ describe( 'EditableUIView', () => {
 		view.render();
 	} );
 
+	afterEach( () => {
+		view.destroy();
+		editableElement.remove();
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'sets initial values of attributes', () => {
-			view = new EditableUIView( locale, editingView );
+			const view = new EditableUIView( locale, editingView );
 
 			expect( view.isFocused ).to.be.false;
 			expect( view.name ).to.be.null;
 			expect( view._externalElement ).to.be.undefined;
 			expect( view._editingView ).to.equal( editingView );
+
+			view.destroy();
 		} );
 
 		it( 'renders element from template when no editableElement', () => {
@@ -47,22 +54,58 @@ describe( 'EditableUIView', () => {
 			expect( view.element.classList.contains( 'ck-content' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'en' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
 			expect( view._externalElement ).to.be.undefined;
 			expect( view.isRendered ).to.be.true;
 		} );
 
 		it( 'accepts editableElement as an argument', () => {
-			view = new EditableUIView( locale, editingView, editableElement );
+			const view = new EditableUIView( locale, editingView, editableElement );
 			view.name = editingViewRoot.rootName;
 
 			view.render();
+
 			expect( view.element ).to.equal( editableElement );
 			expect( view.element ).to.equal( view._editableElement );
 			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'en' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
 			expect( view._hasExternalElement ).to.be.true;
 			expect( view.isRendered ).to.be.true;
+
+			view.destroy();
+		} );
+
+		it( 'sets proper lang and dir attributes (implicit content language)', () => {
+			const locale = new Locale( { uiLanguage: 'ar' } );
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
+
+			view.render();
+
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'ar' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
+		} );
+
+		it( 'sets proper lang and dir attributes (explicit content language)', () => {
+			const locale = new Locale( {
+				uiLanguage: 'pl',
+				contentLanguage: 'ar'
+			} );
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
+
+			view.render();
+
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'ar' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
 		} );
 	} );
 
@@ -123,6 +166,7 @@ describe( 'EditableUIView', () => {
 				expect( secondEditingViewRoot.hasClass( 'ck-blurred' ), 12 ).to.be.false;
 
 				secondEditableElement.remove();
+				secondView.destroy();
 			} );
 		} );
 	} );
@@ -130,12 +174,21 @@ describe( 'EditableUIView', () => {
 	describe( 'destroy()', () => {
 		it( 'calls super#destroy()', () => {
 			const spy = testUtils.sinon.spy( View.prototype, 'destroy' );
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
 
+			view.render();
 			view.destroy();
+
 			sinon.assert.calledOnce( spy );
 		} );
 
 		it( 'can be called multiple times', () => {
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
+
+			view.render();
+
 			expect( () => {
 				view.destroy();
 				view.destroy();
@@ -144,11 +197,11 @@ describe( 'EditableUIView', () => {
 
 		describe( 'when #editableElement as an argument', () => {
 			it( 'reverts the template of editableElement', () => {
-				editableElement = document.createElement( 'div' );
+				const editableElement = document.createElement( 'div' );
 				editableElement.classList.add( 'foo' );
 				editableElement.contentEditable = false;
 
-				view = new EditableUIView( locale, editingView, editableElement );
+				const view = new EditableUIView( locale, editingView, editableElement );
 				view.name = editingViewRoot.rootName;
 
 				view.render();

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

@@ -14,7 +14,7 @@ describe( 'InlineEditableUIView', () => {
 	let view, editableElement, editingView, editingViewRoot, locale;
 
 	beforeEach( () => {
-		locale = new Locale( 'en' );
+		locale = new Locale();
 		editableElement = document.createElement( 'div' );
 
 		editingView = new EditingView();

+ 12 - 1
packages/ckeditor5-ui/tests/editorui/boxed/boxededitoruiview.js

@@ -11,7 +11,7 @@ describe( 'BoxedEditorUIView', () => {
 	let view, element;
 
 	beforeEach( () => {
-		view = new BoxedEditorUIView( new Locale( 'en' ) );
+		view = new BoxedEditorUIView( new Locale() );
 		view.render();
 		element = view.element;
 	} );
@@ -31,6 +31,7 @@ describe( 'BoxedEditorUIView', () => {
 			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( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
 			expect( element.attributes[ 'aria-labelledby' ].value )
 				.to.equal( view.element.firstChild.id )
 				.to.match( /^ck-editor__aria-label_\w+$/ );
@@ -59,5 +60,15 @@ describe( 'BoxedEditorUIView', () => {
 			expect( element.childNodes[ 1 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
 			expect( element.childNodes[ 2 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
 		} );
+
+		it( 'sets the proper "dir" attribute value when using RTL language', () => {
+			const view = new BoxedEditorUIView( new Locale( { uiLanguage: 'ar' } ) );
+
+			view.render();
+
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
+		} );
 	} );
 } );

+ 20 - 1
packages/ckeditor5-ui/tests/editorui/editoruiview.js

@@ -16,7 +16,7 @@ describe( 'EditorUIView', () => {
 	testUtils.createSinonSandbox();
 
 	beforeEach( () => {
-		locale = new Locale( 'en' );
+		locale = new Locale();
 		view = new EditorUIView( locale );
 
 		view.render();
@@ -46,6 +46,25 @@ describe( 'EditorUIView', () => {
 			expect( el.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( el.classList.contains( 'ck-reset_all' ) ).to.be.true;
 		} );
+
+		it( 'sets the right dir attribute to the body region (LTR)', () => {
+			const el = view._bodyCollectionContainer;
+
+			expect( el.getAttribute( 'dir' ) ).to.equal( 'ltr' );
+		} );
+
+		it( 'sets the right dir attribute to the body region (RTL)', () => {
+			const locale = new Locale( { uiLanguage: 'ar' } );
+			const view = new EditorUIView( locale );
+
+			view.render();
+
+			const el = view._bodyCollectionContainer;
+
+			expect( el.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
+		} );
 	} );
 
 	describe( 'destroy()', () => {

+ 4 - 0
packages/ckeditor5-ui/tests/manual/blocktoolbar/blocktoolbar.html

@@ -56,4 +56,8 @@
 	.ck-block-toolbar-button {
 		transform: translateX( -10px );
 	}
+
+	[dir="rtl"] .ck-block-toolbar-button {
+		transform: translateX( 10px );
+	}
 </style>

+ 4 - 0
packages/ckeditor5-ui/tests/toolbar/block/blockbuttonview.js

@@ -22,6 +22,10 @@ describe( 'BlockButtonView', () => {
 		expect( view.element.classList.contains( 'ck-block-toolbar-button' ) ).to.be.true;
 	} );
 
+	it( 'should be initialized as toggleable button', () => {
+		expect( view.isToggleable ).to.be.true;
+	} );
+
 	describe( 'DOM binding', () => {
 		it( 'should react on `view#top` change', () => {
 			view.top = 0;

+ 37 - 0
packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js

@@ -345,6 +345,43 @@ describe( 'BlockToolbar', () => {
 			expect( blockToolbar.buttonView.left ).to.equal( 100 );
 		} );
 
+		it( 'should attach the left side of the button to the right side of the editable when language direction is RTL', () => {
+			editor.locale.uiLanguageDirection = 'rtl';
+
+			setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
+
+			const target = editor.ui.getEditableElement().querySelector( 'p' );
+			const styleMock = testUtils.sinon.stub( window, 'getComputedStyle' );
+
+			styleMock.withArgs( target ).returns( {
+				lineHeight: 'normal',
+				fontSize: '20px',
+				paddingTop: '10px'
+			} );
+
+			styleMock.callThrough();
+
+			testUtils.sinon.stub( editor.ui.getEditableElement(), 'getBoundingClientRect' ).returns( {
+				left: 200,
+				right: 600
+			} );
+
+			testUtils.sinon.stub( target, 'getBoundingClientRect' ).returns( {
+				top: 500,
+				left: 300
+			} );
+
+			testUtils.sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( {
+				width: 100,
+				height: 100
+			} );
+
+			editor.ui.fire( 'update' );
+
+			expect( blockToolbar.buttonView.top ).to.equal( 472 );
+			expect( blockToolbar.buttonView.left ).to.equal( 600 );
+		} );
+
 		it( 'should reposition the #panelView when open on ui#update', () => {
 			blockToolbar.panelView.isVisible = false;
 

+ 2 - 2
packages/ckeditor5-ui/tests/toolbar/enabletoolbarkeyboardfocus.js

@@ -19,7 +19,7 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 		origin = viewCreator();
 		originFocusTracker = new FocusTracker();
 		originKeystrokeHandler = new KeystrokeHandler();
-		toolbar = new ToolbarView();
+		toolbar = new ToolbarView( { t: langString => langString } );
 
 		toolbar.render();
 
@@ -93,7 +93,7 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 		origin = viewCreator();
 		originFocusTracker = new FocusTracker();
 		originKeystrokeHandler = new KeystrokeHandler();
-		toolbar = new ToolbarView();
+		toolbar = new ToolbarView( { t: langString => langString } );
 
 		const toolbarFocusSpy = sinon.spy( toolbar, 'focus' );
 		const originFocusSpy = origin.focus = sinon.spy();

+ 45 - 2
packages/ckeditor5-ui/tests/toolbar/toolbarview.js

@@ -14,12 +14,30 @@ import FocusCycler from '../../src/focuscycler';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import ViewCollection from '../../src/viewcollection';
 import View from '../../src/view';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
+import Locale from '@ckeditor/ckeditor5-utils/src/locale';
 
 describe( 'ToolbarView', () => {
 	let locale, view;
 
+	testUtils.createSinonSandbox();
+
+	before( () => {
+		addTranslations( 'pl', {
+			'Editor toolbar': 'Pasek narzędzi edytora'
+		} );
+		addTranslations( 'en', {
+			'Editor toolbar': 'Editor toolbar'
+		} );
+	} );
+
+	after( () => {
+		clearTranslations();
+	} );
+
 	beforeEach( () => {
-		locale = {};
+		locale = new Locale();
 		view = new ToolbarView( locale );
 		view.render();
 	} );
@@ -61,6 +79,31 @@ describe( 'ToolbarView', () => {
 			expect( view.element.classList.contains( 'ck-toolbar' ) ).to.true;
 		} );
 
+		describe( 'attributes', () => {
+			it( 'should be defined', () => {
+				expect( view.element.getAttribute( 'role' ) ).to.equal( 'toolbar' );
+				expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Editor toolbar' );
+			} );
+
+			it( 'should allow a custom aria-label', () => {
+				const view = new ToolbarView( locale );
+
+				view.ariaLabel = 'Custom label';
+
+				view.render();
+
+				expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Custom label' );
+			} );
+
+			it( 'should allow the aria-label to be translated', () => {
+				const view = new ToolbarView( new Locale( { uiLanguage: 'pl' } ) );
+
+				view.render();
+
+				expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Pasek narzędzi edytora' );
+			} );
+		} );
+
 		describe( 'event listeners', () => {
 			it( 'prevent default on #mousedown', () => {
 				const evt = new Event( 'mousedown', { bubbles: true } );
@@ -116,7 +159,7 @@ describe( 'ToolbarView', () => {
 		} );
 
 		it( 'starts listening for #keystrokes coming from #element', () => {
-			const view = new ToolbarView();
+			const view = new ToolbarView( new Locale() );
 			const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
 			view.render();

+ 10 - 0
packages/ckeditor5-ui/theme/mixins/_dir.css

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+@define-mixin ck-dir $direction {
+	@nest [dir="$(direction)"] & {
+		@mixin-content;
+	}
+}