浏览代码

Merge branch 'master' into memory-test

Maciej Gołaszewski 5 年之前
父节点
当前提交
0d301a1f0b
共有 24 个文件被更改,包括 2370 次插入573 次删除
  1. 8 1
      packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
  2. 29 0
      packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js
  3. 0 6
      packages/ckeditor5-image/tests/image.js
  4. 18 5
      packages/ckeditor5-paragraph/src/insertparagraphcommand.js
  5. 28 6
      packages/ckeditor5-paragraph/tests/insertparagraphcommand.js
  6. 10 76
      packages/ckeditor5-table/src/tablekeyboard.js
  7. 7 3
      packages/ckeditor5-table/tests/converters/upcasttable.js
  8. 19 7
      packages/ckeditor5-table/tests/table-integration.js
  9. 3 0
      packages/ckeditor5-table/tests/tablekeyboard.js
  10. 101 7
      packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css
  11. 15 9
      packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js
  12. 85 0
      packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js
  13. 61 0
      packages/ckeditor5-utils/src/keyboard.js
  14. 108 1
      packages/ckeditor5-utils/tests/keyboard.js
  15. 2 0
      packages/ckeditor5-widget/package.json
  16. 100 114
      packages/ckeditor5-widget/src/widget.js
  17. 0 38
      packages/ckeditor5-widget/src/widgettypearound/utils.js
  18. 502 52
      packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js
  19. 2 2
      packages/ckeditor5-widget/tests/manual/type-around.html
  20. 15 2
      packages/ckeditor5-widget/tests/manual/type-around.js
  21. 7 6
      packages/ckeditor5-widget/tests/widget-integration.js
  22. 151 171
      packages/ckeditor5-widget/tests/widget.js
  23. 1063 56
      packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
  24. 36 11
      packages/ckeditor5-widget/theme/widgettypearound.css

+ 8 - 1
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -82,7 +82,14 @@ function selectionPostFixer( writer, model ) {
 		// Those ranges might overlap but will be corrected later.
 		const correctedRange = tryFixingRange( modelRange, schema );
 
-		if ( correctedRange ) {
+		// "Selection fixing" algorithms sometimes get lost. In consequence, it may happen
+		// that a new range is returned but, in fact, it has the same positions as the original
+		// range anyway. If this range is not discarded, a new selection will be set and that,
+		// for instance, would destroy the selection attributes. Let's make sure that the post-fixer
+		// actually worked first before setting a new selection.
+		//
+		// https://github.com/ckeditor/ckeditor5/issues/6693
+		if ( correctedRange && !correctedRange.isEqual( modelRange ) ) {
 			ranges.push( correctedRange );
 			wasFixed = true;
 		} else {

+ 29 - 0
packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js

@@ -835,6 +835,35 @@ describe( 'Selection post-fixer', () => {
 					'</table>]'
 				);
 			} );
+
+			it( 'should not reset the selection if the final range is the same as the initial one', () => {
+				setModelData( model,
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell>[<image></image>]</tableCell>' +
+						'</tableRow>' +
+					'</table>'
+				);
+
+				// Setting a selection attribute will trigger the post-fixer. However, because this
+				// action does not affect ranges, the post-fixer should not set a new selection and,
+				// in consequence, should not clear the selection attribute (like it normally would when
+				// a new selection is set).
+				// https://github.com/ckeditor/ckeditor5/issues/6693
+				model.change( writer => {
+					writer.setSelectionAttribute( 'foo', 'bar' );
+				} );
+
+				assertEqualMarkup( getModelData( model ),
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell>[<image></image>]</tableCell>' +
+						'</tableRow>' +
+					'</table>'
+				);
+
+				expect( model.document.selection.hasAttribute( 'foo' ) ).to.be.true;
+			} );
 		} );
 
 		describe( 'non-collapsed selection - image scenarios', () => {

+ 0 - 6
packages/ckeditor5-image/tests/image.js

@@ -64,7 +64,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -82,7 +81,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="" src="/assets/sample.png"></img>' +
@@ -103,7 +101,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -111,7 +108,6 @@ describe( 'Image', () => {
 				'</figure>]' +
 				'<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -127,7 +123,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 				'<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -135,7 +130,6 @@ describe( 'Image', () => {
 				'</figure>' +
 				'[<figure class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +

+ 18 - 5
packages/ckeditor5-paragraph/src/insertparagraphcommand.js

@@ -18,6 +18,10 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
  *			position: editor.model.createPositionBefore( element )
  *		} );
  *
+ * If a paragraph is disallowed in the context of the specific position, the command
+ * will attempt to split position ancestors to find a place where it is possible
+ * to insert a paragraph.
+ *
  * **Note**: This command moves the selection to the inserted paragraph.
  *
  * @extends module:core/command~Command
@@ -33,15 +37,24 @@ export default class InsertParagraphCommand extends Command {
 	 */
 	execute( options ) {
 		const model = this.editor.model;
-
-		if ( !model.schema.checkChild( options.position, 'paragraph' ) ) {
-			return;
-		}
+		let position = options.position;
 
 		model.change( writer => {
 			const paragraph = writer.createElement( 'paragraph' );
 
-			model.insertContent( paragraph, options.position );
+			if ( !model.schema.checkChild( position.parent, paragraph ) ) {
+				const allowedParent = model.schema.findAllowedParent( position, paragraph );
+
+				// It could be there's no ancestor limit that would allow paragraph.
+				// In theory, "paragraph" could be disallowed even in the "$root".
+				if ( !allowedParent ) {
+					return;
+				}
+
+				position = writer.split( position, allowedParent ).position;
+			}
+
+			model.insertContent( paragraph, position );
 
 			writer.setSelection( paragraph, 'in' );
 		} );

+ 28 - 6
packages/ckeditor5-paragraph/tests/insertparagraphcommand.js

@@ -23,7 +23,9 @@ describe( 'InsertParagraphCommand', () => {
 			editor.commands.add( 'insertParagraph', command );
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			schema.register( 'heading1', { inheritAllFrom: '$block', allowIn: 'headersOnly' } );
-			schema.register( 'headersOnly', { inheritAllFrom: '$block' } );
+			schema.register( 'allowP', { inheritAllFrom: '$block' } );
+			schema.register( 'disallowP', { inheritAllFrom: '$block', allowIn: [ 'allowP' ] } );
+			model.schema.extend( 'paragraph', { allowIn: [ 'allowP' ] } );
 		} );
 	} );
 
@@ -42,18 +44,38 @@ describe( 'InsertParagraphCommand', () => {
 			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph><heading1>foo</heading1>' );
 		} );
 
-		it( 'should do nothing if the paragraph is not allowed at the provided position', () => {
-			setData( model, '<headersOnly><heading1>foo[]</heading1></headersOnly>' );
+		it( 'should split ancestors down to a limit where a paragraph is allowed', () => {
+			setData( model, '<allowP><disallowP>foo</disallowP></allowP>' );
 
 			command.execute( {
-				position: model.createPositionBefore( root.getChild( 0 ).getChild( 0 ) )
+				// fo[]o
+				position: model.createPositionAt( root.getChild( 0 ).getChild( 0 ), 2 )
 			} );
 
+			expect( getData( model ) ).to.equal(
+				'<allowP>' +
+					'<disallowP>fo</disallowP>' +
+					'<paragraph>[]</paragraph>' +
+					'<disallowP>o</disallowP>' +
+				'</allowP>'
+			);
+		} );
+
+		it( 'should do nothing if the paragraph is not allowed at the provided position', () => {
+			// Create a situation where "paragraph" is disallowed even in the "root".
+			schema.addChildCheck( ( context, childDefinition ) => {
+				if ( context.endsWith( '$root' ) && childDefinition.name == 'paragraph' ) {
+					return false;
+				}
+			} );
+
+			setData( model, '<heading1>foo[]</heading1>' );
+
 			command.execute( {
-				position: model.createPositionAfter( root.getChild( 0 ).getChild( 0 ) )
+				position: model.createPositionBefore( root.getChild( 0 ) )
 			} );
 
-			expect( getData( model ) ).to.equal( '<headersOnly><heading1>foo[]</heading1></headersOnly>' );
+			expect( getData( model ) ).to.equal( '<heading1>foo[]</heading1>' );
 		} );
 
 		describe( 'interation with existing paragraphs in the content', () => {

+ 10 - 76
packages/ckeditor5-table/src/tablekeyboard.js

@@ -13,7 +13,10 @@ import TableWalker from './tablewalker';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import {
+	isArrowKeyCode,
+	getLocalizedArrowKeyCodeDirection
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
 import { findAncestor } from './utils/common';
 
@@ -50,11 +53,10 @@ export default class TableKeyboard extends Plugin {
 		this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
 		this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
 
-		// Note: This listener has the "high+1" priority because we would like to avoid collisions with other features
-		// (like Widgets), which take over the keydown events with the "high" priority. Table navigation takes precedence
-		// over Widgets in that matter (widget arrow handler stops propagation of event if object element was selected
-		// but getNearestSelectionRange didn't returned any range).
-		this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) + 1 } );
+		// Note: This listener has the "high-10" priority because it should allow the Widget plugin to handle the default
+		// behavior first ("high") but it should not be "prevent–defaulted" by the Widget plugin ("high-20") because of
+		// the fake selection retention on the fully selected widget.
+		this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) - 10 } );
 	}
 
 	/**
@@ -163,13 +165,14 @@ export default class TableKeyboard extends Plugin {
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 */
 	_onKeydown( eventInfo, domEventData ) {
+		const editor = this.editor;
 		const keyCode = domEventData.keyCode;
 
 		if ( !isArrowKeyCode( keyCode ) ) {
 			return;
 		}
 
-		const direction = getDirectionFromKeyCode( keyCode, this.editor.locale.contentLanguageDirection );
+		const direction = getLocalizedArrowKeyCodeDirection( keyCode, editor.locale.contentLanguageDirection );
 		const wasHandled = this._handleArrowKeys( direction, domEventData.shiftKey );
 
 		if ( wasHandled ) {
@@ -226,19 +229,6 @@ export default class TableKeyboard extends Plugin {
 			return true;
 		}
 
-		// If this is an object selected and it's not at the start or the end of cell content
-		// then let's allow widget handler to take care of it.
-		const objectElement = selection.getSelectedElement();
-
-		if ( objectElement && model.schema.isObject( objectElement ) ) {
-			return false;
-		}
-
-		// If next to the selection there is an object then this is not the cell boundary (widget handler should handle this).
-		if ( this._isObjectElementNextToSelection( selection, isForward ) ) {
-			return false;
-		}
-
 		// If there isn't any $text position between cell edge and selection then we shall move the selection to next cell.
 		const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward );
 
@@ -304,27 +294,6 @@ export default class TableKeyboard extends Plugin {
 	}
 
 	/**
-	 * Checks if there is an {@link module:engine/model/element~Element element} next to the current
-	 * {@link module:engine/model/selection~Selection model selection} marked in the
-	 * {@link module:engine/model/schema~Schema schema} as an `object`.
-	 *
-	 * @private
-	 * @param {module:engine/model/selection~Selection} modelSelection The selection.
-	 * @param {Boolean} isForward The direction of checking.
-	 * @returns {Boolean}
-	 */
-	_isObjectElementNextToSelection( modelSelection, isForward ) {
-		const model = this.editor.model;
-		const schema = model.schema;
-
-		const probe = model.createSelection( modelSelection );
-		model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } );
-		const objectElement = isForward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
-
-		return objectElement && schema.isObject( objectElement );
-	}
-
-	/**
 	 * Truncates the range so that it spans from the last selection position
 	 * to the last allowed `$text` position (mirrored if `isForward` is false).
 	 *
@@ -515,38 +484,3 @@ export default class TableKeyboard extends Plugin {
 	}
 }
 
-// Returns `true` if the provided key code represents one of the arrow keys.
-//
-// @private
-// @param {Number} keyCode
-// @returns {Boolean}
-function isArrowKeyCode( keyCode ) {
-	return keyCode == keyCodes.arrowright ||
-		keyCode == keyCodes.arrowleft ||
-		keyCode == keyCodes.arrowup ||
-		keyCode == keyCodes.arrowdown;
-}
-
-// Returns the direction name from `keyCode`.
-//
-// @private
-// @param {Number} keyCode
-// @param {String} contentLanguageDirection The content language direction.
-// @returns {'left'|'up'|'right'|'down'} Arrow direction.
-function getDirectionFromKeyCode( keyCode, contentLanguageDirection ) {
-	const isLtrContent = contentLanguageDirection === 'ltr';
-
-	switch ( keyCode ) {
-		case keyCodes.arrowleft:
-			return isLtrContent ? 'left' : 'right';
-
-		case keyCodes.arrowright:
-			return isLtrContent ? 'right' : 'left';
-
-		case keyCodes.arrowup:
-			return 'up';
-
-		case keyCodes.arrowdown:
-			return 'down';
-	}
-}

+ 7 - 3
packages/ckeditor5-table/tests/converters/upcasttable.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
@@ -17,8 +17,8 @@ describe( 'upcastTable()', () => {
 	let editor, model;
 
 	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
+		return ClassicTestEditor
+			.create( '', {
 				plugins: [ TableEditing, Paragraph, ImageEditing, Widget ]
 			} )
 			.then( newEditor => {
@@ -31,6 +31,10 @@ describe( 'upcastTable()', () => {
 			} );
 	} );
 
+	afterEach( () => {
+		editor.destroy();
+	} );
+
 	function expectModel( data ) {
 		assertEqualMarkup( getModelData( model, { withoutSelection: true } ), data );
 	}

+ 19 - 7
packages/ckeditor5-table/tests/table-integration.js

@@ -11,7 +11,7 @@ import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
 import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import {
 	getData as getModelData,
 	setData as setModelData
@@ -27,14 +27,18 @@ describe( 'Table feature – integration', () => {
 		let editor, clipboard;
 
 		beforeEach( () => {
-			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } )
+			return ClassicTestEditor
+				.create( '', { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } )
 				.then( newEditor => {
 					editor = newEditor;
 					clipboard = editor.plugins.get( 'Clipboard' );
 				} );
 		} );
 
+		afterEach( () => {
+			editor.destroy();
+		} );
+
 		it( 'pastes td as p when pasting into the table', () => {
 			setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) );
 
@@ -86,8 +90,8 @@ describe( 'Table feature – integration', () => {
 		let editor, doc, root;
 
 		beforeEach( () => {
-			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } )
+			return ClassicTestEditor
+				.create( '', { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } )
 				.then( newEditor => {
 					editor = newEditor;
 					doc = editor.model.document;
@@ -95,6 +99,10 @@ describe( 'Table feature – integration', () => {
 				} );
 		} );
 
+		afterEach( () => {
+			editor.destroy();
+		} );
+
 		it( 'fixing empty roots should be transparent to undo', () => {
 			expect( editor.getData( { trim: 'none' } ) ).to.equal( '<p>&nbsp;</p>' );
 			expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
@@ -155,13 +163,17 @@ describe( 'Table feature – integration', () => {
 		let editor;
 
 		beforeEach( () => {
-			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Typing ] } )
+			return ClassicTestEditor
+				.create( '', { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Typing ] } )
 				.then( newEditor => {
 					editor = newEditor;
 				} );
 		} );
 
+		afterEach( () => {
+			editor.destroy();
+		} );
+
 		it( 'merges elements without throwing errors', () => {
 			setModelData( editor.model, modelTable( [
 				[ '<blockQuote><paragraph>Foo</paragraph></blockQuote><paragraph>[]Bar</paragraph>' ]

+ 3 - 0
packages/ckeditor5-table/tests/tablekeyboard.js

@@ -2769,6 +2769,9 @@ describe( 'TableKeyboard', () => {
 							[ '20', '21', '22' ]
 						] ) );
 
+						// Note: Two keydowns are necessary because the first one is handled by the WidgetTypeAround plugin
+						// to activate the "fake caret".
+						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
 						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
 
 						assertEqualMarkup( getModelData( model ), modelTable( [

+ 101 - 7
packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css

@@ -13,6 +13,16 @@
 	--ck-color-widget-type-around-button-icon: var(--ck-color-base-background);
 }
 
+@define-mixin ck-widget-type-around-button-visible {
+	opacity: 1;
+	pointer-events: auto;
+}
+
+@define-mixin ck-widget-type-around-button-hidden {
+	opacity: 0;
+	pointer-events: none;
+}
+
 .ck .ck-widget {
 	/*
 	 * Styles of the type around buttons
@@ -22,11 +32,10 @@
 		height: var(--ck-widget-type-around-button-size);
 		background: var(--ck-color-widget-type-around-button);
 		border-radius: 100px;
-
-		pointer-events: none;
-		opacity: 0;
 		transition: opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve), background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);
 
+		@mixin ck-widget-type-around-button-hidden;
+
 		& svg {
 			width: 10px;
 			height: 8px;
@@ -77,8 +86,7 @@
 	&.ck-widget_selected,
 	&:hover {
 		& > .ck-widget__type-around > .ck-widget__type-around__button {
-			pointer-events: auto;
-			opacity: 1;
+			@mixin ck-widget-type-around-button-visible;
 		}
 	}
 
@@ -114,6 +122,75 @@
 	&.ck-widget_with-selection-handle > .ck-widget__type-around > .ck-widget__type-around__button_before {
 		margin-left: 20px;
 	}
+
+	/*
+	 * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard.
+	 */
+	& .ck-widget__type-around__fake-caret {
+		pointer-events: none;
+		height: 1px;
+		animation: ck-widget-type-around-fake-caret-pulse linear 1s infinite normal forwards;
+
+		/*
+		 * The semit-transparent-outline+background combo improves the contrast
+		 * when the background underneath the fake caret is dark.
+		 */
+		outline: solid 1px hsla(0, 0%, 100%, .5);
+		background: var(--ck-color-base-text);
+	}
+
+	/*
+	 * Styles of the widget when the "fake caret" is blinking (e.g. upon keyboard navigation).
+	 * Despite the widget being physically selected in the model, its outline should disappear.
+	 */
+	&.ck-widget_selected {
+		&.ck-widget_type-around_show-fake-caret_before,
+		&.ck-widget_type-around_show-fake-caret_after {
+			outline-color: transparent;
+		}
+	}
+
+	&.ck-widget_type-around_show-fake-caret_before,
+	&.ck-widget_type-around_show-fake-caret_after {
+		/*
+		 * When the "fake caret" is visible we simulate that the widget is not selected
+		 * (despite being physically selected), so the outline color should be for the
+		 * unselected widget.
+		 */
+		&.ck-widget_selected:hover {
+			outline-color: var(--ck-color-widget-hover-border);
+		}
+
+		/*
+		 * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation).
+		 * In this state, the type around buttons would collide with the fake carets so they should disappear.
+		 */
+		& > .ck-widget__type-around > .ck-widget__type-around__button {
+			@mixin ck-widget-type-around-button-hidden;
+		}
+
+		/*
+		 * Fake horizontal caret integration with the selection handle. When the caret is visible, simply
+		 * hide the handle because it intersects with the caret (and does not make much sense anyway).
+		 */
+		&.ck-widget_with-selection-handle {
+			&.ck-widget_selected,
+			&.ck-widget_selected:hover {
+				& > .ck-widget__selection-handle {
+					opacity: 0
+				}
+			}
+		}
+
+		/*
+		 * Fake horizontal caret integration with the resize UI. When the caret is visible, simply
+		 * hide the resize UI because it creates too much noise. It can be visible when the user
+		 * hovers the widget, though.
+		 */
+		&.ck-widget_selected.ck-widget_with-resizer > .ck-widget__resizer {
+			opacity: 0
+		}
+	}
 }
 
 /*
@@ -127,8 +204,7 @@
 		&.ck-widget_selected,
 		&:hover {
 			& > .ck-widget__type-around > .ck-widget__type-around__button {
-				pointer-events: none;
-				opacity: 0;
+				@mixin ck-widget-type-around-button-hidden;
 			}
 		}
 	}
@@ -174,3 +250,21 @@
 		box-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));
 	}
 }
+
+@keyframes ck-widget-type-around-fake-caret-pulse {
+	0% {
+		opacity: 1;
+	}
+	49% {
+		opacity: 1;
+	}
+	50% {
+		opacity: 0;
+	}
+	99% {
+		opacity: 0;
+	}
+	100% {
+		opacity: 1;
+	}
+}

+ 15 - 9
packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js

@@ -65,7 +65,7 @@ export default function injectUnsafeKeystrokesHandling( editor ) {
 			return;
 		}
 
-		if ( isSafeKeystroke( evtData ) || doc.selection.isCollapsed ) {
+		if ( isNonTypingKeystroke( evtData ) || doc.selection.isCollapsed ) {
 			return;
 		}
 
@@ -154,14 +154,20 @@ for ( let code = 112; code <= 135; code++ ) {
 	safeKeycodes.push( code );
 }
 
-// Returns `true` if a keystroke should not cause any content change caused by "typing".
-//
-// Note: This implementation is very simple and will need to be refined with time.
-//
-// @private
-// @param {engine.view.observer.keyObserver.KeyEventData} keyData
-// @returns {Boolean}
-function isSafeKeystroke( keyData ) {
+/**
+ * Returns `true` if a keystroke will **not** result in "typing".
+ *
+ * For instance, keystrokes that result in typing are letters "a-zA-Z", numbers "0-9", delete, backspace, etc.
+ *
+ * Keystrokes that do not cause typing are, for instance, Fn keys (F5, F8, etc.), arrow keys (←, →, ↑, ↓),
+ * Tab (↹), "Windows logo key" (⊞ Win), etc.
+ *
+ * Note: This implementation is very simple and will need to be refined with time.
+ *
+ * @param {module:engine/view/observer/keyobserver~KeyEventData} keyData
+ * @returns {Boolean}
+ */
+export function isNonTypingKeystroke( keyData ) {
 	// Keystrokes which contain Ctrl don't represent typing.
 	if ( keyData.ctrlKey ) {
 		return true;

+ 85 - 0
packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js

@@ -0,0 +1,85 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import {
+	keyCodes
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { isNonTypingKeystroke } from '../../src/utils/injectunsafekeystrokeshandling';
+
+describe( 'unsafe keystroke handling utils', () => {
+	describe( 'isNonTypingKeystroke()', () => {
+		it( 'should return "true" for any keystroke with the Ctrl key', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.a, ctrlKey: true } ), 'Ctrl+a' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ], ctrlKey: true } ), 'Ctrl+0' ).to.be.true;
+		} );
+
+		it( 'should return "true" for all arrow keys', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowup } ), 'arrow up' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowdown } ), 'arrow down' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowleft } ), 'arrow left' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowright } ), 'arrow right' ).to.be.true;
+		} );
+
+		it( 'should return "true" for function (Fn) keystrokes', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f1 } ), 'F1' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f2 } ), 'F2' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f3 } ), 'F3' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f4 } ), 'F4' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f5 } ), 'F5' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f6 } ), 'F6' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f7 } ), 'F7' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f8 } ), 'F8' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f9 } ), 'F9' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f10 } ), 'F10' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f11 } ), 'F11' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f12 } ), 'F12' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 124 } ), 'F13' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 125 } ), 'F14' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 126 } ), 'F15' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 127 } ), 'F16' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 128 } ), 'F17' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 129 } ), 'F18' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 130 } ), 'F19' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 131 } ), 'F20' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 132 } ), 'F21' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 133 } ), 'F22' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 134 } ), 'F23' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 135 } ), 'F24' ).to.be.true;
+		} );
+
+		it( 'should return "true" for other safe keystrokes', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.tab } ), 'Tab' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 16 } ), 'Shift' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 17 } ), 'Ctrl' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 18 } ), 'Alt' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 19 } ), 'Pause' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 20 } ), 'CapsLock' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.esc } ), 'Escape' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 33 } ), 'PageUp' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 34 } ), 'PageDown' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 35 } ), 'Home' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 36 } ), 'End' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 45 } ), 'Insert' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 91 } ), 'Windows' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 93 } ), 'Menu key' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 144 } ), 'NumLock' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 145 } ), 'ScrollLock' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 173 } ), 'Mute/Unmute' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 174 } ), 'Volume up' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 175 } ), 'Volume down' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 176 } ), 'Next song' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 177 } ), 'Previous song' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 178 } ), 'Stop' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 179 } ), 'Play/Pause' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 255 } ), 'Display brightness (increase and decrease)' ).to.be.true;
+		} );
+
+		it( 'should return "false" for the keystrokes that result in typing', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.a } ), 'a' ).to.be.false;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ] } ), '0' ).to.be.false;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false;
+		} );
+	} );
+} );

+ 61 - 0
packages/ckeditor5-utils/src/keyboard.js

@@ -129,6 +129,67 @@ export function getEnvKeystrokeText( keystroke ) {
 		} );
 }
 
+/**
+ * Returns `true` if the provided key code represents one of the arrow keys.
+ *
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
+ * @returns {Boolean}
+ */
+export function isArrowKeyCode( keyCode ) {
+	return keyCode == keyCodes.arrowright ||
+		keyCode == keyCodes.arrowleft ||
+		keyCode == keyCodes.arrowup ||
+		keyCode == keyCodes.arrowdown;
+}
+
+/**
+ * Returns the direction in which the {@link module:engine/model/documentselection~DocumentSelection selection}
+ * will move when a provided arrow key code is pressed considering the language direction of the editor content.
+ *
+ * For instance, in right–to–left (RTL) content languages, pressing the left arrow means moving selection right (forward)
+ * in the model structure. Similarly, pressing the right arrow moves the selection left (backward).
+ *
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
+ * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to
+ * {@link module:utils/locale~Locale#contentLanguageDirection}.
+ * @returns {'left'|'up'|'right'|'down'} Localized arrow direction.
+ */
+export function getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirection ) {
+	const isLtrContent = contentLanguageDirection === 'ltr';
+
+	switch ( keyCode ) {
+		case keyCodes.arrowleft:
+			return isLtrContent ? 'left' : 'right';
+
+		case keyCodes.arrowright:
+			return isLtrContent ? 'right' : 'left';
+
+		case keyCodes.arrowup:
+			return 'up';
+
+		case keyCodes.arrowdown:
+			return 'down';
+	}
+}
+
+/**
+ * Determines if the provided key code moves the {@link module:engine/model/documentselection~DocumentSelection selection}
+ * forward or backward considering the language direction of the editor content.
+ *
+ * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward
+ * in the model structure. Similarly, pressing the right arrow moves the selection backward.
+ *
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
+ * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to
+ * {@link module:utils/locale~Locale#contentLanguageDirection}.
+ * @returns {Boolean}
+ */
+export function isForwardArrowKeyCode( keyCode, contentLanguageDirection ) {
+	const localizedKeyCodeDirection = getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirection );
+
+	return localizedKeyCodeDirection === 'down' || localizedKeyCodeDirection === 'right';
+}
+
 function generateKnownKeyCodes() {
 	const keyCodes = {
 		arrowleft: 37,

+ 108 - 1
packages/ckeditor5-utils/tests/keyboard.js

@@ -4,7 +4,15 @@
  */
 
 import env from '../src/env';
-import { keyCodes, getCode, parseKeystroke, getEnvKeystrokeText } from '../src/keyboard';
+import {
+	keyCodes,
+	getCode,
+	parseKeystroke,
+	getEnvKeystrokeText,
+	isArrowKeyCode,
+	isForwardArrowKeyCode,
+	getLocalizedArrowKeyCodeDirection
+} from '../src/keyboard';
 import { expectToThrowCKEditorError } from './_utils/utils';
 
 describe( 'Keyboard', () => {
@@ -160,4 +168,103 @@ describe( 'Keyboard', () => {
 			} );
 		} );
 	} );
+
+	describe( 'isArrowKeyCode()', () => {
+		it( 'should return "true" for right arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowright ) ).to.be.true;
+		} );
+
+		it( 'should return "true" for left arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowleft ) ).to.be.true;
+		} );
+
+		it( 'should return "true" for up arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowup ) ).to.be.true;
+		} );
+
+		it( 'should return "true" for down arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowdown ) ).to.be.true;
+		} );
+
+		it( 'should return "false" for non-arrow keystrokes', () => {
+			expect( isArrowKeyCode( keyCodes.a ) ).to.be.false;
+			expect( isArrowKeyCode( keyCodes.ctrl ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'getLocalizedArrowKeyCodeDirection()', () => {
+		describe( 'for a left–to–right content language direction', () => {
+			it( 'should return "left" for left arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowleft, 'ltr' ) ).to.equal( 'left' );
+			} );
+
+			it( 'should return "right" for right arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowright, 'ltr' ) ).to.equal( 'right' );
+			} );
+
+			it( 'should return "up" for up arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowup, 'ltr' ) ).to.equal( 'up' );
+			} );
+
+			it( 'should return "down" for down arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowdown, 'ltr' ) ).to.equal( 'down' );
+			} );
+		} );
+
+		describe( 'for a right-to-left content language direction', () => {
+			it( 'should return "right" for left arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowleft, 'rtl' ) ).to.equal( 'right' );
+			} );
+
+			it( 'should return "left" for right arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowright, 'rtl' ) ).to.equal( 'left' );
+			} );
+
+			it( 'should return "up" for up arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowup, 'rtl' ) ).to.equal( 'up' );
+			} );
+
+			it( 'should return "down" for down arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowdown, 'rtl' ) ).to.equal( 'down' );
+			} );
+		} );
+	} );
+
+	describe( 'isForwardArrowKeyCode()', () => {
+		describe( 'for a left–to–right content language direction', () => {
+			it( 'should return "true" for down arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowdown, 'ltr' ) ).to.be.true;
+			} );
+
+			it( 'should return "true" for right arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowright, 'ltr' ) ).to.be.true;
+			} );
+
+			it( 'should return "false" for up arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowup, 'ltr' ) ).to.be.false;
+			} );
+
+			it( 'should return "false" for left arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowleft, 'ltr' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'for a right-to-left content language direction', () => {
+			it( 'should return "true" for down arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowdown, 'rtl' ) ).to.be.true;
+			} );
+
+			it( 'should return "true" for left arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowleft, 'rtl' ) ).to.be.true;
+			} );
+
+			it( 'should return "false" for up arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowup, 'rtl' ) ).to.be.false;
+			} );
+
+			it( 'should return "false" for right arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowright, 'rtl' ) ).to.be.false;
+			} );
+		} );
+	} );
 } );

+ 2 - 0
packages/ckeditor5-widget/package.json

@@ -11,6 +11,7 @@
   "dependencies": {
     "@ckeditor/ckeditor5-core": "^19.0.1",
     "@ckeditor/ckeditor5-engine": "^19.0.1",
+    "@ckeditor/ckeditor5-typing": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
     "lodash-es": "^4.17.15"
@@ -25,6 +26,7 @@
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
     "@ckeditor/ckeditor5-heading": "^19.0.1",
     "@ckeditor/ckeditor5-horizontal-line": "^19.0.1",
+    "@ckeditor/ckeditor5-image": "^19.0.1",
     "@ckeditor/ckeditor5-media-embed": "^19.1.0",
     "@ckeditor/ckeditor5-paragraph": "^19.1.0",
     "@ckeditor/ckeditor5-table": "^19.1.0",

+ 100 - 114
packages/ckeditor5-widget/src/widget.js

@@ -11,10 +11,14 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
 import WidgetTypeAround from './widgettypearound/widgettypearound';
 import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import {
+	isArrowKeyCode,
+	isForwardArrowKeyCode
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 
 import '../theme/widget.css';
+import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 /**
  * The widget plugin. It enables base support for widgets.
@@ -96,8 +100,24 @@ export default class Widget extends Plugin {
 		view.addObserver( MouseObserver );
 		this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
 
-		// Handle custom keydown behaviour.
-		this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } );
+		// There are two keydown listeners working on different priorities. This allows other
+		// features such as WidgetTypeAround or TableKeyboard to attach their listeners in between
+		// and customize the behavior even further in different content/selection scenarios.
+		//
+		// * The first listener handles changing the selection on arrow key press
+		// if the widget is selected or if the selection is next to a widget and the widget
+		// should become selected upon the arrow key press.
+		//
+		// * The second (late) listener makes sure the default browser action on arrow key press is
+		// prevented when a widget is selected. This prevents the selection from being moved
+		// from a fake selection container.
+		this.listenTo( viewDocument, 'keydown', ( ...args ) => {
+			this._handleSelectionChangeOnArrowKeyPress( ...args );
+		}, { priority: 'high' } );
+
+		this.listenTo( viewDocument, 'keydown', ( ...args ) => {
+			this._preventDefaultOnArrowKeyPress( ...args );
+		}, { priority: priorities.get( 'high' ) - 20 } );
 
 		// Handle custom delete behaviour.
 		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
@@ -162,27 +182,92 @@ export default class Widget extends Plugin {
 	}
 
 	/**
-	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events.
+	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and changes
+	 * the model selection when:
+	 *
+	 * * arrow key is pressed when the widget is selected,
+	 * * the selection is next to a widget and the widget should become selected upon the arrow key press.
+	 *
+	 * See {@link #_preventDefaultOnArrowKeyPress}.
+	 *
+	 * @private
+	 * @param {module:utils/eventinfo~EventInfo} eventInfo
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 */
+	_handleSelectionChangeOnArrowKeyPress( eventInfo, domEventData ) {
+		const keyCode = domEventData.keyCode;
+
+		// Checks if the keys were handled and then prevents the default event behaviour and stops
+		// the propagation.
+		if ( !isArrowKeyCode( keyCode ) ) {
+			return;
+		}
+
+		const model = this.editor.model;
+		const schema = model.schema;
+		const modelSelection = model.document.selection;
+		const objectElement = modelSelection.getSelectedElement();
+		const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection );
+
+		// If object element is selected.
+		if ( objectElement && schema.isObject( objectElement ) ) {
+			const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
+			const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );
+
+			if ( newRange ) {
+				model.change( writer => {
+					writer.setSelection( newRange );
+				} );
+
+				domEventData.preventDefault();
+				eventInfo.stop();
+			}
+
+			return;
+		}
+
+		// If selection is next to object element.
+		// Return if not collapsed.
+		if ( !modelSelection.isCollapsed ) {
+			return;
+		}
+
+		const objectElementNextToSelection = this._getObjectElementNextToSelection( isForward );
+
+		if ( objectElementNextToSelection && schema.isObject( objectElementNextToSelection ) ) {
+			this._setSelectionOverElement( objectElementNextToSelection );
+
+			domEventData.preventDefault();
+			eventInfo.stop();
+		}
+	}
+
+	/**
+	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and prevents
+	 * the default browser behavior to make sure the fake selection is not being moved from a fake selection
+	 * container.
+	 *
+	 * See {@link #_handleSelectionChangeOnArrowKeyPress}.
 	 *
 	 * @private
 	 * @param {module:utils/eventinfo~EventInfo} eventInfo
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 */
-	_onKeydown( eventInfo, domEventData ) {
+	_preventDefaultOnArrowKeyPress( eventInfo, domEventData ) {
 		const keyCode = domEventData.keyCode;
-		const isLtrContent = this.editor.locale.contentLanguageDirection === 'ltr';
-		const isForward = keyCode == keyCodes.arrowdown || keyCode == keyCodes[ isLtrContent ? 'arrowright' : 'arrowleft' ];
-		let wasHandled = false;
 
 		// Checks if the keys were handled and then prevents the default event behaviour and stops
 		// the propagation.
-		if ( isArrowKeyCode( keyCode ) ) {
-			wasHandled = this._handleArrowKeys( isForward );
-		} else if ( keyCode === keyCodes.enter ) {
-			wasHandled = this._handleEnterKey( domEventData.shiftKey );
+		if ( !isArrowKeyCode( keyCode ) ) {
+			return;
 		}
 
-		if ( wasHandled ) {
+		const model = this.editor.model;
+		const schema = model.schema;
+		const objectElement = model.document.selection.getSelectedElement();
+
+		// If object element is selected.
+		if ( objectElement && schema.isObject( objectElement ) ) {
 			domEventData.preventDefault();
 			eventInfo.stop();
 		}
@@ -231,89 +316,9 @@ export default class Widget extends Plugin {
 	}
 
 	/**
-	 * Handles arrow keys.
-	 *
-	 * @private
-	 * @param {Boolean} isForward Set to true if arrow key should be handled in forward direction.
-	 * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
-	 */
-	_handleArrowKeys( isForward ) {
-		const model = this.editor.model;
-		const schema = model.schema;
-		const modelDocument = model.document;
-		const modelSelection = modelDocument.selection;
-		const objectElement = modelSelection.getSelectedElement();
-
-		// If object element is selected.
-		if ( objectElement && schema.isObject( objectElement ) ) {
-			const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
-			const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );
-
-			if ( newRange ) {
-				model.change( writer => {
-					writer.setSelection( newRange );
-				} );
-			}
-
-			return true;
-		}
-
-		// If selection is next to object element.
-		// Return if not collapsed.
-		if ( !modelSelection.isCollapsed ) {
-			return;
-		}
-
-		const objectElement2 = this._getObjectElementNextToSelection( isForward );
-
-		if ( !!objectElement2 && schema.isObject( objectElement2 ) ) {
-			this._setSelectionOverElement( objectElement2 );
-
-			return true;
-		}
-	}
-
-	/**
-	 * Handles the enter key, giving users and access to positions in the editable directly before
-	 * (<kbd>Shift</kbd>+<kbd>Enter</kbd>) or after (<kbd>Enter</kbd>) the selected widget.
-	 * It improves the UX, mainly when the widget is the first or last child of the root editable
-	 * and there's no other way to type after or before it.
-	 *
-	 * @private
-	 * @param {Boolean} isBackwards Set to true if the new paragraph is to be inserted before
-	 * the selected widget (<kbd>Shift</kbd>+<kbd>Enter</kbd>).
-	 * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
-	 */
-	_handleEnterKey( isBackwards ) {
-		const model = this.editor.model;
-		const modelSelection = model.document.selection;
-		const selectedElement = modelSelection.getSelectedElement();
-
-		if ( shouldInsertParagraph( selectedElement, model.schema ) ) {
-			model.change( writer => {
-				let position = writer.createPositionAt( selectedElement, isBackwards ? 'before' : 'after' );
-				const paragraph = writer.createElement( 'paragraph' );
-
-				// Split the parent when inside a block element.
-				// https://github.com/ckeditor/ckeditor5/issues/1529
-				if ( model.schema.isBlock( selectedElement.parent ) ) {
-					const paragraphLimit = model.schema.findAllowedParent( position, paragraph );
-
-					position = writer.split( position, paragraphLimit ).position;
-				}
-
-				writer.insert( paragraph, position );
-				writer.setSelection( paragraph, 'in' );
-			} );
-
-			return true;
-		}
-	}
-
-	/**
 	 * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
 	 *
-	 * @private
+	 * @protected
 	 * @param {module:engine/model/element~Element} element
 	 */
 	_setSelectionOverElement( element ) {
@@ -327,7 +332,7 @@ export default class Widget extends Plugin {
 	 * {@link module:engine/model/selection~Selection model selection} exists and is marked in
 	 * {@link module:engine/model/schema~Schema schema} as `object`.
 	 *
-	 * @private
+	 * @protected
 	 * @param {Boolean} forward Direction of checking.
 	 * @returns {module:engine/model/element~Element|null}
 	 */
@@ -364,17 +369,6 @@ export default class Widget extends Plugin {
 	}
 }
 
-// Returns 'true' if provided key code represents one of the arrow keys.
-//
-// @param {Number} keyCode
-// @returns {Boolean}
-function isArrowKeyCode( keyCode ) {
-	return keyCode == keyCodes.arrowright ||
-		keyCode == keyCodes.arrowleft ||
-		keyCode == keyCodes.arrowup ||
-		keyCode == keyCodes.arrowdown;
-}
-
 // Returns `true` when element is a nested editable or is placed inside one.
 //
 // @param {module:engine/view/element~Element}
@@ -408,11 +402,3 @@ function isChild( element, parent ) {
 
 	return Array.from( element.getAncestors() ).includes( parent );
 }
-
-// Checks if enter key should insert paragraph. This should be done only on elements of type object (excluding inline objects).
-//
-// @param {module:engine/model/element~Element} element And element to check.
-// @param {module:engine/model/schema~Schema} schema
-function shouldInsertParagraph( element, schema ) {
-	return element && schema.isObject( element ) && !schema.isInline( element );
-}

+ 0 - 38
packages/ckeditor5-widget/src/widgettypearound/utils.js

@@ -55,41 +55,3 @@ export function getClosestWidgetViewElement( domElement, domConverter ) {
 
 	return domConverter.mapDomToView( widgetDomElement );
 }
-
-/**
- * For the passed widget view element, this helper returns an array of positions which
- * correspond to the "tight spots" around the widget which cannot be accessed due to
- * limitations of selection rendering in web browsers.
- *
- * @param {module:engine/view/element~Element} widgetViewElement
- * @returns {Array.<String>}
- */
-export function getWidgetTypeAroundPositions( widgetViewElement ) {
-	const positions = [];
-
-	if ( isFirstChild( widgetViewElement ) || hasPreviousWidgetSibling( widgetViewElement ) ) {
-		positions.push( 'before' );
-	}
-
-	if ( isLastChild( widgetViewElement ) || hasNextWidgetSibling( widgetViewElement ) ) {
-		positions.push( 'after' );
-	}
-
-	return positions;
-}
-
-function isFirstChild( widget ) {
-	return !widget.previousSibling;
-}
-
-function isLastChild( widget ) {
-	return !widget.nextSibling;
-}
-
-function hasPreviousWidgetSibling( widget ) {
-	return widget.previousSibling && isWidget( widget.previousSibling );
-}
-
-function hasNextWidgetSibling( widget ) {
-	return widget.nextSibling && isWidget( widget.nextSibling );
-}

+ 502 - 52
packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js

@@ -10,17 +10,25 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Template from '@ckeditor/ckeditor5-ui/src/template';
+import {
+	isArrowKeyCode,
+	isForwardArrowKeyCode,
+	keyCodes
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
+import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 import {
 	isTypeAroundWidget,
-	getWidgetTypeAroundPositions,
 	getClosestTypeAroundDomButton,
 	getTypeAroundButtonPosition,
 	getClosestWidgetViewElement
 } from './utils';
 
+import {
+	isNonTypingKeystroke
+} from '@ckeditor/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling';
+
 import returnIcon from '../../theme/icons/return-arrow.svg';
 import '../../theme/widgettypearound.css';
 
@@ -29,6 +37,8 @@ const POSSIBLE_INSERTION_POSITIONS = [ 'before', 'after' ];
 // Do the SVG parsing once and then clone the result <svg> DOM element for each new button.
 const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, 'image/svg+xml' ).firstChild;
 
+const TYPE_AROUND_SELECTION_ATTRIBUTE = 'widget-type-around';
+
 /**
  * A plugin that allows users to type around widgets where normally it is impossible to place the caret due
  * to limitations of web browsers. These "tight spots" occur, for instance, before (or after) a widget being
@@ -46,13 +56,6 @@ export default class WidgetTypeAround extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
-	static get requires() {
-		return [ Paragraph ];
-	}
-
-	/**
-	 * @inheritDoc
-	 */
 	static get pluginName() {
 		return 'WidgetTypeAround';
 	}
@@ -64,32 +67,33 @@ export default class WidgetTypeAround extends Plugin {
 		super( editor );
 
 		/**
-		 * A set containing all widgets in all editor roots that have the type around UI injected in
-		 * {@link #_enableTypeAroundUIInjection}.
-		 *
-		 * Keeping track of them saves time, for instance, when updating their CSS classes.
+		 * A reference to the model widget element that has the "fake caret" active
+		 * on either side of it. It is later used to remove CSS classes associated with the "fake caret"
+		 * when the widget no longer needs it.
 		 *
 		 * @private
-		 * @readonly
-		 * @member {Set} #_widgetsWithTypeAroundUI
+		 * @member {module:engine/model/element~Element|null}
 		 */
-		this._widgetsWithTypeAroundUI = new Set();
+		this._currentFakeCaretModelElement = null;
 	}
 
 	/**
 	 * @inheritDoc
 	 */
-	destroy() {
-		this._widgetsWithTypeAroundUI.clear();
+	init() {
+		this._enableTypeAroundUIInjection();
+		this._enableInsertingParagraphsOnButtonClick();
+		this._enableInsertingParagraphsOnEnterKeypress();
+		this._enableInsertingParagraphsOnTypingKeystroke();
+		this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
+		this._enableDeleteIntegration();
 	}
 
 	/**
 	 * @inheritDoc
 	 */
-	init() {
-		this._enableTypeAroundUIInjection();
-		this._enableDetectionOfTypeAroundWidgets();
-		this._enableInsertingParagraphsOnButtonClick();
+	destroy() {
+		this._currentFakeCaretModelElement = null;
 	}
 
 	/**
@@ -99,13 +103,12 @@ export default class WidgetTypeAround extends Plugin {
 	 * the viewport to the selection in the inserted paragraph.
 	 *
 	 * @protected
-	 * @param {module:engine/view/element~Element} widgetViewElement The view widget element next to which a paragraph is inserted.
+	 * @param {module:engine/model/element~Element} widgetModelElement The model widget element next to which a paragraph is inserted.
 	 * @param {'before'|'after'} position The position where the paragraph is inserted. Either `'before'` or `'after'` the widget.
 	 */
-	_insertParagraph( widgetViewElement, position ) {
+	_insertParagraph( widgetModelElement, position ) {
 		const editor = this.editor;
 		const editingView = editor.editing.view;
-		const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement );
 		let modelPosition;
 
 		if ( position === 'before' ) {
@@ -123,6 +126,35 @@ export default class WidgetTypeAround extends Plugin {
 	}
 
 	/**
+	 * Similar to {@link #_insertParagraph}, this method inserts a paragraph except that it
+	 * does not expect a position but it performs the insertion next to a selected widget
+	 * according to the "widget-type-around" model selection attribute value.
+	 *
+	 * Because this method requires the "widget-type-around" attribute to be set,
+	 * the insertion can only happen when the widget "fake caret" is active (e.g. activated
+	 * using the keyboard).
+	 *
+	 * @private
+	 * @returns {Boolean} Returns `true` when the paragraph was inserted (the attribute was present) and `false` otherwise.
+	 */
+	_insertParagraphAccordingToSelectionAttribute() {
+		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const typeAroundSelectionAttributeValue = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+		if ( !typeAroundSelectionAttributeValue ) {
+			return false;
+		}
+
+		const selectedModelElement = modelSelection.getSelectedElement();
+
+		this._insertParagraph( selectedModelElement, typeAroundSelectionAttributeValue );
+
+		return true;
+	}
+
+	/**
 	 * Creates a listener in the editing conversion pipeline that injects the type around
 	 * UI into every single widget instance created in the editor.
 	 *
@@ -146,49 +178,260 @@ export default class WidgetTypeAround extends Plugin {
 			// Filter out non-widgets and inline widgets.
 			if ( isTypeAroundWidget( viewElement, data.item, schema ) ) {
 				injectUIIntoWidget( conversionApi.writer, buttonTitles, viewElement );
-
-				// Keep track of widgets that have the type around UI injected.
-				// In the #_enableDetectionOfTypeAroundWidgets() we will iterate only over these
-				// widgets instead of all children of the root. This should improve the performance.
-				this._widgetsWithTypeAroundUI.add( viewElement );
 			}
 		}, { priority: 'low' } );
 	}
 
 	/**
-	 * Registers an editing view post-fixer which checks all block widgets in the content
-	 * and adds CSS classes to these which should have the typing around (UI) enabled
-	 * and visible for the users.
+	 * Brings support for the "fake caret" that appears when either:
+	 *
+	 * * the selection moves from a position next to a widget (to a widget) using arrow keys,
+	 * * the arrow key is pressed when the widget is already selected.
+	 *
+	 * The "fake caret" lets the user know that they can start typing or just press
+	 * enter to insert a paragraph at the position next to a widget as suggested by the "fake caret".
+	 *
+	 * The "fake caret" disappears when the user changes the selection or the editor
+	 * gets blurred.
+	 *
+	 * The whole idea is as follows:
+	 *
+	 * 1. A user does one of the 2 scenarios described at the beginning.
+	 * 2. The "keydown" listener is executed and the decision is made whether to show or hide the "fake caret".
+	 * 3. If it should show up, the "widget-type-around" model selection attribute is set indicating
+	 *    on which side of the widget it should appear.
+	 * 4. The selection dispatcher reacts to the selection attribute and sets CSS classes responsible for the
+	 *    "fake caret" on the view widget.
+	 * 5. If the "fake caret" should disappear, the selection attribute is removed and the dispatcher
+	 *    does the CSS class clean-up in the view.
+	 * 6. Additionally, "change:range" and FocusTracker#isFocused listeners also remove the selection
+	 *    attribute (the former also removes widget CSS classes).
 	 *
 	 * @private
 	 */
-	_enableDetectionOfTypeAroundWidgets() {
+	_enableTypeAroundFakeCaretActivationUsingKeyboardArrows() {
 		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const schema = model.schema;
 		const editingView = editor.editing.view;
 
+		// This is the main listener responsible for the "fake caret".
+		// Note: The priority must precede the default Widget class keydown handler ("high") and the
+		// TableKeyboard keydown handler ("high-10").
+		editingView.document.on( 'keydown', ( evt, domEventData ) => {
+			if ( isArrowKeyCode( domEventData.keyCode ) ) {
+				this._handleArrowKeyPress( evt, domEventData );
+			}
+		}, { priority: priorities.get( 'high' ) + 10 } );
+
+		// This listener makes sure the widget type around selection attribute will be gone from the model
+		// selection as soon as the model range changes. This attribute only makes sense when a widget is selected
+		// (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else),
+		// let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered.
+		modelSelection.on( 'change:range', ( evt, data ) => {
+			// Do not reset the selection attribute when the change was indirect.
+			if ( !data.directChange ) {
+				return;
+			}
+
+			const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			if ( !typeAroundSelectionAttribute ) {
+				return;
+			}
+
+			// Get rid of the widget type around attribute of the selection on every change:range.
+			// If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode.
+			editor.model.change( writer => {
+				writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+			} );
+		} );
+
+		// React to changes of the model selection attribute made by the arrow keys listener.
+		// If the block widget is selected and the attribute changes, downcast the attribute to special
+		// CSS classes associated with the active ("fake horizontal caret") mode of the widget.
+		editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+			const writer = conversionApi.writer;
+
+			if ( this._currentFakeCaretModelElement ) {
+				const selectedViewElement = conversionApi.mapper.toViewElement( this._currentFakeCaretModelElement );
+
+				if ( selectedViewElement ) {
+					// Get rid of CSS classes associated with the active ("fake horizontal caret") mode from the view widget.
+					writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement );
+
+					this._currentFakeCaretModelElement = null;
+				}
+			}
+
+			const selectedModelElement = data.selection.getSelectedElement();
+
+			if ( !selectedModelElement ) {
+				return;
+			}
+
+			const selectedViewElement = conversionApi.mapper.toViewElement( selectedModelElement );
+
+			if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
+				return;
+			}
+
+			const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			if ( !typeAroundSelectionAttribute ) {
+				return;
+			}
+
+			writer.addClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), selectedViewElement );
+
+			// Remember the view widget that got the "fake-caret" CSS class. This class should be removed ASAP when the
+			// selection changes
+			this._currentFakeCaretModelElement = selectedModelElement;
+		} );
+
+		this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
+			if ( !isFocused ) {
+				editor.model.change( writer => {
+					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+				} );
+			}
+		} );
+
 		function positionToWidgetCssClass( position ) {
-			return `ck-widget_can-type-around_${ position }`;
+			return `ck-widget_type-around_show-fake-caret_${ position }`;
+		}
+	}
+
+	/**
+	 * A listener executed on each "keydown" in the view document, a part of
+	 * {@link #_enableTypeAroundFakeCaretActivationUsingKeyboardArrows}.
+	 *
+	 * It decides whether the arrow key press should activate the "fake caret" or not (also whether it should
+	 * be deactivated).
+	 *
+	 * The "fake caret" activation is done by setting the "widget-type-around" model selection attribute
+	 * in this listener and stopping&preventing the event that would normally be handled by the Widget
+	 * plugin that is responsible for the regular keyboard navigation near/across all widgets (that
+	 * includes inline widgets, which are ignored by the WidgetTypeAround plugin).
+	 *
+	 * @private
+	 */
+	_handleArrowKeyPress( evt, domEventData ) {
+		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const schema = model.schema;
+		const editingView = editor.editing.view;
+
+		const keyCode = domEventData.keyCode;
+		const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection );
+		const selectedViewElement = editingView.document.selection.getSelectedElement();
+		const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
+		let shouldStopAndPreventDefault;
+
+		// Handle keyboard navigation when a type-around-compatible widget is currently selected.
+		if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
+			shouldStopAndPreventDefault = this._handleArrowKeyPressOnSelectedWidget( isForward );
+		}
+		// Handle keyboard arrow navigation when the selection is next to a type-around-compatible widget
+		// and the widget is about to be selected.
+		else if ( modelSelection.isCollapsed ) {
+			shouldStopAndPreventDefault = this._handleArrowKeyPressWhenSelectionNextToAWidget( isForward );
+		}
+
+		if ( shouldStopAndPreventDefault ) {
+			domEventData.preventDefault();
+			evt.stop();
 		}
+	}
 
-		editingView.document.registerPostFixer( writer => {
-			for ( const widgetViewElement of this._widgetsWithTypeAroundUI ) {
-				// If the widget is no longer attached to the root (for instance, because it was removed),
-				// there is no need to update its classes and we can safely forget about it.
-				if ( !widgetViewElement.isAttached() ) {
-					this._widgetsWithTypeAroundUI.delete( widgetViewElement );
-				} else {
-					// Update widgets' classes depending on possible positions for paragraph insertion.
-					const positions = getWidgetTypeAroundPositions( widgetViewElement );
-
-					// Remove all classes. In theory we could remove only these that will not be added a few lines later,
-					// but since there are only two... KISS.
-					writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), widgetViewElement );
-
-					// Set CSS classes related to possible positions. They are used so the UI knows which buttons to display.
-					writer.addClass( positions.map( positionToWidgetCssClass ), widgetViewElement );
+	/**
+	 * Handles the keyboard navigation on "keydown" when a widget is currently selected and activates or deactivates
+	 * the "fake caret" for that widget, depending on the current value of the "widget-type-around" model
+	 * selection attribute and the direction of the pressed arrow key.
+	 *
+	 * @private
+	 * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement
+	 * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
+	 * @returns {Boolean} `true` when the key press was handled and no other keydown listener of the editor should
+	 * process the event any further. `false` otherwise.
+	 */
+	_handleArrowKeyPressOnSelectedWidget( isForward ) {
+		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+		let shouldStopAndPreventDefault = false;
+
+		model.change( writer => {
+			// If the selection already has the attribute...
+			if ( typeAroundSelectionAttribute ) {
+				const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' );
+
+				// If the keyboard arrow works against the value of the selection attribute...
+				// then remove the selection attribute but prevent default DOM actions
+				// and do not let the Widget plugin listener move the selection. This brings
+				// the widget back to the state, for instance, like if was selected using the mouse.
+				//
+				// **Note**: If leaving the widget when the "fake caret" is active, then the default
+				// Widget handler will change the selection and, in turn, this will automatically discard
+				// the selection attribute.
+				if ( !isLeavingWidget ) {
+					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+					shouldStopAndPreventDefault = true;
 				}
 			}
+			// If the selection didn't have the attribute, let's set it now according to the direction of the arrow
+			// key press. This also means we cannot let the Widget plugin listener move the selection.
+			else {
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' );
+
+				shouldStopAndPreventDefault = true;
+			}
 		} );
+
+		return shouldStopAndPreventDefault;
+	}
+
+	/**
+	 * Handles the keyboard navigation on "keydown" when **no** widget is selected but the selection is **directly** next
+	 * to one and upon the "fake caret" should become active for this widget upon arrow key press
+	 * (AKA entering/selecting the widget).
+	 *
+	 * **Note**: This code mirrors the implementation from the Widget plugin but also adds the selection attribute.
+	 * Unfortunately, there's no safe way to let the Widget plugin do the selection part first and then just set the
+	 * selection attribute here in the WidgetTypeAround plugin. This is why this code must duplicate some from the Widget plugin.
+	 *
+	 * @private
+	 * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement
+	 * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
+	 * @returns {Boolean} `true` when the key press was handled and no other keydown listener of the editor should
+	 * process the event any further. `false` otherwise.
+	 */
+	_handleArrowKeyPressWhenSelectionNextToAWidget( isForward ) {
+		const editor = this.editor;
+		const model = editor.model;
+		const schema = model.schema;
+		const widgetPlugin = editor.plugins.get( 'Widget' );
+
+		// This is the widget the selection is about to be set on.
+		const modelElementNextToSelection = widgetPlugin._getObjectElementNextToSelection( isForward );
+		const viewElementNextToSelection = editor.editing.mapper.toViewElement( modelElementNextToSelection );
+
+		if ( isTypeAroundWidget( viewElementNextToSelection, modelElementNextToSelection, schema ) ) {
+			model.change( writer => {
+				widgetPlugin._setSelectionOverElement( modelElementNextToSelection );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'before' : 'after' );
+			} );
+
+			// The change() block above does the same job as the Widget plugin. The event can
+			// be safely canceled.
+			return true;
+		}
+
+		return false;
 	}
 
 	/**
@@ -211,13 +454,180 @@ export default class WidgetTypeAround extends Plugin {
 
 			const buttonPosition = getTypeAroundButtonPosition( button );
 			const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter );
+			const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement );
 
-			this._insertParagraph( widgetViewElement, buttonPosition );
+			this._insertParagraph( widgetModelElement, buttonPosition );
 
 			domEventData.preventDefault();
 			evt.stop();
 		} );
 	}
+
+	/**
+	 * Creates the "enter" key listener on the view document that allows the user to insert a paragraph
+	 * near the widget when either:
+	 *
+	 * * The "fake caret" was first activated using the arrow keys,
+	 * * The entire widget is selected in the model.
+	 *
+	 * In the first case, the new paragraph is inserted according to the "widget-type-around" selection
+	 * attribute (see {@link #_handleArrowKeyPress}).
+	 *
+	 * It the second case, the new paragraph is inserted based on whether a soft (Shift+Enter) keystroke
+	 * was pressed or not.
+	 *
+	 * @private
+	 */
+	_enableInsertingParagraphsOnEnterKeypress() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+
+		this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => {
+			const selectedViewElement = editingView.document.selection.getSelectedElement();
+			const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
+			const schema = editor.model.schema;
+			let wasHandled;
+
+			// First check if the widget is selected and there's a type around selection attribute associated
+			// with the "fake caret" that would tell where to insert a new paragraph.
+			if ( this._insertParagraphAccordingToSelectionAttribute() ) {
+				wasHandled = true;
+			}
+			// Then, if there is no selection attribute associated with the "fake caret", check if the widget
+			// simply is selected and create a new paragraph according to the keystroke (Shift+)Enter.
+			else if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
+				this._insertParagraph( selectedModelElement, domEventData.isSoft ? 'before' : 'after' );
+
+				wasHandled = true;
+			}
+
+			if ( wasHandled ) {
+				domEventData.preventDefault();
+				evt.stop();
+			}
+		} );
+	}
+
+	/**
+	 * Similar to the {@link #_enableInsertingParagraphsOnEnterKeypress}, it allows the user
+	 * to insert a paragraph next to a widget when the "fake caret" was activated using arrow
+	 * keys but it responds to "typing keystrokes" instead of "enter".
+	 *
+	 * "Typing keystrokes" are keystrokes that insert new content into the document
+	 * like, for instance, letters ("a") or numbers ("4"). The "keydown" listener enabled by this method
+	 * will insert a new paragraph according to the "widget-type-around" model selection attribute
+	 * as the user simply starts typing, which creates the impression that the "fake caret"
+	 * behaves like a "real one" rendered by the browser (AKA your text appears where the caret was).
+	 *
+	 * **Note**: ATM this listener creates 2 undo steps: one for the "insertParagraph" command
+	 * and the second for the actual typing. It's not a disaster but this may need to be fixed
+	 * sooner or later.
+	 *
+	 * Learn more in {@link module:typing/utils/injectunsafekeystrokeshandling}.
+	 *
+	 * @private
+	 */
+	_enableInsertingParagraphsOnTypingKeystroke() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const keyCodesHandledSomewhereElse = [
+			keyCodes.enter,
+			keyCodes.delete,
+			keyCodes.backspace
+		];
+
+		// Note: The priority must precede the default Widget class keydown handler ("high") and the
+		// TableKeyboard keydown handler ("high + 1").
+		editingView.document.on( 'keydown', ( evt, domEventData ) => {
+			// Don't handle enter/backspace/delete here. They are handled in dedicated listeners.
+			if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) {
+				this._insertParagraphAccordingToSelectionAttribute();
+			}
+		}, { priority: priorities.get( 'high' ) + 1 } );
+	}
+
+	/**
+	 * It creates a "delete" event listener on the view document to handle cases when delete/backspace
+	 * is pressed and the "fake caret" is currently active.
+	 *
+	 * The "fake caret" should create an illusion of a "real browser caret" so that when it appears
+	 * before/after a widget, pressing delete/backspace should remove a widget or delete a content
+	 * before/after a widget (depending on the content surrounding the widget).
+	 *
+	 * @private
+	 */
+	_enableDeleteIntegration() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const model = editor.model;
+		const schema = model.schema;
+
+		// Note: The priority must precede the default Widget class delete handler.
+		this.listenTo( editingView.document, 'delete', ( evt, domEventData ) => {
+			const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			// This listener handles only these cases when the "fake caret" is active.
+			if ( !typeAroundSelectionAttributeValue ) {
+				return;
+			}
+
+			const direction = domEventData.direction;
+			const selectedModelWidget = model.document.selection.getSelectedElement();
+
+			const isFakeCaretBefore = typeAroundSelectionAttributeValue === 'before';
+			const isForwardDelete = direction == 'forward';
+			const shouldDeleteEntireWidget = isFakeCaretBefore === isForwardDelete;
+
+			if ( shouldDeleteEntireWidget ) {
+				editor.execute( 'delete', {
+					selection: model.createSelection( selectedModelWidget, 'on' )
+				} );
+			} else {
+				const range = schema.getNearestSelectionRange(
+					model.createPositionAt( selectedModelWidget, typeAroundSelectionAttributeValue ),
+					direction
+				);
+
+				// If there is somewhere to move selection to, then there will be something to delete.
+				if ( range ) {
+					// If the range is NOT collapsed, then we know that the range contains an object (see getNearestSelectionRange() docs).
+					if ( !range.isCollapsed ) {
+						model.change( writer => {
+							writer.setSelection( range );
+							editor.execute( isForwardDelete ? 'forwardDelete' : 'delete' );
+						} );
+					} else {
+						const probe = model.createSelection( range.start );
+						model.modifySelection( probe, { direction } );
+
+						// If the range is collapsed, let's see if a non-collapsed range exists that can could be deleted.
+						// If such range exists, use the editor command because it it safe for collaboration (it merges where it can).
+						if ( !probe.focus.isEqual( range.start ) ) {
+							model.change( writer => {
+								writer.setSelection( range );
+								editor.execute( isForwardDelete ? 'forwardDelete' : 'delete' );
+							} );
+						}
+						// If there is no non-collapsed range to be deleted then we are sure that there is an empty element
+						// next to a widget that should be removed. "delete" and "forwardDelete" commands cannot get rid of it
+						// so calling Model#deleteContent here manually.
+						else {
+							const deepestEmptyRangeAncestor = getDeepestEmptyElementAncestor( schema, range.start.parent );
+
+							model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), {
+								doNotAutoparagraph: true
+							} );
+						}
+					}
+				}
+			}
+
+			// If some content was deleted, don't let the handler from the Widget plugin kick in.
+			// If nothing was deleted, then the default handler will have nothing to do anyway.
+			domEventData.preventDefault();
+			evt.stop();
+		}, { priority: priorities.get( 'high' ) + 1 } );
+	}
 }
 
 // Injects the type around UI into a view widget instance.
@@ -232,6 +642,7 @@ function injectUIIntoWidget( viewWriter, buttonTitles, widgetViewElement ) {
 		const wrapperDomElement = this.toDomElement( domDocument );
 
 		injectButtons( wrapperDomElement, buttonTitles );
+		injectFakeCaret( wrapperDomElement );
 
 		return wrapperDomElement;
 	} );
@@ -266,3 +677,42 @@ function injectButtons( wrapperDomElement, buttonTitles ) {
 		wrapperDomElement.appendChild( buttonTemplate.render() );
 	}
 }
+
+// @param {HTMLElement} wrapperDomElement
+function injectFakeCaret( wrapperDomElement ) {
+	const caretTemplate = new Template( {
+		tag: 'div',
+		attributes: {
+			class: [
+				'ck',
+				'ck-widget__type-around__fake-caret'
+			]
+		}
+	} );
+
+	wrapperDomElement.appendChild( caretTemplate.render() );
+}
+
+// Returns the ancestor of an element closest to the root which is empty. For instance,
+// for `<baz>`:
+//
+//		<foo>abc<bar><baz></baz></bar></foo>
+//
+// it returns `<bar>`.
+//
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/model/element~Element} element
+// @returns {module:engine/model/element~Element|null}
+function getDeepestEmptyElementAncestor( schema, element ) {
+	let deepestEmptyAncestor = element;
+
+	for ( const ancestor of element.getAncestors( { parentFirst: true } ) ) {
+		if ( ancestor.childCount > 1 || schema.isLimit( ancestor ) ) {
+			break;
+		}
+
+		deepestEmptyAncestor = ancestor;
+	}
+
+	return deepestEmptyAncestor;
+}

+ 2 - 2
packages/ckeditor5-widget/tests/manual/type-around.html

@@ -39,13 +39,13 @@
 					<td>&nbsp;</td>
 					<td>
 						<figure class="media">
-							<div data-oembed-url="https://www.youtube.com/watch?v=ZVv7UMQPEWk"><div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;"><iframe src="https://www.youtube.com/embed/ZVv7UMQPEWk" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen=""></iframe></div></div>
+							<div data-oembed-url="https://www.youtube.com/watch?v=ZVv7UMQPEWk"></div>
 						</figure>
 					</td>
 				</tr>
 				<tr>
 					<td>&nbsp;</td>
-					<td>
+					<td style="background: hsl(0,100%,0%)">
 						<p>&nbsp;</p>
 						<figure class="image"><img src="sample.jpg" alt="bar">
 							<figcaption>Caption</figcaption>

+ 15 - 2
packages/ckeditor5-widget/tests/manual/type-around.js

@@ -9,6 +9,9 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
 import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
+import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
+import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
+import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
@@ -96,7 +99,15 @@ document.querySelector( '#toggleReadOnly' ).addEventListener( 'click', () => {
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, HorizontalLine, InlineWidget, MediaEmbed ],
+		plugins: [
+			ArticlePluginSet,
+			HorizontalLine,
+			InlineWidget,
+			MediaEmbed,
+			TableProperties,
+			TableCellProperties,
+			ImageResize
+		],
 		toolbar: [
 			'heading',
 			'|',
@@ -123,7 +134,9 @@ ClassicEditor
 			contentToolbar: [
 				'tableColumn',
 				'tableRow',
-				'mergeTableCells'
+				'mergeTableCells',
+				'tableProperties',
+				'tableCellProperties'
 			]
 		}
 	} )

+ 7 - 6
packages/ckeditor5-widget/tests/widget-integration.js

@@ -6,6 +6,7 @@
 /* global document */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Widget from '../src/widget';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
@@ -31,7 +32,7 @@ describe( 'Widget - integration', () => {
 		editorElement = document.createElement( 'div' );
 		document.body.appendChild( editorElement );
 
-		return ClassicEditor.create( editorElement, { plugins: [ Widget, Typing ] } )
+		return ClassicEditor.create( editorElement, { plugins: [ Paragraph, Widget, Typing ] } )
 			.then( newEditor => {
 				editor = newEditor;
 				model = editor.model;
@@ -113,7 +114,7 @@ describe( 'Widget - integration', () => {
 
 		expect( getViewData( view ) ).to.equal(
 			'<p>[]</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'
@@ -139,7 +140,7 @@ describe( 'Widget - integration', () => {
 		sinon.assert.called( preventDefault );
 
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">{foo bar}</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'
@@ -164,7 +165,7 @@ describe( 'Widget - integration', () => {
 		sinon.assert.called( preventDefault );
 
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo</figcaption>' +
 				'<figcaption contenteditable="true">{bar}</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -191,7 +192,7 @@ describe( 'Widget - integration', () => {
 		sinon.assert.called( preventDefault );
 
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">{foo bar}</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'
@@ -243,7 +244,7 @@ describe( 'Widget - integration', () => {
 
 		expect( getViewData( view ) ).to.equal(
 			'<p>[]</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'

+ 151 - 171
packages/ckeditor5-widget/tests/widget.js

@@ -6,6 +6,8 @@
 /* global document */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Widget from '../src/widget';
 import WidgetTypeAround from '../src/widgettypearound/widgettypearound';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
@@ -28,7 +30,7 @@ describe( 'Widget', () => {
 
 		return ClassicTestEditor
 			.create( element, {
-				plugins: [ Widget, Typing ]
+				plugins: [ Paragraph, Widget, Typing, Enter ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -198,7 +200,7 @@ describe( 'Widget', () => {
 		setModelData( model, '[<widget>foo bar</widget>]' );
 
 		expect( getViewData( view ) ).to.equal(
-			'[<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_selected" ' +
+			'[<div class="ck-widget ck-widget_selected" ' +
 			'contenteditable="false">' +
 				'foo bar' +
 				'<b></b>' +
@@ -221,11 +223,11 @@ describe( 'Widget', () => {
 		expect( getViewData( view ) ).to.equal(
 
 			'<p>{foo</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_selected" contenteditable="false">' +
+			'<div class="ck-widget ck-widget_selected" contenteditable="false">' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>' +
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_selected" ' +
+			'<div class="ck-widget ck-widget_selected" ' +
 			'contenteditable="false">' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -244,7 +246,7 @@ describe( 'Widget', () => {
 
 		expect( getViewData( view ) ).to.equal(
 			'<p>foo</p>' +
-			'[<div class="ck-widget ck-widget_can-type-around_after ck-widget_selected" contenteditable="false">' +
+			'[<div class="ck-widget ck-widget_selected" contenteditable="false">' +
 				'foo' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -257,7 +259,7 @@ describe( 'Widget', () => {
 
 		expect( getViewData( view ) ).to.equal(
 			'<p>{}foo</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'foo' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -269,7 +271,7 @@ describe( 'Widget', () => {
 		setModelData( model, '<widget><editable>foo bar</editable></widget><editable>[baz]</editable>' );
 
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -283,112 +285,128 @@ describe( 'Widget', () => {
 			test(
 				'should move selection forward from selected object - right arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowright,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowright, keyCodes.arrowright ],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should move selection forward from selected object - down arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowdown,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowdown, keyCodes.arrowdown ],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should move selection backward from selected object - left arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowleft,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowleft, keyCodes.arrowleft ],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
 			test(
 				'should move selection backward from selected object - up arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowup,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowup, keyCodes.arrowup ],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
 			test(
 				'should move selection to next widget - right arrow',
 				'[<widget></widget>]<widget></widget>',
-				keyCodes.arrowright,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowright, keyCodes.arrowright ],
 				'<widget></widget>[<widget></widget>]'
 			);
 
 			test(
 				'should move selection to next widget - down arrow',
 				'[<widget></widget>]<widget></widget>',
-				keyCodes.arrowdown,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowdown, keyCodes.arrowdown ],
 				'<widget></widget>[<widget></widget>]'
 			);
 
 			test(
 				'should move selection to previous widget - left arrow',
 				'<widget></widget>[<widget></widget>]',
-				keyCodes.arrowleft,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowleft, keyCodes.arrowleft ],
 				'[<widget></widget>]<widget></widget>'
 			);
 
 			test(
 				'should move selection to previous widget - up arrow',
 				'<widget></widget>[<widget></widget>]',
-				keyCodes.arrowup,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowup, keyCodes.arrowup ],
 				'[<widget></widget>]<widget></widget>'
 			);
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
-				'should do nothing on non-collapsed selection next to object - right arrow',
-				'<paragraph>ba[r]</paragraph><widget></widget>',
+				'should do nothing on non-collapsed selection next to an inline widget - right arrow',
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>',
 				keyCodes.arrowright,
-				'<paragraph>ba[r]</paragraph><widget></widget>'
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>'
 			);
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
-				'should do nothing on non-collapsed selection next to object - down arrow',
-				'<paragraph>ba[r]</paragraph><widget></widget>',
+				'should do nothing on non-collapsed selection next to an inline widget - down arrow',
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>',
 				keyCodes.arrowdown,
-				'<paragraph>ba[r]</paragraph><widget></widget>'
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>'
 			);
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
-				'should do nothing on non-collapsed selection next to object - left arrow',
-				'<widget></widget><paragraph>[b]ar</paragraph>',
+				'should do nothing on non-collapsed selection next to an inline widget - left arrow',
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>',
 				keyCodes.arrowleft,
-				'<widget></widget><paragraph>[b]ar</paragraph>'
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>'
 			);
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
-				'should do nothing on non-collapsed selection next to object - up arrow',
-				'<widget></widget><paragraph>[b]ar</paragraph>',
+				'should do nothing on non-collapsed selection next to an inline widget - up arrow',
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>',
 				keyCodes.arrowup,
-				'<widget></widget><paragraph>[b]ar</paragraph>'
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>'
 			);
 
 			test(
 				'should not move selection if there is no correct location - right arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowright,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowright, keyCodes.arrowright ],
 				'<paragraph>foo</paragraph>[<widget></widget>]'
 			);
 
 			test(
 				'should not move selection if there is no correct location - down arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowdown,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowdown, keyCodes.arrowdown ],
 				'<paragraph>foo</paragraph>[<widget></widget>]'
 			);
 
 			test(
 				'should not move selection if there is no correct location - left arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowleft,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowleft, keyCodes.arrowleft ],
 				'[<widget></widget>]<paragraph>foo</paragraph>'
 			);
 
 			test(
 				'should not move selection if there is no correct location - up arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowup,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowup, keyCodes.arrowup ],
 				'[<widget></widget>]<paragraph>foo</paragraph>'
 			);
 
@@ -409,10 +427,12 @@ describe( 'Widget', () => {
 				setModelData( model, '<paragraph>foo</paragraph>[<widget></widget>]' );
 				viewDocument.on( 'keydown', keydownHandler );
 
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				viewDocument.fire( 'keydown', domEventDataMock );
 				viewDocument.fire( 'keydown', domEventDataMock );
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<widget></widget>]' );
-				sinon.assert.calledOnce( domEventDataMock.preventDefault );
+				sinon.assert.calledTwice( domEventDataMock.preventDefault );
 				sinon.assert.notCalled( keydownHandler );
 			} );
 
@@ -425,10 +445,12 @@ describe( 'Widget', () => {
 				setModelData( model, '[<widget></widget>]<paragraph>foo</paragraph>' );
 				viewDocument.on( 'keydown', keydownHandler );
 
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				viewDocument.fire( 'keydown', domEventDataMock );
 				viewDocument.fire( 'keydown', domEventDataMock );
 
 				expect( getModelData( model ) ).to.equal( '[<widget></widget>]<paragraph>foo</paragraph>' );
-				sinon.assert.calledOnce( domEventDataMock.preventDefault );
+				sinon.assert.calledTwice( domEventDataMock.preventDefault );
 				sinon.assert.notCalled( keydownHandler );
 			} );
 
@@ -491,84 +513,132 @@ describe( 'Widget', () => {
 			test(
 				'should work correctly with modifier key: right arrow + ctrl',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowright, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowright, ctrlKey: true },
+					{ keyCode: keyCodes.arrowright, ctrlKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should work correctly with modifier key: right arrow + alt',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowright, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowright, altKey: true },
+					{ keyCode: keyCodes.arrowright, altKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should work correctly with modifier key: right arrow + shift',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowright, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowright, shiftKey: true },
+					{ keyCode: keyCodes.arrowright, shiftKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should work correctly with modifier key: down arrow + ctrl',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowdown, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowdown, ctrlKey: true },
+					{ keyCode: keyCodes.arrowdown, ctrlKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should work correctly with modifier key: down arrow + alt',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowdown, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowdown, altKey: true },
+					{ keyCode: keyCodes.arrowdown, altKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should work correctly with modifier key: down arrow + shift',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowdown, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowdown, shiftKey: true },
+					{ keyCode: keyCodes.arrowdown, shiftKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 
 			test(
 				'should work correctly with modifier key: left arrow + ctrl',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowleft, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowleft, ctrlKey: true },
+					{ keyCode: keyCodes.arrowleft, ctrlKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
 			test(
 				'should work correctly with modifier key: left arrow + alt',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowleft, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowleft, altKey: true },
+					{ keyCode: keyCodes.arrowleft, altKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
 			test(
 				'should work correctly with modifier key: left arrow + shift',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowleft, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowleft, shiftKey: true },
+					{ keyCode: keyCodes.arrowleft, shiftKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
 			test(
 				'should work correctly with modifier key: up arrow + ctrl',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowup, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowup, ctrlKey: true },
+					{ keyCode: keyCodes.arrowup, ctrlKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
 			test(
 				'should work correctly with modifier key: up arrow + alt',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowup, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowup, altKey: true },
+					{ keyCode: keyCodes.arrowup, altKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
 			test(
 				'should work correctly with modifier key: up arrow + shift',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowup, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowup, shiftKey: true },
+					{ keyCode: keyCodes.arrowup, shiftKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 
@@ -695,7 +765,8 @@ describe( 'Widget', () => {
 				test(
 					'should move selection forward from selected object - left arrow',
 					'[<widget></widget>]<paragraph>foo</paragraph>',
-					keyCodes.arrowleft,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowleft, keyCodes.arrowleft ],
 					'<widget></widget><paragraph>[]foo</paragraph>',
 					null,
 					'rtl'
@@ -704,7 +775,8 @@ describe( 'Widget', () => {
 				test(
 					'should move selection backward from selected object - right arrow',
 					'<paragraph>foo</paragraph>[<widget></widget>]',
-					keyCodes.arrowright,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowright, keyCodes.arrowright ],
 					'<paragraph>foo[]</paragraph><widget></widget>',
 					null,
 					'rtl'
@@ -713,7 +785,8 @@ describe( 'Widget', () => {
 				test(
 					'should move selection to next widget - left arrow',
 					'[<widget></widget>]<widget></widget>',
-					keyCodes.arrowleft,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowleft, keyCodes.arrowleft ],
 					'<widget></widget>[<widget></widget>]',
 					null,
 					'rtl'
@@ -722,7 +795,8 @@ describe( 'Widget', () => {
 				test(
 					'should move selection to previous widget - right arrow',
 					'<widget></widget>[<widget></widget>]',
-					keyCodes.arrowright,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowright, keyCodes.arrowright ],
 					'[<widget></widget>]<widget></widget>',
 					null,
 					'rtl'
@@ -730,118 +804,33 @@ describe( 'Widget', () => {
 			} );
 		} );
 
-		describe( 'enter', () => {
-			test(
-				'should insert a paragraph after the selected widget upon Enter',
-				'[<widget></widget>]',
-				keyCodes.enter,
-				'<widget></widget><paragraph>[]</paragraph>'
-			);
-
-			test(
-				'should insert a paragraph before the selected widget upon Shift+Enter',
-				'[<widget></widget>]',
-				{ keyCode: keyCodes.enter, shiftKey: true },
-				'<paragraph>[]</paragraph><widget></widget>'
-			);
-
-			test(
-				'should insert a paragraph when not a first-child of the root',
-				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.enter,
-				'<widget></widget><paragraph>[]</paragraph><paragraph>foo</paragraph>'
-			);
-
-			test(
-				'should insert a paragraph when not a last-child of the root',
-				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.enter, shiftKey: true },
-				'<paragraph>foo</paragraph><paragraph>[]</paragraph><widget></widget>'
-			);
+		function test( name, data, actions, expected, expectedView, contentLanguageDirection = 'ltr' ) {
+			it( name, () => {
+				testUtils.sinon.stub( editor.locale, 'contentLanguageDirection' ).value( contentLanguageDirection );
 
-			test(
-				'should insert a paragraph only when an entire widget is selected (#1)',
-				'<widget><nested>[foo] bar</nested></widget>',
-				keyCodes.enter,
-				'<widget><nested>[] bar</nested></widget>'
-			);
+				if ( !Array.isArray( actions ) ) {
+					actions = [ actions ];
+				}
 
-			test(
-				'should insert a paragraph only when an entire widget is selected (#2)',
-				'<paragraph>f[oo</paragraph><widget></widget><paragraph>b]ar</paragraph>',
-				keyCodes.enter,
-				'<paragraph>f[]ar</paragraph>'
-			);
+				actions = actions.map( action => {
+					if ( typeof action === 'object' ) {
+						return action;
+					}
 
-			// https://github.com/ckeditor/ckeditor5/issues/1529
-			it( 'should split parent when widget is inside a block element', () => {
-				model.schema.register( 'allowP', {
-					inheritAllFrom: '$block'
-				} );
-				model.schema.register( 'disallowP', {
-					inheritAllFrom: '$block',
-					allowIn: [ 'allowP' ]
+					return {
+						keyCode: action
+					};
 				} );
-				model.schema.extend( 'widget', {
-					allowIn: [ 'allowP', 'disallowP' ]
-				} );
-				model.schema.extend( 'paragraph', {
-					allowIn: [ 'allowP' ]
-				} );
-
-				editor.conversion.for( 'downcast' ).elementToElement( { model: 'parent', view: 'parent' } );
-				editor.conversion.for( 'downcast' ).elementToElement( { model: 'allowP', view: 'allowP' } );
-				editor.conversion.for( 'downcast' ).elementToElement( { model: 'disallowP', view: 'disallowP' } );
-
-				setModelData( model, '<allowP><disallowP>[<widget></widget>]</disallowP></allowP>' );
-
-				viewDocument.fire( 'keydown', new DomEventData(
-					viewDocument,
-					{ target: document.createElement( 'div' ), preventDefault() {} },
-					{ keyCode: keyCodes.enter }
-				) );
-
-				expect( getModelData( model ) ).to.equal(
-					'<allowP><disallowP><widget></widget></disallowP><paragraph>[]</paragraph><disallowP></disallowP></allowP>'
-				);
-			} );
-
-			test(
-				'should do nothing if selected is inline object',
-				'<paragraph>foo[<inline-widget></inline-widget>]bar</paragraph>',
-				keyCodes.enter,
-				'<paragraph>foo[]bar</paragraph>'
-			);
-
-			test(
-				'should insert a paragraph after the selected widget inside an element that is not a block upon Enter',
-				'<blockQuote>[<widget></widget>]</blockQuote>',
-				keyCodes.enter,
-				'<blockQuote><widget></widget><paragraph>[]</paragraph></blockQuote>'
-			);
-
-			test(
-				'should insert a paragraph before the selected widget inside an element that is not a block upon Shift+Enter',
-				'<blockQuote>[<widget></widget>]</blockQuote>',
-				{ keyCode: keyCodes.enter, shiftKey: true },
-				'<blockQuote><paragraph>[]</paragraph><widget></widget></blockQuote>'
-			);
-		} );
-
-		function test( name, data, keyCodeOrMock, expected, expectedView, contentLanguageDirection = 'ltr' ) {
-			it( name, () => {
-				testUtils.sinon.stub( editor.locale, 'contentLanguageDirection' ).value( contentLanguageDirection );
-
-				const domEventDataMock = ( typeof keyCodeOrMock == 'object' ) ? keyCodeOrMock : {
-					keyCode: keyCodeOrMock
-				};
 
 				setModelData( model, data );
-				viewDocument.fire( 'keydown', new DomEventData(
-					viewDocument,
-					{ target: document.createElement( 'div' ), preventDefault() {} },
-					domEventDataMock
-				) );
+
+				for ( const action of actions ) {
+					viewDocument.fire( 'keydown', new DomEventData(
+						viewDocument,
+						{ target: document.createElement( 'div' ), preventDefault() {} },
+						action
+					) );
+				}
 
 				expect( getModelData( model ) ).to.equal( expected );
 
@@ -1274,7 +1263,7 @@ describe( 'Widget', () => {
 
 			return ClassicTestEditor
 				.create( element, {
-					plugins: [ Widget, Typing ]
+					plugins: [ Paragraph, Widget, Typing ]
 				} )
 				.then( newEditor => {
 					editor = newEditor;
@@ -1349,18 +1338,16 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<div class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 					'<div class="' +
 						'ck-widget ' +
-						'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 						'ck-widget_with-selection-handle" contenteditable="false"' +
 					'>' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'</div>' +
-					'<div class="ck-widget ck-widget_can-type-around_before ck-widget_with-selection-handle" contenteditable="false">' +
+					'<div class="ck-widget ck-widget_with-selection-handle" contenteditable="false">' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'</div>' +
@@ -1390,23 +1377,22 @@ describe( 'Widget', () => {
 			viewDocument.fire( 'mousedown', domEventDataMock );
 
 			expect( getViewData( view ) ).to.equal(
-				'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+				'<div class="ck-widget ck-widget_with-selection-handle" ' +
 				'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'</div>' +
 				'[<div class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 					'<div ' +
-					'class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+					'class="ck-widget ck-widget_with-selection-handle" ' +
 					'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'</div>' +
-					'<div class="ck-widget ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+					'<div class="ck-widget ck-widget_with-selection-handle" ' +
 					'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -1415,7 +1401,7 @@ describe( 'Widget', () => {
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'</div>]' +
 					'<div ' +
-					'class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+					'class="ck-widget ck-widget_with-selection-handle" ' +
 					'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -1443,7 +1429,6 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 				'[<div class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 					'<figcaption contenteditable="true">foo bar</figcaption>' +
@@ -1479,12 +1464,10 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 				'<div class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 					'<div class="' +
 						'ck-widget ' +
-						'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 						'ck-widget_with-selection-handle" contenteditable="false"' +
 					'>' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
@@ -1492,10 +1475,9 @@ describe( 'Widget', () => {
 					'</div>' +
 					'[<div class="' +
 						'ck-widget ' +
-						'ck-widget_can-type-around_before ' +
 						'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 					'>' +
-						'<div class="ck-widget ck-widget_can-type-around_before ck-widget_with-selection-handle" contenteditable="false">' +
+						'<div class="ck-widget ck-widget_with-selection-handle" contenteditable="false">' +
 							'<div class="ck ck-widget__selection-handle"></div>' +
 							'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 						'</div>' +
@@ -1525,13 +1507,11 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 				'<div class="' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 					'<figcaption contenteditable="true">[' +
 						'<div class="' +
 							'ck-widget ' +
-							'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 							'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 						'>' +
 							'<div class="ck ck-widget__selection-handle"></div>' +

文件差异内容过多而无法显示
+ 1063 - 56
packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js


+ 36 - 11
packages/ckeditor5-widget/theme/widgettypearound.css

@@ -38,17 +38,6 @@
 	}
 
 	/*
-	 * Hide the type around buttons depending on which directions the widget supports.
-	 */
-	&:not(.ck-widget_can-type-around_before) > .ck-widget__type-around > .ck-widget__type-around__button_before {
-		display: none;
-	}
-
-	&:not(.ck-widget_can-type-around_after) > .ck-widget__type-around > .ck-widget__type-around__button_after {
-		display: none;
-	}
-
-	/*
 	 * Styles for the buttons when:
 	 * - the widget is selected,
 	 * - or the button is being hovered (regardless of the widget state).
@@ -64,6 +53,42 @@
 			z-index: calc(var(--ck-z-default) + 1);
 		}
 	}
+
+	/*
+	 * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard.
+	 */
+	& > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		display: none;
+		position: absolute;
+		left: 0;
+		right: 0;
+	}
+
+	/*
+	 * When the widget is hovered the "fake caret" would normally be narrower than the
+	 * extra outline displayed around the widget. Let's extend the "fake caret" to match
+	 * the full width of the widget.
+	 */
+	&:hover > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		left: calc( -1 * var(--ck-widget-outline-thickness) );
+		right: calc( -1 * var(--ck-widget-outline-thickness) );
+	}
+
+	/*
+	 * Styles for the horizontal "fake caret" when it should be displayed before the widget (backward keyboard navigation).
+	 */
+	&.ck-widget_type-around_show-fake-caret_before > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		top: calc( -1 * var(--ck-widget-outline-thickness) - 1px );
+		display: block;
+	}
+
+	/*
+	 * Styles for the horizontal "fake caret" when it should be displayed after the widget (forward keyboard navigation).
+	 */
+	&.ck-widget_type-around_show-fake-caret_after > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		bottom: calc( -1 * var(--ck-widget-outline-thickness) - 1px );
+		display: block;
+	}
 }
 
 /*