Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

T/ckeditor5/1151: Brought support for right–to–left (RTL) languages to various UI components #520

Merged
merged 24 commits into from
Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9a0b95
TMP.
oleq Jun 10, 2019
d33ea88
Merge branch 'master' into t/ckeditor5/1151
oleq Jun 26, 2019
0bbc34e
Merge branch 'master' into t/ckeditor5/1151
oleq Jul 4, 2019
7789d41
Implemented the ck-dir mixin to make theme development for RTL much e…
oleq Jul 5, 2019
b4c63cc
Improved the `DropdownView` component so it displays its panel on bot…
oleq Jul 6, 2019
80efccf
Added the DOM `dir` attribute do the `.ck-body` collection to let its…
oleq Jul 6, 2019
aca23ed
Added the DOM `dir` attribute to the `BoxedEditorUIView` container.
oleq Jul 6, 2019
3c3391a
Configured the `BlockToolbar` plugin to display on both sides of edit…
oleq Jul 6, 2019
10d871f
Tests: Added DropdownView panel positioning tests for LTR and RTL lan…
oleq Jul 8, 2019
2bb9481
Tests: Added EditableUIView tests for lang and dir attributes.
oleq Jul 9, 2019
533c32b
Tests: Code refactoring to prevent memory bloating.
oleq Jul 9, 2019
c829d73
Tests: Added EditorUIView tests for integration with LTR/RTL languages.
oleq Jul 9, 2019
a44f3c2
Tests: Added BoxedEditorUIView test for integration with LTR/RTL lang…
oleq Jul 9, 2019
ff237fb
Tests: Added BlockToolbar test for integration with LTR/RTL languages.
oleq Jul 9, 2019
68f63f9
Docs: Added information about styling the block toolbar button when u…
oleq Jul 10, 2019
8af41b4
Tests: Updated EditableUIView tests to the latest Locale API.
oleq Jul 10, 2019
dd701ed
Docs: Fixed a broken link in the DropdownView class.
oleq Jul 11, 2019
4b7685f
Merge branch 'master' into t/ckeditor5/1151
oleq Jul 11, 2019
6ef5baf
Merge branch 'master' into t/ckeditor5/1151
oleq Jul 25, 2019
8d0a230
Merge branch 'master' into t/ckeditor5/1151
oleq Aug 5, 2019
8d547e7
Dropped the editor content dir="auto" support.
oleq Aug 5, 2019
ea3effa
Keep the style.
Reinmar Aug 6, 2019
c0daf34
Aligned the code to the latest Locale class API.
oleq Aug 9, 2019
31881a8
Merge branch 'master' into t/ckeditor5/1151
Reinmar Aug 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/features/blocktoolbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ 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
Reinmar marked this conversation as resolved.
Show resolved Hide resolved
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 {
Reinmar marked this conversation as resolved.
Show resolved Hide resolved
transform: translateX( 10px );
}
```

## Installation

<info-box hint>
Expand Down
37 changes: 28 additions & 9 deletions src/dropdown/dropdownview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.languageDirection === 'ltr' ) {
return [ southEast, southWest, northEast, northWest ];
} else {
return [ southWest, southEast, northWest, northEast ];
}
}
}

/**
Expand Down Expand Up @@ -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;
4 changes: 3 additions & 1 deletion src/editableui/editableuiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export default class EditableUIView extends View {
'ck-content',
'ck-editor__editable',
'ck-rounded-corners'
]
],
lang: locale.contentLanguage,
dir: locale.contentLanguageDirection
}
} );

Expand Down
2 changes: 1 addition & 1 deletion src/editorui/boxed/boxededitoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default class BoxedEditorUIView extends EditorUIView {
'ck-rounded-corners'
],
role: 'application',
dir: 'ltr',
dir: locale.languageDirection,
lang: locale.language,
'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`
},
Expand Down
4 changes: 3 additions & 1 deletion src/editorui/editoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -77,7 +78,8 @@ export default class EditorUIView extends View {
'ck-reset_all',
'ck-body',
'ck-rounded-corners'
]
],
dir: locale.languageDirection,
},
children: this.body
} ).render();
Expand Down
20 changes: 18 additions & 2 deletions src/toolbar/block/blocktoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -361,9 +369,17 @@ export default class BlockToolbar extends Plugin {
target: targetElement,
positions: [
( contentRect, buttonRect ) => {
let left;

if ( this.editor.locale.languageDirection === '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
};
}
]
Expand Down
170 changes: 88 additions & 82 deletions tests/dropdown/dropdownview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -19,7 +18,10 @@ describe( 'DropdownView', () => {
testUtils.createSinonSandbox();

beforeEach( () => {
locale = { t() {} };
locale = {
languageDirection: 'ltr',
t() {}
};

buttonView = new ButtonView( locale );
panelView = new DropdownPanelView( locale );
Expand Down Expand Up @@ -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';

Expand All @@ -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.languageDirection = '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
} ) );
} );
} );
} );
Expand Down Expand Up @@ -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'
} );
} );
} );
} );
Loading