Kaynağa Gözat

Refactored `bindTwoStepCaretToAttribute()` while moving left.

Oskar Wróbel 7 yıl önce
ebeveyn
işleme
307ef6eff0

+ 93 - 87
packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js

@@ -8,6 +8,8 @@
  */
 
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import Range from '@ckeditor/ckeditor5-engine/src/model/range';
+import first from '@ckeditor/ckeditor5-utils/src/first';
 
 /**
  * This helper adds two-steps caret movement behaviour for given attribute.
@@ -16,19 +18,19 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  * then typing does not expand this attribute. Additional arrow key press is needed to "enter" to the text
  * and start typing with this attribute. The same is is for leaving this text.
  *
- * When behaviour is enabled for bold attribute and caret is just before the attribute element then pressing right arrow
- * will move caret to the attribute element instead of moving after next character:
+ * When behaviour is enabled for `linkHref` attribute and caret is just before the attribute element then pressing
+ * right arrow will move caret inside the attribute element instead of moving after next character:
  *
- * 		<p>foo[]<strong>bar</strong>biz<p> `->` <p>foo<strong>[]foo</strong>barr<p>
+ * 		<p>foo{}<a>bar</a>biz<p> `->` <p>foo<a>{}foo</a>barr<p>
  *
- * The same is for "leaving" text:
+ * The same is for "leaving" attribute element:
  *
- * 		<p>foo<strong>bar[]</strong>biz<p> `->` <p>foo<strong>bar</strong>[]biz<p>
+ * 		<p>foo<a>bar{}</a>biz<p> `->` <p>foo<a>bar</a>{}biz<p>
  *
  * And when moving left:
  *
- * 		<p>foo<strong>bar</strong>[]biz<p> `<-` <p>foo<strong>bar[]</strong>biz<p>
- * 		<p>foo<strong>[]bar</strong>biz<p> `<-` <p>foo[]<strong>bar</strong>biz<p>
+ * 		<p>foo<a>bar</a>{}biz<p> `<-` <p>foo<a>bar{}</a>biz<p>
+ * 		<p>foo<a>{}bar</a>biz<p> `<-` <p>foo{}<a>bar</a>biz<p>
  *
  * @param {module:core/editor/editor~Editor} editor The Editor instance.
  * @param {module:utils/dom/emittermixin~Emitter} emitter The emitter to which this behavior should be added.
@@ -39,105 +41,109 @@ export default function bindTwoStepCaretToAttribute( editor, emitter, attribute
 	const editingView = editor.editing.view;
 	const modelSelection = model.document.selection;
 
-	// Creates a closure for each helper call to make possible to keep states.
-	( function twoStepCaretMovementHandler( emitter, attribute ) {
-		// When set as `true` it means that first step has been made and default gravity was programmatically
-		// restored on `keydown` event and `keyup` event should not override it.
-		// Used only while moving left.
-		let isFirstStepMade = false;
-
-		// Listen to keyboard events and handle cursor after move.
-		emitter.listenTo( editingView, 'keyup', ( evt, data ) => {
-			// Only left arrow is handled on keyup.
-			if ( data.keyCode != keyCodes.arrowleft ) {
+	// Listen to keyboard events and handle cursor before the move.
+	emitter.listenTo( editingView, 'keydown', ( evt, data ) => {
+		const arrowRightPressed = data.keyCode == keyCodes.arrowright;
+		const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
+
+		// When neither left or right arrow has been pressed then do noting.
+		if ( !arrowRightPressed && !arrowLeftPressed ) {
+			return;
+		}
+
+		// This implementation works only for collapsed selection.
+		if ( !modelSelection.isCollapsed ) {
+			return;
+		}
+
+		const position = modelSelection.getFirstPosition();
+
+		// Moving right.
+		if ( arrowRightPressed ) {
+			// If gravity is already overridden then do nothing.
+			// It means that we already enter `foo<a>{}bar</a>biz` or left `foo<a>bar</a>{}biz` text with attribute
+			// and gravity will be restored just after caret movement.
+			if ( modelSelection.isGravityOverridden ) {
 				return;
 			}
 
-			// Current implementation works only for collapsed selection.
-			if ( !modelSelection.isCollapsed ) {
-				return;
-			}
-
-			const position = modelSelection.getFirstPosition();
-
-			// If caret sticks to beginning or end of Text with attribute and first step has not been made yet let's make it.
-			if ( !isFirstStepMade && isStickToAttribute( position.nodeBefore, position.nodeAfter, attribute ) ) {
-				// While moving left we need to override gravity for the first step.
+			// If caret sticks to the bound of Text with attribute it means that we are going to
+			// enter `foo{}<a>bar</a>biz` or leave `foo<a>bar{}</a>biz` the text with attribute.
+			if ( isStickToAttribute( position.nodeAfter, position.nodeBefore, attribute ) ) {
+				// So we need to prevent caret from being moved.
+				data.preventDefault();
+				// And override default selection gravity.
 				model.change( writer => writer.overrideSelectionGravity() );
 			}
-		} );
 
-		// Listen to keyboard events and handle cursor before move.
-		emitter.listenTo( editingView, 'keydown', ( evt, data ) => {
-			const arrowRightPressed = data.keyCode == keyCodes.arrowright;
-			const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
+		// Moving left.
+		} else {
+			// If caret sticks to the bound of Text with attribute and gravity is already overridden it means that
+			// we are going to enter `foo<a>bar</a>{}biz` or leave `foo<a>{}bar</a>biz` text with attribute.
+			if ( modelSelection.isGravityOverridden && isStickToAttribute( position.nodeBefore, position.nodeAfter, attribute ) ) {
+				// So we need to prevent cater from being moved.
+				data.preventDefault();
+				// And restore the gravity.
+				model.change( writer => writer.restoreSelectionGravity() );
 
-			// When neither left or right arrow has been pressed then do noting.
-			if ( !arrowRightPressed && !arrowLeftPressed ) {
 				return;
 			}
 
-			// Current implementation works only for collapsed selection.
-			if ( !modelSelection.isCollapsed ) {
-				return;
-			}
+			// If we are here we need to check if caret is a one character before the text with attribute bound
+			// `foo<a>bar</a>b{}iz` or `foo<a>b{}ar</a>biz`.
+			const nextPosition = getPreviousPosition( position );
 
-			const position = modelSelection.getFirstPosition();
-
-			// Moving left.
-			// This is a second part of moving caret to the left. When first step has been handled by `keyup`
-			// event (after caret movement) we need to handle second step using `keydown` event (before caret movement).
-			if ( arrowLeftPressed ) {
-				// If default gravity is not overridden then do nothing.
-				// It means that second step might be already made or caret does not stick to the Text with attribute.
-				if ( !modelSelection.isGravityOverridden ) {
-					return;
-				}
-
-				// If caret sticks to beginning or end of Text with attribute
-				// it means that first step is already made and we need to make the second.
-				if ( isStickToAttribute( position.nodeBefore, position.nodeAfter, attribute ) ) {
-					// Prevent cater from being moved.
-					data.preventDefault();
-					// Restore default gravity.
-					model.change( writer => writer.restoreSelectionGravity() );
-					// Remember that second step has been made (needed by `keyup` listener).
-					isFirstStepMade = true;
-				}
-
-			// Moving right.
-			// Here situation is easy to handle because gravity in the first step
-			// is consistent with default gravity and for second step is enough to override it.
-			} else {
-				// If default gravity is already overridden then do nothing.
-				// It means that second step has been already made.
-				if ( modelSelection.isGravityOverridden ) {
-					return;
-				}
-
-				// If caret sticks to beginning or end of Text with attribute it means that first step has been made
-				// and we need to make a second step.
-				if ( isStickToAttribute( position.nodeAfter, position.nodeBefore, attribute ) ) {
-					// Prevent caret from being moved.
-					data.preventDefault();
-					// And override default selection gravity.
-					model.change( writer => writer.overrideSelectionGravity() );
-				}
+			// When there is no position it means that parent bound has been reached.
+			if ( !nextPosition ) {
+				return;
 			}
-		} );
 
-		// Clear state every time when selection is changed directly by the user.
-		emitter.listenTo( modelSelection, 'change:range', ( evt, data ) => {
-			if ( data.directChange ) {
-				isFirstStepMade = false;
+			// When caret is going stick to the bound of Text with attribute after movement then we need to override
+			// the gravity before the move. But we need to do it in a custom way otherwise `selection#change:range`
+			// event following the overriding will restore the gravity.
+			if ( isStickToAttribute( nextPosition.nodeBefore, nextPosition.nodeAfter, attribute ) ) {
+				model.change( writer => {
+					// So let's override the gravity.
+					writer.overrideSelectionGravity( restore => {
+						// But skip the following `change:range` event and restore the gravity on the next one.
+						let counter = 0;
+
+						emitter.listenTo( modelSelection, 'change:range', ( evt, data ) => {
+							if ( counter++ && data.directChange ) {
+								restore();
+								evt.off();
+							}
+						} );
+					} );
+				} );
 			}
-		} );
-	}( emitter, attribute ) );
+		}
+	} );
 }
 
+// @param {module:engine/model/node~Node} nextNode Node before the position.
+// @param {module:engine/model/node~Node} prevNode Node after the position.
+// @param {String} attribute Attribute name.
+// @returns {Boolean} `true` when position between the nodes sticks to the bound of text with given attribute.
 function isStickToAttribute( nextNode, prevNode, attribute ) {
 	const isAttrInNext = nextNode ? nextNode.hasAttribute( attribute ) : false;
 	const isAttrInPrev = prevNode ? prevNode.hasAttribute( attribute ) : false;
 
 	return isAttrInNext && !isAttrInPrev || !isAttrInNext && isAttrInPrev;
 }
+
+// @param {module:engine/model/position~Position} position Initial position.
+// @returns {module:engine/model/position~Position|undefined} Previous position according to initial position in range.
+function getPreviousPosition( position ) {
+	const iterator = Range.createIn( position.parent ).getPositions( {
+		direction: 'backward',
+		singleCharacters: true,
+		startPosition: position
+	} );
+
+	// First position is the same as initial so we need to skip it.
+	first( iterator );
+
+	// Get position before the previous node of initial position.
+	return first( iterator );
+}

+ 52 - 111
packages/ckeditor5-engine/tests/utils/bindtwostepcarettoattribute.js

@@ -50,9 +50,6 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should "enter" the text with attribute in two steps', () => {
 			setData( model, '<$text c="true">foo[]</$text><$text a="true" b="true">bar</$text>' );
 
-			// Firing keyup event will simulate that caret is here as a result of right arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
-
 			// Gravity is not overridden, caret is at the beginning of the text but is "outside" of the text.
 			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
 			expect( selection.isGravityOverridden ).to.false;
@@ -81,9 +78,6 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should "leave" the text with attribute in two steps', () => {
 			setData( model, '<$text a="true" b="true">bar[]</$text><$text c="true">foo</$text>' );
 
-			// Firing keyup event will simulate that caret is here as a result of right arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
-
 			// Gravity is not overridden, caret is at the end of the text but is "inside" of the text.
 			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
 			expect( selection.isGravityOverridden ).to.false;
@@ -112,9 +106,6 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should do nothing for not bound attribute (at the beginning)', () => {
 			setData( model, '[]<$text c="true">foo</$text>' );
 
-			// Firing keyup event will simulate that caret is here as a result of right arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
-
 			viewDoc.fire( 'keydown', getEventData( {
 				keyCode: keyCodes.arrowright,
 				preventDefault: preventDefaultSpy
@@ -127,9 +118,6 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should do nothing for not bound attribute (at the end)', () => {
 			setData( model, '<$text c="true">foo[]</$text>' );
 
-			// Firing keyup event will simulate that caret is here as a result of right arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
-
 			viewDoc.fire( 'keydown', getEventData( {
 				keyCode: keyCodes.arrowright,
 				preventDefault: preventDefaultSpy
@@ -138,27 +126,30 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 			sinon.assert.notCalled( preventDefaultSpy );
 			expect( selection.isGravityOverridden ).to.false;
 		} );
-
-		it( 'should do nothing for non-collapsed selection', () => {
-			setData( model, '<$text c="true">fo[o]</$text><$text a="true" b="true">bar</$text>' );
-
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
-			viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowright } ) );
-
-			expect( selection.isGravityOverridden ).to.false;
-		} );
 	} );
 
 	describe( 'moving left', () => {
 		it( 'should "enter" the text with attribute in two steps', () => {
-			setData( model, '<$text>foo</$text><$text a="true" b="true">bar[]</$text><$text c="true">biz</$text>' );
+			setData( model, '<$text>foo</$text><$text a="true" b="true">bar</$text><$text c="true">b[]iz</$text>' );
 
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
+			// Gravity is not overridden, caret is a one character after the and of the text.
+			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
+			expect( selection.isGravityOverridden ).to.false;
+
+			// Press left key.
+			viewDoc.fire( 'keydown', getEventData( {
+				keyCode: keyCodes.arrowleft,
+				preventDefault: preventDefaultSpy
+			} ) );
+
+			// Caret movement was not blocked.
+			sinon.assert.notCalled( preventDefaultSpy );
 
-			// Gravity is overridden, caret is at the end of the text but is "outside" of the text.
+			// So we need to move caret one character left like it should be done in the real world.
+			// Caret should ends up at the end of text with attribute but still outside of it.
+			model.change( writer => writer.setSelection( new Range( new Position( model.document.getRoot(), [ 6 ] ) ) ) );
+
+			// Gravity is overridden.
 			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
 			expect( selection.isGravityOverridden ).to.true;
 
@@ -167,8 +158,6 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 				keyCode: keyCodes.arrowleft,
 				preventDefault: preventDefaultSpy
 			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
 
 			// Caret movement was blocked but now is "inside" the text.
 			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
@@ -180,18 +169,34 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 				keyCode: keyCodes.arrowleft,
 				preventDefault: preventDefaultSpy
 			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
 
 			// Caret movement was not blocked this time (still once) so everything works normally.
 			sinon.assert.calledOnce( preventDefaultSpy );
+
+			// And again we need to move the caret like it should be done in the real world to be shure that everything is
+			// like it should to be.
+			model.change( writer => writer.setSelection( new Range( new Position( model.document.getRoot(), [ 5 ] ) ) ) );
 		} );
 
 		it( 'should "leave" the text with attribute in two steps', () => {
-			setData( model, '<$text c="true">foo</$text><$text a="true" b="true">[]bar</$text>' );
+			setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
+
+			// Gravity is not overridden, caret is a one character after the beginning of the text.
+			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
+			expect( selection.isGravityOverridden ).to.false;
 
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
+			// Press left key.
+			viewDoc.fire( 'keydown', getEventData( {
+				keyCode: keyCodes.arrowleft,
+				preventDefault: preventDefaultSpy
+			} ) );
+
+			// Caret movement was not blocked.
+			sinon.assert.notCalled( preventDefaultSpy );
+
+			// So we need to move caret one character left like it should be done in the real world.
+			// Caret should ends up at the beginning of text with attribute but still inside of it.
+			model.change( writer => writer.setSelection( new Range( new Position( model.document.getRoot(), [ 3 ] ) ) ) );
 
 			// Gravity is overridden, caret is at the beginning of the text and is "inside" of the text.
 			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
@@ -202,8 +207,6 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 				keyCode: keyCodes.arrowleft,
 				preventDefault: preventDefaultSpy
 			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
 
 			// Gravity is not overridden, caret movement was blocked but now is "outside" the text.
 			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
@@ -215,8 +218,6 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 				keyCode: keyCodes.arrowleft,
 				preventDefault: preventDefaultSpy
 			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
 
 			// Caret movement was not blocked this time (still once) so everything works normally.
 			sinon.assert.calledOnce( preventDefaultSpy );
@@ -225,15 +226,10 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should do nothing for not bound attribute (at the beginning)', () => {
 			setData( model, '<$text c="true">[]foo</$text>' );
 
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
 			viewDoc.fire( 'keydown', getEventData( {
 				keyCode: keyCodes.arrowright,
 				preventDefault: preventDefaultSpy
 			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
 
 			sinon.assert.notCalled( preventDefaultSpy );
 			expect( selection.isGravityOverridden ).to.false;
@@ -242,84 +238,21 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should do nothing for not bound attribute (at the end)', () => {
 			setData( model, '<$text c="true">foo</$text>[]' );
 
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
 			viewDoc.fire( 'keydown', getEventData( {
 				keyCode: keyCodes.arrowright,
 				preventDefault: preventDefaultSpy
 			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
 
 			sinon.assert.notCalled( preventDefaultSpy );
 			expect( selection.isGravityOverridden ).to.false;
 		} );
 
-		it( 'should do nothing for non-collapsed selection', () => {
-			setData( model, '<$text c="true">foo</$text><$text a="true" b="true">[b]ar</$text>', { lastRangeBackward: true } );
+		it( 'should do nothing when caret is at the beginning of block element', () => {
+			setData( model, '[]foo', { lastRangeBackward: true } );
 
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
-			expect( selection.isGravityOverridden ).to.false;
-		} );
-
-		// There is no need to test it while moving right, because moving right does not use additional state.
-		it( 'should work when external changes are made meanwhile', () => {
-			setData( model, '<$text>foo</$text><$text a="true" b="true">bar[]</$text><$text c="true">biz</$text>' );
-
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
-			// Gravity is overridden, caret is at the end of the text but is "outside" of the text.
-			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
-			expect( selection.isGravityOverridden ).to.true;
-
-			// External changes.
-			model.change( writer => {
-				writer.insertText( 'abc', Position.createAt( editor.model.document.getRoot() ) );
-			} );
-
-			// Press left key.
-			viewDoc.fire( 'keydown', getEventData( {
-				keyCode: keyCodes.arrowleft,
-				preventDefault: preventDefaultSpy
-			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
-			// Caret movement was blocked but now is "inside" the text.
-			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
-			expect( selection.isGravityOverridden ).to.false;
-			sinon.assert.calledOnce( preventDefaultSpy );
-		} );
-
-		// There is no need to test it while moving right, because moving right does not use additional state.
-		it( 'should not block caret when while doing two steps movement and text is removed by external change', () => {
-			setData( model, '<$text c="true">foo</$text><$text a="true" b="true">[]bar</$text>biz' );
-
-			// Firing keyup event will simulate that caret is here as a result of left arrow key press.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
-			// Gravity is overridden, caret is at the beginning of the text and is "inside" of the text.
-			expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
-			expect( selection.isGravityOverridden ).to.true;
-
-			// External changes.
-			model.change( writer => {
-				writer.remove( Range.createFromPositionAndShift( new Position( editor.model.document.getRoot(), [ 2 ] ), 5 ) );
-			} );
-
-			// Press left key.
-			viewDoc.fire( 'keydown', getEventData( {
-				keyCode: keyCodes.arrowleft,
-				preventDefault: preventDefaultSpy
-			} ) );
-			// Moving left needs additional keyup event to check that everything is right.
-			viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
-
-			sinon.assert.notCalled( preventDefaultSpy );
+			expect( () => {
+				viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowleft } ) );
+			} ).to.not.throw();
 		} );
 	} );
 
@@ -345,6 +278,14 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		} ).to.not.throw();
 	} );
 
+	it( 'should do nothing for non-collapsed selection', () => {
+		setData( model, '<$text c="true">fo[o]</$text><$text a="true" b="true">bar</$text>' );
+
+		viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowright } ) );
+
+		expect( selection.isGravityOverridden ).to.false;
+	} );
+
 	function getEventData( data ) {
 		data.target = document.body;